1use std::collections::BTreeMap;
50use std::fmt;
51use std::fs;
52use std::io;
53use std::path::{Path, PathBuf};
54
55use serde::{Deserialize, Serialize};
56
57use super::patch::FileId;
58
59#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct FileIdMap {
74 path_to_id: BTreeMap<PathBuf, FileId>,
76 id_to_path: BTreeMap<FileId, PathBuf>,
78}
79
80impl Default for FileIdMap {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl FileIdMap {
87 #[must_use]
89 pub const fn new() -> Self {
90 Self {
91 path_to_id: BTreeMap::new(),
92 id_to_path: BTreeMap::new(),
93 }
94 }
95
96 pub fn track_new(&mut self, path: PathBuf) -> Result<FileId, FileIdMapError> {
109 if self.path_to_id.contains_key(&path) {
110 return Err(FileIdMapError::PathAlreadyTracked(path));
111 }
112 let id = FileId::random();
113 self.path_to_id.insert(path.clone(), id);
114 self.id_to_path.insert(id, path);
115 Ok(id)
116 }
117
118 pub fn track_rename(
127 &mut self,
128 old_path: &Path,
129 new_path: PathBuf,
130 ) -> Result<FileId, FileIdMapError> {
131 let id = self
132 .path_to_id
133 .remove(old_path)
134 .ok_or_else(|| FileIdMapError::PathNotTracked(old_path.to_path_buf()))?;
135
136 if let Some(&existing_id) = self.path_to_id.get(&new_path)
138 && existing_id != id
139 {
140 self.path_to_id.insert(old_path.to_path_buf(), id);
142 return Err(FileIdMapError::PathAlreadyTracked(new_path));
143 }
144
145 self.id_to_path.insert(id, new_path.clone());
146 self.path_to_id.insert(new_path, id);
147 Ok(id)
148 }
149
150 pub fn track_copy(
161 &mut self,
162 src_path: &Path,
163 dst_path: PathBuf,
164 ) -> Result<FileId, FileIdMapError> {
165 if !self.path_to_id.contains_key(src_path) {
166 return Err(FileIdMapError::PathNotTracked(src_path.to_path_buf()));
167 }
168 if self.path_to_id.contains_key(&dst_path) {
169 return Err(FileIdMapError::PathAlreadyTracked(dst_path));
170 }
171 let new_id = FileId::random();
173 self.path_to_id.insert(dst_path.clone(), new_id);
174 self.id_to_path.insert(new_id, dst_path);
175 Ok(new_id)
176 }
177
178 pub fn track_delete(&mut self, path: &Path) -> Result<FileId, FileIdMapError> {
186 let id = self
187 .path_to_id
188 .remove(path)
189 .ok_or_else(|| FileIdMapError::PathNotTracked(path.to_path_buf()))?;
190 self.id_to_path.remove(&id);
191 Ok(id)
192 }
193
194 #[must_use]
200 pub fn id_for_path(&self, path: &Path) -> Option<FileId> {
201 self.path_to_id.get(path).copied()
202 }
203
204 #[must_use]
207 pub fn path_for_id(&self, id: FileId) -> Option<&Path> {
208 self.id_to_path.get(&id).map(PathBuf::as_path)
209 }
210
211 #[must_use]
213 pub fn contains_path(&self, path: &Path) -> bool {
214 self.path_to_id.contains_key(path)
215 }
216
217 #[must_use]
219 pub fn contains_id(&self, id: FileId) -> bool {
220 self.id_to_path.contains_key(&id)
221 }
222
223 #[must_use]
225 pub fn len(&self) -> usize {
226 self.path_to_id.len()
227 }
228
229 #[must_use]
231 pub fn is_empty(&self) -> bool {
232 self.path_to_id.is_empty()
233 }
234
235 pub fn iter(&self) -> impl Iterator<Item = (&Path, FileId)> {
237 self.path_to_id.iter().map(|(p, &id)| (p.as_path(), id))
238 }
239
240 pub fn load(path: &Path) -> Result<Self, FileIdMapError> {
252 match fs::read_to_string(path) {
253 Ok(content) => {
254 let records: Vec<FileIdRecord> =
255 serde_json::from_str(&content).map_err(FileIdMapError::Json)?;
256 Self::from_records(records)
257 }
258 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(Self::new()),
259 Err(e) => Err(FileIdMapError::Io(e)),
260 }
261 }
262
263 pub fn save(&self, path: &Path) -> Result<(), FileIdMapError> {
275 if let Some(parent) = path.parent() {
276 fs::create_dir_all(parent).map_err(FileIdMapError::Io)?;
277 }
278
279 let records: Vec<FileIdRecord> = self
280 .path_to_id
281 .iter()
282 .map(|(p, &id)| FileIdRecord {
283 path: p.clone(),
284 file_id: id,
285 })
286 .collect();
287
288 let json = serde_json::to_string_pretty(&records).map_err(FileIdMapError::Json)?;
289
290 let tmp_path = path.with_extension("tmp");
292 fs::write(&tmp_path, json).map_err(FileIdMapError::Io)?;
293 fs::rename(&tmp_path, path).map_err(FileIdMapError::Io)?;
294
295 Ok(())
296 }
297
298 fn from_records(records: Vec<FileIdRecord>) -> Result<Self, FileIdMapError> {
304 let mut map = Self::new();
305 for record in records {
306 if map.path_to_id.contains_key(&record.path) {
308 return Err(FileIdMapError::DuplicatePath(record.path));
309 }
310 if map.id_to_path.contains_key(&record.file_id) {
312 return Err(FileIdMapError::DuplicateFileId(record.file_id));
313 }
314 map.id_to_path.insert(record.file_id, record.path.clone());
315 map.path_to_id.insert(record.path, record.file_id);
316 }
317 Ok(map)
318 }
319}
320
321#[derive(Serialize, Deserialize, Debug, Clone)]
327struct FileIdRecord {
328 path: PathBuf,
329 file_id: FileId,
330}
331
332#[derive(Debug)]
338pub enum FileIdMapError {
339 PathAlreadyTracked(PathBuf),
341 PathNotTracked(PathBuf),
343 DuplicatePath(PathBuf),
345 DuplicateFileId(FileId),
347 Io(io::Error),
349 Json(serde_json::Error),
351}
352
353impl fmt::Display for FileIdMapError {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 match self {
356 Self::PathAlreadyTracked(p) => {
357 write!(f, "path already tracked: {}", p.display())
358 }
359 Self::PathNotTracked(p) => {
360 write!(f, "path not tracked: {}", p.display())
361 }
362 Self::DuplicatePath(p) => {
363 write!(f, "corrupt fileids: duplicate path entry: {}", p.display())
364 }
365 Self::DuplicateFileId(id) => {
366 write!(f, "corrupt fileids: duplicate FileId: {id}")
367 }
368 Self::Io(e) => write!(f, "I/O error: {e}"),
369 Self::Json(e) => write!(f, "JSON error: {e}"),
370 }
371 }
372}
373
374impl std::error::Error for FileIdMapError {
375 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
376 match self {
377 Self::Io(e) => Some(e),
378 Self::Json(e) => Some(e),
379 _ => None,
380 }
381 }
382}
383
384#[cfg(test)]
389#[allow(clippy::all, clippy::pedantic, clippy::nursery)]
390mod tests {
391 use super::*;
392
393 #[test]
398 fn track_new_assigns_fresh_id() {
399 let mut map = FileIdMap::new();
400 let id = map.track_new("src/main.rs".into()).unwrap();
401 assert!(map.contains_path(Path::new("src/main.rs")));
402 assert!(map.contains_id(id));
403 assert_eq!(map.id_for_path(Path::new("src/main.rs")), Some(id));
404 assert_eq!(map.path_for_id(id), Some(Path::new("src/main.rs")));
405 assert_eq!(map.len(), 1);
406 }
407
408 #[test]
409 fn track_new_generates_unique_ids() {
410 let mut map = FileIdMap::new();
411 let id_a = map.track_new("a.rs".into()).unwrap();
412 let id_b = map.track_new("b.rs".into()).unwrap();
413 assert_ne!(id_a, id_b);
415 }
416
417 #[test]
418 fn track_new_rejects_duplicate_path() {
419 let mut map = FileIdMap::new();
420 map.track_new("src/lib.rs".into()).unwrap();
421 let err = map.track_new("src/lib.rs".into()).unwrap_err();
422 assert!(matches!(err, FileIdMapError::PathAlreadyTracked(_)));
423 assert_eq!(map.len(), 1); }
425
426 #[test]
427 fn track_rename_preserves_file_id() {
428 let mut map = FileIdMap::new();
429 let id = map.track_new("foo.rs".into()).unwrap();
430
431 let returned_id = map
432 .track_rename(Path::new("foo.rs"), "bar.rs".into())
433 .unwrap();
434
435 assert_eq!(returned_id, id, "FileId must be unchanged by rename");
436 assert!(!map.contains_path(Path::new("foo.rs")));
437 assert!(map.contains_path(Path::new("bar.rs")));
438 assert_eq!(map.id_for_path(Path::new("bar.rs")), Some(id));
439 assert_eq!(map.path_for_id(id), Some(Path::new("bar.rs")));
440 assert_eq!(map.len(), 1);
441 }
442
443 #[test]
444 fn track_rename_rejects_unknown_source() {
445 let mut map = FileIdMap::new();
446 let err = map
447 .track_rename(Path::new("missing.rs"), "dest.rs".into())
448 .unwrap_err();
449 assert!(matches!(err, FileIdMapError::PathNotTracked(_)));
450 }
451
452 #[test]
453 fn track_rename_rejects_occupied_destination() {
454 let mut map = FileIdMap::new();
455 map.track_new("a.rs".into()).unwrap();
456 map.track_new("b.rs".into()).unwrap();
457
458 let err = map
459 .track_rename(Path::new("a.rs"), "b.rs".into())
460 .unwrap_err();
461 assert!(matches!(err, FileIdMapError::PathAlreadyTracked(_)));
462 assert_eq!(map.len(), 2);
464 assert!(map.contains_path(Path::new("a.rs")));
465 assert!(map.contains_path(Path::new("b.rs")));
466 }
467
468 #[test]
469 fn track_copy_assigns_new_id() {
470 let mut map = FileIdMap::new();
471 let src_id = map.track_new("src/lib.rs".into()).unwrap();
472
473 let dst_id = map
474 .track_copy(Path::new("src/lib.rs"), "src/lib_copy.rs".into())
475 .unwrap();
476
477 assert_ne!(dst_id, src_id, "copy gets a new FileId");
478 assert_eq!(map.len(), 2);
479 assert_eq!(map.id_for_path(Path::new("src/lib.rs")), Some(src_id));
480 assert_eq!(map.id_for_path(Path::new("src/lib_copy.rs")), Some(dst_id));
481 }
482
483 #[test]
484 fn track_copy_rejects_unknown_source() {
485 let mut map = FileIdMap::new();
486 let err = map
487 .track_copy(Path::new("missing.rs"), "dest.rs".into())
488 .unwrap_err();
489 assert!(matches!(err, FileIdMapError::PathNotTracked(_)));
490 }
491
492 #[test]
493 fn track_copy_rejects_occupied_destination() {
494 let mut map = FileIdMap::new();
495 map.track_new("a.rs".into()).unwrap();
496 map.track_new("b.rs".into()).unwrap();
497
498 let err = map
499 .track_copy(Path::new("a.rs"), "b.rs".into())
500 .unwrap_err();
501 assert!(matches!(err, FileIdMapError::PathAlreadyTracked(_)));
502 assert_eq!(map.len(), 2);
503 }
504
505 #[test]
506 fn track_delete_removes_both_mappings() {
507 let mut map = FileIdMap::new();
508 let id = map.track_new("gone.rs".into()).unwrap();
509
510 let returned = map.track_delete(Path::new("gone.rs")).unwrap();
511
512 assert_eq!(returned, id);
513 assert!(!map.contains_path(Path::new("gone.rs")));
514 assert!(!map.contains_id(id));
515 assert_eq!(map.len(), 0);
516 }
517
518 #[test]
519 fn track_delete_rejects_unknown_path() {
520 let mut map = FileIdMap::new();
521 let err = map.track_delete(Path::new("nope.rs")).unwrap_err();
522 assert!(matches!(err, FileIdMapError::PathNotTracked(_)));
523 }
524
525 #[test]
526 fn track_new_after_delete_same_path() {
527 let mut map = FileIdMap::new();
528 let id1 = map.track_new("file.rs".into()).unwrap();
529 map.track_delete(Path::new("file.rs")).unwrap();
530 let id2 = map.track_new("file.rs".into()).unwrap();
531
532 assert_ne!(id1, id2);
534 assert_eq!(map.len(), 1);
535 }
536
537 #[test]
542 fn map_len_matches_both_directions() {
543 let mut map = FileIdMap::new();
544 assert_eq!(map.path_to_id.len(), map.id_to_path.len());
545
546 map.track_new("a.rs".into()).unwrap();
547 assert_eq!(map.path_to_id.len(), map.id_to_path.len());
548
549 map.track_new("b.rs".into()).unwrap();
550 assert_eq!(map.path_to_id.len(), map.id_to_path.len());
551
552 map.track_rename(Path::new("a.rs"), "c.rs".into()).unwrap();
553 assert_eq!(map.path_to_id.len(), map.id_to_path.len());
554
555 map.track_delete(Path::new("b.rs")).unwrap();
556 assert_eq!(map.path_to_id.len(), map.id_to_path.len());
557 }
558
559 #[test]
560 fn iter_returns_sorted_paths() {
561 let mut map = FileIdMap::new();
562 map.track_new("z.rs".into()).unwrap();
563 map.track_new("a.rs".into()).unwrap();
564 map.track_new("m.rs".into()).unwrap();
565
566 let paths: Vec<_> = map.iter().map(|(p, _)| p.to_path_buf()).collect();
567 let mut sorted = paths.clone();
568 sorted.sort();
569 assert_eq!(paths, sorted, "iter must return paths in sorted order");
570 }
571
572 #[test]
577 fn save_and_load_round_trip() {
578 let dir = tempfile::tempdir().unwrap();
579 let fileids_path = dir.path().join(".manifold").join("fileids");
580
581 let mut map = FileIdMap::new();
582 let id_a = map.track_new("src/main.rs".into()).unwrap();
583 let id_b = map.track_new("src/lib.rs".into()).unwrap();
584 map.save(&fileids_path).unwrap();
585
586 let loaded = FileIdMap::load(&fileids_path).unwrap();
587 assert_eq!(loaded.len(), 2);
588 assert_eq!(loaded.id_for_path(Path::new("src/main.rs")), Some(id_a));
589 assert_eq!(loaded.id_for_path(Path::new("src/lib.rs")), Some(id_b));
590 assert_eq!(loaded.path_for_id(id_a), Some(Path::new("src/main.rs")));
591 assert_eq!(loaded.path_for_id(id_b), Some(Path::new("src/lib.rs")));
592 }
593
594 #[test]
595 fn load_missing_file_returns_empty_map() {
596 let dir = tempfile::tempdir().unwrap();
597 let fileids_path = dir.path().join(".manifold").join("fileids");
598
599 let map = FileIdMap::load(&fileids_path).unwrap();
600 assert!(map.is_empty());
601 }
602
603 #[test]
604 fn save_creates_parent_directories() {
605 let dir = tempfile::tempdir().unwrap();
606 let nested = dir
607 .path()
608 .join("deep")
609 .join("nested")
610 .join(".manifold")
611 .join("fileids");
612
613 let map = FileIdMap::new();
614 map.save(&nested).unwrap();
615 assert!(nested.exists());
616 }
617
618 #[test]
619 fn save_produces_valid_json() {
620 let dir = tempfile::tempdir().unwrap();
621 let fileids_path = dir.path().join("fileids");
622
623 let mut map = FileIdMap::new();
624 map.track_new("src/main.rs".into()).unwrap();
625 map.save(&fileids_path).unwrap();
626
627 let content = fs::read_to_string(&fileids_path).unwrap();
628 let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
629 assert!(parsed.is_array());
630 let arr = parsed.as_array().unwrap();
631 assert_eq!(arr.len(), 1);
632 assert!(arr[0].get("path").is_some());
633 assert!(arr[0].get("file_id").is_some());
634 }
635
636 #[test]
637 fn save_is_deterministic() {
638 let make = || {
640 let dir = tempfile::tempdir().unwrap();
641 let p = dir.path().join("fileids");
642 let mut map = FileIdMap::new();
643 let id1 = FileId::new(0x1111_1111_1111_1111_1111_1111_1111_1111);
645 let id2 = FileId::new(0x2222_2222_2222_2222_2222_2222_2222_2222);
646 map.path_to_id.insert("a.rs".into(), id1);
647 map.id_to_path.insert(id1, "a.rs".into());
648 map.path_to_id.insert("b.rs".into(), id2);
649 map.id_to_path.insert(id2, "b.rs".into());
650 map.save(&p).unwrap();
651 (dir, p)
652 };
653 let (_dir1, p1) = make();
654 let (_dir2, p2) = make();
655 let c1 = fs::read_to_string(&p1).unwrap();
656 let c2 = fs::read_to_string(&p2).unwrap();
657 assert_eq!(c1, c2, "save must be deterministic");
658 }
659
660 #[test]
661 fn load_detects_duplicate_paths() {
662 let dir = tempfile::tempdir().unwrap();
663 let p = dir.path().join("fileids");
664 let json = r#"[
666 {"path": "foo.rs", "file_id": "00000000000000000000000000000001"},
667 {"path": "foo.rs", "file_id": "00000000000000000000000000000002"}
668 ]"#;
669 fs::write(&p, json).unwrap();
670 let err = FileIdMap::load(&p).unwrap_err();
671 assert!(matches!(err, FileIdMapError::DuplicatePath(_)));
672 }
673
674 #[test]
675 fn load_detects_duplicate_file_ids() {
676 let dir = tempfile::tempdir().unwrap();
677 let p = dir.path().join("fileids");
678 let json = r#"[
680 {"path": "a.rs", "file_id": "00000000000000000000000000000001"},
681 {"path": "b.rs", "file_id": "00000000000000000000000000000001"}
682 ]"#;
683 fs::write(&p, json).unwrap();
684 let err = FileIdMap::load(&p).unwrap_err();
685 assert!(matches!(err, FileIdMapError::DuplicateFileId(_)));
686 }
687
688 #[test]
704 fn concurrent_rename_and_edit_same_file_id() {
705 let mut base_map = FileIdMap::new();
707 let foo_id = base_map.track_new("foo.rs".into()).unwrap();
708
709 let mut map_a = base_map.clone();
711 let rename_id = map_a
712 .track_rename(Path::new("foo.rs"), "bar.rs".into())
713 .unwrap();
714 assert_eq!(rename_id, foo_id, "FileId unchanged across rename");
715 assert!(!map_a.contains_path(Path::new("foo.rs")));
716 assert!(map_a.contains_path(Path::new("bar.rs")));
717
718 let map_b = base_map.clone();
720 let modify_id = map_b
721 .id_for_path(Path::new("foo.rs"))
722 .expect("foo.rs must be tracked");
723 assert_eq!(modify_id, foo_id, "FileId unchanged across modify");
724
725 assert_eq!(rename_id, modify_id, "Same FileId seen in both workspaces");
736 }
737
738 #[test]
741 fn copy_gets_new_file_id() {
742 let mut map = FileIdMap::new();
743 let orig_id = map.track_new("original.rs".into()).unwrap();
744 let copy_id = map
745 .track_copy(Path::new("original.rs"), "copy.rs".into())
746 .unwrap();
747
748 assert_ne!(orig_id, copy_id, "copy must have a new FileId");
749
750 assert_eq!(map.id_for_path(Path::new("original.rs")), Some(orig_id));
752 }
753
754 #[test]
759 fn file_id_map_error_display_all_variants() {
760 let errors: &[FileIdMapError] = &[
761 FileIdMapError::PathAlreadyTracked("a.rs".into()),
762 FileIdMapError::PathNotTracked("b.rs".into()),
763 FileIdMapError::DuplicatePath("c.rs".into()),
764 FileIdMapError::DuplicateFileId(FileId::new(0)),
765 FileIdMapError::Io(io::Error::new(io::ErrorKind::PermissionDenied, "oops")),
766 FileIdMapError::Json(serde_json::from_str::<FileId>("!").unwrap_err()),
767 ];
768 for err in errors {
769 let msg = format!("{err}");
770 assert!(!msg.is_empty(), "error variant must have display text");
771 }
772 }
773
774 #[test]
775 fn file_id_map_error_source() {
776 let io_err = FileIdMapError::Io(io::Error::new(io::ErrorKind::NotFound, "gone"));
777 assert!(std::error::Error::source(&io_err).is_some());
778
779 let path_err = FileIdMapError::PathNotTracked("x.rs".into());
780 assert!(std::error::Error::source(&path_err).is_none());
781 }
782
783 #[test]
788 fn empty_map_state() {
789 let map = FileIdMap::new();
790 assert!(map.is_empty());
791 assert_eq!(map.len(), 0);
792 assert_eq!(map.iter().count(), 0);
793 assert!(map.id_for_path(Path::new("any.rs")).is_none());
794 assert!(map.path_for_id(FileId::new(0)).is_none());
795 }
796
797 #[test]
798 fn default_is_empty() {
799 let map = FileIdMap::default();
800 assert!(map.is_empty());
801 }
802}