A file-based task manager
1//! Encoding for property value blobs.
2//!
3//! Each value is prefixed with `size: N\n` and followed by exactly `N`
4//! bytes plus a trailing `\n` separator. Length-prefix means values may
5//! contain any bytes (including newlines) without escaping.
6//!
7//! Reading is forgiving: blobs that don't start with `size: ` are decoded
8//! using the legacy line-split format (one value per non-empty line). New
9//! writes always use the size-prefix format, so legacy blobs migrate
10//! automatically on the next save.
11
12/// Serialize a list of values as length-prefixed blocks.
13pub fn encode(values: &[String]) -> Vec<u8> {
14 let mut out = Vec::new();
15 for v in values {
16 out.extend_from_slice(format!("size: {}\n", v.len()).as_bytes());
17 out.extend_from_slice(v.as_bytes());
18 out.push(b'\n');
19 }
20 out
21}
22
23/// Parse a property value blob.
24pub fn decode(bytes: &[u8]) -> Vec<String> {
25 if bytes.starts_with(b"size: ") {
26 return decode_size_prefixed(bytes);
27 }
28 String::from_utf8_lossy(bytes)
29 .lines()
30 .filter(|l| !l.is_empty())
31 .map(str::to_string)
32 .collect()
33}
34
35fn decode_size_prefixed(bytes: &[u8]) -> Vec<String> {
36 let mut out = Vec::new();
37 let mut rest = bytes;
38 while !rest.is_empty() {
39 let Some(nl) = rest.iter().position(|b| *b == b'\n') else {
40 break;
41 };
42 let header = std::str::from_utf8(&rest[..nl]).unwrap_or("");
43 let Some(size_str) = header.strip_prefix("size: ") else {
44 break;
45 };
46 let Ok(size) = size_str.parse::<usize>() else {
47 break;
48 };
49 rest = &rest[nl + 1..];
50 if rest.len() < size + 1 {
51 break;
52 }
53 let value = String::from_utf8_lossy(&rest[..size]).into_owned();
54 out.push(value);
55 rest = &rest[size + 1..];
56 }
57 out
58}
59
60#[cfg(test)]
61mod test {
62 use super::*;
63
64 #[test]
65 fn round_trip_simple_values() {
66 let values = vec!["high".to_string(), "alpha".to_string()];
67 let bytes = encode(&values);
68 assert_eq!(decode(&bytes), values);
69 }
70
71 #[test]
72 fn round_trip_with_embedded_newline() {
73 let values = vec!["line1\nline2\n\nline4".to_string(), "next".to_string()];
74 let bytes = encode(&values);
75 assert_eq!(decode(&bytes), values);
76 }
77
78 #[test]
79 fn round_trip_empty_string_value() {
80 let values = vec!["".to_string(), "non-empty".to_string()];
81 let bytes = encode(&values);
82 assert_eq!(decode(&bytes), values);
83 }
84
85 #[test]
86 fn empty_list_encodes_to_empty_blob() {
87 assert_eq!(encode(&[]), Vec::<u8>::new());
88 assert!(decode(&[]).is_empty());
89 }
90
91 #[test]
92 fn legacy_line_split_format_still_decodes() {
93 let legacy = b"high\nalpha\nbeta\n";
94 assert_eq!(
95 decode(legacy),
96 vec!["high".to_string(), "alpha".to_string(), "beta".to_string()]
97 );
98 }
99
100 #[test]
101 fn legacy_format_skips_blank_lines() {
102 let legacy = b"high\n\nalpha\n";
103 assert_eq!(
104 decode(legacy),
105 vec!["high".to_string(), "alpha".to_string()]
106 );
107 }
108}