1use std::collections::HashSet;
14use std::io::{self, BufRead, Write};
15use std::path::{Path, PathBuf};
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17
18use gravityfile_core::{
19 FileNode, FileTree, InodeInfo, NodeKind, ScanConfig, ScanWarning, Timestamps, TreeStats,
20 WarningKind,
21};
22
23use crate::snapshot::{ImportOptions, SnapshotError};
24
25const MAX_NAME: usize = 32 * 1024;
27
28const SKIPPED_STRING: usize = 1 << 20;
30
31pub fn write(tree: &FileTree, out: &mut dyn Write) -> io::Result<()> {
41 let timestamp = tree
42 .scanned_at
43 .duration_since(UNIX_EPOCH)
44 .map_or(0, |elapsed| elapsed.as_secs());
45 writeln!(
46 out,
47 "[1,2,{{\"progname\":\"gravityfile\",\"progver\":\"{}\",\"timestamp\":{timestamp}}},",
48 env!("CARGO_PKG_VERSION")
49 )?;
50 let failed: HashSet<&Path> = tree
51 .warnings
52 .iter()
53 .filter(|warning| warning.kind != WarningKind::BrokenSymlink)
54 .map(|warning| warning.path.as_path())
55 .collect();
56 let mut path = tree.root_path.clone();
57 out.write_all(b"[")?;
58 write_info(
59 out,
60 &tree.root,
61 tree.root_path.as_os_str(),
62 failed.contains(path.as_path()),
63 )?;
64 let mut stack = vec![tree.root.children.iter()];
65 while let Some(children) = stack.last_mut() {
66 let Some(node) = children.next() else {
67 out.write_all(b"]")?;
68 stack.pop();
69 path.pop();
70 continue;
71 };
72 out.write_all(b",\n")?;
73 path.push(node.file_name());
74 let failed = failed.contains(path.as_path());
75 if node.is_dir() {
76 out.write_all(b"[")?;
77 write_info(out, node, node.file_name(), failed)?;
78 stack.push(node.children.iter());
79 } else {
80 write_info(out, node, node.file_name(), failed)?;
81 path.pop();
82 }
83 }
84 out.write_all(b"]\n")?;
85 out.flush()
86}
87
88fn write_info(
89 out: &mut dyn Write,
90 node: &FileNode,
91 name: &std::ffi::OsStr,
92 read_error: bool,
93) -> io::Result<()> {
94 out.write_all(b"{\"name\":")?;
95 write_bytes(out, name.as_encoded_bytes())?;
96 write!(
97 out,
98 ",\"asize\":{},\"dsize\":{}",
99 node.len,
100 node.blocks.saturating_mul(512)
101 )?;
102 if let Some(identity) = node.inode {
103 write!(
104 out,
105 ",\"dev\":{},\"ino\":{},\"hlnkc\":true",
106 identity.device, identity.inode
107 )?;
108 }
109 if matches!(node.kind, NodeKind::Symlink { .. } | NodeKind::Other) {
110 out.write_all(b",\"notreg\":true")?;
111 }
112 if read_error {
113 out.write_all(b",\"read_error\":true")?;
114 }
115 if let Ok(modified) = node.timestamps.modified.duration_since(UNIX_EPOCH) {
116 write!(out, ",\"mtime\":{}", modified.as_secs())?;
117 }
118 out.write_all(b"}")
119}
120
121fn write_bytes(out: &mut dyn Write, bytes: &[u8]) -> io::Result<()> {
125 out.write_all(b"\"")?;
126 let mut start = 0;
127 for (index, &byte) in bytes.iter().enumerate() {
128 if byte == b'"' || byte == b'\\' || byte < 0x20 {
129 out.write_all(&bytes[start..index])?;
130 match byte {
131 b'"' => out.write_all(b"\\\"")?,
132 b'\\' => out.write_all(b"\\\\")?,
133 b'\n' => out.write_all(b"\\n")?,
134 b'\t' => out.write_all(b"\\t")?,
135 _ => write!(out, "\\u{byte:04x}")?,
136 }
137 start = index + 1;
138 }
139 }
140 out.write_all(&bytes[start..])?;
141 out.write_all(b"\"")
142}
143
144pub fn read(input: impl BufRead, options: &ImportOptions) -> Result<FileTree, SnapshotError> {
150 let mut parser = Parser {
151 input,
152 offset: 0,
153 entries: 0,
154 max_entries: options.max_entries,
155 };
156 parser.expect(b'[')?;
157 let major = parser.integer()?;
158 if major != 1 {
159 return Err(parser.error(format!("unsupported ncdu dump version {major}")));
160 }
161 parser.expect(b',')?;
162 parser.integer()?;
163 parser.expect(b',')?;
164 let scanned_at = parser.metadata()?;
165 parser.expect(b',')?;
166 let (root, root_path, warnings) = parser.tree()?;
167 parser.expect(b']')?;
168 if parser.peek()?.is_some() {
169 return Err(parser.error("data after the end of the dump".into()));
170 }
171
172 let mut config = ScanConfig::new(&root_path);
173 config.apparent_size = options.apparent;
174 config.count_hard_links = options.count_hard_links;
175 let mut tree = FileTree::new(
176 root,
177 root_path,
178 config,
179 TreeStats::new(),
180 Duration::ZERO,
181 warnings,
182 );
183 tree.root.update_counts();
184 tree.remeasure(options.apparent, options.count_hard_links);
185 if let Some(scanned_at) = scanned_at {
186 tree.scanned_at = scanned_at;
187 }
188 Ok(tree)
189}
190
191#[derive(Default)]
193struct Info {
194 name: Option<Vec<u8>>,
195 asize: u64,
196 dsize: u64,
197 dev: Option<u64>,
198 ino: Option<u64>,
199 nlink: u64,
200 hlnkc: bool,
201 notreg: bool,
202 read_error: bool,
203 excluded: bool,
204 mtime: Option<u64>,
205 mode: Option<u64>,
206}
207
208struct Parser<R> {
209 input: R,
210 offset: u64,
211 entries: u64,
212 max_entries: Option<u64>,
213}
214
215struct Open {
217 node: FileNode,
218 dev: u64,
219}
220
221impl<R: BufRead> Parser<R> {
222 fn error(&self, message: String) -> SnapshotError {
223 SnapshotError::Syntax {
224 offset: self.offset,
225 message,
226 }
227 }
228
229 fn peek_raw(&mut self) -> Result<Option<u8>, SnapshotError> {
230 Ok(self.input.fill_buf()?.first().copied())
231 }
232
233 fn bump(&mut self) {
234 self.input.consume(1);
235 self.offset += 1;
236 }
237
238 fn peek(&mut self) -> Result<Option<u8>, SnapshotError> {
240 while let Some(byte) = self.peek_raw()? {
241 if !byte.is_ascii_whitespace() {
242 return Ok(Some(byte));
243 }
244 self.bump();
245 }
246 Ok(None)
247 }
248
249 fn next(&mut self) -> Result<u8, SnapshotError> {
250 match self.peek()? {
251 Some(byte) => {
252 self.bump();
253 Ok(byte)
254 }
255 None => Err(self.error("unexpected end of dump".into())),
256 }
257 }
258
259 fn expect(&mut self, wanted: u8) -> Result<(), SnapshotError> {
260 let found = self.next()?;
261 if found == wanted {
262 Ok(())
263 } else {
264 Err(self.error(format!(
265 "expected '{}', found '{}'",
266 wanted as char,
267 found.escape_ascii()
268 )))
269 }
270 }
271
272 fn number_text(&mut self) -> Result<String, SnapshotError> {
274 self.peek()?;
275 let mut text = String::new();
276 while let Some(byte) = self.peek_raw()? {
277 if !(byte.is_ascii_digit() || matches!(byte, b'-' | b'+' | b'.' | b'e' | b'E')) {
278 break;
279 }
280 if text.len() >= 64 {
281 return Err(self.error("number too long".into()));
282 }
283 text.push(byte as char);
284 self.bump();
285 }
286 if text.is_empty() {
287 return Err(self.error("expected a number".into()));
288 }
289 Ok(text)
290 }
291
292 fn integer(&mut self) -> Result<u64, SnapshotError> {
293 let text = self.number_text()?;
294 text.parse()
295 .map_err(|_| self.error(format!("expected a non-negative integer, found {text}")))
296 }
297
298 fn whole_number(&mut self) -> Result<u64, SnapshotError> {
300 let text = self.number_text()?;
301 if let Ok(value) = text.parse::<u64>() {
302 return Ok(value);
303 }
304 match text.parse::<f64>() {
305 Ok(value) if value.is_finite() && value >= 0.0 && value < u64::MAX as f64 => {
306 Ok(value as u64)
307 }
308 _ => Err(self.error(format!("expected a non-negative number, found {text}"))),
309 }
310 }
311
312 fn boolean(&mut self) -> Result<bool, SnapshotError> {
313 match self.next()? {
314 b't' => self.literal(b"rue").map(|()| true),
315 b'f' => self.literal(b"alse").map(|()| false),
316 b'n' => self.literal(b"ull").map(|()| false),
317 other => Err(self.error(format!(
318 "expected a boolean, found '{}'",
319 other.escape_ascii()
320 ))),
321 }
322 }
323
324 fn literal(&mut self, rest: &[u8]) -> Result<(), SnapshotError> {
325 for &wanted in rest {
326 match self.peek_raw()? {
327 Some(byte) if byte == wanted => self.bump(),
328 _ => return Err(self.error("invalid literal".into())),
329 }
330 }
331 Ok(())
332 }
333
334 fn string(&mut self, limit: usize) -> Result<Vec<u8>, SnapshotError> {
336 self.expect(b'"')?;
337 let mut bytes = Vec::new();
338 loop {
339 let Some(byte) = self.peek_raw()? else {
340 return Err(self.error("unterminated string".into()));
341 };
342 self.bump();
343 match byte {
344 b'"' => return Ok(bytes),
345 b'\\' => {
346 let Some(escape) = self.peek_raw()? else {
347 return Err(self.error("unterminated string".into()));
348 };
349 self.bump();
350 match escape {
351 b'"' | b'\\' | b'/' => bytes.push(escape),
352 b'b' => bytes.push(0x08),
353 b'f' => bytes.push(0x0c),
354 b'n' => bytes.push(b'\n'),
355 b'r' => bytes.push(b'\r'),
356 b't' => bytes.push(b'\t'),
357 b'u' => {
358 let character = self.unicode_escape()?;
359 let mut buffer = [0; 4];
360 bytes.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes());
361 }
362 other => {
363 return Err(
364 self.error(format!("invalid escape '\\{}'", other.escape_ascii()))
365 );
366 }
367 }
368 }
369 _ => bytes.push(byte),
370 }
371 if bytes.len() > limit {
372 return Err(self.error(format!("string longer than {limit} bytes")));
373 }
374 }
375 }
376
377 fn hex4(&mut self) -> Result<u32, SnapshotError> {
378 let mut value = 0;
379 for _ in 0..4 {
380 let digit = self
381 .peek_raw()?
382 .and_then(|byte| (byte as char).to_digit(16))
383 .ok_or_else(|| self.error("invalid \\u escape".into()))?;
384 self.bump();
385 value = value * 16 + digit;
386 }
387 Ok(value)
388 }
389
390 fn unicode_escape(&mut self) -> Result<char, SnapshotError> {
393 let first = self.hex4()?;
394 if (0xd800..0xdc00).contains(&first) && self.peek_raw()? == Some(b'\\') {
395 self.bump();
396 if self.peek_raw()? != Some(b'u') {
397 return Err(self.error("invalid surrogate pair".into()));
398 }
399 self.bump();
400 let second = self.hex4()?;
401 if (0xdc00..0xe000).contains(&second) {
402 let code = 0x10000 + ((first - 0xd800) << 10) + (second - 0xdc00);
403 return Ok(char::from_u32(code).unwrap_or(char::REPLACEMENT_CHARACTER));
404 }
405 return Ok(char::REPLACEMENT_CHARACTER);
406 }
407 Ok(char::from_u32(first).unwrap_or(char::REPLACEMENT_CHARACTER))
408 }
409
410 fn skip_value(&mut self) -> Result<(), SnapshotError> {
412 let mut open = Vec::new();
414 loop {
415 match self.peek()? {
416 Some(byte @ (b'[' | b'{')) => {
417 self.bump();
418 let close = if byte == b'[' { b']' } else { b'}' };
419 if self.peek()? == Some(close) {
420 self.bump();
421 } else {
422 open.push(close);
423 if close == b'}' {
424 self.key()?;
425 }
426 continue;
427 }
428 }
429 Some(b'"') => {
430 self.string(SKIPPED_STRING)?;
431 }
432 Some(b't' | b'f' | b'n') => {
433 self.boolean()?;
434 }
435 Some(_) => {
436 self.number_text()?;
437 }
438 None => return Err(self.error("unexpected end of dump".into())),
439 }
440 loop {
442 let Some(&close) = open.last() else {
443 return Ok(());
444 };
445 match self.next()? {
446 b',' => {
447 if close == b'}' {
448 self.key()?;
449 }
450 break;
451 }
452 byte if byte == close => {
453 open.pop();
454 }
455 other => {
456 return Err(self.error(format!("unexpected '{}'", other.escape_ascii())));
457 }
458 }
459 }
460 }
461 }
462
463 fn key(&mut self) -> Result<(), SnapshotError> {
465 self.string(SKIPPED_STRING)?;
466 self.expect(b':')
467 }
468
469 fn metadata(&mut self) -> Result<Option<SystemTime>, SnapshotError> {
471 self.expect(b'{')?;
472 let mut timestamp = None;
473 if self.peek()? == Some(b'}') {
474 self.bump();
475 return Ok(None);
476 }
477 loop {
478 let key = self.string(256)?;
479 self.expect(b':')?;
480 if key == b"timestamp" {
481 timestamp = since_epoch(self.whole_number()?);
482 } else {
483 self.skip_value()?;
484 }
485 match self.next()? {
486 b',' => continue,
487 b'}' => return Ok(timestamp),
488 other => return Err(self.error(format!("unexpected '{}'", other.escape_ascii()))),
489 }
490 }
491 }
492
493 fn info(&mut self) -> Result<Info, SnapshotError> {
494 self.expect(b'{')?;
495 let mut info = Info {
496 nlink: 1,
497 ..Info::default()
498 };
499 if self.peek()? == Some(b'}') {
500 self.bump();
501 return Ok(info);
502 }
503 loop {
504 let key = self.string(256)?;
505 self.expect(b':')?;
506 match key.as_slice() {
507 b"name" => info.name = Some(self.string(MAX_NAME)?),
508 b"asize" => info.asize = self.integer()?,
509 b"dsize" => info.dsize = self.integer()?,
510 b"dev" => info.dev = Some(self.integer()?),
511 b"ino" => info.ino = Some(self.integer()?),
512 b"nlink" => info.nlink = self.integer()?,
513 b"hlnkc" => info.hlnkc = self.boolean()?,
514 b"notreg" => info.notreg = self.boolean()?,
515 b"read_error" => info.read_error = self.boolean()?,
516 b"mtime" => info.mtime = Some(self.whole_number()?),
517 b"mode" => info.mode = Some(self.integer()?),
518 b"excluded" => {
519 info.excluded = if self.peek()? == Some(b'"') {
520 self.string(256)?;
521 true
522 } else {
523 self.boolean()?
524 }
525 }
526 _ => self.skip_value()?,
527 }
528 match self.next()? {
529 b',' => continue,
530 b'}' => return Ok(info),
531 other => return Err(self.error(format!("unexpected '{}'", other.escape_ascii()))),
532 }
533 }
534 }
535
536 fn node(&self, info: &Info, directory: bool, dev: u64) -> Result<FileNode, SnapshotError> {
538 let name = info
539 .name
540 .as_deref()
541 .ok_or_else(|| self.error("entry without a name".into()))?;
542 let modified = info.mtime.and_then(since_epoch).unwrap_or(UNIX_EPOCH);
543 let timestamps = Timestamps::with_modified(modified);
544 let mut node = if directory {
545 FileNode::new_directory("", timestamps)
546 } else {
547 let executable = info.mode.is_some_and(|mode| mode & 0o111 != 0);
548 let mut node = FileNode::new_file("", 0, 0, timestamps, executable);
549 if info.notreg {
550 node.kind = NodeKind::Other;
551 }
552 node
553 };
554 node.set_file_name(&bytes_to_os(name));
555 node.len = info.asize;
556 node.blocks = info.dsize.div_ceil(512);
557 if !directory
558 && (info.hlnkc || info.nlink > 1)
559 && let Some(inode) = info.ino
560 {
561 node.inode = Some(InodeInfo::new(inode, info.dev.unwrap_or(dev)));
562 }
563 Ok(node)
564 }
565
566 fn count_entry(&mut self) -> Result<(), SnapshotError> {
567 if let Some(limit) = self.max_entries
568 && self.entries >= limit
569 {
570 return Err(SnapshotError::EntryLimitExceeded { limit });
571 }
572 self.entries += 1;
573 Ok(())
574 }
575
576 fn tree(&mut self) -> Result<(FileNode, PathBuf, Vec<ScanWarning>), SnapshotError> {
578 let mut warnings = Vec::new();
579 self.expect(b'[')?;
580 let info = self.info()?;
581 let root_path = PathBuf::from(bytes_to_os(info.name.as_deref().unwrap_or_default()));
582 if root_path.as_os_str().is_empty() {
583 return Err(self.error("the root directory has no name".into()));
584 }
585 let dev = info.dev.unwrap_or(0);
586 let mut root = self.node(&info, true, dev)?;
587 root.set_file_name(root_path.file_name().unwrap_or(root_path.as_os_str()));
588 if info.read_error {
589 warnings.push(read_error(&root_path));
590 }
591 let mut path = root_path.clone();
592 let mut stack = vec![Open { node: root, dev }];
593 loop {
594 match self.next()? {
595 b',' => {}
596 b']' => {
597 let finished = stack.pop().expect("open directory");
598 let Some(parent) = stack.last_mut() else {
599 return Ok((finished.node, root_path, warnings));
600 };
601 parent.node.children.push(finished.node);
602 path.pop();
603 continue;
604 }
605 other => return Err(self.error(format!("unexpected '{}'", other.escape_ascii()))),
606 }
607 let parent_dev = stack.last().expect("open directory").dev;
608 self.count_entry()?;
609 if self.peek()? == Some(b'[') {
610 self.bump();
611 let info = self.info()?;
612 let node = self.node(&info, true, parent_dev)?;
613 path.push(node.file_name());
614 if info.read_error {
615 warnings.push(read_error(&path));
616 }
617 let dev = info.dev.unwrap_or(parent_dev);
618 if info.excluded {
619 self.skip_directory_rest()?;
621 path.pop();
622 continue;
623 }
624 stack.push(Open { node, dev });
625 } else {
626 let info = self.info()?;
627 if info.excluded {
628 continue;
629 }
630 let node = self.node(&info, false, parent_dev)?;
631 if info.read_error {
632 warnings.push(read_error(&path.join(node.file_name())));
633 }
634 stack
635 .last_mut()
636 .expect("open directory")
637 .node
638 .children
639 .push(node);
640 }
641 }
642 }
643
644 fn skip_directory_rest(&mut self) -> Result<(), SnapshotError> {
647 loop {
648 match self.next()? {
649 b',' => self.skip_value()?,
650 b']' => return Ok(()),
651 other => return Err(self.error(format!("unexpected '{}'", other.escape_ascii()))),
652 }
653 }
654 }
655}
656
657fn read_error(path: &Path) -> ScanWarning {
658 ScanWarning::new(
659 path,
660 WarningKind::ReadError,
661 "could not be read when the dump was made",
662 )
663}
664
665fn since_epoch(secs: u64) -> Option<SystemTime> {
668 UNIX_EPOCH.checked_add(Duration::from_secs(secs))
669}
670
671#[cfg(unix)]
672fn bytes_to_os(bytes: &[u8]) -> std::ffi::OsString {
673 use std::os::unix::ffi::OsStringExt;
674 std::ffi::OsString::from_vec(bytes.to_vec())
675}
676
677#[cfg(not(unix))]
678fn bytes_to_os(bytes: &[u8]) -> std::ffi::OsString {
679 String::from_utf8_lossy(bytes).into_owned().into()
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 fn import(dump: &str, apparent: bool) -> FileTree {
687 read(
688 dump.as_bytes(),
689 &ImportOptions {
690 apparent,
691 ..ImportOptions::default()
692 },
693 )
694 .unwrap()
695 }
696
697 fn child<'a>(node: &'a FileNode, name: &str) -> &'a FileNode {
698 node.children
699 .iter()
700 .find(|child| child.name == name)
701 .unwrap_or_else(|| panic!("{name} missing"))
702 }
703
704 const SAMPLE: &str = r#"[1,2,{"progname":"ncdu","progver":"2.4","timestamp":1700000000},
705 [{"name":"/data","asize":4096,"dsize":4096,"dev":12},
706 {"name":"a.bin","asize":1000,"dsize":4096,"mtime":1600000000.5},
707 [{"name":"sub","asize":4096,"dsize":4096},
708 {"name":"linked","asize":500,"dsize":4096,"ino":7,"nlink":2,"hlnkc":true},
709 {"name":"fifo","notreg":true},
710 {"name":"skipped","asize":99999,"excluded":"pattern"},
711 [{"name":"gone","excluded":"otherfs"},{"name":"inside","asize":5}]],
712 {"name":"linked-too","asize":500,"dsize":4096,"ino":7,"nlink":2,"hlnkc":true},
713 {"name":"bad","read_error":true,"future":{"nested":[1,{"x":[]}]}}]]"#;
714
715 #[test]
716 fn reads_sizes_hard_links_and_exclusions() {
717 let tree = import(SAMPLE, false);
718 assert_eq!(tree.root_path, Path::new("/data"));
719 assert_eq!(tree.root.name, "data");
720 assert_eq!(
721 tree.root.size,
722 4096 * 4,
723 "root, a.bin, sub, one copy of linked"
724 );
725 assert_eq!(child(&tree.root, "a.bin").size, 4096);
726 let sub = child(&tree.root, "sub");
727 assert!(matches!(child(sub, "fifo").kind, NodeKind::Other));
728 assert!(
729 sub.children
730 .iter()
731 .all(|node| node.name != "skipped" && node.name != "gone")
732 );
733 assert_eq!(
734 tree.stats.total_files, 4,
735 "a.bin, both links and bad; the fifo is not a regular file"
736 );
737 assert_eq!(
738 tree.warnings
739 .iter()
740 .map(|w| w.path.clone())
741 .collect::<Vec<_>>(),
742 [PathBuf::from("/data/bad")]
743 );
744 assert_eq!(
745 tree.scanned_at,
746 UNIX_EPOCH + Duration::from_secs(1_700_000_000)
747 );
748 assert_eq!(
749 child(&tree.root, "a.bin").timestamps.modified,
750 UNIX_EPOCH + Duration::from_secs(1_600_000_000)
751 );
752
753 let tree = import(SAMPLE, true);
754 assert_eq!(tree.root.size, 4096 + 1000 + 4096 + 500);
755 }
756
757 #[test]
758 fn writing_and_reading_back_preserves_the_tree() {
759 let original = import(SAMPLE, false);
760 let mut dump = Vec::new();
761 write(&original, &mut dump).unwrap();
762 let restored = read(dump.as_slice(), &ImportOptions::default()).unwrap();
763 assert_eq!(restored.root.size, original.root.size);
764 assert_eq!(restored.stats.total_files, original.stats.total_files);
765 assert_eq!(restored.root_path, original.root_path);
766 assert_eq!(restored.warnings.len(), 1);
767 let restored_sub = child(&restored.root, "sub");
768 assert_eq!(restored_sub.size, child(&original.root, "sub").size);
769 assert!(matches!(child(restored_sub, "fifo").kind, NodeKind::Other));
770 }
771
772 #[cfg(unix)]
773 #[test]
774 fn names_round_trip_as_raw_bytes() {
775 use std::os::unix::ffi::OsStrExt;
776 let mut tree = import(SAMPLE, false);
777 let name = std::ffi::OsStr::from_bytes(b"bad-\xff \"quoted\" back\\slash\nline");
778 tree.root.children[0].set_file_name(name);
779 let mut dump = Vec::new();
780 write(&tree, &mut dump).unwrap();
781 assert!(dump.windows(5).any(|window| window == b"bad-\xff"));
782 let restored = read(dump.as_slice(), &ImportOptions::default()).unwrap();
783 assert!(
784 restored
785 .root
786 .children
787 .iter()
788 .any(|node| node.file_name() == name)
789 );
790 }
791
792 #[test]
793 fn decodes_escapes_and_surrogate_pairs() {
794 let dump = r#"[1,2,{},[{"name":"/r"},{"name":"\u00e9\ud83d\ude00\t\/","asize":1}]]"#;
795 let tree = import(dump, true);
796 assert_eq!(tree.root.children[0].name, "é😀\t/");
797 }
798
799 #[test]
800 fn deep_dumps_parse_on_a_small_stack() {
801 std::thread::Builder::new()
802 .stack_size(128 * 1024)
803 .spawn(|| {
804 let depth = 50_000;
805 let mut dump = String::from(r#"[1,2,{},[{"name":"/r"}"#);
806 for _ in 0..depth {
807 dump.push_str(r#",[{"name":"d","dsize":512}"#);
808 }
809 dump.push_str(&"]".repeat(depth + 2));
810 let tree = import(&dump, false);
811 assert_eq!(tree.root.size, 512 * depth as u64);
812 assert_eq!(tree.stats.total_dirs, depth as u64 + 1);
813 })
814 .unwrap()
815 .join()
816 .unwrap();
817 }
818
819 #[test]
820 fn malformed_dumps_are_errors_not_panics() {
821 for dump in [
822 "",
823 "[",
824 "[2,0,{},[{\"name\":\"/r\"}]]",
825 "[1,2,{},[{\"name\":\"/r\"}]",
826 "[1,2,{},[{\"name\":\"/r\"},{\"asize\":1}]]",
827 "[1,2,{},[{\"name\":\"/r\"},{\"name\":\"x\",\"asize\":-1}]]",
828 "[1,2,{},[{\"name\":\"/r\"},{\"name\":\"x\",\"asize\":1e400}]]",
829 "[1,2,{},[{\"name\":\"/r\"},{\"name\":\"\\u12\"}]]",
830 "[1,2,{},[{\"name\":\"/r\"}]] trailing",
831 "[1,2,{},[{}]]",
832 ] {
833 assert!(
834 read(dump.as_bytes(), &ImportOptions::default()).is_err(),
835 "{dump:?}"
836 );
837 }
838 let limited = ImportOptions {
839 max_entries: Some(2),
840 ..ImportOptions::default()
841 };
842 assert!(matches!(
843 read(SAMPLE.as_bytes(), &limited),
844 Err(SnapshotError::EntryLimitExceeded { limit: 2 })
845 ));
846 }
847
848 #[test]
850 fn times_out_of_range_are_unknown() {
851 let max = u64::MAX;
852 let dump = format!(
853 r#"[1,2,{{"timestamp":{max}}},[{{"name":"/r"}},{{"name":"x","mtime":{max}}}]]"#
854 );
855 let tree = import(&dump, true);
856 assert_eq!(tree.root.children[0].timestamps.modified, UNIX_EPOCH);
857 }
858}