use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Extent {
pub lcn: Option<u64>,
pub length: u64,
}
#[derive(Debug, Clone, Default)]
pub struct RunMap {
runs: Vec<Extent>,
starts: Vec<u64>,
}
impl RunMap {
pub fn new(runs: Vec<Extent>) -> Result<Self> {
let mut starts = Vec::with_capacity(runs.len() + 1);
let mut acc: u64 = 0;
for ext in &runs {
starts.push(acc);
acc = acc.checked_add(ext.length).ok_or_else(|| {
crate::Error::InvalidImage("ntfs: run-list VCN length overflow".into())
})?;
}
starts.push(acc);
Ok(Self { runs, starts })
}
pub fn runs(&self) -> &[Extent] {
&self.runs
}
pub fn total_clusters(&self) -> u64 {
*self.starts.last().unwrap_or(&0)
}
pub fn lookup(&self, vcn: u64) -> Option<(Extent, u64)> {
if vcn >= self.total_clusters() {
return None;
}
let idx = match self.starts.binary_search(&vcn) {
Ok(i) => i,
Err(i) => i - 1,
};
let mut idx = idx;
while idx + 1 < self.runs.len() && self.starts[idx + 1] <= vcn {
idx += 1;
}
Some((self.runs[idx], vcn - self.starts[idx]))
}
}
impl From<RunMap> for Vec<Extent> {
fn from(m: RunMap) -> Self {
m.runs
}
}
pub fn decode(buf: &[u8]) -> Result<Vec<Extent>> {
let mut out = Vec::new();
let mut cursor = 0usize;
let mut prev_lcn: i64 = 0;
while cursor < buf.len() {
let header = buf[cursor];
if header == 0 {
break;
}
cursor += 1;
let len_size = (header & 0x0F) as usize;
let off_size = ((header >> 4) & 0x0F) as usize;
if len_size == 0 || len_size > 8 || off_size > 8 {
return Err(crate::Error::InvalidImage(format!(
"ntfs: bad run-list header 0x{header:02x} at offset {cursor}"
)));
}
if cursor + len_size + off_size > buf.len() {
return Err(crate::Error::InvalidImage(
"ntfs: run-list truncated".into(),
));
}
let length = read_unsigned_le(&buf[cursor..cursor + len_size]);
cursor += len_size;
let lcn = if off_size == 0 {
None
} else {
let delta = read_signed_le(&buf[cursor..cursor + off_size]);
cursor += off_size;
prev_lcn = prev_lcn
.checked_add(delta)
.ok_or_else(|| crate::Error::InvalidImage("ntfs: run-list LCN overflow".into()))?;
if prev_lcn < 0 {
return Err(crate::Error::InvalidImage(format!(
"ntfs: run-list produced negative LCN {prev_lcn}"
)));
}
Some(prev_lcn as u64)
};
out.push(Extent { lcn, length });
}
Ok(out)
}
fn read_unsigned_le(b: &[u8]) -> u64 {
let mut v = 0u64;
for (i, &byte) in b.iter().enumerate() {
v |= (byte as u64) << (8 * i);
}
v
}
fn read_signed_le(b: &[u8]) -> i64 {
let n = b.len();
if n == 0 {
return 0;
}
let mut v = 0i64;
for (i, &byte) in b.iter().enumerate() {
v |= (byte as i64) << (8 * i);
}
if n < 8 {
let sign_bit = 1i64 << (8 * n - 1);
if v & sign_bit != 0 {
v |= -1i64 << (8 * n);
}
}
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_single_run() {
let runs = decode(&[0x21, 0x18, 0x34, 0x12, 0x00]).unwrap();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].length, 24);
assert_eq!(runs[0].lcn, Some(4660));
}
#[test]
fn decode_sparse_run() {
let runs = decode(&[0x01, 0x08, 0x00]).unwrap();
assert_eq!(runs[0].lcn, None);
assert_eq!(runs[0].length, 8);
}
#[test]
fn decode_two_runs_relative() {
let runs = decode(&[0x21, 0x10, 0x00, 0x01, 0x21, 0x08, 0x00, 0x01, 0x00]).unwrap();
assert_eq!(runs.len(), 2);
assert_eq!(runs[0].lcn, Some(256));
assert_eq!(runs[1].lcn, Some(512));
}
#[test]
fn decode_negative_delta() {
let runs = decode(&[0x11, 0x04, 0x10, 0x11, 0x04, 0xFF, 0x00]).unwrap();
assert_eq!(runs[0].lcn, Some(0x10));
assert_eq!(runs[1].lcn, Some(0x0F));
}
#[test]
fn read_signed_le_full_width() {
assert_eq!(read_signed_le(&[0xFF; 8]), -1);
assert_eq!(read_signed_le(&0i64.to_le_bytes()), 0);
assert_eq!(read_signed_le(&i64::MIN.to_le_bytes()), i64::MIN);
assert_eq!(read_signed_le(&i64::MAX.to_le_bytes()), i64::MAX);
assert_eq!(
read_signed_le(&0x1234_5678_9abc_def0i64.to_le_bytes()),
0x1234_5678_9abc_def0
);
}
#[test]
fn decode_eight_byte_offset_positive() {
let runs = decode(&[
0x81, 0x04, 0xF0, 0xDE, 0xBC, 0x9A, 0x78, 0x56, 0x34, 0x12, 0x00,
])
.unwrap();
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].length, 4);
assert_eq!(runs[0].lcn, Some(0x1234_5678_9abc_def0));
}
#[test]
fn decode_eight_byte_offset_negative_is_clean_error() {
let runs = decode(&[
0x81, 0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
]);
assert!(matches!(runs, Err(crate::Error::InvalidImage(_))));
}
#[test]
fn run_map_lookup_matches_a_linear_walk() {
let runs = vec![
Extent {
lcn: Some(100),
length: 3,
},
Extent {
lcn: None,
length: 2,
},
Extent {
lcn: Some(50),
length: 4,
},
];
let map = RunMap::new(runs.clone()).unwrap();
assert_eq!(map.total_clusters(), 9);
assert_eq!(map.runs(), &runs[..]);
for vcn in 0..9u64 {
let mut walked = 0u64;
let mut want = None;
for ext in &runs {
if vcn < walked + ext.length {
want = Some((*ext, vcn - walked));
break;
}
walked += ext.length;
}
let got = map.lookup(vcn).unwrap();
let want = want.unwrap();
assert_eq!((got.0.lcn, got.1), (want.0.lcn, want.1), "vcn {vcn}");
}
assert!(map.lookup(9).is_none());
assert!(map.lookup(u64::MAX).is_none());
}
#[test]
fn run_map_skips_zero_length_extents() {
let map = RunMap::new(vec![
Extent {
lcn: Some(7),
length: 0,
},
Extent {
lcn: Some(9),
length: 2,
},
])
.unwrap();
assert_eq!(map.lookup(0).unwrap().0.lcn, Some(9));
assert_eq!(
map.lookup(1).unwrap(),
(
Extent {
lcn: Some(9),
length: 2
},
1
)
);
assert!(map.lookup(2).is_none());
}
#[test]
fn run_map_empty() {
let map = RunMap::new(Vec::new()).unwrap();
assert_eq!(map.total_clusters(), 0);
assert!(map.lookup(0).is_none());
}
}