-
-
Notifications
You must be signed in to change notification settings - Fork 2
Sparse file support #2727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Sparse file support #2727
Changes from all commits
03b1e13
b1eb5dd
3f5496e
835ae46
150d3fb
ca3f4fa
c13054f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,9 +20,9 @@ | |
| pub(crate) use self::timestamp::{TimeSource, TimestampStrategy}; | ||
| use crate::{ | ||
| cli::{CipherAlgorithmArgs, CompressionAlgorithmArgs, HashAlgorithmArgs, MissingTimePolicy}, | ||
| utils::{self, PathPartExt, fs::HardlinkResolver}, | ||
| utils::{self, PathPartExt, fs::HardlinkResolver, sparse::detect_sparse_map}, | ||
| }; | ||
| use anyhow::Context; | ||
|
Check warning on line 25 in cli/src/command/core.rs
|
||
| pub(crate) use iter::ReorderByIndex; | ||
| pub(crate) use path_filter::PathFilter; | ||
| use path_slash::*; | ||
|
|
@@ -42,7 +42,7 @@ | |
|
|
||
| /// Detected format of an @archive source. | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub(crate) enum SourceFormat { | ||
|
Check warning on line 45 in cli/src/command/core.rs
|
||
| /// PNA archive format (detected by magic bytes) | ||
| Pna, | ||
| /// mtree manifest format (text-based) | ||
|
|
@@ -53,7 +53,7 @@ | |
| /// | ||
| /// Returns `SourceFormat::Pna` if the data starts with PNA magic bytes, | ||
| /// otherwise returns `SourceFormat::Mtree`. | ||
| pub(crate) fn detect_format<R: io::BufRead>(reader: &mut R) -> io::Result<SourceFormat> { | ||
|
Check warning on line 56 in cli/src/command/core.rs
|
||
| let buf = reader.fill_buf()?; | ||
|
|
||
| Ok(if buf.starts_with(PNA_HEADER) { | ||
|
|
@@ -355,6 +355,7 @@ | |
| pub(crate) option: WriteOptions, | ||
| pub(crate) keep_options: KeepOptions, | ||
| pub(crate) pathname_editor: PathnameEditor, | ||
| pub(crate) sparse: bool, | ||
| } | ||
|
|
||
| #[derive(Clone, Debug)] | ||
|
|
@@ -810,6 +811,41 @@ | |
| Ok(()) | ||
| } | ||
|
|
||
| /// Writes file data from a path, detecting and preserving sparse regions. | ||
| /// | ||
| /// If the file is sparse, only data regions are written and the sparse map is set on the entry. | ||
| /// If the file is not sparse, falls back to normal write behavior. | ||
| pub(crate) fn write_sparse_from_path( | ||
| entry: &mut EntryBuilder, | ||
| path: impl AsRef<Path>, | ||
| ) -> io::Result<()> { | ||
| use io::Seek; | ||
|
|
||
| let path = path.as_ref(); | ||
| let mut file = fs::File::open(path)?; | ||
|
|
||
| if let Some(sparse_map) = detect_sparse_map(&file)? { | ||
| // Write only data regions using chunked I/O to avoid memory exhaustion | ||
| const CHUNK_SIZE: usize = 64 * 1024; | ||
| let mut buf = vec![0u8; CHUNK_SIZE]; | ||
| for region in sparse_map.regions() { | ||
| file.seek(io::SeekFrom::Start(region.offset()))?; | ||
| let mut remaining = region.size(); | ||
| while remaining > 0 { | ||
| let to_read = (remaining as usize).min(CHUNK_SIZE); | ||
| file.read_exact(&mut buf[..to_read])?; | ||
| entry.write_all(&buf[..to_read])?; | ||
| remaining -= to_read as u64; | ||
| } | ||
| } | ||
| entry.set_sparse_map(sparse_map); | ||
| Ok(()) | ||
| } else { | ||
| // Not sparse, use normal write | ||
| write_from_path(entry, path) | ||
| } | ||
| } | ||
|
Comment on lines
+814
to
+847
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same Line 835 has the same Proposed fix- let to_read = (remaining as usize).min(CHUNK_SIZE);
+ let to_read = usize::try_from(remaining).unwrap_or(CHUNK_SIZE).min(CHUNK_SIZE);🤖 Prompt for AI Agents |
||
|
|
||
| #[inline] | ||
| pub(crate) fn write_from_path(writer: &mut impl Write, path: impl AsRef<Path>) -> io::Result<()> { | ||
| let path = path.as_ref(); | ||
|
|
@@ -843,6 +879,7 @@ | |
| option, | ||
| keep_options, | ||
| pathname_editor, | ||
| sparse, | ||
| }: &CreateOptions, | ||
| ) -> io::Result<Option<NormalEntry>> { | ||
| let CollectedEntry { | ||
|
|
@@ -869,7 +906,11 @@ | |
| } | ||
| StoreAs::File => { | ||
| let mut entry = EntryBuilder::new_file(entry_name, option)?; | ||
| write_from_path(&mut entry, path)?; | ||
| if *sparse { | ||
| write_sparse_from_path(&mut entry, path)?; | ||
| } else { | ||
| write_from_path(&mut entry, path)?; | ||
| } | ||
| apply_metadata(entry, path, keep_options, metadata)?.build() | ||
| } | ||
| StoreAs::Dir => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -26,7 +26,10 @@ | |||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| use anyhow::Context; | ||||||||||||||||||||||||||||||||
| use clap::{ArgGroup, Parser, ValueHint}; | ||||||||||||||||||||||||||||||||
| use pna::{DataKind, EntryName, EntryReference, NormalEntry, Permission, ReadOptions, prelude::*}; | ||||||||||||||||||||||||||||||||
| use pna::{ | ||||||||||||||||||||||||||||||||
| DataKind, EntryName, EntryReference, NormalEntry, Permission, ReadOptions, SparseMap, | ||||||||||||||||||||||||||||||||
| prelude::*, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| #[cfg(target_os = "macos")] | ||||||||||||||||||||||||||||||||
| use std::os::macos::fs::FileTimesExt; | ||||||||||||||||||||||||||||||||
| #[cfg(windows)] | ||||||||||||||||||||||||||||||||
|
|
@@ -899,7 +902,9 @@ | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| match entry_kind { | ||||||||||||||||||||||||||||||||
| DataKind::File => { | ||||||||||||||||||||||||||||||||
| if *safe_writes { | ||||||||||||||||||||||||||||||||
| let sparse_map = item.sparse_map(); | ||||||||||||||||||||||||||||||||
| if *safe_writes && sparse_map.is_none() { | ||||||||||||||||||||||||||||||||
| // Safe writes (atomic rename) - only for non-sparse files | ||||||||||||||||||||||||||||||||
| let mut safe_writer = SafeWriter::new(&path)?; | ||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||
| let mut writer = | ||||||||||||||||||||||||||||||||
|
|
@@ -911,6 +916,12 @@ | |||||||||||||||||||||||||||||||
| // Set timestamps before persist; after rename we lose the file handle | ||||||||||||||||||||||||||||||||
| restore_timestamps(safe_writer.as_file_mut(), item.metadata(), keep_options)?; | ||||||||||||||||||||||||||||||||
| safe_writer.persist()?; | ||||||||||||||||||||||||||||||||
| } else if let Some(sparse_map) = sparse_map { | ||||||||||||||||||||||||||||||||
| // Sparse file restoration - write data regions at correct offsets | ||||||||||||||||||||||||||||||||
| let mut file = utils::fs::file_create(&path, remove_existing)?; | ||||||||||||||||||||||||||||||||
| let mut reader = item.reader(ReadOptions::with_password(password))?; | ||||||||||||||||||||||||||||||||
| restore_sparse_file(&mut file, &mut reader, sparse_map)?; | ||||||||||||||||||||||||||||||||
| restore_timestamps(&mut file, item.metadata(), keep_options)?; | ||||||||||||||||||||||||||||||||
|
Comment on lines
+919
to
+924
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing explicit file removal before creation in the sparse path. The non-sparse, non-safe-writes branch (Line 926-929) explicitly removes the existing path before calling Proposed fix } else if let Some(sparse_map) = sparse_map {
// Sparse file restoration - write data regions at correct offsets
+ if remove_existing {
+ utils::io::ignore_not_found(utils::fs::remove_path(&path))?;
+ }
let mut file = utils::fs::file_create(&path, remove_existing)?;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| if remove_existing { | ||||||||||||||||||||||||||||||||
| utils::io::ignore_not_found(utils::fs::remove_path(&path))?; | ||||||||||||||||||||||||||||||||
|
|
@@ -1057,7 +1068,7 @@ | |||||||||||||||||||||||||||||||
| MacMetadataStrategy::Always | ||||||||||||||||||||||||||||||||
| ) && item.mac_metadata().is_some(); | ||||||||||||||||||||||||||||||||
| #[cfg(not(target_os = "macos"))] | ||||||||||||||||||||||||||||||||
| let skip_xattr_acl = false; | ||||||||||||||||||||||||||||||||
|
Check warning on line 1071 in cli/src/command/extract.rs
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #[cfg(unix)] | ||||||||||||||||||||||||||||||||
| if !skip_xattr_acl { | ||||||||||||||||||||||||||||||||
|
|
@@ -1311,6 +1322,65 @@ | |||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /// Restores a sparse file by writing only data regions and seeking over holes. | ||||||||||||||||||||||||||||||||
| /// | ||||||||||||||||||||||||||||||||
| /// This creates a sparse file on filesystems that support it by using seek | ||||||||||||||||||||||||||||||||
| /// to skip over hole regions instead of writing zeros. | ||||||||||||||||||||||||||||||||
| fn restore_sparse_file( | ||||||||||||||||||||||||||||||||
| file: &mut fs::File, | ||||||||||||||||||||||||||||||||
| reader: &mut impl Read, | ||||||||||||||||||||||||||||||||
| sparse_map: &SparseMap, | ||||||||||||||||||||||||||||||||
| ) -> io::Result<()> { | ||||||||||||||||||||||||||||||||
| use io::Seek; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let expected_data_size = sparse_map.data_size().ok_or_else(|| { | ||||||||||||||||||||||||||||||||
| io::Error::new( | ||||||||||||||||||||||||||||||||
| io::ErrorKind::InvalidData, | ||||||||||||||||||||||||||||||||
| "Sparse map data size overflow (corrupted archive)", | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| })?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Write each data region at its correct offset using chunked I/O | ||||||||||||||||||||||||||||||||
| const CHUNK_SIZE: usize = 64 * 1024; | ||||||||||||||||||||||||||||||||
| let mut buf = vec![0u8; CHUNK_SIZE]; | ||||||||||||||||||||||||||||||||
| let mut total_read = 0u64; | ||||||||||||||||||||||||||||||||
| for region in sparse_map.regions() { | ||||||||||||||||||||||||||||||||
| file.seek(io::SeekFrom::Start(region.offset()))?; | ||||||||||||||||||||||||||||||||
| let mut remaining = region.size(); | ||||||||||||||||||||||||||||||||
| while remaining > 0 { | ||||||||||||||||||||||||||||||||
| let to_read = (remaining as usize).min(CHUNK_SIZE); | ||||||||||||||||||||||||||||||||
| reader.read_exact(&mut buf[..to_read]).map_err(|e| { | ||||||||||||||||||||||||||||||||
| io::Error::new( | ||||||||||||||||||||||||||||||||
| e.kind(), | ||||||||||||||||||||||||||||||||
| format!( | ||||||||||||||||||||||||||||||||
| "Failed to read sparse data at offset {}: {} \ | ||||||||||||||||||||||||||||||||
| (expected {} bytes total, read {} so far)", | ||||||||||||||||||||||||||||||||
| region.offset(), | ||||||||||||||||||||||||||||||||
| e, | ||||||||||||||||||||||||||||||||
| expected_data_size, | ||||||||||||||||||||||||||||||||
| total_read | ||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| })?; | ||||||||||||||||||||||||||||||||
| file.write_all(&buf[..to_read])?; | ||||||||||||||||||||||||||||||||
| remaining -= to_read as u64; | ||||||||||||||||||||||||||||||||
| total_read += to_read as u64; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+1348
to
+1368
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential infinite loop on 32-bit platforms due to On a 32-bit target, Proposed fix using safe conversion while remaining > 0 {
- let to_read = (remaining as usize).min(CHUNK_SIZE);
+ let to_read = usize::try_from(remaining).unwrap_or(CHUNK_SIZE).min(CHUNK_SIZE);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Verify total bytes read matches expected | ||||||||||||||||||||||||||||||||
| debug_assert_eq!( | ||||||||||||||||||||||||||||||||
| total_read, expected_data_size, | ||||||||||||||||||||||||||||||||
| "Sparse data size mismatch: read {}, expected {}", | ||||||||||||||||||||||||||||||||
| total_read, expected_data_size | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Set the file length to the logical size (handles trailing holes) | ||||||||||||||||||||||||||||||||
| file.set_len(sparse_map.logical_size())?; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| fn ensure_directory_components(path: &Path, unlink_first: bool) -> io::Result<()> { | ||||||||||||||||||||||||||||||||
| if path.as_os_str().is_empty() { | ||||||||||||||||||||||||||||||||
| return Ok(()); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manual chunked reading loop for writing sparse file data regions can be simplified by using
std::io::copywith atakeadapter. This is more idiomatic and removes the need for manual buffer management, improving readability and maintainability. It's also important to check that the number of copied bytes matches the expected region size to handle cases where the source file might be modified during the operation.