The problem
I wanted to open ext4 images on Windows, which has no native ext4
support. Linux has
ext4-lwext4, a clean userspace ext4 implementation. This sits
between them: WinFsp on one side, an ext4 filesystem on
the other, translating between the two sets of concepts. Anything that
speaks normal Windows file APIs gets ext4 for free.
Why these choices
- WinFsp over Dokan. It ships a real kernel-mode driver instead of needing a filesystem filter driver, so there's no signing or attestation step. Also pure Rust instead of C++.
- ext4-lwext4 over mounting a VHD. No admin rights, no disk-part, no loop device. It reads an image file directly in userspace, which makes it easy to test.
- Rust. The trait that gets implemented has a dozen methods that must all return Windows error codes. Having the compiler check the signatures removes most of the ways to get this wrong.
- Image file, not a physical drive. Deliberately limited. It means the driver can't destroy a real partition, so it can be tested without paranoia.
The mount path is six lines
Most of the complexity of this project is hidden inside the two calls
that matter. What I wrote by hand is the FileSystemContext
impl underneath.
let mut host = WinExtFs::new(context, crc32(CRC32_INIT, &data) as u32);
host.host.mount("Z:").unwrap();
host.host.start().unwrap();
loop {}
The serial number is a CRC of the image, so remounting the same image
gives Windows a consistent identity. The final loop {} is
not a bug - it keeps the process alive because the host has no other
reason to hold the thread.
Windows wants a FILETIME
ext4 hands back Unix timestamps, Windows wants 100-nanosecond intervals since 1601. That offset is the number in the middle, and getting it wrong produces timestamps that land in the wrong century with no error at all.
let mut volume_params = VolumeParams::new();
volume_params.filesystem_name("ext4");
volume_params.volume_creation_time(windows_filetime);
volume_params.volume_serial_number(serial_number);
Directory reads are a marker walk
Windows pages through directories using a marker: "give me what comes after this name." The trap is that you iterate the whole directory on every call and skip until you hit the marker. Get that wrong and Explorer shows a directory with everything but the first entry.
let mut marker_passed = marker.is_none();
let marker_name = match marker.inner_as_cstr() {
Some(m) => Some(m.to_string_lossy()),
None => None,
};
for dir in directories {
if !marker_passed {
if marker_name == Some(name) { marker_passed = true; }
continue;
}
let mut dirinfo: DirInfo<255> = DirInfo::new();
if dirinfo.set_name(name.clone()).is_err() { continue; }
dirinfo.append_to_buffer(buffer, &mut bytes_transferred);
}
Current limitations
- Reads and writes aren't implemented.
readandwriteare still empty, so files show up but won't open. That's the "stability issues" on the card. - File sizes are hardcoded to zero.
file_sizeandallocation_sizeare both set to 0 inget_file_info. Explorer shows every file as empty. - Only regular files and directories. Symlinks, block devices, and FIFOs hit a
panic!. Should return a proper error code instead. - Security descriptors are fake.
attributes: 0x10with a null descriptor. Nothing enforces permissions yet. - Timestamps are "now". Every entry gets the current time rather than the real mtime from ext4, because the metadata isn't threaded through.