use gix_object::bstr::{BStr, ByteSlice}; use crate::{parse, store_impl::packed}; #[derive(Debug, PartialEq, Eq)] enum Peeled { Unspecified, Partial, Fully, } /// Information parsed from the header of a packed ref file #[derive(Debug, PartialEq, Eq)] pub struct Header { peeled: Peeled, pub sorted: bool, } impl Default for Header { fn default() -> Self { Header { peeled: Peeled::Unspecified, sorted: false, } } } /// Return the bytes before the next line ending as a [`BStr`]. /// /// On success, `input` is advanced past the line ending. The returned slice /// does not include the line ending. fn until_line_end_without_separator<'a>(input: &mut &'a [u8]) -> Result<&'a BStr, ()> { let line_end = input.iter().position(|b| *b == b'\r' || *b == b'\n').ok_or(())?; let out = input[..line_end].as_bstr(); let mut maybe_start_of_newline = &input[line_end..]; parse::newline(&mut maybe_start_of_newline)?; *input = maybe_start_of_newline; Ok(out) } /// Parse a `packed-refs` header line. /// /// A valid header starts with `# pack-refs with: ` and ends with a line ending. /// Known space-separated traits after the prefix populate the returned /// [`Header`]: `peeled`, `fully-peeled`, and `sorted`. Unknown traits are /// ignored. /// /// On success, `input` is advanced past the entire header line, including its /// line ending. pub fn header(input: &mut &[u8]) -> Result { let Some(rest) = input.strip_prefix(b"# pack-refs with: ") else { return Err(()); }; *input = rest; let traits = until_line_end_without_separator(input)?; let mut peeled = Peeled::Unspecified; let mut sorted = false; for token in traits.split_str(b" ") { if token == b"fully-peeled" { peeled = Peeled::Fully; } else if token == b"peeled" { peeled = Peeled::Partial; } else if token == b"sorted" { sorted = true; } } Ok(Header { peeled, sorted }) } /// Parse one packed reference entry and its optional peeled object line. /// /// The reference line has the form ` ` followed by a /// line ending. If the following line starts with `^`, it is parsed as the /// peeled object id for the returned [`packed::Reference`]. /// Object ids are parsed according to `object_hash`. /// /// On success, `input` is advanced past the reference line and, if present, the /// peeled object line. pub fn reference<'a>(input: &mut &'a [u8], object_hash: gix_hash::Kind) -> Result, ()> { let target = parse::hex_hash(input, object_hash)?; let Some(rest) = input.strip_prefix(b" ") else { return Err(()); }; *input = rest; let name = until_line_end_without_separator(input)?.try_into().map_err(|_| ())?; let object = if let Some(rest) = input.strip_prefix(b"^") { *input = rest; let object = parse::hex_hash(input, object_hash)?; parse::newline(input)?; Some(object) } else { None }; Ok(packed::Reference { name, target, object }) } /// Extract the name bytes from a packed-refs record without validating the /// name as a [`FullNameRef`][crate::FullNameRef] or the hash as hex. /// /// Used by the binary search in [`super::Buffer::binary_search_by`], where /// only the name bytes are needed for ordered comparison — the final match /// is re-parsed through [`reference`] which validates fully. /// /// Returns `None` when the record does not have the expected /// `` shape (including an empty name or input /// shorter than the hash plus separator). pub(crate) fn name_at_record_start(input: &[u8], object_hash: gix_hash::Kind) -> Option<&[u8]> { let hex_len = object_hash.len_in_hex(); if input.get(hex_len) != Some(&b' ') { return None; } let after = &input[hex_len + 1..]; let end = after.iter().position(|&b| b == b'\r' || b == b'\n')?; if end == 0 { return None; } if after[end] == b'\r' && after.get(end + 1) != Some(&b'\n') { return None; } Some(&after[..end]) } /// Return the byte offset of the packed-refs record containing `offset`. /// /// If `offset` points into a peeled object line, the returned offset points to /// the owning reference record instead. pub(crate) fn record_start_at_offset(input: &[u8], offset: usize) -> usize { input[..offset] .rfind(b"\n") .and_then(|pos| { let candidate = pos + 1; let b = input.get(candidate)?; if *b == b'^' { input[..pos].rfind(b"\n").map(|pos| pos + 1) } else { Some(candidate) } }) .unwrap_or(0) } /// Return the packed-refs record containing `offset`. /// /// The returned slice begins at the owning reference line. It includes the /// remainder of `input`, which is enough for callers that only need to parse /// the first record. pub(crate) fn record_at_offset(input: &[u8], offset: usize) -> &[u8] { &input[record_start_at_offset(input, offset)..] } #[cfg(test)] mod tests;