Skip to main content

sigmf/utils/
file.rs

1//! Low-level tar helpers for `.sigmf` archives.
2//!
3//! A SigMF archive is a tar file whose members are `.sigmf-meta`,
4//! `.sigmf-data`, and optionally `.sigmf-collection` files. These functions
5//! build deterministic tar headers for such members and locate members inside
6//! an existing archive. Most users should work with [`Archive`] instead.
7//!
8//! Functions that look members up by name take the member path without its
9//! extension (including any directory prefix, for example `"dir/rec"`) and the
10//! extension without a leading dot (for example `"sigmf-meta"`). When several
11//! members match, the first one in archive order is used.
12
13use crate::{Archive, MAX_META_SIZE, SigmfError, collection, recording, utils::hash::*};
14use std::fs::File;
15use std::io::Read;
16use tar::{Archive as TarArchive, Entry, Header};
17
18/// Builds a tar header for a `{filename}.sigmf-meta` member of
19/// `meta_bytes.len()` bytes.
20///
21/// The header is a GNU tar header for a regular file with mode `0644`,
22/// uid/gid `0`, and mtime `0`, so archives are reproducible.
23///
24/// # Errors
25///
26/// Returns [`SigmfError::Io`] if the member path cannot be stored in a tar
27/// header (for example, because it is too long).
28pub fn get_meta_header(filename: &str, meta_bytes: &[u8]) -> Result<Header, SigmfError> {
29    get_header(&format!("{filename}.sigmf-meta"), meta_bytes.len() as u64)
30}
31
32/// Builds a tar header for a `{filename}.sigmf-data` member of
33/// `data_bytes.len()` bytes.
34///
35/// The header is a GNU tar header for a regular file with mode `0644`,
36/// uid/gid `0`, and mtime `0`, so archives are reproducible.
37///
38/// # Errors
39///
40/// Returns [`SigmfError::Io`] if the member path cannot be stored in a tar
41/// header (for example, because it is too long).
42pub fn get_data_header(filename: &str, data_bytes: &[u8]) -> Result<Header, SigmfError> {
43    get_header(&format!("{filename}.sigmf-data"), data_bytes.len() as u64)
44}
45
46/// Builds a tar header for a dataset member stored at exactly `member_path`
47/// with a size of `data_bytes.len()` bytes.
48///
49/// Unlike [`get_data_header`], no extension is appended, which allows
50/// datasets named by `core:dataset`.
51///
52/// The header is a GNU tar header for a regular file with mode `0644`,
53/// uid/gid `0`, and mtime `0`, so archives are reproducible.
54///
55/// # Errors
56///
57/// Returns [`SigmfError::InvalidMetadata`] if `member_path` is not valid
58/// UTF-8, or [`SigmfError::Io`] if it cannot be stored in a tar header.
59pub fn get_data_header_for_member(
60    member_path: &std::path::Path,
61    data_bytes: &[u8],
62) -> Result<Header, SigmfError> {
63    let path = member_path.to_str().ok_or_else(|| {
64        SigmfError::InvalidMetadata("archive member path must be valid UTF-8".into())
65    })?;
66    get_header(path, data_bytes.len() as u64)
67}
68
69/// Builds a tar header for a `{filename}.sigmf-collection` member of
70/// `collection_bytes.len()` bytes.
71///
72/// The header is a GNU tar header for a regular file with mode `0644`,
73/// uid/gid `0`, and mtime `0`, so archives are reproducible.
74///
75/// # Errors
76///
77/// Returns [`SigmfError::Io`] if the member path cannot be stored in a tar
78/// header (for example, because it is too long).
79pub fn get_collection_header(
80    filename: &str,
81    collection_bytes: &[u8],
82) -> Result<Header, SigmfError> {
83    get_header(
84        &format!("{filename}.sigmf-collection"),
85        collection_bytes.len() as u64,
86    )
87}
88
89fn get_header(path: &str, size: u64) -> Result<Header, SigmfError> {
90    let mut header = Header::new_gnu();
91    header.set_size(size);
92    header.set_path(path)?;
93    header.set_mode(0o644);
94    header.set_gid(0);
95    header.set_uid(0);
96    header.set_mtime(0);
97    header.set_entry_type(tar::EntryType::Regular);
98    header.set_cksum();
99    Ok(header)
100}
101
102/// Reads the archive member `{name}.{ext}` from the tar file at
103/// `archive_path`, returning its contents and tar header.
104///
105/// No size limit is applied to the member; the whole member is read into
106/// memory.
107///
108/// # Errors
109///
110/// Returns [`SigmfError::NotFound`] if no member matches, or
111/// [`SigmfError::Io`] if the archive cannot be opened or read.
112pub fn get_bytes_in_archive_from_name_ext(
113    archive_path: &str,
114    name: &str,
115    ext: &str,
116) -> Result<(Vec<u8>, Header), SigmfError> {
117    let file = File::open(archive_path)?;
118    let mut archive = TarArchive::new(file);
119
120    for file in archive.entries()? {
121        let mut file = file?;
122        let header = file.header().clone();
123        let file_path = file.path()?.to_path_buf();
124
125        if archive_member_name(&file_path, ext).as_deref() == Some(name) {
126            let mut buffer = Vec::new();
127            file.read_to_end(&mut buffer)?;
128            return Ok((buffer, header));
129        }
130    }
131
132    Err(SigmfError::NotFound("File not found".to_string()))
133}
134
135/// Returns the size in bytes of the archive member `{name}.{ext}` in the tar
136/// file at `archive_path`, without reading its contents.
137///
138/// # Errors
139///
140/// Returns [`SigmfError::NotFound`] if no member matches, or
141/// [`SigmfError::Io`] if the archive cannot be opened or read.
142pub fn get_size_in_archive_from_name_ext(
143    archive_path: &str,
144    name: &str,
145    ext: &str,
146) -> Result<u64, SigmfError> {
147    let file = File::open(archive_path)?;
148    let mut archive = TarArchive::new(file);
149
150    for entry in archive.entries()? {
151        let entry = entry?;
152        let file_path = entry.path()?.to_path_buf();
153        if archive_member_name(&file_path, ext).as_deref() == Some(name) {
154            return Ok(entry.size());
155        }
156    }
157
158    Err(SigmfError::NotFound("File not found".to_string()))
159}
160
161/// Copies the archive member `{name}.{ext}` from the tar file at
162/// `archive_path` into `builder`, preserving its original header.
163///
164/// # Errors
165///
166/// Returns [`SigmfError::NotFound`] if no member matches, or
167/// [`SigmfError::Io`] if reading the source archive or writing to `builder`
168/// fails.
169pub fn append_archive_member_from_name_ext(
170    archive_path: &str,
171    name: &str,
172    ext: &str,
173    builder: &mut tar::Builder<File>,
174) -> Result<(), SigmfError> {
175    let file = File::open(archive_path)?;
176    let mut archive = TarArchive::new(file);
177    for entry in archive.entries()? {
178        let mut entry = entry?;
179        let file_path = entry.path()?.to_path_buf();
180        if archive_member_name(&file_path, ext).as_deref() == Some(name) {
181            let header = entry.header().clone();
182            builder.append(&header, &mut entry)?;
183            return Ok(());
184        }
185    }
186    Err(SigmfError::NotFound(format!(
187        "archive member {name:?} with extension {ext:?} was not found"
188    )))
189}
190
191/// Finds the `.sigmf-meta` member of the tar file at `archive_path` whose
192/// SHA-512 digest equals `hash`, returning its contents and tar header.
193///
194/// `hash` is compared exactly against the lowercase hexadecimal digest
195/// produced by [`hash_from_slice`].
196///
197/// # Errors
198///
199/// Returns [`SigmfError::ResourceLimit`] if a metadata member examined before
200/// a match exceeds the 1,000,000-byte metadata size limit,
201/// [`SigmfError::NotFound`] if no member matches, or [`SigmfError::Io`] if the
202/// archive cannot be opened or read.
203pub fn get_bytes_in_archive_from_hash(
204    archive_path: &str,
205    hash: &str,
206) -> Result<(Vec<u8>, Header), SigmfError> {
207    let file = File::open(archive_path)?;
208    let mut archive = TarArchive::new(file);
209
210    for file in archive.entries()? {
211        let mut file = file?;
212        let file_path = file.path()?.to_path_buf();
213        if file_path
214            .extension()
215            .and_then(|extension| extension.to_str())
216            != Some("sigmf-meta")
217        {
218            continue;
219        }
220        let header = file.header().clone();
221        if file.size() > MAX_META_SIZE as u64 {
222            return Err(SigmfError::ResourceLimit(format!(
223                "metadata member {} exceeds the {} byte limit",
224                file_path.display(),
225                MAX_META_SIZE
226            )));
227        }
228        let mut buffer = Vec::new();
229        file.read_to_end(&mut buffer)?;
230
231        let hash_str = hash_from_slice(&buffer[..])?;
232        if hash_str == hash {
233            return Ok((buffer, header));
234        }
235    }
236
237    Err(SigmfError::NotFound(format!(
238        "File not found using hash {}",
239        hash
240    )))
241}
242
243/// Parses a `.sigmf-meta` tar entry and registers it in `archive` as a
244/// recording named `file_name`.
245///
246/// The recording reference records the SHA-512 digest of the metadata bytes,
247/// and the parsed [`Metadata`](crate::meta::Metadata) is cached in `archive`.
248///
249/// # Errors
250///
251/// Returns [`SigmfError::ResourceLimit`] if the entry exceeds the
252/// 1,000,000-byte metadata size limit, [`SigmfError::Io`] if it cannot be
253/// read, or a parsing error if it is not valid SigMF metadata.
254pub fn process_meta_file(
255    file: &mut Entry<File>,
256    file_name: &str,
257    archive: &mut Archive,
258) -> Result<(), SigmfError> {
259    if file.size() > MAX_META_SIZE as u64 {
260        return Err(SigmfError::ResourceLimit(format!(
261            "metadata member exceeds the {} byte limit",
262            MAX_META_SIZE
263        )));
264    }
265    let mut buffer = Vec::new();
266    file.read_to_end(&mut buffer)?;
267    let metadata = crate::meta::Metadata::from_slice(&buffer)?;
268    let hash = hash_from_slice(&buffer)?;
269    archive.add_recording_with_metadata(recording::RecordingRef::new(file_name, &hash), metadata);
270    Ok(())
271}
272
273/// Parses a `.sigmf-collection` tar entry and stores it in `archive` as the
274/// collection named `name`.
275///
276/// # Errors
277///
278/// Returns [`SigmfError::ResourceLimit`] if the entry exceeds the
279/// 1,000,000-byte metadata size limit, [`SigmfError::Io`] if it cannot be
280/// read, [`SigmfError::InvalidMetadata`] (or a JSON error) if it is not a
281/// valid collection, or an error from
282/// [`Archive::set_collection_member`] if `name` is not a valid top-level
283/// member name.
284pub fn process_collection_file(
285    file: &mut Entry<File>,
286    name: &str,
287    archive: &mut Archive,
288) -> Result<(), SigmfError> {
289    if file.size() > MAX_META_SIZE as u64 {
290        return Err(SigmfError::ResourceLimit(format!(
291            "collection member exceeds the {} byte limit",
292            MAX_META_SIZE
293        )));
294    }
295    let mut buffer = Vec::new();
296    file.read_to_end(&mut buffer)?;
297    let value = crate::json::parse_unique_value(&buffer)?;
298    let collection: collection::Collection = serde_json::from_value(value)
299        .map_err(|error| SigmfError::InvalidMetadata(error.to_string()))?;
300
301    archive.set_collection_member(name, collection)
302}
303
304fn archive_member_name(path: &std::path::Path, extension: &str) -> Option<String> {
305    if path.extension()?.to_str()? != extension {
306        return None;
307    }
308    let stem = path.file_stem()?;
309    let name = path.with_file_name(stem);
310    name.to_str().map(str::to_owned)
311}