imessage_database/util/
streamtyped.rs1use crate::error::streamtyped::StreamTypedError;
9
10const START_PATTERN: [u8; 2] = [0x0001, 0x002b];
14
15const END_PATTERN: [u8; 2] = [0x0086, 0x0084];
19
20pub fn parse(mut stream: Vec<u8>) -> Result<String, StreamTypedError> {
58 for idx in 0..stream.len() {
60 if idx + 2 > stream.len() {
61 return Err(StreamTypedError::NoStartPattern);
62 }
63 let part = &stream[idx..idx + 2];
64
65 if part == START_PATTERN {
66 stream.drain(..idx + 2);
68 break;
69 }
70 }
71
72 for idx in 1..stream.len() {
74 if idx >= stream.len() - 2 {
75 return Err(StreamTypedError::NoEndPattern);
76 }
77 let part = &stream[idx..idx + 2];
78
79 if part == END_PATTERN {
80 stream.truncate(idx);
82 break;
83 }
84 }
85
86 match String::from_utf8(stream)
89 .map_err(|non_utf8| String::from_utf8_lossy(non_utf8.as_bytes()).into_owned())
90 {
91 Ok(string) => drop_chars(1, string),
94 Err(string) => drop_chars(3, string),
97 }
98}
99
100fn drop_chars(offset: usize, mut string: String) -> Result<String, StreamTypedError> {
102 let (position, _) = string
104 .char_indices()
105 .nth(offset)
106 .ok_or(StreamTypedError::InvalidPrefix)?;
107
108 string.drain(..position);
110 Ok(string)
111}
112
113#[cfg(test)]
114mod tests {
115 use std::env::current_dir;
116 use std::fs::File;
117 use std::io::Read;
118 use std::vec;
119
120 use crate::util::streamtyped::{drop_chars, parse};
121
122 #[test]
123 fn test_parse_text_clean() {
124 let cases = [
125 ("plain text", "AttributedBodyTextOnly", "Noter test"),
126 ("text with a space", "AttributedBodyTextOnly2", "Test 3"),
127 ("url", "URL", "https://github.com/ReagentX/Logria"),
128 ];
129 for (label, fixture, expected) in cases {
130 let plist_path = current_dir()
131 .unwrap()
132 .as_path()
133 .join("test_data/typedstream")
134 .join(fixture);
135 let mut file = File::open(plist_path).unwrap();
136 let mut bytes = vec![];
137 file.read_to_end(&mut bytes).unwrap();
138 let parsed = parse(bytes).unwrap();
139
140 assert_eq!(parsed, expected, "{label}");
141 }
142 }
143
144 #[test]
145 fn test_parse_text_weird_font() {
146 let plist_path = current_dir()
147 .unwrap()
148 .as_path()
149 .join("test_data/typedstream/WeirdText");
150 let mut file = File::open(plist_path).unwrap();
151 let mut bytes = vec![];
152 file.read_to_end(&mut bytes).unwrap();
153 let parsed = parse(bytes).unwrap();
154
155 let expected = "𝖍𝖊𝖑𝖑𝖔 𝖜𝖔𝖗𝖑𝖉".to_string();
156
157 assert_eq!(parsed, expected);
158 }
159
160 #[test]
161 fn test_parse_text_multi_part() {
162 let plist_path = current_dir()
163 .unwrap()
164 .as_path()
165 .join("test_data/typedstream/MultiPart");
166 let mut file = File::open(plist_path).unwrap();
167 let mut bytes = vec![];
168 file.read_to_end(&mut bytes).unwrap();
169 let parsed = parse(bytes).unwrap();
170
171 let expected = "\u{FFFC}test 1\u{FFFC}test 2 \u{FFFC}test 3".to_string();
172
173 assert_eq!(parsed, expected);
174 }
175
176 #[test]
177 fn test_parse_text_app() {
178 let plist_path = current_dir()
180 .unwrap()
181 .as_path()
182 .join("test_data/typedstream/ExtraData");
183 let mut file = File::open(plist_path).unwrap();
184 let mut bytes = vec![];
185 file.read_to_end(&mut bytes).unwrap();
186 let parsed = parse(bytes).unwrap();
187
188 let expected = "This is parsing";
189
190 assert_eq!(&parsed[..expected.len()], expected);
191 }
192
193 #[test]
194 fn test_parse_text_long() {
195 let plist_path = current_dir()
196 .unwrap()
197 .as_path()
198 .join("test_data/typedstream/LongMessage");
199 let mut file = File::open(plist_path).unwrap();
200 let mut bytes = vec![];
201 file.read_to_end(&mut bytes).unwrap();
202 let parsed = parse(bytes).unwrap();
203
204 let expected = "Sed nibh velit,";
205
206 assert_eq!(&parsed[..expected.len()], expected);
207 assert_eq!(parsed.len(), 2359);
208 }
209
210 #[test]
211 fn test_parse_text_blank() {
212 let plist_path = current_dir()
213 .unwrap()
214 .as_path()
215 .join("test_data/typedstream/Blank");
216 let mut file = File::open(plist_path).unwrap();
217 let mut bytes = vec![];
218 file.read_to_end(&mut bytes).unwrap();
219 let parsed = parse(bytes);
220
221 assert!(&parsed.is_err());
222 }
223
224 #[test]
225 fn test_parse_text_multi_part_deleted() {
226 let plist_path = current_dir()
227 .unwrap()
228 .as_path()
229 .join("test_data/typedstream/MultiPartWithDeleted");
230 let mut file = File::open(plist_path).unwrap();
231 let mut bytes = vec![];
232 file.read_to_end(&mut bytes).unwrap();
233 let parsed = parse(bytes).unwrap();
234 println!("{parsed:?}");
235
236 let expected = "From arbitrary byte stream:\r\u{FFFC}To native Rust data structures:\r";
237
238 assert_eq!(parsed, expected);
239 }
240
241 #[test]
242 fn test_parse_text_attachment() {
243 let plist_path = current_dir()
244 .unwrap()
245 .as_path()
246 .join("test_data/typedstream/Attachment");
247 let mut file = File::open(plist_path).unwrap();
248 let mut bytes = vec![];
249 file.read_to_end(&mut bytes).unwrap();
250 let parsed = parse(bytes).unwrap();
251 println!("{parsed:?}");
252
253 let expected =
254 "\u{FFFC}This is how the notes look to me fyi, in case it helps make sense of anything";
255
256 assert_eq!(parsed, expected);
257 }
258
259 #[test]
260 fn test_parse_text_array() {
261 let plist_path = current_dir()
262 .unwrap()
263 .as_path()
264 .join("test_data/typedstream/Array");
265 let mut file = File::open(plist_path).unwrap();
266 let mut bytes = vec![];
267 file.read_to_end(&mut bytes).unwrap();
268 let parsed = parse(bytes).unwrap();
269 println!("{parsed:?}");
270
271 let expected = "A single ChatGPT instance takes 5MW of power to run";
272
273 assert_eq!(parsed, expected);
274 }
275
276 #[test]
277 fn test_cant_drop_all() {
278 assert!(drop_chars(1000, String::from("Hello world")).is_err());
279 }
280}