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