Skip to main content

vfs/package_format/
mod.rs

1pub mod generated;
2#[cfg(not(target_arch = "wasm32"))]
3pub mod pack;
4pub mod versioned;
5
6use std::ops::Range;
7
8use crate::posix::vfs::{VfsError, VfsResult};
9
10pub const AOSPKG_MAGIC: [u8; 4] = [0x89, b'A', b'O', b'S'];
11pub const AOSPKG_FORMAT_VERSION: u16 = 1;
12pub const AOSPKG_HEADER_LEN: usize = 16;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct AospkgHeader {
16    pub manifest_len: u32,
17    pub index_len: u32,
18    pub manifest: Range<usize>,
19    pub index: Range<usize>,
20    pub mount: Range<usize>,
21}
22
23/// Parse the 16-byte `.aospkg` header and return checked chunk ranges.
24///
25/// The container is startup-critical and deliberately simple:
26/// header + chunk1 manifest + chunk2 mount index + chunk3 uncompressed mount.tar.
27/// Projection decodes only chunk1; `TarFileSystem::open` decodes chunk2 and
28/// serves file reads from chunk3 offsets. Do not reintroduce tar header scans,
29/// JSON manifest parsing, or whole-archive reads on these paths.
30pub fn parse_aospkg_header(bytes: &[u8]) -> VfsResult<AospkgHeader> {
31    parse_aospkg_header_from_prefix(bytes, bytes.len())
32}
33
34pub fn parse_aospkg_header_from_prefix(header: &[u8], file_len: usize) -> VfsResult<AospkgHeader> {
35    if header.len() < AOSPKG_HEADER_LEN {
36        return Err(VfsError::new(
37            "EINVAL",
38            format!(
39                ".aospkg header truncated: {} bytes < {AOSPKG_HEADER_LEN} bytes",
40                header.len()
41            ),
42        ));
43    }
44
45    if file_len < AOSPKG_HEADER_LEN {
46        return Err(VfsError::new(
47            "EINVAL",
48            format!(".aospkg file truncated: {file_len} bytes < {AOSPKG_HEADER_LEN} bytes"),
49        ));
50    }
51
52    if header[0..4] != AOSPKG_MAGIC {
53        return Err(VfsError::new("EINVAL", "invalid .aospkg magic"));
54    }
55    let format_version = u16::from_le_bytes([header[4], header[5]]);
56    if format_version != AOSPKG_FORMAT_VERSION {
57        return Err(VfsError::new(
58            "EINVAL",
59            format!("unsupported .aospkg format version: {format_version}"),
60        ));
61    }
62    let flags = u16::from_le_bytes([header[6], header[7]]);
63    if flags != 0 {
64        return Err(VfsError::new(
65            "EINVAL",
66            format!("unsupported .aospkg flags: {flags}"),
67        ));
68    }
69
70    let manifest_len = u32::from_le_bytes([header[8], header[9], header[10], header[11]]);
71    let index_len = u32::from_le_bytes([header[12], header[13], header[14], header[15]]);
72    let manifest_len_usize = usize::try_from(manifest_len).map_err(|_| {
73        VfsError::new(
74            "EOVERFLOW",
75            format!(".aospkg manifest length overflows usize: {manifest_len} bytes"),
76        )
77    })?;
78    let index_len_usize = usize::try_from(index_len).map_err(|_| {
79        VfsError::new(
80            "EOVERFLOW",
81            format!(".aospkg index length overflows usize: {index_len} bytes"),
82        )
83    })?;
84    let index_start = AOSPKG_HEADER_LEN
85        .checked_add(manifest_len_usize)
86        .ok_or_else(|| VfsError::new("EOVERFLOW", ".aospkg manifest range overflows usize"))?;
87    let mount_start = index_start
88        .checked_add(index_len_usize)
89        .ok_or_else(|| VfsError::new("EOVERFLOW", ".aospkg index range overflows usize"))?;
90    if mount_start > file_len {
91        return Err(VfsError::new(
92            "EINVAL",
93            format!(
94                ".aospkg chunks exceed file size: header {AOSPKG_HEADER_LEN} bytes + manifest {manifest_len} bytes + index {index_len} bytes > {file_len} bytes"
95            ),
96        ));
97    }
98
99    Ok(AospkgHeader {
100        manifest_len,
101        index_len,
102        manifest: AOSPKG_HEADER_LEN..index_start,
103        index: index_start..mount_start,
104        mount: mount_start..file_len,
105    })
106}
107
108pub fn encode_aospkg_header(manifest_len: usize, index_len: usize) -> VfsResult<[u8; 16]> {
109    let manifest_len = u32::try_from(manifest_len).map_err(|_| {
110        VfsError::new(
111            "EOVERFLOW",
112            format!(".aospkg manifest chunk too large: {manifest_len} bytes > u32::MAX bytes"),
113        )
114    })?;
115    let index_len = u32::try_from(index_len).map_err(|_| {
116        VfsError::new(
117            "EOVERFLOW",
118            format!(".aospkg index chunk too large: {index_len} bytes > u32::MAX bytes"),
119        )
120    })?;
121    let mut header = [0u8; AOSPKG_HEADER_LEN];
122    header[0..4].copy_from_slice(&AOSPKG_MAGIC);
123    header[4..6].copy_from_slice(&AOSPKG_FORMAT_VERSION.to_le_bytes());
124    header[6..8].copy_from_slice(&0u16.to_le_bytes());
125    header[8..12].copy_from_slice(&manifest_len.to_le_bytes());
126    header[12..16].copy_from_slice(&index_len.to_le_bytes());
127    Ok(header)
128}
129
130/// Read and decode only the chunk1 `PackageManifest` from a `.aospkg` file:
131/// 16-byte header, seek, decode. This is the startup-critical projection read —
132/// it must never parse tar headers, decode chunk2, or touch chunk3. Shared by
133/// every host-side consumer (sidecar projection, actor plugin) so container
134/// framing has exactly one implementation.
135#[cfg(not(target_arch = "wasm32"))]
136pub fn read_manifest_chunk_from_file(
137    path: &std::path::Path,
138) -> VfsResult<generated::v1::PackageManifest> {
139    use std::io::{Read, Seek, SeekFrom};
140    let mut file = std::fs::File::open(path)
141        .map_err(|e| VfsError::new("EIO", format!("open {}: {e}", path.display())))?;
142    let file_len = file
143        .metadata()
144        .map_err(|e| VfsError::new("EIO", format!("stat {}: {e}", path.display())))?
145        .len();
146    let file_len = usize::try_from(file_len).map_err(|_| {
147        VfsError::new(
148            "EOVERFLOW",
149            format!(
150                "{} is too large to address on this platform",
151                path.display()
152            ),
153        )
154    })?;
155    let mut header = [0u8; AOSPKG_HEADER_LEN];
156    file.read_exact(&mut header)
157        .map_err(|e| VfsError::new("EIO", format!("read {} header: {e}", path.display())))?;
158    let parsed = parse_aospkg_header_from_prefix(&header, file_len)?;
159    let mut manifest = vec![0u8; parsed.manifest.len()];
160    file.seek(SeekFrom::Start(parsed.manifest.start as u64))
161        .map_err(|e| VfsError::new("EIO", format!("seek {} manifest: {e}", path.display())))?;
162    file.read_exact(&mut manifest)
163        .map_err(|e| VfsError::new("EIO", format!("read {} manifest: {e}", path.display())))?;
164    versioned::decode_package_manifest(&manifest).map_err(|error| {
165        VfsError::new(
166            "EINVAL",
167            format!("decode package manifest in {}: {error}", path.display()),
168        )
169    })
170}
171
172pub fn validate_mount_range(
173    header: &AospkgHeader,
174    offset: u64,
175    size: u64,
176) -> VfsResult<Range<usize>> {
177    let mount_base = u64::try_from(header.mount.start).map_err(|_| {
178        VfsError::new(
179            "EOVERFLOW",
180            format!(
181                ".aospkg mount base overflows u64: {} bytes",
182                header.mount.start
183            ),
184        )
185    })?;
186    let absolute_start = mount_base
187        .checked_add(offset)
188        .ok_or_else(|| VfsError::new("EOVERFLOW", ".aospkg member start overflows u64"))?;
189    let absolute_end = absolute_start
190        .checked_add(size)
191        .ok_or_else(|| VfsError::new("EOVERFLOW", ".aospkg member end overflows u64"))?;
192    let file_len = u64::try_from(header.mount.end).map_err(|_| {
193        VfsError::new(
194            "EOVERFLOW",
195            format!(
196                ".aospkg file length overflows u64: {} bytes",
197                header.mount.end
198            ),
199        )
200    })?;
201    if absolute_end > file_len {
202        return Err(VfsError::new(
203            "EIO",
204            format!(
205                ".aospkg member range exceeds file size: mountBase {mount_base} bytes + offset {offset} bytes + size {size} bytes > {file_len} bytes"
206            ),
207        ));
208    }
209    let start = usize::try_from(absolute_start).map_err(|_| {
210        VfsError::new(
211            "EOVERFLOW",
212            format!(".aospkg member start overflows usize: {absolute_start} bytes"),
213        )
214    })?;
215    let end = usize::try_from(absolute_end).map_err(|_| {
216        VfsError::new(
217            "EOVERFLOW",
218            format!(".aospkg member end overflows usize: {absolute_end} bytes"),
219        )
220    })?;
221    Ok(start..end)
222}