tagtree: count max_length in characters, not bytes
TagConfig, TagError::TooLong, and the crate docs all describe max_length
in characters; validate_with measured bytes. A multi-byte tag under the
limit was reported as TooLong rather than InvalidChar.
Byte length bounds character count from above, so the cheap check still
gates the O(n) one and the ASCII path is unchanged. Non-ASCII tags were
rejected before and are rejected now; only the reported error moves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 files changed,
+33 insertions,
-1 deletion
| 2 |
2 |
|
|
| 3 |
3 |
|
Notable changes to tagtree. Versions follow semver.
|
| 4 |
4 |
|
|
|
5 |
+ |
## Unreleased
|
|
6 |
+ |
|
|
7 |
+ |
### Fixed
|
|
8 |
+ |
|
|
9 |
+ |
- `max_length` now counts characters, matching what `TagConfig`, `TagError::TooLong`,
|
|
10 |
+ |
and the crate docs have always said. It measured bytes, so a multi-byte tag under
|
|
11 |
+ |
the limit was reported as `TooLong` instead of `InvalidChar`. Only the reported
|
|
12 |
+ |
error changes: non-ASCII tags were rejected before and are rejected now.
|
|
13 |
+ |
|
| 5 |
14 |
|
## 0.4.0 (2026-07-16)
|
| 6 |
15 |
|
|
| 7 |
16 |
|
### Breaking
|
| 141 |
141 |
|
if tag.is_empty() {
|
| 142 |
142 |
|
return Err(TagError::Empty);
|
| 143 |
143 |
|
}
|
| 144 |
|
- |
if tag.len() > config.max_length {
|
|
144 |
+ |
// `max_length` is documented in characters. Byte length is an exact upper
|
|
145 |
+ |
// bound on character count, so the cheap check gates the O(n) one: only a
|
|
146 |
+ |
// tag whose bytes overflow can possibly overflow in characters.
|
|
147 |
+ |
if tag.len() > config.max_length && tag.chars().count() > config.max_length {
|
| 145 |
148 |
|
return Err(TagError::TooLong { max: config.max_length });
|
| 146 |
149 |
|
}
|
| 147 |
150 |
|
if tag.starts_with(SEPARATOR) || tag.ends_with(SEPARATOR) {
|
| 1027 |
1030 |
|
}
|
| 1028 |
1031 |
|
|
| 1029 |
1032 |
|
#[test]
|
|
1033 |
+ |
fn max_length_counts_characters_not_bytes() {
|
|
1034 |
+ |
let cfg = TagConfig {
|
|
1035 |
+ |
max_depth: 10,
|
|
1036 |
+ |
max_length: 5,
|
|
1037 |
+ |
semantic_depth: 0,
|
|
1038 |
+ |
};
|
|
1039 |
+ |
// 3 chars, 9 bytes. Over the limit in bytes, under it in characters,
|
|
1040 |
+ |
// so the charset check gets to report the real problem.
|
|
1041 |
+ |
assert_eq!(
|
|
1042 |
+ |
validate_with("日本語", &cfg),
|
|
1043 |
+ |
Err(TagError::InvalidChar('日'))
|
|
1044 |
+ |
);
|
|
1045 |
+ |
// Still TooLong when the character count genuinely overflows.
|
|
1046 |
+ |
assert_eq!(
|
|
1047 |
+ |
validate_with("日本語日本語", &cfg),
|
|
1048 |
+ |
Err(TagError::TooLong { max: 5 })
|
|
1049 |
+ |
);
|
|
1050 |
+ |
}
|
|
1051 |
+ |
|
|
1052 |
+ |
#[test]
|
| 1030 |
1053 |
|
fn config_enforces_semantic_depth() {
|
| 1031 |
1054 |
|
// semantic_depth=1 requires at least 2 segments
|
| 1032 |
1055 |
|
assert!(validate_with("genre.rock", &STRICT).is_ok());
|