A file-based task manager
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 92d69a30ff8e29caba866ea33aab70d2cee199cf 108 lines 3.2 kB view raw
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}