Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "blkreader"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
authors = ["SF-Zhou"]
description = "Read file data directly from block device using extent information"
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ blkreader /path/to/file --allow-fallback
| `--zero-unwritten` | Fill unwritten extents with zeros instead of reading raw block data |
| `--allow-fallback` | Allow fallback to regular file I/O when safe |
| `--no-cache` | Disable block device caching |
| `--dry-run` | Skip actual device reads (for testing extent mapping) |

## Options

Expand All @@ -175,6 +176,16 @@ When disabled (default), unwritten extents are read directly from the block devi

When enabled, if the queried extents fully cover the read range and contain no unwritten extents, the read will be performed using regular file I/O instead of direct block device I/O. This avoids the need for root privileges in such cases.

### `dry_run` (default: `false`)

When enabled, no actual I/O operations are performed on block devices or files. Instead, the operation pretends to successfully read the requested amount of data. This is useful for:

- Testing extent mapping logic without performing time-consuming I/O operations
- Validating that a file's extents are accessible
- Debugging and development without needing root privileges

The extent information is still queried via FIEMAP to ensure the file structure is valid, but the actual data reading step is skipped.

## Direct I/O Alignment Requirements

When using the library API to read directly from block devices (not using fallback mode), the following alignment requirements must be met:
Expand Down
7 changes: 6 additions & 1 deletion src/bin/blkreader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ struct Args {
#[arg(long)]
no_cache: bool,

/// Dry run mode - skip actual device reads
#[arg(long)]
dry_run: bool,

/// Alignment for direct IO.
#[arg(long, default_value_t = 512)]
alignment: u64,
Expand Down Expand Up @@ -130,7 +134,8 @@ fn run(args: &Args) -> io::Result<()> {
.with_cache(!args.no_cache)
.with_fill_holes(args.fill_holes)
.with_zero_unwritten(args.zero_unwritten)
.with_allow_fallback(args.allow_fallback);
.with_allow_fallback(args.allow_fallback)
.with_dry_run(args.dry_run);

// Open output file or use stdout
let mut output: Box<dyn Write> = if let Some(output_path) = &args.output {
Expand Down
30 changes: 29 additions & 1 deletion src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ pub struct Options {
/// When disabled, partial reads are allowed and the actual number of
/// bytes read is returned (similar to [`std::io::Read::read`]).
pub read_exact: bool,

/// Dry run mode - skip actual device reads.
///
/// When enabled, no actual I/O operations are performed on block devices
/// or files. Instead, the operation pretends to successfully read the
/// requested amount of data.
///
/// This is useful for testing the extent mapping logic and validating
/// that a file's extents are accessible without performing time-consuming
/// I/O operations.
///
/// When disabled (default), normal read operations are performed.
pub dry_run: bool,
}

impl Default for Options {
Expand All @@ -52,6 +65,7 @@ impl Default for Options {
zero_unwritten: false,
allow_fallback: false,
read_exact: false,
dry_run: false,
}
}
}
Expand Down Expand Up @@ -97,6 +111,17 @@ impl Options {
self.read_exact = exact;
self
}

/// Enable or disable dry run mode.
///
/// When enabled, no actual I/O operations are performed. Instead, the
/// operation pretends to successfully read the requested amount of data.
/// This is useful for testing extent mapping logic without performing
/// time-consuming I/O operations.
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
}

#[cfg(test)]
Expand All @@ -111,6 +136,7 @@ mod tests {
assert!(!opts.zero_unwritten);
assert!(!opts.allow_fallback);
assert!(!opts.read_exact);
assert!(!opts.dry_run);
}

#[test]
Expand All @@ -120,12 +146,14 @@ mod tests {
.with_fill_holes(true)
.with_zero_unwritten(true)
.with_allow_fallback(true)
.with_read_exact(true);
.with_read_exact(true)
.with_dry_run(true);

assert!(!opts.enable_cache);
assert!(opts.fill_holes);
assert!(opts.zero_unwritten);
assert!(opts.allow_fallback);
assert!(opts.read_exact);
assert!(opts.dry_run);
}
}
24 changes: 19 additions & 5 deletions src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ impl<'a> ReadContext<'a> {
extents: Vec<FiemapExtent>,
) -> io::Result<State> {
// Check if we read the exact requested length
let bytes_read = if self.options.read_exact {
let bytes_read = if self.options.dry_run {
// In dry run mode, simulate read without actual I/O
buf.len()
} else if self.options.read_exact {
self.file.read_exact_at(buf, offset)?;
buf.len()
} else {
Expand Down Expand Up @@ -290,7 +293,11 @@ impl<'a> ReadContext<'a> {
// Read from device
let buf_start = bytes_read;
let buf_end = buf_start + read_len;
let actual_read = device.read_at(&mut buf[buf_start..buf_end], physical_offset)?;
let actual_read = device.read_at(
&mut buf[buf_start..buf_end],
physical_offset,
self.options.dry_run,
)?;

bytes_read += actual_read;
current_offset = read_start + actual_read as u64;
Expand Down Expand Up @@ -344,13 +351,18 @@ impl DeviceHandle {
}

/// Read data from the device at the specified physical offset.
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
fn read_at(&self, buf: &mut [u8], offset: u64, dry_run: bool) -> io::Result<usize> {
let file = match self {
DeviceHandle::Cached(cached) => &cached.file,
DeviceHandle::Uncached(uncached) => &uncached.file,
};

let bytes = FileExt::read_at(file, buf, offset)?;
let bytes = if dry_run {
// In dry run mode, simulate read without actual I/O
buf.len()
} else {
FileExt::read_at(file, buf, offset)?
};
Ok(bytes)
}
}
Expand Down Expand Up @@ -390,13 +402,15 @@ mod tests {
.with_fill_holes(true)
.with_zero_unwritten(true)
.with_allow_fallback(true)
.with_read_exact(false);
.with_read_exact(false)
.with_dry_run(true);

assert!(!opts.enable_cache);
assert!(opts.fill_holes);
assert!(opts.zero_unwritten);
assert!(opts.allow_fallback);
assert!(!opts.read_exact);
assert!(opts.dry_run);
}

#[test]
Expand Down