Skip to main content

max / makenotwork

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>
Author: Max Johnson <me@maxj.phd> · 2026-07-20 17:57 UTC
Commit: 36254a8e1e018e4ab14d61cad9f672a0ed10270b
Parent: 2fc5764
2 files changed, +33 insertions, -1 deletion
@@ -2,6 +2,15 @@
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,7 +141,10 @@ pub fn validate_with(tag: &str, config: &TagConfig) -> Result<(), TagError> {
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,6 +1030,26 @@ mod tests {
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());