Skip to main content

max / makenotwork

Pin SEPARATOR ASCII invariant at compile time The byte-level scans walk str::bytes and slice at the offsets they find, which is only sound while the separator is ASCII. Replace the six SEPARATOR as u8 casts with a SEPARATOR_BYTE const whose initializer asserts is_ascii(), so a non-ASCII separator fails the build instead of introducing mid-codepoint slice panics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-17 02:36 UTC
Commit: 2b7defca0c0f9aaf8086f67054df563f7dabccba
Parent: 91f9fea
1 file changed, +15 insertions, -6 deletions
@@ -34,6 +34,15 @@ use std::fmt;
34 34 /// Separator between tag levels.
35 35 pub const SEPARATOR: char = '.';
36 36
37 + /// `SEPARATOR` as a single byte, for the scans that walk `str::bytes` and slice at
38 + /// the byte offsets they find. Those are only sound while the separator is ASCII:
39 + /// a multi-byte separator would match a continuation byte and slice mid-codepoint.
40 + /// The assert pins that invariant at compile time.
41 + const SEPARATOR_BYTE: u8 = {
42 + assert!(SEPARATOR.is_ascii(), "SEPARATOR must be ASCII; byte-level scans slice at byte offsets");
43 + SEPARATOR as u8
44 + };
45 +
37 46 // ---------------------------------------------------------------------------
38 47 // Configuration
39 48 // ---------------------------------------------------------------------------
@@ -249,7 +258,7 @@ pub fn prefix_at_depth(tag: &str, n: usize) -> Option<&str> {
249 258 }
250 259 let mut dots_seen = 0;
251 260 for (i, ch) in tag.bytes().enumerate() {
252 - if ch == SEPARATOR as u8 {
261 + if ch == SEPARATOR_BYTE {
253 262 dots_seen += 1;
254 263 if dots_seen == n {
255 264 return Some(&tag[..i]);
@@ -297,7 +306,7 @@ pub fn ancestors(tag: &str) -> Vec<&str> {
297 306 pub fn is_ancestor_of(ancestor: &str, descendant: &str) -> bool {
298 307 descendant.len() > ancestor.len()
299 308 && descendant.starts_with(ancestor)
300 - && descendant.as_bytes()[ancestor.len()] == SEPARATOR as u8
309 + && descendant.as_bytes()[ancestor.len()] == SEPARATOR_BYTE
301 310 }
302 311
303 312 /// Find the longest common ancestor of two tags, or `None` if they share no prefix.
@@ -315,7 +324,7 @@ pub fn common_ancestor<'a>(a: &'a str, b: &str) -> Option<&'a str> {
315 324 if ca != cb {
316 325 break;
317 326 }
318 - if ca == SEPARATOR as u8 {
327 + if ca == SEPARATOR_BYTE {
319 328 last_sep = Some(i);
320 329 }
321 330 }
@@ -327,7 +336,7 @@ pub fn common_ancestor<'a>(a: &'a str, b: &str) -> Option<&'a str> {
327 336 && (min_len == a.len() || min_len == b.len())
328 337 {
329 338 let longer = if a.len() > b.len() { a } else { b };
330 - if longer.as_bytes()[min_len] == SEPARATOR as u8 {
339 + if longer.as_bytes()[min_len] == SEPARATOR_BYTE {
331 340 return Some(&a[..min_len]);
332 341 }
333 342 }
@@ -422,7 +431,7 @@ pub fn children_at_prefix(prefix: &str, tags: &[impl AsRef<str>]) -> Vec<String>
422 431 } else {
423 432 tag.starts_with(prefix)
424 433 && tag.len() > prefix.len()
425 - && tag.as_bytes()[prefix.len()] == SEPARATOR as u8
434 + && tag.as_bytes()[prefix.len()] == SEPARATOR_BYTE
426 435 };
427 436 if !matches || tag.len() <= strip_len {
428 437 continue;
@@ -455,7 +464,7 @@ pub fn subtree<'a>(prefix: &str, tags: &'a [impl AsRef<str>]) -> Vec<&'a str> {
455 464 *tag == prefix
456 465 || (tag.starts_with(prefix)
457 466 && tag.len() > prefix.len()
458 - && tag.as_bytes()[prefix.len()] == SEPARATOR as u8)
467 + && tag.as_bytes()[prefix.len()] == SEPARATOR_BYTE)
459 468 })
460 469 .collect()
461 470 }