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}