1use std::{fmt, path::Path, sync::Arc};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
7use sley_core::{ObjectFormat as GitObjectFormat, ObjectId as GitObjectId};
8
9use super::{
10 ContentHash, SpoolId, StateId,
11 tree_git_layout::{RawGitMode, git_canonical_order},
12};
13
14const TREE_FORMAT_VERSION: u8 = 3;
18const TREE_FORMAT_VERSION_V4: u8 = 4;
22const TREE_V4_LEAF_PREFIX: &str = "tree-v4-leaf";
25const TREE_V4_NODE_PREFIX: &str = "tree-v4-node";
27const TREE_EMPTY_PREFIX: &str = "tree";
31const ENTRY_KIND_BLOB: u8 = 0;
32const ENTRY_KIND_TREE: u8 = 1;
33const ENTRY_KIND_SYMLINK: u8 = 2;
34const ENTRY_KIND_GITLINK: u8 = 3;
35const ENTRY_KIND_SPOOLLINK: u8 = 4;
39const GIT_OBJECT_FORMAT_SHA1: u8 = 1;
40const GIT_OBJECT_FORMAT_SHA256: u8 = 2;
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum TreeScheme {
49 V3Flat,
51 V4Salted,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum TreeError {
60 InvalidName(String),
61 InvalidStructure(String),
62}
63
64impl std::error::Error for TreeError {}
65
66impl fmt::Display for TreeError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 TreeError::InvalidName(msg) => write!(f, "invalid tree entry name: {}", msg),
70 TreeError::InvalidStructure(msg) => write!(f, "invalid tree structure: {}", msg),
71 }
72 }
73}
74
75#[repr(u8)]
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
79pub enum FileMode {
80 Normal,
81 Executable,
82 Symlink,
83 Gitlink,
84 Spoollink,
90}
91
92impl FileMode {
93 pub fn to_byte(&self) -> u8 {
94 match self {
95 FileMode::Normal => 0,
96 FileMode::Executable => 1,
97 FileMode::Symlink => 2,
98 FileMode::Gitlink => 3,
99 FileMode::Spoollink => 4,
100 }
101 }
102
103 pub fn from_byte(b: u8) -> Option<Self> {
104 match b {
105 0 => Some(FileMode::Normal),
106 1 => Some(FileMode::Executable),
107 2 => Some(FileMode::Symlink),
108 3 => Some(FileMode::Gitlink),
109 4 => Some(FileMode::Spoollink),
110 _ => None,
111 }
112 }
113
114 pub fn to_unix_mode(&self) -> u32 {
118 match self {
119 FileMode::Normal => 0o100644,
120 FileMode::Executable => 0o100755,
121 FileMode::Symlink => 0o120000,
122 FileMode::Gitlink => 0o160000,
123 FileMode::Spoollink => 0,
124 }
125 }
126}
127
128#[repr(u8)]
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
132pub enum EntryType {
133 Blob,
134 Tree,
135 Symlink,
136 Gitlink,
137 Spoollink,
139}
140
141impl EntryType {
142 pub fn to_byte(&self) -> u8 {
143 match self {
144 EntryType::Blob => 0,
145 EntryType::Tree => 1,
146 EntryType::Symlink => 2,
147 EntryType::Gitlink => 3,
148 EntryType::Spoollink => 4,
149 }
150 }
151
152 pub fn from_byte(b: u8) -> Option<Self> {
153 match b {
154 0 => Some(EntryType::Blob),
155 1 => Some(EntryType::Tree),
156 2 => Some(EntryType::Symlink),
157 3 => Some(EntryType::Gitlink),
158 4 => Some(EntryType::Spoollink),
159 _ => None,
160 }
161 }
162}
163
164#[derive(Clone, Debug, PartialEq, Eq)]
167pub enum TreeEntryTarget {
168 Blob {
169 hash: ContentHash,
170 executable: bool,
171 },
172 Tree {
173 hash: ContentHash,
174 },
175 Symlink {
176 hash: ContentHash,
177 },
178 Gitlink {
179 target: GitObjectId,
180 },
181 Spoollink {
187 spool_id: SpoolId,
188 state_id: StateId,
189 },
190}
191
192impl TreeEntryTarget {
193 pub fn entry_type(&self) -> EntryType {
194 match self {
195 TreeEntryTarget::Blob { .. } => EntryType::Blob,
196 TreeEntryTarget::Tree { .. } => EntryType::Tree,
197 TreeEntryTarget::Symlink { .. } => EntryType::Symlink,
198 TreeEntryTarget::Gitlink { .. } => EntryType::Gitlink,
199 TreeEntryTarget::Spoollink { .. } => EntryType::Spoollink,
200 }
201 }
202
203 pub fn mode(&self) -> FileMode {
204 match self {
205 TreeEntryTarget::Blob {
206 executable: true, ..
207 } => FileMode::Executable,
208 TreeEntryTarget::Blob { .. } => FileMode::Normal,
209 TreeEntryTarget::Tree { .. } => FileMode::Normal,
210 TreeEntryTarget::Symlink { .. } => FileMode::Symlink,
211 TreeEntryTarget::Gitlink { .. } => FileMode::Gitlink,
212 TreeEntryTarget::Spoollink { .. } => FileMode::Spoollink,
213 }
214 }
215
216 pub fn content_hash(&self) -> Option<ContentHash> {
217 match self {
218 TreeEntryTarget::Blob { hash, .. }
219 | TreeEntryTarget::Tree { hash }
220 | TreeEntryTarget::Symlink { hash } => Some(*hash),
221 TreeEntryTarget::Gitlink { .. } | TreeEntryTarget::Spoollink { .. } => None,
222 }
223 }
224
225 pub fn gitlink_target(&self) -> Option<GitObjectId> {
226 match self {
227 TreeEntryTarget::Gitlink { target } => Some(*target),
228 _ => None,
229 }
230 }
231
232 pub fn spoollink_target(&self) -> Option<(&SpoolId, StateId)> {
235 match self {
236 TreeEntryTarget::Spoollink { spool_id, state_id } => Some((spool_id, *state_id)),
237 _ => None,
238 }
239 }
240
241 fn encoded_payload_len(&self) -> usize {
242 match self {
243 TreeEntryTarget::Blob { hash, .. }
244 | TreeEntryTarget::Tree { hash }
245 | TreeEntryTarget::Symlink { hash } => hash.as_bytes().len(),
246 TreeEntryTarget::Gitlink { target } => target.as_bytes().len(),
247 TreeEntryTarget::Spoollink { spool_id, state_id } => {
248 4 + spool_id.as_str().len() + state_id.as_bytes().len()
249 }
250 }
251 }
252
253 fn write_payload(&self, layout_flags: u8, mut emit: impl FnMut(&[u8])) {
261 emit(&[self.mode().to_byte() | layout_flags]);
262 emit(&[self.entry_type().to_byte()]);
263 match self {
264 TreeEntryTarget::Blob { hash, .. }
265 | TreeEntryTarget::Tree { hash }
266 | TreeEntryTarget::Symlink { hash } => emit(hash.as_bytes()),
267 TreeEntryTarget::Gitlink { target } => {
268 emit(&[git_format_to_tag(target.format())]);
269 emit(target.as_bytes());
270 }
271 TreeEntryTarget::Spoollink { spool_id, state_id } => {
272 emit(&(spool_id.as_str().len() as u32).to_le_bytes());
273 emit(spool_id.as_str().as_bytes());
274 emit(state_id.as_bytes());
275 }
276 };
277 }
278}
279
280pub fn validate_name(name: &str) -> Result<(), TreeError> {
283 if name.is_empty() {
284 return Err(TreeError::InvalidName("entry name cannot be empty".into()));
285 }
286 if name == "." || name == ".." {
287 return Err(TreeError::InvalidName(format!(
288 "'{}' is not a valid entry name",
289 name
290 )));
291 }
292 if name.contains('/') || name.contains('\\') {
293 return Err(TreeError::InvalidName(
294 "entry name cannot contain path separators".into(),
295 ));
296 }
297 if name.bytes().any(|b| b < 0x20 || b == 0x7f) {
298 return Err(TreeError::InvalidName(
299 "entry name contains control characters".into(),
300 ));
301 }
302 if name.len() > u16::MAX as usize {
303 return Err(TreeError::InvalidName(
304 "entry name exceeds the HTR4 u16 length bound".into(),
305 ));
306 }
307 Ok(())
308}
309
310#[derive(Clone, Debug, PartialEq, Eq)]
311pub struct TreeEntry {
312 name: String,
313 target: TreeEntryTarget,
314 git_mode: Option<RawGitMode>,
318}
319
320impl TreeEntry {
321 pub(crate) fn validate(&self) -> Result<(), TreeError> {
322 validate_name(&self.name)?;
323 if let Some(mode) = self.git_mode {
324 self.check_raw_git_mode(mode)?;
325 }
326 Ok(())
327 }
328
329 fn new(name: String, target: TreeEntryTarget) -> Self {
330 Self {
331 name,
332 target,
333 git_mode: None,
334 }
335 }
336
337 pub fn with_raw_git_mode(self, mode: RawGitMode) -> Result<Self, TreeError> {
342 let canonical = RawGitMode::canonical(self.entry_type(), self.is_executable());
343 if canonical == Some(mode) {
344 return Ok(Self {
345 git_mode: None,
346 ..self
347 });
348 }
349 self.check_raw_git_mode(mode)?;
350 Ok(self.with_checked_raw_git_mode(mode))
351 }
352
353 pub(crate) fn with_checked_raw_git_mode(self, mode: RawGitMode) -> Self {
354 Self {
355 git_mode: Some(mode),
356 ..self
357 }
358 }
359
360 pub fn raw_git_mode(&self) -> Option<RawGitMode> {
362 self.git_mode
363 }
364
365 pub fn same_meaning(&self, other: &Self) -> bool {
370 self.name == other.name && self.target == other.target
371 }
372
373 fn drop_raw_git_mode(&mut self) {
374 self.git_mode = None;
375 }
376
377 pub fn git_mode(&self) -> Option<RawGitMode> {
380 self.git_mode
381 .or_else(|| RawGitMode::canonical(self.entry_type(), self.is_executable()))
382 }
383
384 pub fn file(
385 name: impl Into<String>,
386 hash: ContentHash,
387 executable: bool,
388 ) -> Result<Self, TreeError> {
389 let name = name.into();
390 validate_name(&name)?;
391 Ok(Self::new(name, TreeEntryTarget::Blob { hash, executable }))
392 }
393
394 pub fn directory(name: impl Into<String>, hash: ContentHash) -> Result<Self, TreeError> {
395 let name = name.into();
396 validate_name(&name)?;
397 Ok(Self::new(name, TreeEntryTarget::Tree { hash }))
398 }
399
400 pub fn symlink(name: impl Into<String>, hash: ContentHash) -> Result<Self, TreeError> {
401 let name = name.into();
402 validate_name(&name)?;
403 Ok(Self::new(name, TreeEntryTarget::Symlink { hash }))
404 }
405
406 pub fn gitlink(name: impl Into<String>, target: GitObjectId) -> Result<Self, TreeError> {
407 let name = name.into();
408 validate_name(&name)?;
409 Ok(Self::new(name, TreeEntryTarget::Gitlink { target }))
410 }
411
412 pub fn spoollink(
415 name: impl Into<String>,
416 spool_id: SpoolId,
417 state_id: StateId,
418 ) -> Result<Self, TreeError> {
419 let name = name.into();
420 validate_name(&name)?;
421 Ok(Self::new(
422 name,
423 TreeEntryTarget::Spoollink { spool_id, state_id },
424 ))
425 }
426
427 pub fn name(&self) -> &str {
428 &self.name
429 }
430
431 pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), TreeError> {
432 let name = name.into();
433 validate_name(&name)?;
434 self.name = name;
435 Ok(())
436 }
437
438 pub fn with_mode(&self, mode: FileMode) -> Result<Self, TreeError> {
439 match (&self.target, mode) {
440 (TreeEntryTarget::Blob { hash, .. }, FileMode::Normal | FileMode::Executable) => {
441 Self::file(self.name.clone(), *hash, mode == FileMode::Executable)
442 }
443 (TreeEntryTarget::Symlink { .. }, FileMode::Symlink)
444 | (TreeEntryTarget::Tree { .. }, _)
445 | (TreeEntryTarget::Gitlink { .. }, FileMode::Gitlink)
446 | (TreeEntryTarget::Spoollink { .. }, FileMode::Spoollink)
447 if mode == self.mode() =>
448 {
449 Ok(self.clone())
450 }
451 _ => Err(TreeError::InvalidStructure(format!(
452 "cannot apply mode {:?} to {:?} entry '{}'",
453 mode,
454 self.entry_type(),
455 self.name
456 ))),
457 }
458 }
459
460 pub fn target(&self) -> &TreeEntryTarget {
461 &self.target
462 }
463
464 pub fn entry_type(&self) -> EntryType {
465 self.target.entry_type()
466 }
467
468 pub fn mode(&self) -> FileMode {
469 self.target.mode()
470 }
471
472 pub fn content_hash(&self) -> Option<ContentHash> {
473 self.target.content_hash()
474 }
475
476 pub fn leaf_content_hash(&self) -> Option<ContentHash> {
477 match self.target {
478 TreeEntryTarget::Blob { hash, .. } | TreeEntryTarget::Symlink { hash } => Some(hash),
479 TreeEntryTarget::Tree { .. }
480 | TreeEntryTarget::Gitlink { .. }
481 | TreeEntryTarget::Spoollink { .. } => None,
482 }
483 }
484
485 pub fn require_content_hash(&self) -> ContentHash {
486 self.content_hash()
487 .expect("tree entry target does not carry a Heddle content hash")
488 }
489
490 pub fn blob_hash(&self) -> Option<ContentHash> {
491 match self.target {
492 TreeEntryTarget::Blob { hash, .. } => Some(hash),
493 _ => None,
494 }
495 }
496
497 pub fn tree_hash(&self) -> Option<ContentHash> {
498 match self.target {
499 TreeEntryTarget::Tree { hash } => Some(hash),
500 _ => None,
501 }
502 }
503
504 pub fn symlink_hash(&self) -> Option<ContentHash> {
505 match self.target {
506 TreeEntryTarget::Symlink { hash } => Some(hash),
507 _ => None,
508 }
509 }
510
511 pub fn gitlink_target(&self) -> Option<GitObjectId> {
512 self.target.gitlink_target()
513 }
514
515 pub fn spoollink_target(&self) -> Option<(&SpoolId, StateId)> {
517 self.target.spoollink_target()
518 }
519
520 pub fn is_tree(&self) -> bool {
521 self.entry_type() == EntryType::Tree
522 }
523
524 pub fn is_blob(&self) -> bool {
525 self.entry_type() == EntryType::Blob
526 }
527
528 pub fn is_symlink(&self) -> bool {
529 self.entry_type() == EntryType::Symlink
530 }
531
532 pub fn is_gitlink(&self) -> bool {
533 self.entry_type() == EntryType::Gitlink
534 }
535
536 pub fn is_spoollink(&self) -> bool {
537 self.entry_type() == EntryType::Spoollink
538 }
539
540 pub fn is_executable(&self) -> bool {
541 self.mode() == FileMode::Executable
542 }
543
544 pub(crate) fn encoded_len(&self, source_position: Option<u32>) -> usize {
547 let flags = self.layout_flags(source_position);
548 1 + 1
549 + self.target.encoded_payload_len()
550 + self.name.len()
551 + 1
552 + super::tree_git_layout::layout_trailer_len(flags)
553 }
554
555 pub fn decoded_size(&self) -> usize {
557 self.name.len() + self.target.encoded_payload_len()
558 }
559
560 pub(crate) fn update_hasher(&self, hasher: &mut blake3::Hasher, source_position: Option<u32>) {
564 let flags = self.layout_flags(source_position);
565 self.target.write_payload(flags, |bytes| {
566 hasher.update(bytes);
567 });
568 hasher.update(self.name.as_bytes());
569 hasher.update(&[0]);
570 self.write_layout_trailer(source_position, |bytes| {
571 hasher.update(bytes);
572 });
573 }
574}
575
576#[derive(Clone, Debug, PartialEq, Eq)]
592pub struct Tree {
593 entries: Arc<Vec<TreeEntry>>,
597 scheme: TreeScheme,
599 salts: Arc<Vec<[u8; 32]>>,
603 source_positions: Arc<Vec<u32>>,
609}
610
611impl Tree {
612 pub fn new() -> Self {
613 Self {
614 entries: Arc::new(Vec::new()),
615 scheme: TreeScheme::V3Flat,
616 salts: Arc::new(Vec::new()),
617 source_positions: Arc::new(Vec::new()),
618 }
619 }
620
621 pub fn from_entries(mut entries: Vec<TreeEntry>) -> Self {
626 entries.iter_mut().for_each(TreeEntry::drop_raw_git_mode);
627 entries.sort_by(|a, b| a.name.cmp(&b.name));
628 Self {
629 entries: Arc::new(entries),
630 scheme: TreeScheme::V3Flat,
631 salts: Arc::new(Vec::new()),
632 source_positions: Arc::new(Vec::new()),
633 }
634 }
635
636 pub fn from_git_entries(entries: Vec<TreeEntry>) -> Result<Self, TreeError> {
644 let canonical = entries
645 .windows(2)
646 .all(|pair| git_canonical_order(&pair[0], &pair[1]) == std::cmp::Ordering::Less);
647 let mut paired: Vec<(TreeEntry, u32)> = Vec::with_capacity(entries.len());
648 for (position, entry) in entries.into_iter().enumerate() {
649 let position = u32::try_from(position).map_err(|_| {
650 TreeError::InvalidStructure("git tree has more than u32::MAX entries".into())
651 })?;
652 paired.push((entry, position));
653 }
654 paired.sort_by(|a, b| a.0.name.cmp(&b.0.name));
655 if let Some(pair) = paired
656 .windows(2)
657 .find(|pair| pair[0].0.name == pair[1].0.name)
658 {
659 return Err(TreeError::InvalidStructure(format!(
660 "duplicate entry name '{}'",
661 pair[0].0.name
662 )));
663 }
664 let (entries, positions): (Vec<TreeEntry>, Vec<u32>) = paired.into_iter().unzip();
665 let tree = Self {
666 entries: Arc::new(entries),
667 scheme: TreeScheme::V3Flat,
668 salts: Arc::new(Vec::new()),
669 source_positions: Arc::new(if canonical { Vec::new() } else { positions }),
670 };
671 tree.validate()?;
672 Ok(tree)
673 }
674
675 pub fn from_entries_salted_v4(
684 mut entries: Vec<TreeEntry>,
685 salts: Vec<[u8; 32]>,
686 ) -> Result<Self, TreeError> {
687 entries.iter_mut().for_each(TreeEntry::drop_raw_git_mode);
688 if entries.len() != salts.len() {
689 return Err(TreeError::InvalidStructure(format!(
690 "v4 tree has {} entries but {} salts",
691 entries.len(),
692 salts.len()
693 )));
694 }
695 let mut paired: Vec<(TreeEntry, [u8; 32])> = entries.into_iter().zip(salts).collect();
696 paired.sort_by(|a, b| a.0.name.cmp(&b.0.name));
697 let (entries, salts): (Vec<TreeEntry>, Vec<[u8; 32]>) = paired.into_iter().unzip();
698 Self::try_from_decoded_entries_salted_v4(entries, salts)
699 }
700
701 pub fn try_from_decoded_entries(entries: Vec<TreeEntry>) -> Result<Self, TreeError> {
707 Self::try_from_decoded_layout(entries, Vec::new())
708 }
709
710 pub(crate) fn try_from_decoded_layout(
713 entries: Vec<TreeEntry>,
714 source_positions: Vec<u32>,
715 ) -> Result<Self, TreeError> {
716 let tree = Self {
717 entries: Arc::new(entries),
718 scheme: TreeScheme::V3Flat,
719 salts: Arc::new(Vec::new()),
720 source_positions: Arc::new(source_positions),
721 };
722 tree.validate()?;
723 Ok(tree)
724 }
725
726 pub fn try_from_decoded_entries_salted_v4(
730 entries: Vec<TreeEntry>,
731 salts: Vec<[u8; 32]>,
732 ) -> Result<Self, TreeError> {
733 if entries.len() != salts.len() {
734 return Err(TreeError::InvalidStructure(format!(
735 "v4 tree has {} entries but {} salts",
736 entries.len(),
737 salts.len()
738 )));
739 }
740 let tree = Self {
741 entries: Arc::new(entries),
742 scheme: TreeScheme::V4Salted,
743 salts: Arc::new(salts),
744 source_positions: Arc::new(Vec::new()),
745 };
746 tree.validate()?;
747 Ok(tree)
748 }
749
750 pub fn scheme(&self) -> TreeScheme {
752 self.scheme
753 }
754
755 pub fn salts(&self) -> &[[u8; 32]] {
757 &self.salts
758 }
759
760 pub fn salt_at(&self, index: usize) -> Option<[u8; 32]> {
763 self.salts.get(index).copied()
764 }
765
766 pub fn source_positions(&self) -> &[u32] {
769 &self.source_positions
770 }
771
772 pub fn source_position_at(&self, index: usize) -> Option<u32> {
774 self.source_positions.get(index).copied()
775 }
776
777 pub fn has_git_layout(&self) -> bool {
780 !self.source_positions.is_empty()
781 || self.entries.iter().any(|entry| entry.git_mode.is_some())
782 }
783
784 pub fn requires_canonical_body(&self) -> bool {
788 self.scheme == TreeScheme::V4Salted || self.has_git_layout()
789 }
790
791 pub fn git_ordered_entries(&self) -> Vec<&TreeEntry> {
794 let mut ordered: Vec<(usize, &TreeEntry)> = self.entries.iter().enumerate().collect();
795 if self.source_positions.is_empty() {
796 ordered.sort_by(|a, b| git_canonical_order(a.1, b.1));
797 } else {
798 ordered.sort_by_key(|(index, _)| self.source_positions.get(*index).copied());
799 }
800 ordered.into_iter().map(|(_, entry)| entry).collect()
801 }
802
803 pub fn validate(&self) -> Result<(), TreeError> {
804 match self.scheme {
805 TreeScheme::V3Flat => {
806 if !self.salts.is_empty() {
807 return Err(TreeError::InvalidStructure(
808 "v3 tree must not carry per-entry salts".into(),
809 ));
810 }
811 }
812 TreeScheme::V4Salted => {
813 if self.salts.len() != self.entries.len() {
814 return Err(TreeError::InvalidStructure(format!(
815 "v4 tree has {} entries but {} salts",
816 self.entries.len(),
817 self.salts.len()
818 )));
819 }
820 if self.entries.iter().any(|entry| entry.git_mode.is_some()) {
821 return Err(TreeError::InvalidStructure(
822 "v4 trees do not record a git source layout".into(),
823 ));
824 }
825 }
826 }
827 let mut previous_name: Option<&str> = None;
828 for entry in self.entries.iter() {
829 entry.validate()?;
830 if let Some(previous) = previous_name
831 && previous >= entry.name.as_str()
832 {
833 return Err(TreeError::InvalidStructure(
834 "entries must be strictly sorted by name".to_string(),
835 ));
836 }
837 previous_name = Some(&entry.name);
838 }
839 self.validate_source_positions()
840 }
841
842 fn validate_source_positions(&self) -> Result<(), TreeError> {
846 if self.source_positions.is_empty() {
847 return Ok(());
848 }
849 if self.scheme != TreeScheme::V3Flat {
850 return Err(TreeError::InvalidStructure(
851 "only v3 trees record a git source order".into(),
852 ));
853 }
854 if self.source_positions.len() != self.entries.len() {
855 return Err(TreeError::InvalidStructure(format!(
856 "tree has {} entries but {} source positions",
857 self.entries.len(),
858 self.source_positions.len()
859 )));
860 }
861 let mut seen = vec![false; self.entries.len()];
862 for position in self.source_positions.iter() {
863 let slot = usize::try_from(*position)
864 .ok()
865 .and_then(|index| seen.get_mut(index))
866 .ok_or_else(|| {
867 TreeError::InvalidStructure(format!(
868 "source position {position} is out of range"
869 ))
870 })?;
871 if *slot {
872 return Err(TreeError::InvalidStructure(format!(
873 "source position {position} is repeated"
874 )));
875 }
876 *slot = true;
877 }
878 let ordered = self.git_ordered_entries();
879 if ordered
880 .windows(2)
881 .all(|pair| git_canonical_order(pair[0], pair[1]) == std::cmp::Ordering::Less)
882 {
883 return Err(TreeError::InvalidStructure(
884 "recorded git source order is the canonical order".into(),
885 ));
886 }
887 Ok(())
888 }
889
890 fn drop_git_layout(&mut self) {
893 if !self.source_positions.is_empty() {
894 self.source_positions = Arc::new(Vec::new());
895 }
896 if self.entries.iter().any(|entry| entry.git_mode.is_some()) {
897 Arc::make_mut(&mut self.entries)
898 .iter_mut()
899 .for_each(TreeEntry::drop_raw_git_mode);
900 }
901 }
902
903 pub fn entries(&self) -> &[TreeEntry] {
904 &self.entries
905 }
906
907 pub fn get(&self, name: &str) -> Option<&TreeEntry> {
908 let index = self
909 .entries
910 .binary_search_by(|entry| entry.name.as_str().cmp(name))
911 .ok()?;
912 self.entries.get(index)
913 }
914
915 pub fn insert(&mut self, mut entry: TreeEntry) {
916 self.drop_git_layout();
917 entry.drop_raw_git_mode();
918 match self.scheme {
919 TreeScheme::V3Flat => {
920 let entries = Arc::make_mut(&mut self.entries);
921 entries.retain(|e| e.name != entry.name);
922 let pos = entries
923 .iter()
924 .position(|e| e.name > entry.name)
925 .unwrap_or(entries.len());
926 entries.insert(pos, entry);
927 }
928 TreeScheme::V4Salted => {
929 self.insert_salted(entry, rand::random());
933 }
934 }
935 }
936
937 fn insert_salted(&mut self, entry: TreeEntry, salt: [u8; 32]) {
940 debug_assert_eq!(self.scheme, TreeScheme::V4Salted);
941 let entries = Arc::make_mut(&mut self.entries);
942 let salts = Arc::make_mut(&mut self.salts);
943 if let Some(existing) = entries.iter().position(|e| e.name == entry.name) {
944 entries.remove(existing);
945 salts.remove(existing);
946 }
947 let pos = entries
948 .iter()
949 .position(|e| e.name > entry.name)
950 .unwrap_or(entries.len());
951 entries.insert(pos, entry);
952 salts.insert(pos, salt);
953 }
954
955 pub fn remove(&mut self, name: &str) -> Option<TreeEntry> {
956 let pos = self.entries.iter().position(|e| e.name == name)?;
957 self.drop_git_layout();
958 if matches!(self.scheme, TreeScheme::V4Salted) {
959 Arc::make_mut(&mut self.salts).remove(pos);
960 }
961 Some(Arc::make_mut(&mut self.entries).remove(pos))
962 }
963
964 pub fn is_empty(&self) -> bool {
965 self.entries.is_empty()
966 }
967
968 pub fn len(&self) -> usize {
969 self.entries.len()
970 }
971
972 pub fn hash(&self) -> ContentHash {
973 match self.scheme {
974 TreeScheme::V3Flat => self.flat_hash_v3(),
975 TreeScheme::V4Salted => self.merkle_root_v4(),
976 }
977 }
978
979 fn flat_hash_v3(&self) -> ContentHash {
981 let total_len: usize = self
982 .entries
983 .iter()
984 .enumerate()
985 .map(|(index, entry)| entry.encoded_len(self.source_position_at(index)))
986 .sum();
987 ContentHash::compute_typed_with_len(TREE_EMPTY_PREFIX, total_len as u64, |hasher| {
988 for (index, entry) in self.entries.iter().enumerate() {
989 entry.update_hasher(hasher, self.source_position_at(index));
990 }
991 })
992 }
993
994 fn v4_leaf_hash(entry: &TreeEntry, salt: &[u8; 32]) -> ContentHash {
1005 let preimage = Self::v4_leaf_preimage(entry, salt);
1006 ContentHash::compute_typed(TREE_V4_LEAF_PREFIX, &preimage)
1007 }
1008
1009 fn v4_leaf_preimage(entry: &TreeEntry, salt: &[u8; 32]) -> Vec<u8> {
1011 let name = entry.name.as_bytes();
1012 let mut buf =
1014 Vec::with_capacity(32 + 2 + entry.target.encoded_payload_len() + 2 + name.len());
1015 buf.extend_from_slice(salt);
1016 entry
1017 .target
1018 .write_payload(entry.layout_flags(None), |bytes| {
1019 buf.extend_from_slice(bytes)
1020 });
1021 buf.extend_from_slice(&(name.len() as u16).to_le_bytes());
1023 buf.extend_from_slice(name);
1024 entry.write_layout_trailer(None, |bytes| buf.extend_from_slice(bytes));
1025 buf
1026 }
1027
1028 pub fn v4_leaf_hash_at(&self, index: usize) -> Option<ContentHash> {
1033 if self.scheme != TreeScheme::V4Salted {
1034 return None;
1035 }
1036 let entry = self.entries.get(index)?;
1037 let salt = self.salts.get(index)?;
1038 Some(Self::v4_leaf_hash(entry, salt))
1039 }
1040
1041 pub fn v4_leaf_hash_for(&self, name: &str) -> Option<ContentHash> {
1044 let index = self
1045 .entries
1046 .binary_search_by(|entry| entry.name.as_str().cmp(name))
1047 .ok()?;
1048 self.v4_leaf_hash_at(index)
1049 }
1050
1051 fn merkle_root_v4(&self) -> ContentHash {
1054 let mut leaves: Vec<ContentHash> = self
1055 .entries
1056 .iter()
1057 .zip(self.salts.iter())
1058 .map(|(entry, salt)| Self::v4_leaf_hash(entry, salt))
1059 .collect();
1060 merkle_root_from_leaves(&mut leaves)
1061 }
1062
1063 pub fn iter(&self) -> impl Iterator<Item = &TreeEntry> {
1064 self.entries.iter()
1065 }
1066
1067 pub fn get_path(&self, path: &Path) -> Option<&TreeEntry> {
1068 let name = path.file_name()?.to_str()?;
1069 if path.parent().is_none_or(|p| p.as_os_str().is_empty()) {
1070 self.get(name)
1071 } else {
1072 None
1073 }
1074 }
1075}
1076
1077fn merkle_root_from_leaves(leaves: &mut [ContentHash]) -> ContentHash {
1089 leaves.sort_unstable();
1090 merkle_tree_hash(leaves)
1091}
1092
1093fn merkle_tree_hash(leaves: &[ContentHash]) -> ContentHash {
1095 match leaves.len() {
1096 0 => ContentHash::compute_typed(TREE_EMPTY_PREFIX, b""),
1098 1 => leaves[0],
1099 n => {
1100 let k = 1usize << ((usize::BITS - 1) - (n - 1).leading_zeros());
1105 let left = merkle_tree_hash(&leaves[..k]);
1106 let right = merkle_tree_hash(&leaves[k..]);
1107 let mut hasher = ContentHash::typed_hasher(TREE_V4_NODE_PREFIX, 64);
1108 hasher.update(left.as_bytes());
1109 hasher.update(right.as_bytes());
1110 ContentHash::from_bytes(hasher.finalize().into())
1111 }
1112 }
1113}
1114
1115#[derive(Clone, Debug, PartialEq, Eq)]
1119pub enum PartialTreeLeaf {
1120 Visible { entry: TreeEntry, salt: [u8; 32] },
1121 Redacted { leaf_hash: ContentHash },
1122}
1123
1124impl PartialTreeLeaf {
1125 pub fn leaf_hash(&self) -> ContentHash {
1127 match self {
1128 PartialTreeLeaf::Visible { entry, salt } => Tree::v4_leaf_hash(entry, salt),
1129 PartialTreeLeaf::Redacted { leaf_hash } => *leaf_hash,
1130 }
1131 }
1132
1133 pub fn is_redacted(&self) -> bool {
1134 matches!(self, PartialTreeLeaf::Redacted { .. })
1135 }
1136}
1137
1138#[derive(Clone, Debug, PartialEq, Eq)]
1146pub struct PartialTree {
1147 declared_root: ContentHash,
1148 leaves: Vec<PartialTreeLeaf>,
1149}
1150
1151impl PartialTree {
1152 pub fn new(declared_root: ContentHash, leaves: Vec<PartialTreeLeaf>) -> Self {
1156 Self {
1157 declared_root,
1158 leaves,
1159 }
1160 }
1161
1162 pub fn from_leaves_verified(
1164 declared_root: ContentHash,
1165 leaves: Vec<PartialTreeLeaf>,
1166 ) -> Result<Self, TreeError> {
1167 let partial = Self::new(declared_root, leaves);
1168 partial.verify()?;
1169 Ok(partial)
1170 }
1171
1172 pub fn project(
1176 tree: &Tree,
1177 redacted: &std::collections::HashSet<ContentHash>,
1178 ) -> Result<Self, TreeError> {
1179 if tree.scheme != TreeScheme::V4Salted {
1180 return Err(TreeError::InvalidStructure(
1181 "cannot project a redacted tree from a non-v4 tree".into(),
1182 ));
1183 }
1184 tree.validate()?;
1185 let declared_root = tree.hash();
1186 let leaves = tree
1187 .entries
1188 .iter()
1189 .zip(tree.salts.iter())
1190 .map(|(entry, salt)| {
1191 let leaf_hash = Tree::v4_leaf_hash(entry, salt);
1192 if redacted.contains(&leaf_hash) {
1193 PartialTreeLeaf::Redacted { leaf_hash }
1194 } else {
1195 PartialTreeLeaf::Visible {
1196 entry: entry.clone(),
1197 salt: *salt,
1198 }
1199 }
1200 })
1201 .collect();
1202 Ok(Self {
1203 declared_root,
1204 leaves,
1205 })
1206 }
1207
1208 pub fn declared_root(&self) -> ContentHash {
1209 self.declared_root
1210 }
1211
1212 pub fn leaves(&self) -> &[PartialTreeLeaf] {
1213 &self.leaves
1214 }
1215
1216 pub fn redacted_count(&self) -> usize {
1217 self.leaves.iter().filter(|leaf| leaf.is_redacted()).count()
1218 }
1219
1220 pub fn has_redactions(&self) -> bool {
1221 self.leaves.iter().any(PartialTreeLeaf::is_redacted)
1222 }
1223
1224 pub fn reconstruct_root(&self) -> ContentHash {
1226 let mut leaves: Vec<ContentHash> =
1227 self.leaves.iter().map(PartialTreeLeaf::leaf_hash).collect();
1228 merkle_root_from_leaves(&mut leaves)
1229 }
1230
1231 pub fn verify(&self) -> Result<(), TreeError> {
1233 let found = self.reconstruct_root();
1234 if found != self.declared_root {
1235 return Err(TreeError::InvalidStructure(format!(
1236 "partial tree reconstructs {found} but declares {}",
1237 self.declared_root
1238 )));
1239 }
1240 Ok(())
1241 }
1242
1243 pub fn visible_tree(&self) -> Result<Tree, TreeError> {
1251 let mut entries = Vec::new();
1252 let mut salts = Vec::new();
1253 for leaf in &self.leaves {
1254 if let PartialTreeLeaf::Visible { entry, salt } = leaf {
1255 entries.push(entry.clone());
1256 salts.push(*salt);
1257 }
1258 }
1259 Tree::from_entries_salted_v4(entries, salts)
1260 }
1261
1262 pub fn into_tree(self) -> Result<Tree, TreeError> {
1266 self.verify()?;
1267 let mut entries = Vec::with_capacity(self.leaves.len());
1268 let mut salts = Vec::with_capacity(self.leaves.len());
1269 for leaf in self.leaves {
1270 match leaf {
1271 PartialTreeLeaf::Visible { entry, salt } => {
1272 entries.push(entry);
1273 salts.push(salt);
1274 }
1275 PartialTreeLeaf::Redacted { .. } => {
1276 return Err(TreeError::InvalidStructure(
1277 "cannot materialize a redacted leaf into a full tree".into(),
1278 ));
1279 }
1280 }
1281 }
1282 Tree::from_entries_salted_v4(entries, salts)
1283 }
1284}
1285
1286#[derive(Deserialize)]
1294#[serde(deny_unknown_fields)]
1295struct EncodedTreeV2 {
1296 version: u8,
1297 entries: Vec<EncodedTreeEntryV2>,
1298 #[serde(default)]
1302 salts: Option<Vec<[u8; 32]>>,
1303 #[serde(default)]
1306 source_positions: Option<Vec<u32>>,
1307}
1308
1309#[derive(Deserialize)]
1310#[serde(deny_unknown_fields)]
1311struct EncodedTreeEntryV2 {
1312 name: String,
1313 kind: u8,
1314 hash: Option<ContentHash>,
1315 executable: Option<bool>,
1316 git_format: Option<u8>,
1317 git_oid: Option<Vec<u8>>,
1318 #[serde(default)]
1322 spool_id: Option<SpoolId>,
1323 #[serde(default)]
1324 spool_state_id: Option<StateId>,
1325 #[serde(default)]
1327 git_mode: Option<String>,
1328}
1329
1330impl Serialize for EncodedTreeV2 {
1334 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1335 use serde::ser::SerializeMap;
1336 let len =
1337 2 + usize::from(self.salts.is_some()) + usize::from(self.source_positions.is_some());
1338 let mut map = serializer.serialize_map(Some(len))?;
1339 map.serialize_entry("version", &self.version)?;
1340 map.serialize_entry("entries", &self.entries)?;
1341 if let Some(salts) = &self.salts {
1342 map.serialize_entry("salts", salts)?;
1343 }
1344 if let Some(positions) = &self.source_positions {
1345 map.serialize_entry("source_positions", positions)?;
1346 }
1347 map.end()
1348 }
1349}
1350
1351impl Serialize for EncodedTreeEntryV2 {
1352 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1353 use serde::ser::SerializeMap;
1354 let len = 6
1355 + usize::from(self.spool_id.is_some())
1356 + usize::from(self.spool_state_id.is_some())
1357 + usize::from(self.git_mode.is_some());
1358 let mut map = serializer.serialize_map(Some(len))?;
1359 map.serialize_entry("name", &self.name)?;
1360 map.serialize_entry("kind", &self.kind)?;
1361 map.serialize_entry("hash", &self.hash)?;
1362 map.serialize_entry("executable", &self.executable)?;
1363 map.serialize_entry("git_format", &self.git_format)?;
1364 map.serialize_entry("git_oid", &self.git_oid)?;
1365 if let Some(spool_id) = &self.spool_id {
1366 map.serialize_entry("spool_id", spool_id)?;
1367 }
1368 if let Some(state_id) = &self.spool_state_id {
1369 map.serialize_entry("spool_state_id", state_id)?;
1370 }
1371 if let Some(git_mode) = &self.git_mode {
1372 map.serialize_entry("git_mode", git_mode)?;
1373 }
1374 map.end()
1375 }
1376}
1377
1378impl Serialize for Tree {
1379 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1380 where
1381 S: Serializer,
1382 {
1383 EncodedTreeV2::from(self).serialize(serializer)
1384 }
1385}
1386
1387impl<'de> Deserialize<'de> for Tree {
1388 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1389 where
1390 D: Deserializer<'de>,
1391 {
1392 let encoded = EncodedTreeV2::deserialize(deserializer)?;
1393 Tree::try_from(encoded).map_err(de::Error::custom)
1394 }
1395}
1396
1397#[derive(Debug)]
1398pub enum TreeDecodeError {
1399 Decode(rmp_serde::decode::Error),
1400 Invalid(TreeError),
1401}
1402
1403impl From<rmp_serde::decode::Error> for TreeDecodeError {
1404 fn from(error: rmp_serde::decode::Error) -> Self {
1405 Self::Decode(error)
1406 }
1407}
1408
1409impl From<TreeError> for TreeDecodeError {
1410 fn from(error: TreeError) -> Self {
1411 Self::Invalid(error)
1412 }
1413}
1414
1415impl From<&Tree> for EncodedTreeV2 {
1416 fn from(tree: &Tree) -> Self {
1417 let (version, salts) = match tree.scheme {
1418 TreeScheme::V3Flat => (TREE_FORMAT_VERSION, None),
1419 TreeScheme::V4Salted => (TREE_FORMAT_VERSION_V4, Some(tree.salts.as_ref().clone())),
1420 };
1421 Self {
1422 version,
1423 entries: tree.entries.iter().map(EncodedTreeEntryV2::from).collect(),
1424 salts,
1425 source_positions: (!tree.source_positions.is_empty())
1426 .then(|| tree.source_positions.as_ref().clone()),
1427 }
1428 }
1429}
1430
1431impl From<&TreeEntry> for EncodedTreeEntryV2 {
1432 fn from(entry: &TreeEntry) -> Self {
1433 let name = entry.name.clone();
1434 let git_mode = entry.git_mode.map(|mode| {
1435 let mut digits = Vec::new();
1436 mode.write_digits(&mut digits);
1437 String::from_utf8_lossy(&digits).into_owned()
1438 });
1439 let empty = Self {
1440 name,
1441 kind: ENTRY_KIND_BLOB,
1442 hash: None,
1443 executable: None,
1444 git_format: None,
1445 git_oid: None,
1446 spool_id: None,
1447 spool_state_id: None,
1448 git_mode,
1449 };
1450 match entry.target() {
1451 TreeEntryTarget::Blob { hash, executable } => Self {
1452 kind: ENTRY_KIND_BLOB,
1453 hash: Some(*hash),
1454 executable: Some(*executable),
1455 ..empty
1456 },
1457 TreeEntryTarget::Tree { hash } => Self {
1458 kind: ENTRY_KIND_TREE,
1459 hash: Some(*hash),
1460 ..empty
1461 },
1462 TreeEntryTarget::Symlink { hash } => Self {
1463 kind: ENTRY_KIND_SYMLINK,
1464 hash: Some(*hash),
1465 ..empty
1466 },
1467 TreeEntryTarget::Gitlink { target } => Self {
1468 kind: ENTRY_KIND_GITLINK,
1469 git_format: Some(git_format_to_tag(target.format())),
1470 git_oid: Some(target.as_bytes().to_vec()),
1471 ..empty
1472 },
1473 TreeEntryTarget::Spoollink { spool_id, state_id } => Self {
1474 kind: ENTRY_KIND_SPOOLLINK,
1475 spool_id: Some(spool_id.clone()),
1476 spool_state_id: Some(*state_id),
1477 ..empty
1478 },
1479 }
1480 }
1481}
1482
1483impl TryFrom<EncodedTreeV2> for Tree {
1484 type Error = TreeError;
1485
1486 fn try_from(encoded: EncodedTreeV2) -> Result<Self, Self::Error> {
1487 let mut entries = Vec::with_capacity(encoded.entries.len());
1488 for entry in encoded.entries {
1489 entries.push(TreeEntry::try_from(entry)?);
1490 }
1491 let source_positions = encoded.source_positions.unwrap_or_default();
1492 match encoded.version {
1493 TREE_FORMAT_VERSION => {
1494 if encoded.salts.is_some_and(|salts| !salts.is_empty()) {
1495 return Err(TreeError::InvalidStructure(
1496 "v3 tree body must not carry salts".into(),
1497 ));
1498 }
1499 Tree::try_from_decoded_layout(entries, source_positions)
1500 }
1501 TREE_FORMAT_VERSION_V4 => {
1502 if !source_positions.is_empty() {
1503 return Err(TreeError::InvalidStructure(
1504 "v4 tree body must not carry source positions".into(),
1505 ));
1506 }
1507 let salts = encoded.salts.ok_or_else(|| {
1508 TreeError::InvalidStructure("v4 tree body is missing its salts".into())
1509 })?;
1510 Tree::try_from_decoded_entries_salted_v4(entries, salts)
1513 }
1514 other => Err(TreeError::InvalidStructure(format!(
1515 "unsupported tree format version {other}; this binary writes {TREE_FORMAT_VERSION} (v3) or {TREE_FORMAT_VERSION_V4} (v4)"
1516 ))),
1517 }
1518 }
1519}
1520
1521impl Tree {
1522 pub fn decode_current_msgpack(data: &[u8]) -> Result<Self, TreeDecodeError> {
1523 let encoded: EncodedTreeV2 = rmp_serde::from_slice(data)?;
1524 Ok(Tree::try_from(encoded)?)
1525 }
1526}
1527
1528impl TryFrom<EncodedTreeEntryV2> for TreeEntry {
1529 type Error = TreeError;
1530
1531 fn try_from(encoded: EncodedTreeEntryV2) -> Result<Self, Self::Error> {
1532 let git_mode = encoded
1533 .git_mode
1534 .as_deref()
1535 .map(|digits| RawGitMode::parse(digits.as_bytes()))
1536 .transpose()?;
1537 let entry = Self::try_from_encoded_target(encoded)?;
1538 match git_mode {
1539 Some(mode) => {
1540 entry.check_raw_git_mode(mode)?;
1541 Ok(entry.with_checked_raw_git_mode(mode))
1542 }
1543 None => Ok(entry),
1544 }
1545 }
1546}
1547
1548impl TreeEntry {
1549 fn try_from_encoded_target(encoded: EncodedTreeEntryV2) -> Result<Self, TreeError> {
1550 match encoded.kind {
1551 ENTRY_KIND_BLOB => TreeEntry::file(
1552 encoded.name,
1553 required_hash(encoded.hash, ENTRY_KIND_BLOB)?,
1554 encoded.executable.unwrap_or(false),
1555 ),
1556 ENTRY_KIND_TREE => {
1557 TreeEntry::directory(encoded.name, required_hash(encoded.hash, ENTRY_KIND_TREE)?)
1558 }
1559 ENTRY_KIND_SYMLINK => TreeEntry::symlink(
1560 encoded.name,
1561 required_hash(encoded.hash, ENTRY_KIND_SYMLINK)?,
1562 ),
1563 ENTRY_KIND_GITLINK => {
1564 let format = git_format_from_tag(required_git_format(
1565 encoded.git_format,
1566 ENTRY_KIND_GITLINK,
1567 )?)?;
1568 let oid = encoded.git_oid.ok_or_else(|| {
1569 TreeError::InvalidStructure("gitlink entry is missing git_oid".into())
1570 })?;
1571 let target = GitObjectId::from_raw(format, &oid).map_err(|err| {
1572 TreeError::InvalidStructure(format!("invalid gitlink target: {err}"))
1573 })?;
1574 TreeEntry::gitlink(encoded.name, target)
1575 }
1576 ENTRY_KIND_SPOOLLINK => {
1577 let spool_id = encoded.spool_id.ok_or_else(|| {
1578 TreeError::InvalidStructure("spoollink entry is missing spool_id".into())
1579 })?;
1580 let state_id = encoded.spool_state_id.ok_or_else(|| {
1581 TreeError::InvalidStructure("spoollink entry is missing spool_state_id".into())
1582 })?;
1583 TreeEntry::spoollink(encoded.name, spool_id, state_id)
1584 }
1585 other => Err(TreeError::InvalidStructure(format!(
1586 "unknown tree entry kind {other}"
1587 ))),
1588 }
1589 }
1590}
1591
1592fn required_hash(hash: Option<ContentHash>, kind: u8) -> Result<ContentHash, TreeError> {
1593 hash.ok_or_else(|| TreeError::InvalidStructure(format!("entry kind {kind} is missing hash")))
1594}
1595
1596fn required_git_format(format: Option<u8>, kind: u8) -> Result<u8, TreeError> {
1597 format.ok_or_else(|| {
1598 TreeError::InvalidStructure(format!("entry kind {kind} is missing git_format"))
1599 })
1600}
1601
1602pub(crate) fn git_format_to_tag(format: GitObjectFormat) -> u8 {
1603 match format {
1604 GitObjectFormat::Sha1 => GIT_OBJECT_FORMAT_SHA1,
1605 GitObjectFormat::Sha256 => GIT_OBJECT_FORMAT_SHA256,
1606 }
1607}
1608
1609pub(crate) fn git_format_from_tag(tag: u8) -> Result<GitObjectFormat, TreeError> {
1610 match tag {
1611 GIT_OBJECT_FORMAT_SHA1 => Ok(GitObjectFormat::Sha1),
1612 GIT_OBJECT_FORMAT_SHA256 => Ok(GitObjectFormat::Sha256),
1613 other => Err(TreeError::InvalidStructure(format!(
1614 "unknown git object format tag {other}"
1615 ))),
1616 }
1617}
1618
1619impl Default for Tree {
1620 fn default() -> Self {
1621 Self::new()
1622 }
1623}
1624
1625impl IntoIterator for Tree {
1626 type Item = TreeEntry;
1627 type IntoIter = std::vec::IntoIter<TreeEntry>;
1628
1629 fn into_iter(self) -> Self::IntoIter {
1630 Arc::try_unwrap(self.entries)
1631 .unwrap_or_else(|entries| (*entries).clone())
1632 .into_iter()
1633 }
1634}
1635
1636impl<'a> IntoIterator for &'a Tree {
1637 type Item = &'a TreeEntry;
1638 type IntoIter = std::slice::Iter<'a, TreeEntry>;
1639
1640 fn into_iter(self) -> Self::IntoIter {
1641 self.entries.iter()
1642 }
1643}
1644
1645#[cfg(test)]
1646mod spoollink_tests {
1647 use super::*;
1648
1649 #[test]
1650 fn spoollink_entry_shape() {
1651 let spool_id = SpoolId::parse("acme/child").unwrap();
1652 let state_id = StateId::from_bytes([9u8; 32]);
1653 let entry = TreeEntry::spoollink("child", spool_id.clone(), state_id).unwrap();
1654
1655 assert!(entry.is_spoollink());
1656 assert_eq!(entry.entry_type(), EntryType::Spoollink);
1657 assert_eq!(entry.mode(), FileMode::Spoollink);
1658 assert_eq!(entry.content_hash(), None);
1660 assert_eq!(entry.leaf_content_hash(), None);
1661 assert_eq!(entry.gitlink_target(), None);
1662 assert_eq!(entry.spoollink_target(), Some((&spool_id, state_id)));
1663 }
1664
1665 #[test]
1666 fn spoollink_roundtrips_through_encoded_tree_v2() {
1667 let spool_id = SpoolId::parse("acme/child").unwrap();
1668 let state_id = StateId::from_bytes([2u8; 32]);
1669
1670 let blob_hash = ContentHash::compute(b"hello");
1673 let tree = Tree::from_entries(vec![
1674 TreeEntry::file("a_blob", blob_hash, false).unwrap(),
1675 TreeEntry::spoollink("z_child", spool_id.clone(), state_id).unwrap(),
1676 ]);
1677
1678 let bytes = rmp_serde::to_vec(&tree).unwrap();
1679 let decoded = Tree::decode_current_msgpack(&bytes).unwrap();
1680
1681 assert_eq!(decoded, tree, "tree round-trip must be lossless");
1682
1683 let child = decoded
1684 .get("z_child")
1685 .expect("spoollink survives round-trip");
1686 assert_eq!(child.spoollink_target(), Some((&spool_id, state_id)));
1687 assert_eq!(child.entry_type(), EntryType::Spoollink);
1688
1689 assert_eq!(decoded.hash(), tree.hash());
1691 }
1692
1693 #[test]
1694 fn file_mode_spoollink_has_no_git_mode() {
1695 assert_eq!(FileMode::Spoollink.to_unix_mode(), 0);
1698 assert_ne!(FileMode::Spoollink.to_unix_mode(), 0o160000);
1699 assert_eq!(
1700 FileMode::from_byte(FileMode::Spoollink.to_byte()),
1701 Some(FileMode::Spoollink)
1702 );
1703 assert_eq!(
1704 EntryType::from_byte(EntryType::Spoollink.to_byte()),
1705 Some(EntryType::Spoollink)
1706 );
1707 }
1708}
1709
1710#[cfg(test)]
1711#[path = "tree_v4_tests.rs"]
1712mod tree_v4_tests;
1713
1714#[cfg(test)]
1715mod cow_tests {
1716 use super::*;
1717
1718 fn fixture() -> Tree {
1719 Tree::from_entries(vec![
1720 TreeEntry::file("a", ContentHash::compute(b"a"), false).unwrap(),
1721 TreeEntry::file("b", ContentHash::compute(b"b"), true).unwrap(),
1722 ])
1723 }
1724
1725 #[test]
1726 fn clone_shares_entries_until_mutated() {
1727 let original = fixture();
1728 let mut clone = original.clone();
1729 assert!(Arc::ptr_eq(&original.entries, &clone.entries));
1730
1731 clone.insert(TreeEntry::file("c", ContentHash::compute(b"c"), false).unwrap());
1732
1733 assert!(!Arc::ptr_eq(&original.entries, &clone.entries));
1734 assert!(original.get("c").is_none());
1735 assert!(clone.get("c").is_some());
1736 }
1737
1738 #[test]
1739 fn clone_mutation_preserves_original_hash_and_encoding() {
1740 let original = fixture();
1741 let original_hash = original.hash();
1742 let original_bytes = rmp_serde::to_vec_named(&original).unwrap();
1743 let mut clone = original.clone();
1744
1745 assert!(clone.remove("a").is_some());
1746
1747 assert_eq!(original.hash(), original_hash);
1748 assert_eq!(rmp_serde::to_vec_named(&original).unwrap(), original_bytes);
1749 assert_ne!(clone.hash(), original_hash);
1750 }
1751
1752 #[test]
1753 fn clone_and_mutate_roundtrips_through_durable_encoding() {
1754 let mut tree = fixture().clone();
1755 tree.insert(TreeEntry::directory("dir", ContentHash::compute(b"dir")).unwrap());
1756 let encoded = rmp_serde::to_vec_named(&tree).unwrap();
1757 let decoded: Tree = rmp_serde::from_slice(&encoded).unwrap();
1758
1759 assert_eq!(decoded, tree);
1760 assert_eq!(decoded.hash(), tree.hash());
1761 }
1762}
1763
1764#[cfg(test)]
1765#[path = "tree_golden_tests.rs"]
1766mod tree_golden_tests;