use std;
use std::collections::HashMap;
use std::io;
use std::io::Read;
use std::io::Seek;
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt};
use cast::u16;
use crc;
use failure::Error;
use failure::ResultExt;
use assumption_failed;
use not_found;
use parse_error;
use read_le16;
use read_le32;
use unsupported_feature;
use Time;
const EXT4_SUPER_MAGIC: u16 = 0xEF53;
const INODE_BASE_LEN: usize = 128;
const XATTR_MAGIC: u32 = 0xEA02_0000;
bitflags! {
struct CompatibleFeature: u32 {
const DIR_PREALLOC = 0x0001;
const IMAGIC_INODES = 0x0002;
const HAS_JOURNAL = 0x0004;
const EXT_ATTR = 0x0008;
const RESIZE_INODE = 0x0010;
const DIR_INDEX = 0x0020;
const SPARSE_SUPER2 = 0x0200;
}
}
bitflags! {
struct CompatibleFeatureReadOnly: u32 {
const SPARSE_SUPER = 0x0001;
const LARGE_FILE = 0x0002;
const BTREE_DIR = 0x0004;
const HUGE_FILE = 0x0008;
const GDT_CSUM = 0x0010;
const DIR_NLINK = 0x0020;
const EXTRA_ISIZE = 0x0040;
const QUOTA = 0x0100;
const BIGALLOC = 0x0200;
const METADATA_CSUM = 0x0400;
const READONLY = 0x1000;
const PROJECT = 0x2000;
}
}
bitflags! {
struct IncompatibleFeature: u32 {
const COMPRESSION = 0x0001;
const FILETYPE = 0x0002;
const RECOVER = 0x0004;
const JOURNAL_DEV = 0x0008;
const META_BG = 0x0010;
const EXTENTS = 0x0040;
const SIXTY_FOUR_BIT = 0x0080;
const MMP = 0x0100;
const FLEX_BG = 0x0200;
const EA_INODE = 0x0400;
const DIRDATA = 0x1000;
const CSUM_SEED = 0x2000;
const LARGEDIR = 0x4000;
const INLINE_DATA = 0x8000;
const ENCRYPT = 0x10000;
}
}
pub fn superblock<R>(mut reader: R, options: &::Options) -> Result<::SuperBlock<R>, Error>
where
R: io::Read + io::Seek,
{
let mut entire_superblock = [0u8; 1024];
reader.read_exact(&mut entire_superblock)?;
let mut inner = io::Cursor::new(&mut entire_superblock[..]);
inner.read_u32::<LittleEndian>()?;
let s_blocks_count_lo = inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let s_first_data_block = inner.read_u32::<LittleEndian>()?;
let s_log_block_size = inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let s_blocks_per_group = inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let s_inodes_per_group = inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
let s_magic = inner.read_u16::<LittleEndian>()?;
ensure!(
EXT4_SUPER_MAGIC == s_magic,
not_found(format!("invalid magic number: {:x}", s_magic))
);
let s_state = inner.read_u16::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let s_creator_os = inner.read_u32::<LittleEndian>()?;
ensure!(
0 == s_creator_os,
unsupported_feature(format!(
"only support filesystems created on linux, not '{}'",
s_creator_os
))
);
let s_rev_level = inner.read_u32::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let s_inode_size = inner.read_u16::<LittleEndian>()?;
inner.read_u16::<LittleEndian>()?;
let s_feature_compat = inner.read_u32::<LittleEndian>()?;
let compatible_features = CompatibleFeature::from_bits_truncate(s_feature_compat);
let load_xattrs = compatible_features.contains(CompatibleFeature::EXT_ATTR);
let s_feature_incompat = inner.read_u32::<LittleEndian>()?;
let incompatible_features =
IncompatibleFeature::from_bits(s_feature_incompat).ok_or_else(|| {
parse_error(format!(
"completely unsupported incompatible feature flag: {:b}",
s_feature_incompat
))
})?;
let supported_incompatible_features = IncompatibleFeature::FILETYPE
| IncompatibleFeature::EXTENTS
| IncompatibleFeature::FLEX_BG
| IncompatibleFeature::RECOVER
| IncompatibleFeature::SIXTY_FOUR_BIT;
if incompatible_features.intersects(!supported_incompatible_features) {
return Err(parse_error(format!(
"some unsupported incompatible feature flags: {:?}",
incompatible_features & !supported_incompatible_features
)));
}
let long_structs = incompatible_features.contains(IncompatibleFeature::SIXTY_FOUR_BIT);
let s_feature_ro_compat = inner.read_u32::<LittleEndian>()?;
let compatible_features_read_only =
CompatibleFeatureReadOnly::from_bits_truncate(s_feature_ro_compat);
let has_checksums =
compatible_features_read_only.contains(CompatibleFeatureReadOnly::METADATA_CSUM);
ensure!(
!(has_checksums
&& compatible_features_read_only.contains(CompatibleFeatureReadOnly::GDT_CSUM)),
assumption_failed("metadata checksums are incompatible with the GDT checksum feature")
);
ensure!(
has_checksums || ::Checksums::Required != options.checksums,
not_found("checksums are disabled, but required by options")
);
let mut s_uuid = [0; 16];
inner.read_exact(&mut s_uuid)?;
let mut s_volume_name = [0u8; 16];
inner.read_exact(&mut s_volume_name)?;
let mut s_last_mounted = [0u8; 64];
inner.read_exact(&mut s_last_mounted)?;
inner.read_u32::<LittleEndian>()?;
inner.read_u8()?;
inner.read_u8()?;
inner.read_u16::<LittleEndian>()?;
let mut s_journal_uuid = [0u8; 16];
inner.read_exact(&mut s_journal_uuid)?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let mut s_hash_seed = [0u8; 4 * 4];
inner.read_exact(&mut s_hash_seed)?;
inner.read_u8()?;
inner.read_u8()?;
let s_desc_size = inner.read_u16::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
inner.read_u32::<LittleEndian>()?;
let mut s_jnl_blocks = [0; 17 * 4];
inner.read_exact(&mut s_jnl_blocks)?;
let s_blocks_count_hi = if !long_structs {
None
} else {
Some(inner.read_u32::<LittleEndian>()?)
};
if has_checksums {
inner.seek(io::SeekFrom::End(-4))?;
let s_checksum = inner.read_u32::<LittleEndian>()?;
let expected = ext4_style_crc32c_le(!0, &inner.into_inner()[..(1024 - 4)]);
ensure!(
s_checksum == expected,
assumption_failed(format!(
"superblock reports checksums supported, but didn't match: {:x} != {:x}",
s_checksum, expected
))
);
}
{
const S_STATE_UNMOUNTED_CLEANLY: u16 = 0b01;
const S_STATE_ERRORS_DETECTED: u16 = 0b10;
if s_state & S_STATE_UNMOUNTED_CLEANLY == 0 || s_state & S_STATE_ERRORS_DETECTED != 0 {
return Err(parse_error(format!(
"filesystem is not in a clean state: {:b}",
s_state
)));
}
}
if 0 == s_inodes_per_group {
return Err(parse_error("inodes per group cannot be zero".to_string()));
}
let block_size: u32 = match s_log_block_size {
0 => 1024,
1 => 2048,
2 => 4096,
6 => 65536,
_ => {
return Err(parse_error(format!(
"unexpected block size: 2^{}",
s_log_block_size + 10
)));
}
};
if !long_structs {
ensure!(
0 == s_desc_size,
assumption_failed(format!(
"outside long mode, block group desc size must be zero, not {}",
s_desc_size
))
);
}
ensure!(
1 == s_rev_level,
unsupported_feature(format!("rev level {}", s_rev_level))
);
let group_table_pos = if 1024 == block_size {
1024 + 1024 } else {
block_size
};
reader.seek(io::SeekFrom::Start(u64::from(group_table_pos)))?;
let blocks_count = (u64::from(s_blocks_count_lo)
+ (u64::from(s_blocks_count_hi.unwrap_or(0)) << 32)
- u64::from(s_first_data_block) + u64::from(s_blocks_per_group) - 1)
/ u64::from(s_blocks_per_group);
let groups = ::block_groups::BlockGroups::new(
&mut reader,
blocks_count,
s_desc_size,
s_inodes_per_group,
block_size,
s_inode_size,
)?;
let uuid_checksum = if has_checksums {
Some(ext4_style_crc32c_le(!0, &s_uuid))
} else {
None
};
Ok(::SuperBlock {
inner: reader,
load_xattrs,
uuid_checksum,
groups,
})
}
pub struct ParsedInode {
pub stat: ::Stat,
pub flags: ::InodeFlags,
pub core: [u8; ::INODE_CORE_SIZE],
pub checksum_prefix: Option<u32>,
}
pub fn inode<F>(
mut data: Vec<u8>,
load_block: F,
uuid_checksum: Option<u32>,
number: u32,
) -> Result<ParsedInode, Error>
where
F: FnOnce(u64) -> Result<Vec<u8>, Error>,
{
ensure!(
data.len() >= INODE_BASE_LEN,
assumption_failed("inode isn't bigger than the minimum length")
);
let i_mode = read_le16(&data[0x00..0x02]);
let i_uid = read_le16(&data[0x02..0x04]);
let i_size_lo = read_le32(&data[0x04..0x08]);
let i_atime = read_le32(&data[0x08..0x0C]);
let i_ctime = read_le32(&data[0x0C..0x10]);
let i_mtime = read_le32(&data[0x10..0x14]);
let i_gid = read_le16(&data[0x18..0x1A]);
let i_links_count = read_le16(&data[0x1A..0x1C]);
let i_flags = read_le32(&data[0x20..0x24]);
let mut i_block = [0u8; ::INODE_CORE_SIZE];
i_block.clone_from_slice(&data[0x28..0x64]);
let i_generation = read_le32(&data[0x64..0x68]);
let i_file_acl_lo = read_le32(&data[0x68..0x6C]);
let i_size_high = read_le32(&data[0x6C..0x70]);
let l_i_file_acl_high = read_le16(&data[0x76..0x78]);
let l_i_uid_high = read_le16(&data[0x78..0x7A]);
let l_i_gid_high = read_le16(&data[0x7A..0x7C]);
let l_i_checksum_lo = read_le16(&data[0x7C..0x7E]);
let i_extra_isize = if data.len() < 0x82 {
0
} else {
read_le16(&data[0x80..0x82])
};
let inode_end = INODE_BASE_LEN + i_extra_isize as usize;
ensure!(
inode_end <= data.len(),
assumption_failed(format!(
"more extra inode ({}) than inode ({})",
inode_end,
data.len()
))
);
let i_checksum_hi = if i_extra_isize < 2 + 2 {
None
} else {
Some(read_le16(&data[0x82..0x84]))
};
let i_ctime_extra = if i_extra_isize < 6 + 2 {
None
} else {
Some(read_le32(&data[0x84..0x88]))
};
let i_mtime_extra = if i_extra_isize < 10 + 2 {
None
} else {
Some(read_le32(&data[0x88..0x8C]))
};
let i_atime_extra = if i_extra_isize < 14 + 2 {
None
} else {
Some(read_le32(&data[0x8C..0x90]))
};
let i_crtime = if i_extra_isize < 18 + 2 {
None
} else {
Some(read_le32(&data[0x90..0x94]))
};
let i_crtime_extra = if i_extra_isize < 22 + 2 {
None
} else {
Some(read_le32(&data[0x94..0x98]))
};
let mut checksum_prefix = None;
if let Some(uuid_checksum) = uuid_checksum {
data[0x7C] = 0;
data[0x7D] = 0;
let mut bytes = [0u8; 8];
LittleEndian::write_u32(&mut bytes[0..4], number);
LittleEndian::write_u32(&mut bytes[4..8], i_generation);
checksum_prefix = Some(ext4_style_crc32c_le(uuid_checksum, &bytes));
if i_checksum_hi.is_some() {
data[0x82] = 0;
data[0x83] = 0;
}
let computed = ext4_style_crc32c_le(checksum_prefix.unwrap(), &data);
if let Some(high) = i_checksum_hi {
let expected = u32::from(l_i_checksum_lo) | (u32::from(high) << 16);
ensure!(
expected == computed,
assumption_failed(format!(
"full checksum mismatch: on-disc: {:08x} computed: {:08x}",
expected, computed
))
);
} else {
let short_computed = u16(computed & 0xFFFF).unwrap();
ensure!(
l_i_checksum_lo == short_computed,
assumption_failed(format!(
"short checksum mismatch: on-disc: {:04x} computed: {:04x}",
l_i_checksum_lo, short_computed
))
);
}
}
let mut xattrs = HashMap::new();
if inode_end + 4 <= data.len() && XATTR_MAGIC == read_le32(&data[inode_end..(inode_end + 4)]) {
let table_start = &data[inode_end + 4..];
read_xattrs(&mut xattrs, table_start, table_start)?;
}
if 0 != i_file_acl_lo || 0 != l_i_file_acl_high {
let block = u64::from(i_file_acl_lo) | (u64::from(l_i_file_acl_high) << 32);
xattr_block(&mut xattrs, load_block(block)?, uuid_checksum, block)
.with_context(|_| format_err!("loading xattr block {}", block))?
}
let stat = ::Stat {
extracted_type: ::FileType::from_mode(i_mode).ok_or_else(|| {
unsupported_feature(format!("unexpected file type in mode: {:b}", i_mode))
})?,
file_mode: i_mode & 0b111_111_111_111,
uid: u32::from(i_uid) | (u32::from(l_i_uid_high) << 16),
gid: u32::from(i_gid) | (u32::from(l_i_gid_high) << 16),
size: u64::from(i_size_lo) | (u64::from(i_size_high) << 32),
atime: Time {
epoch_secs: i_atime,
nanos: i_atime_extra,
},
ctime: Time {
epoch_secs: i_ctime,
nanos: i_ctime_extra,
},
mtime: Time {
epoch_secs: i_mtime,
nanos: i_mtime_extra,
},
btime: i_crtime.map(|epoch_secs| Time {
epoch_secs,
nanos: i_crtime_extra,
}),
link_count: i_links_count,
xattrs,
};
Ok(ParsedInode {
stat,
flags: ::InodeFlags::from_bits(i_flags).ok_or_else(|| {
unsupported_feature(format!("unrecognised inode flags: {:b}", i_flags))
})?,
core: i_block,
checksum_prefix,
})
}
fn xattr_block(
xattrs: &mut HashMap<String, Vec<u8>>,
mut data: Vec<u8>,
uuid_checksum: Option<u32>,
block_number: u64,
) -> Result<(), Error> {
ensure!(
data.len() > 0x20,
assumption_failed("xattr block is way too short")
);
ensure!(
XATTR_MAGIC == read_le32(&data[0x00..0x04]),
assumption_failed("xattr block contained invalid magic number")
);
let x_blocks_used = read_le32(&data[0x08..0x0C]);
let x_checksum = read_le32(&data[0x10..0x14]);
if let Some(uuid_checksum) = uuid_checksum {
data[0x10] = 0;
data[0x11] = 0;
data[0x12] = 0;
data[0x13] = 0;
let mut bytes = [0u8; 8];
LittleEndian::write_u64(&mut bytes[0..8], block_number);
let base = ext4_style_crc32c_le(uuid_checksum, &bytes);
let computed = ext4_style_crc32c_le(base, &data);
ensure!(
x_checksum == computed,
assumption_failed(format!(
"xattr block checksum invalid: on-disk: {:08x}, computed: {:08x}",
x_checksum, computed
))
);
}
ensure!(
1 == x_blocks_used,
unsupported_feature(format!(
"must have exactly one xattr block, not {}",
x_blocks_used
))
);
read_xattrs(xattrs, &data[0x20..], &data[..])
}
fn read_xattrs(
xattrs: &mut HashMap<String, Vec<u8>>,
mut reading: &[u8],
block_offset_start: &[u8],
) -> Result<(), Error> {
loop {
ensure!(
reading.len() > 0x10,
assumption_failed("out of block while reading xattr header")
);
let e_name_len = reading[0x00];
let e_name_prefix_magic = reading[0x01];
let e_value_offset = read_le16(&reading[0x02..0x04]);
let e_block = read_le32(&reading[0x04..0x08]);
if 0 == e_name_len && 0 == e_name_prefix_magic && 0 == e_value_offset && 0 == e_block {
break;
}
let e_value_size = read_le32(&reading[0x08..0x0C]);
let end_of_name = 0x10 + e_name_len as usize;
ensure!(
reading.len() > end_of_name,
assumption_failed("out of block while reading xattr name")
);
let name_suffix = &reading[0x10..end_of_name];
let name = format!(
"{}{}",
match e_name_prefix_magic {
0 => "",
1 => "user.",
2 => "system.posix_acl_access",
3 => "system.posix_acl_default",
4 => "trusted.",
6 => "security.",
7 => "system.",
_ => bail!(unsupported_feature(format!(
"unsupported name prefix encoding: {}",
e_name_prefix_magic
))),
},
std::str::from_utf8(name_suffix)
.with_context(|_| format_err!("name is invalid utf-8"))?
);
let start = e_value_offset as usize;
let end = start + e_value_size as usize;
ensure!(
start <= block_offset_start.len() && end <= block_offset_start.len(),
assumption_failed(format!(
"xattr value out of range: {}-{} > {}",
start,
end,
block_offset_start.len()
))
);
xattrs.insert(name, block_offset_start[start..end].to_vec());
let next_record = end_of_name + ((4 - (end_of_name % 4)) % 4);
reading = &reading[next_record..];
}
Ok(())
}
pub fn ext4_style_crc32c_le(seed: u32, buf: &[u8]) -> u32 {
crc::crc32::update(seed ^ (!0), &crc::crc32::CASTAGNOLI_TABLE, buf) ^ (!0u32)
}
#[cfg(test)]
mod tests {
use super::ext4_style_crc32c_le;
#[test]
fn crcs() {
assert_eq!(0xffff_ffffu32, !0);
assert_eq!(0x1cf96d7cu32, 0xe3069283u32 ^ !0);
assert_crc(0x1cf96d7c, !0, b"123456789");
assert_crc(0x58e3fa20, 0, b"123456789");
}
fn assert_crc(ex: u32, seed: u32, input: &[u8]) {
let ac = ext4_style_crc32c_le(seed, input);
if ex != ac {
panic!(
"CRC didn't match! ex: {:08x}, ac: {:08x}, len: {}",
ex,
ac,
input.len()
);
}
}
}