/// pub mod from_tree { use std::collections::VecDeque; use bstr::{BStr, BString, ByteSlice, ByteVec}; use gix_object::{tree, tree::EntryKind}; use gix_traverse::tree::{Visit, depthfirst, visit::Action}; use crate::{ Entry, PathStorage, State, Version, entry::{Flags, Mode, Stat}, }; /// The error returned by [State::from_tree()]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error("The path \"{path}\" is invalid")] InvalidComponent { path: BString, source: gix_validate::path::component::Error, }, #[error(transparent)] Traversal(#[from] gix_traverse::tree::depthfirst::Error), } /// Initialization impl State { /// Return a new and empty in-memory index assuming the given `object_hash`. pub fn new(object_hash: gix_hash::Kind) -> Self { State { object_hash, timestamp: filetime::FileTime::now(), version: Version::V2, entries: vec![], path_backing: vec![], is_sparse: false, tree: None, link: None, resolve_undo: None, untracked: None, fs_monitor: None, offset_table_at_decode_time: false, end_of_index_at_decode_time: false, } } /// Create an index [`State`] by traversing `tree` recursively, accessing sub-trees /// with `objects`. /// `validate` is used to determine which validations to perform on every path component we see. /// /// # Security /// /// This currently trusts tree shape beyond individual path-component validation, and is exploitable with /// malicious trees that contain file/directory conflicts like `a` and `a/x`. Such trees can produce an /// index [`State`] with inconsistent paths, which may panic or confuse downstream checkout/index consumers. /// /// A previous normalization pass tried to remove these conflicts after traversal, but only checked adjacent /// entries. That was incomplete because [`Entry::cmp_filepaths()`] can order unrelated paths between a file /// and its conflicting child, for example `a`, `a.`, `a/x`. /// /// **No extension data is currently produced**. pub fn from_tree( tree: &gix_hash::oid, objects: Find, validate: gix_validate::path::component::Options, ) -> Result where Find: gix_object::Find, { let _span = gix_features::trace::coarse!("gix_index::State::from_tree()"); let mut delegate = CollectEntries::new(validate); match depthfirst(tree.to_owned(), depthfirst::State::default(), &objects, &mut delegate) { Ok(()) => {} Err(gix_traverse::tree::breadthfirst::Error::Cancelled) => { let (path, err) = delegate .invalid_path .take() .expect("cancellation only happens on validation error"); return Err(Error::InvalidComponent { path, source: err }); } Err(err) => return Err(err.into()), } let CollectEntries { entries, path_backing, path: _, path_deque: _, validate: _, invalid_path, } = delegate; if let Some((path, err)) = invalid_path { return Err(Error::InvalidComponent { path, source: err }); } Ok(State { object_hash: tree.kind(), timestamp: filetime::FileTime::now(), version: Version::V2, entries, path_backing, is_sparse: false, tree: None, link: None, resolve_undo: None, untracked: None, fs_monitor: None, offset_table_at_decode_time: false, end_of_index_at_decode_time: false, }) } } struct CollectEntries { entries: Vec, path_backing: PathStorage, path: BString, path_deque: VecDeque, validate: gix_validate::path::component::Options, invalid_path: Option<(BString, gix_validate::path::component::Error)>, } impl CollectEntries { pub fn new(validate: gix_validate::path::component::Options) -> CollectEntries { CollectEntries { entries: Vec::new(), path_backing: Vec::new(), path: BString::default(), path_deque: VecDeque::new(), validate, invalid_path: None, } } fn push_element(&mut self, name: &BStr) { if name.is_empty() { return; } if !self.path.is_empty() { self.path.push(b'/'); } self.path.push_str(name); if self.invalid_path.is_none() { if let Err(err) = gix_validate::path::component(name, None, self.validate) { self.invalid_path = Some((self.path.clone(), err)); } } } pub fn add_entry(&mut self, entry: &tree::EntryRef<'_>) { let mode = match entry.mode.kind() { EntryKind::Tree => unreachable!("visit_non_tree() called us"), EntryKind::Blob => Mode::FILE, EntryKind::BlobExecutable => Mode::FILE_EXECUTABLE, EntryKind::Link => Mode::SYMLINK, EntryKind::Commit => Mode::COMMIT, }; // There are leaf-names that require special validation, specific to their mode. // Double-validate just for this case, as the previous validation didn't know the mode yet. if self.invalid_path.is_none() { let start = self.path.rfind_byte(b'/').map(|pos| pos + 1).unwrap_or_default(); if let Err(err) = gix_validate::path::component( self.path[start..].as_ref(), (entry.mode.kind() == EntryKind::Link).then_some(gix_validate::path::component::Mode::Symlink), self.validate, ) { self.invalid_path = Some((self.path.clone(), err)); } } let path_start = self.path_backing.len(); self.path_backing.extend_from_slice(&self.path); let new_entry = Entry { stat: Stat::default(), id: entry.oid.into(), flags: Flags::empty(), mode, path: path_start..self.path_backing.len(), }; self.entries.push(new_entry); } fn determine_action(&self) -> Action { if self.invalid_path.is_none() { std::ops::ControlFlow::Continue(true) } else { std::ops::ControlFlow::Break(()) } } } impl Visit for CollectEntries { fn pop_back_tracked_path_and_set_current(&mut self) { self.path = self.path_deque.pop_back().unwrap_or_default(); } fn pop_front_tracked_path_and_set_current(&mut self) { self.path = self .path_deque .pop_front() .expect("every call is matched with push_tracked_path_component"); } fn push_back_tracked_path_component(&mut self, component: &BStr) { self.push_element(component); self.path_deque.push_back(self.path.clone()); } fn push_path_component(&mut self, component: &BStr) { self.push_element(component); } fn pop_path_component(&mut self) { if let Some(pos) = self.path.rfind_byte(b'/') { self.path.resize(pos, 0); } else { self.path.clear(); } } fn visit_tree(&mut self, _entry: &gix_object::tree::EntryRef<'_>) -> Action { self.determine_action() } fn visit_nontree(&mut self, entry: &gix_object::tree::EntryRef<'_>) -> Action { self.add_entry(entry); self.determine_action() } } }