Skip to main content

heddle_object_model/compact/
tree.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use sley::{ObjectFormat as GitObjectFormat, ObjectId as GitObjectId};
4
5use super::{
6    Result, invalid,
7    io::{Reader, Writer, varint_len},
8};
9use crate::object::{ContentHash, EntryType, FileMode, SpoolId, StateId, Tree, TreeEntry};
10
11const TREE_MAGIC: &[u8; 4] = b"HCT1";
12
13/// Whether `bytes` begin with the compact-tree frame discriminator.
14pub fn is_tree_frame(bytes: &[u8]) -> bool {
15    bytes.starts_with(TREE_MAGIC)
16}
17
18/// Exact raw-frame size for `tree`, including its per-tree entry count.
19pub fn encoded_tree_size(tree: &Tree) -> usize {
20    varint_len(tree.len())
21        + tree.len() * 2
22        + tree
23            .entries()
24            .iter()
25            .map(|entry| varint_len(entry.name().len()) + entry.name().len() + target_len(entry))
26            .sum::<usize>()
27}
28
29/// Encode name-sorted trees as columnar mode/kind/name/target payloads.
30pub fn encode_tree_frame(trees: &[Tree]) -> Result<Vec<u8>> {
31    let mut output = Writer::new(TREE_MAGIC);
32    output.put_u64(trees.len() as u64);
33    for tree in trees {
34        tree.validate()?;
35        output.put_u64(tree.len() as u64);
36        for entry in tree.entries() {
37            output.put_u8(entry.mode().to_byte());
38        }
39        for entry in tree.entries() {
40            output.put_u8(entry.entry_type().to_byte());
41        }
42        for entry in tree.entries() {
43            output.put_bytes(entry.name().as_bytes());
44        }
45        for entry in tree.entries() {
46            encode_target(&mut output, entry);
47        }
48    }
49    Ok(output.finish())
50}
51
52/// Decode and whole-frame-verify every tree in a compact frame.
53pub fn decode_tree_frame(bytes: &[u8]) -> Result<Vec<Tree>> {
54    let mut input = Reader::verified(bytes, TREE_MAGIC)?;
55    let tree_count = input.get_count("tree frame")?;
56    let mut trees = Vec::with_capacity(tree_count);
57    for _ in 0..tree_count {
58        trees.push(decode_tree(&mut input)?);
59    }
60    input.finish()?;
61    Ok(trees)
62}
63
64fn decode_tree(input: &mut Reader<'_>) -> Result<Tree> {
65    let count = input.get_count("tree entry")?;
66    let modes = (0..count)
67        .map(|_| decode_mode(input.get_u8()?))
68        .collect::<Result<Vec<_>>>()?;
69    let kinds = (0..count)
70        .map(|_| decode_kind(input.get_u8()?))
71        .collect::<Result<Vec<_>>>()?;
72    let names = (0..count)
73        .map(|_| {
74            String::from_utf8(input.get_bytes()?).map_err(|_| invalid("tree name is not UTF-8"))
75        })
76        .collect::<Result<Vec<_>>>()?;
77    let mut entries = Vec::with_capacity(count);
78    for index in 0..count {
79        entries.push(decode_entry(
80            input,
81            names[index].clone(),
82            kinds[index],
83            modes[index],
84        )?);
85    }
86    let tree = Tree::from_entries(entries);
87    tree.validate()?;
88    Ok(tree)
89}
90
91fn encode_target(output: &mut Writer, entry: &TreeEntry) {
92    match entry.entry_type() {
93        EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
94            output.put_fixed(entry.require_content_hash().as_bytes());
95        }
96        EntryType::Gitlink => {
97            let target = entry.gitlink_target().expect("gitlink target");
98            output.put_u8(git_format_tag(target.format()));
99            output.put_fixed(target.as_bytes());
100        }
101        EntryType::Spoollink => {
102            let (spool, state) = entry.spoollink_target().expect("spoollink target");
103            output.put_bytes(spool.as_str().as_bytes());
104            output.put_fixed(state.as_bytes());
105        }
106    }
107}
108
109fn decode_entry(
110    input: &mut Reader<'_>,
111    name: String,
112    kind: EntryType,
113    mode: FileMode,
114) -> Result<TreeEntry> {
115    let entry = match kind {
116        EntryType::Blob => TreeEntry::file(
117            name,
118            ContentHash::from_bytes(input.get_fixed()?),
119            mode == FileMode::Executable,
120        )?,
121        EntryType::Tree => TreeEntry::directory(name, ContentHash::from_bytes(input.get_fixed()?))?,
122        EntryType::Symlink => {
123            TreeEntry::symlink(name, ContentHash::from_bytes(input.get_fixed()?))?
124        }
125        EntryType::Gitlink => {
126            let format = decode_git_format(input.get_u8()?)?;
127            let oid_len = match format {
128                GitObjectFormat::Sha1 => 20,
129                GitObjectFormat::Sha256 => 32,
130            };
131            let target = GitObjectId::from_raw(format, input.take(oid_len)?)
132                .map_err(|error| super::CompactError::GitObjectId(error.to_string()))?;
133            TreeEntry::gitlink(name, target)?
134        }
135        EntryType::Spoollink => {
136            let spool = String::from_utf8(input.get_bytes()?)
137                .map_err(|_| invalid("spool id is not UTF-8"))?;
138            TreeEntry::spoollink(
139                name,
140                SpoolId::parse(spool)
141                    .map_err(|error| super::CompactError::SpoolId(error.to_string()))?,
142                StateId::from_bytes(input.get_fixed()?),
143            )?
144        }
145    };
146    if entry.mode() != mode {
147        return Err(invalid(format!(
148            "tree kind/mode mismatch for {}: {kind:?}/{mode:?}",
149            entry.name()
150        )));
151    }
152    Ok(entry)
153}
154
155fn target_len(entry: &TreeEntry) -> usize {
156    match entry.entry_type() {
157        EntryType::Blob | EntryType::Tree | EntryType::Symlink => 32,
158        EntryType::Gitlink => {
159            1 + entry
160                .gitlink_target()
161                .expect("gitlink target")
162                .as_bytes()
163                .len()
164        }
165        EntryType::Spoollink => {
166            let (spool, _) = entry.spoollink_target().expect("spoollink target");
167            varint_len(spool.as_str().len()) + spool.as_str().len() + 32
168        }
169    }
170}
171
172fn decode_mode(value: u8) -> Result<FileMode> {
173    FileMode::from_byte(value).ok_or_else(|| invalid(format!("invalid tree mode {value}")))
174}
175
176fn decode_kind(value: u8) -> Result<EntryType> {
177    EntryType::from_byte(value).ok_or_else(|| invalid(format!("invalid tree kind {value}")))
178}
179
180fn git_format_tag(format: GitObjectFormat) -> u8 {
181    match format {
182        GitObjectFormat::Sha1 => 1,
183        GitObjectFormat::Sha256 => 2,
184    }
185}
186
187fn decode_git_format(value: u8) -> Result<GitObjectFormat> {
188    match value {
189        1 => Ok(GitObjectFormat::Sha1),
190        2 => Ok(GitObjectFormat::Sha256),
191        _ => Err(invalid(format!("invalid git object format {value}"))),
192    }
193}