Skip to main content

bcp_types/
file_tree.rs

1use crate::error::TypeError;
2use crate::fields::{
3    decode_bytes_value, decode_field_header, decode_varint_value, encode_bytes_field,
4    encode_nested_field, encode_varint_field, skip_field,
5};
6
7/// FILE_TREE block — represents a directory structure.
8///
9/// Used to give the LLM spatial context about the project layout.
10/// Entries are nested recursively: directories contain child entries,
11/// which may themselves be directories.
12///
13/// Field layout within body:
14///
15/// ```text
16/// ┌──────────┬───────────┬───────────┬────────────────────────────┐
17/// │ Field ID │ Wire Type │ Name      │ Description                │
18/// ├──────────┼───────────┼───────────┼────────────────────────────┤
19/// │ 1        │ Bytes     │ root_path │ Root directory path        │
20/// │ 2        │ Nested    │ entries   │ Repeated FileEntry         │
21/// └──────────┴───────────┴───────────┴────────────────────────────┘
22/// ```
23///
24/// Each `entries` field (ID=2) contains one `FileEntry` encoded as
25/// nested TLV. Multiple entries produce multiple field-2 occurrences,
26/// similar to protobuf repeated fields.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct FileTreeBlock {
29    pub root_path: String,
30    pub entries: Vec<FileEntry>,
31}
32
33/// A single entry in a file tree — either a file or a directory.
34///
35/// Nested fields within a `FileEntry`:
36///
37/// ```text
38/// ┌──────────┬───────────┬──────────┬─────────────────────────────┐
39/// │ Field ID │ Wire Type │ Name     │ Description                 │
40/// ├──────────┼───────────┼──────────┼─────────────────────────────┤
41/// │ 1        │ Bytes     │ name     │ Entry name (not full path)  │
42/// │ 2        │ Varint    │ kind     │ 0=file, 1=directory         │
43/// │ 3        │ Varint    │ size     │ File size in bytes          │
44/// │ 4        │ Nested    │ children │ Repeated FileEntry (dirs)   │
45/// └──────────┴───────────┴──────────┴─────────────────────────────┘
46/// ```
47///
48/// The `children` field is recursive: a directory entry contains nested
49/// `FileEntry` values, each encoded as a nested TLV sub-message.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct FileEntry {
52    pub name: String,
53    pub kind: FileEntryKind,
54    pub size: u64,
55    pub children: Vec<FileEntry>,
56}
57
58/// Whether a file tree entry is a regular file or a directory.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum FileEntryKind {
61    File = 0,
62    Directory = 1,
63}
64
65impl FileEntry {
66    /// Encode this entry into TLV bytes (used as nested field payload).
67    fn encode(&self) -> Vec<u8> {
68        let mut buf = Vec::new();
69        encode_bytes_field(&mut buf, 1, self.name.as_bytes());
70        encode_varint_field(&mut buf, 2, self.kind as u64);
71        encode_varint_field(&mut buf, 3, self.size);
72        for child in &self.children {
73            encode_nested_field(&mut buf, 4, &child.encode());
74        }
75        buf
76    }
77
78    /// Decode a `FileEntry` from nested TLV bytes.
79    fn decode(mut buf: &[u8]) -> Result<Self, TypeError> {
80        let mut name: Option<String> = None;
81        let mut kind: Option<FileEntryKind> = None;
82        let mut size: u64 = 0;
83        let mut children = Vec::new();
84
85        while !buf.is_empty() {
86            let (header, n) = decode_field_header(buf)?;
87            buf = &buf[n..];
88
89            match header.field_id {
90                1 => {
91                    let (data, n) = decode_bytes_value(buf)?;
92                    buf = &buf[n..];
93                    name = Some(String::from_utf8_lossy(data).into_owned());
94                }
95                2 => {
96                    let (v, n) = decode_varint_value(buf)?;
97                    buf = &buf[n..];
98                    kind = Some(match v {
99                        0 => FileEntryKind::File,
100                        1 => FileEntryKind::Directory,
101                        other => {
102                            return Err(TypeError::InvalidEnumValue {
103                                enum_name: "FileEntryKind",
104                                value: other as u8,
105                            });
106                        }
107                    });
108                }
109                3 => {
110                    let (v, n) = decode_varint_value(buf)?;
111                    buf = &buf[n..];
112                    size = v;
113                }
114                4 => {
115                    let (data, n) = decode_bytes_value(buf)?;
116                    buf = &buf[n..];
117                    children.push(FileEntry::decode(data)?);
118                }
119                _ => {
120                    let n = skip_field(buf, header.wire_type)?;
121                    buf = &buf[n..];
122                }
123            }
124        }
125
126        Ok(Self {
127            name: name.ok_or(TypeError::MissingRequiredField { field: "name" })?,
128            kind: kind.ok_or(TypeError::MissingRequiredField { field: "kind" })?,
129            size,
130            children,
131        })
132    }
133}
134
135impl FileTreeBlock {
136    /// Serialize this block's fields into a TLV-encoded body.
137    pub fn encode_body(&self) -> Vec<u8> {
138        let mut buf = Vec::new();
139        encode_bytes_field(&mut buf, 1, self.root_path.as_bytes());
140        for entry in &self.entries {
141            encode_nested_field(&mut buf, 2, &entry.encode());
142        }
143        buf
144    }
145
146    /// Deserialize a FILE_TREE block from a TLV-encoded body.
147    pub fn decode_body(mut buf: &[u8]) -> Result<Self, TypeError> {
148        let mut root_path: Option<String> = None;
149        let mut entries = Vec::new();
150
151        while !buf.is_empty() {
152            let (header, n) = decode_field_header(buf)?;
153            buf = &buf[n..];
154
155            match header.field_id {
156                1 => {
157                    let (data, n) = decode_bytes_value(buf)?;
158                    buf = &buf[n..];
159                    root_path = Some(String::from_utf8_lossy(data).into_owned());
160                }
161                2 => {
162                    let (data, n) = decode_bytes_value(buf)?;
163                    buf = &buf[n..];
164                    entries.push(FileEntry::decode(data)?);
165                }
166                _ => {
167                    let n = skip_field(buf, header.wire_type)?;
168                    buf = &buf[n..];
169                }
170            }
171        }
172
173        Ok(Self {
174            root_path: root_path.ok_or(TypeError::MissingRequiredField { field: "root_path" })?,
175            entries,
176        })
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn roundtrip_flat_tree() {
186        let block = FileTreeBlock {
187            root_path: "/project".to_string(),
188            entries: vec![
189                FileEntry {
190                    name: "Cargo.toml".to_string(),
191                    kind: FileEntryKind::File,
192                    size: 256,
193                    children: vec![],
194                },
195                FileEntry {
196                    name: "README.md".to_string(),
197                    kind: FileEntryKind::File,
198                    size: 1024,
199                    children: vec![],
200                },
201            ],
202        };
203        let body = block.encode_body();
204        let decoded = FileTreeBlock::decode_body(&body).unwrap();
205        assert_eq!(decoded, block);
206    }
207
208    #[test]
209    fn roundtrip_nested_directories() {
210        let block = FileTreeBlock {
211            root_path: "/app".to_string(),
212            entries: vec![FileEntry {
213                name: "src".to_string(),
214                kind: FileEntryKind::Directory,
215                size: 0,
216                children: vec![
217                    FileEntry {
218                        name: "main.rs".to_string(),
219                        kind: FileEntryKind::File,
220                        size: 512,
221                        children: vec![],
222                    },
223                    FileEntry {
224                        name: "lib".to_string(),
225                        kind: FileEntryKind::Directory,
226                        size: 0,
227                        children: vec![FileEntry {
228                            name: "utils.rs".to_string(),
229                            kind: FileEntryKind::File,
230                            size: 128,
231                            children: vec![],
232                        }],
233                    },
234                ],
235            }],
236        };
237        let body = block.encode_body();
238        let decoded = FileTreeBlock::decode_body(&body).unwrap();
239        assert_eq!(decoded, block);
240    }
241
242    #[test]
243    fn empty_tree() {
244        let block = FileTreeBlock {
245            root_path: "/empty".to_string(),
246            entries: vec![],
247        };
248        let body = block.encode_body();
249        let decoded = FileTreeBlock::decode_body(&body).unwrap();
250        assert_eq!(decoded, block);
251    }
252}