Compare commits

..

18 commits

Author SHA1 Message Date
9cb6ee6446 tried to implement file cluster extension (bug it's still bugged) 2025-08-03 01:32:48 +02:00
343e8c6c77 Update README.md 2025-08-02 17:36:41 +02:00
4df3713c9d DirEntry: fixed bug with writing name 2025-08-02 17:25:52 +02:00
bdd01bd70e implemented setattr (properly) 2025-08-02 17:22:32 +02:00
c9e8833ac6 implemented rmdir 2025-08-02 00:36:35 +02:00
2ed107478e correctly update file size after write 2025-08-01 22:42:13 +02:00
ea3e2a76c4 enter the Rc-RefCell madness 2025-08-01 18:09:45 +02:00
2b01b9ff0e implemented read 2025-08-01 01:08:48 +02:00
e1d458a384 FatFuse: moved drop -> destroy 2025-07-31 23:52:06 +02:00
19340bd4ee look up inodes by path
also DirEntry::name now no longer returns option invalid chars get
replaced by ?
2025-07-31 23:47:45 +02:00
7f6c304709 ls/ll works now 2025-07-31 01:16:53 +02:00
bdcda26d77 cluster 0 as 'empty' marker 2025-07-31 01:13:32 +02:00
7a4d89a09a removed some FUSE ops that will not be implemented (for now) 2025-07-31 01:11:27 +02:00
df20ae6f0c fixed and update fat-mount 2025-07-31 01:07:48 +02:00
f708ab0b50 moved the Rc<RefCell<>> back into FatFs 2025-07-31 01:07:28 +02:00
8b55d8d13c strip 0 terminator from long name 2025-07-31 00:22:16 +02:00
026e145ccb added fat-mount 2025-07-30 22:00:38 +02:00
833fb71108 implemented FUSE lookup function 2025-07-30 21:59:34 +02:00
21 changed files with 2700 additions and 849 deletions

393
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "android-tzdata" name = "android-tzdata"
version = "0.1.1" version = "0.1.1"
@ -17,6 +26,56 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anstream"
version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.98" version = "1.0.98"
@ -41,6 +100,21 @@ version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.30" version = "1.2.30"
@ -74,12 +148,52 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "compact_str"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "compact_string"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5255b88d8ea09573f588088dea17fbea682b4442abea6761a15d1da2c3a76c5c"
dependencies = [
"serde",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "ctrlc"
version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"nix 0.30.1",
"windows-sys",
]
[[package]] [[package]]
name = "enum_dispatch" name = "enum_dispatch"
version = "0.3.13" version = "0.3.13"
@ -92,6 +206,29 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "env_filter"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]] [[package]]
name = "fat-bits" name = "fat-bits"
version = "0.1.0" version = "0.1.0"
@ -99,9 +236,11 @@ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags",
"chrono", "chrono",
"compact_str",
"enum_dispatch", "enum_dispatch",
"log",
"static_assertions", "static_assertions",
"thiserror", "thiserror 2.0.12",
] ]
[[package]] [[package]]
@ -117,13 +256,27 @@ name = "fat-fuse"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags",
"chrono", "chrono",
"compact_string",
"fat-bits", "fat-bits",
"fuser", "fuser",
"fxhash",
"libc", "libc",
"log", "log",
"rand", "rand",
"thiserror", "thiserror 2.0.12",
]
[[package]]
name = "fat-mount"
version = "0.1.0"
dependencies = [
"anyhow",
"ctrlc",
"env_logger",
"fat-fuse",
"fuser",
] ]
[[package]] [[package]]
@ -135,13 +288,22 @@ dependencies = [
"libc", "libc",
"log", "log",
"memchr", "memchr",
"nix", "nix 0.29.0",
"page_size", "page_size",
"pkg-config", "pkg-config",
"smallvec", "smallvec",
"zerocopy", "zerocopy",
] ]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.3" version = "0.3.3"
@ -178,6 +340,42 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.77"
@ -218,6 +416,18 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -233,6 +443,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]] [[package]]
name = "page_size" name = "page_size"
version = "0.6.0" version = "0.6.0"
@ -249,6 +465,21 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@ -291,12 +522,67 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.21" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -326,13 +612,33 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@ -352,6 +658,12 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.2+wasi-0.2.4" version = "0.14.2+wasi-0.2.4"
@ -500,6 +812,79 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"
version = "0.39.0" version = "0.39.0"

View file

@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["fat-bits", "fat-dump", "fat-fuse"] members = ["fat-bits", "fat-dump", "fat-fuse", "fat-mount"]

View file

@ -1 +1,9 @@
specification: [https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf](https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf) # FUSE implementation for the FAT file system
End-to-end implementation of a FAT driver, with the goal of being able to mount disk images with complete read-write support.
Uses (fuser)[https://docs.rs/fuser/latest/fuser/] as the underlying FUSE library.
## Specification
[https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf](https://academy.cba.mit.edu/classes/networking_communications/SD/FAT.pdf)

View file

@ -8,8 +8,11 @@ anyhow = "1.0.98"
bitflags = "2.9.1" bitflags = "2.9.1"
chrono = { version = "0.4.41", default-features = false, features = [ chrono = { version = "0.4.41", default-features = false, features = [
"alloc", "alloc",
"clock",
"std", "std",
] } ] }
compact_str = "0.9.0"
enum_dispatch = "0.3.13" enum_dispatch = "0.3.13"
log = "0.4.27"
static_assertions = "1.1.0" static_assertions = "1.1.0"
thiserror = "2.0.12" thiserror = "2.0.12"

View file

@ -236,6 +236,7 @@ impl Bpb {
if self.fat_type() == FatType::Fat32 { if self.fat_type() == FatType::Fat32 {
return None; return None;
} }
Some(self.fat_offset() + self.sector_to_offset(self.num_fats() as u32 * self.fat_size())) Some(self.fat_offset() + self.sector_to_offset(self.num_fats() as u32 * self.fat_size()))
} }

View file

@ -1,8 +1,6 @@
use std::time::SystemTime; use chrono::{DateTime, Datelike, Local, NaiveDate, NaiveTime, Timelike};
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, Timelike, Utc}; #[derive(Debug, Clone, Copy)]
#[derive(Debug)]
pub struct Date { pub struct Date {
repr: u16, repr: u16,
} }
@ -22,8 +20,7 @@ impl Date {
Ok(date) Ok(date)
} }
#[allow(dead_code)] fn from_day_month_year(day: u8, month: u8, year: u16) -> anyhow::Result<Date> {
pub fn from_day_month_year(day: u8, month: u8, year: u16) -> anyhow::Result<Date> {
anyhow::ensure!(day <= 31, "invalid day: {}", day); anyhow::ensure!(day <= 31, "invalid day: {}", day);
anyhow::ensure!(month <= 12, "invalid month: {}", month); anyhow::ensure!(month <= 12, "invalid month: {}", month);
anyhow::ensure!(1980 <= year && year <= 2107, "invalid year: {}", year); anyhow::ensure!(1980 <= year && year <= 2107, "invalid year: {}", year);
@ -33,10 +30,7 @@ impl Date {
Ok(Date { repr }) Ok(Date { repr })
} }
#[allow(dead_code)] pub fn from_datetime(datetime: DateTime<Local>) -> anyhow::Result<Date> {
pub fn from_system_time(time: SystemTime) -> anyhow::Result<Date> {
let datetime: DateTime<Utc> = time.into();
let date = datetime.date_naive(); let date = datetime.date_naive();
Date::from_day_month_year( Date::from_day_month_year(
@ -46,6 +40,10 @@ impl Date {
) )
} }
pub fn repr(&self) -> u16 {
self.repr
}
pub fn day(&self) -> u8 { pub fn day(&self) -> u8 {
(self.repr & 0x1F) as u8 (self.repr & 0x1F) as u8
} }
@ -63,7 +61,7 @@ impl Date {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, Copy)]
pub struct Time { pub struct Time {
repr: u16, repr: u16,
} }
@ -75,8 +73,7 @@ impl Time {
Ok(time) Ok(time)
} }
#[allow(dead_code)] fn from_seconds_minutes_hours(seconds: u8, minutes: u8, hours: u8) -> anyhow::Result<Time> {
pub fn from_seconds_minutes_hours(seconds: u8, minutes: u8, hours: u8) -> anyhow::Result<Time> {
anyhow::ensure!(seconds <= 58 && seconds % 2 == 0, "invalid seconds: {}", seconds); anyhow::ensure!(seconds <= 58 && seconds % 2 == 0, "invalid seconds: {}", seconds);
anyhow::ensure!(minutes <= 59, "invalid minutes: {}", minutes); anyhow::ensure!(minutes <= 59, "invalid minutes: {}", minutes);
anyhow::ensure!(hours <= 23, "invalid hours: {}", hours); anyhow::ensure!(hours <= 23, "invalid hours: {}", hours);
@ -86,10 +83,7 @@ impl Time {
Ok(Time { repr }) Ok(Time { repr })
} }
#[allow(dead_code)] pub fn from_datetime(datetime: DateTime<Local>) -> anyhow::Result<Time> {
pub fn from_system_time(time: SystemTime) -> anyhow::Result<Time> {
let datetime: DateTime<Utc> = time.into();
let time = datetime.time(); let time = datetime.time();
let seconds = (time.second() as u8) & !0x01; let seconds = (time.second() as u8) & !0x01;
@ -97,6 +91,10 @@ impl Time {
Time::from_seconds_minutes_hours(seconds, time.minute() as u8, time.hour() as u8) Time::from_seconds_minutes_hours(seconds, time.minute() as u8, time.hour() as u8)
} }
pub fn repr(&self) -> u16 {
self.repr
}
pub fn second(&self) -> u8 { pub fn second(&self) -> u8 {
2 * (self.repr & 0x1F) as u8 2 * (self.repr & 0x1F) as u8
} }

View file

@ -1,10 +1,16 @@
use std::fmt::Display; use std::fmt::Display;
use std::io::Read; use std::io::{Read, Write};
use std::time::SystemTime;
use bitflags::bitflags; use bitflags::bitflags;
use chrono::{NaiveDate, NaiveDateTime, TimeDelta}; use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, TimeDelta, Timelike};
use compact_str::CompactString;
use log::debug;
use crate::FatFs;
use crate::datetime::{Date, Time}; use crate::datetime::{Date, Time};
use crate::iter::ClusterChainReader;
use crate::subslice::SubSliceMut;
use crate::utils::{load_u16_le, load_u32_le}; use crate::utils::{load_u16_le, load_u32_le};
bitflags! { bitflags! {
@ -64,30 +70,29 @@ pub struct DirEntry {
file_size: u32, file_size: u32,
// buffer for holding short name str representation checksum: u8,
// initial dot if hidden (1) long_name: Option<CompactString>,
// stem (8)
// dot (1) n_longname_slots: u8,
// extension (3)
short_name: [u8; 13], offset: u64,
long_name: Option<String>,
} }
impl Display for DirEntry { impl Display for DirEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = self.name_str().unwrap_or("<unknown>"); let mut name = self.name_string();
// add slash to end of dir_names if self.attr.contains(Attr::Directory) {
let dir_slash = if self.is_dir() { "/" } else { " " }; name.push('/');
}
write!( write!(
f, f,
"{} {}{}", "{} {}",
self.attr, self.attr,
// self.create_time().format("%a %b %d %H:%M:%S%.3f %Y"), // self.create_time().format("%a %b %d %H:%M:%S%.3f %Y"),
// self.write_time().format("%a %b %d %H:%M:%S%.3f %Y"), // self.write_time().format("%a %b %d %H:%M:%S%.3f %Y"),
name, name,
dir_slash,
)?; )?;
Ok(()) Ok(())
@ -95,12 +100,13 @@ impl Display for DirEntry {
} }
impl DirEntry { impl DirEntry {
pub fn load(bytes: &[u8]) -> anyhow::Result<DirEntry> { pub fn load(bytes: &[u8], offset: u64) -> anyhow::Result<DirEntry> {
assert_eq!(bytes.len(), 32); assert_eq!(bytes.len(), 32);
let name: [u8; 11] = bytes[..11].try_into().unwrap();
let attr = Attr::from_bits_truncate(bytes[11]); let attr = Attr::from_bits_truncate(bytes[11]);
let name = bytes[..11].try_into().unwrap();
let create_time_tenths = bytes[13]; let create_time_tenths = bytes[13];
anyhow::ensure!( anyhow::ensure!(
create_time_tenths <= 199, create_time_tenths <= 199,
@ -136,32 +142,6 @@ impl DirEntry {
) )
} }
let mut short_name = [0; 13];
let mut short_name_iter = short_name.iter_mut();
if attr.contains(Attr::Hidden) {
// if hidden: lead with '.'
*short_name_iter.next().unwrap() = b'.';
}
if name[0] != 0 && name[0] != 0xe5 {
// dir_entry is not free
for c in name[..8].iter().filter(|&x| x != &0x20) {
*short_name_iter.next().unwrap() = *c;
}
if &name[8..] != &[0x20, 0x20, 0x20] {
// if extension is not empty: add dot and extension
*short_name_iter.next().unwrap() = b'.';
for c in name[8..].iter().filter(|&x| x != &0x20) {
*short_name_iter.next().unwrap() = *c;
}
}
}
Ok(DirEntry { Ok(DirEntry {
name, name,
attr, attr,
@ -173,11 +153,95 @@ impl DirEntry {
write_time, write_time,
write_date, write_date,
file_size, file_size,
short_name,
long_name: None, long_name: None,
n_longname_slots: 0,
checksum: Self::checksum(&bytes[..11]),
offset,
}) })
} }
pub fn create(_name: &str, attr: Attr) -> anyhow::Result<Self> {
// TODO
let now: DateTime<Local> = SystemTime::now().into();
let create_date = Date::from_datetime(now)?;
let create_time = Time::from_datetime(now)?;
let create_time_tenths = (now.time().nanosecond() / 100_000_000) as u8;
let name = [0; 11];
Ok(DirEntry {
name,
attr,
create_time_tenths,
create_time,
create_date,
last_access_date: create_date,
first_cluster: 0,
write_time: create_time,
write_date: create_date,
file_size: 0,
checksum: Self::checksum(&name),
long_name: None,
n_longname_slots: 0,
offset: !0,
})
}
fn write(&self, mut writer: impl Write) -> std::io::Result<()> {
let mut buf = [0; 32];
buf[..11].copy_from_slice(self.name());
buf[11] = self.attr().bits();
buf[12] = 0;
buf[13] = self.create_time_tenths;
buf[13..15].copy_from_slice(&self.create_time.repr().to_le_bytes());
buf[16..18].copy_from_slice(&self.create_date.repr().to_le_bytes());
buf[18..20].copy_from_slice(&self.last_access_date.repr().to_le_bytes());
buf[20..22].copy_from_slice(&((self.first_cluster() >> 16) as u16).to_le_bytes());
buf[22..24].copy_from_slice(&self.write_time.repr().to_le_bytes());
buf[24..26].copy_from_slice(&self.write_date.repr().to_le_bytes());
buf[26..28].copy_from_slice(&(self.first_cluster as u16).to_le_bytes());
buf[28..].copy_from_slice(&self.file_size.to_le_bytes());
debug!("self: {self:?}");
debug!("writing new dir entry: {:?}", buf);
writer.write_all(&buf)?;
Ok(())
}
/// write this DisEntry back to the underlying data
pub fn write_back(&self, fat_fs: &FatFs) -> std::io::Result<()> {
eprintln!("making new SubSliceMut at offset {:#X}", self.offset);
let sub_slice = SubSliceMut::new(fat_fs.inner.clone(), self.offset, 32);
self.write(sub_slice)
}
/// erase this DirEntry
pub fn erase(self, fat_fs: &FatFs) -> std::io::Result<()> {
let mut sub_slice = SubSliceMut::new(fat_fs.inner.clone(), self.offset, 32);
// set first byte to 0xE5 (free)
sub_slice.write_all(&[0xe5])?;
// paste over with zeros
sub_slice.write_all(&[0; 31])
// TODO: erase long filename entries as well
}
/// indicates this DirEntry is empty /// indicates this DirEntry is empty
/// ///
/// can be either simply empty (0xe5) or the sentinel (0x00) that indicates that all following /// can be either simply empty (0xe5) or the sentinel (0x00) that indicates that all following
@ -206,8 +270,6 @@ impl DirEntry {
return false; return false;
} }
// &self.name[..2] == &[b'.', b' ']
self.name[0] == b'.' && &self.name[1..] == &[b' '; 10] self.name[0] == b'.' && &self.name[1..] == &[b' '; 10]
} }
@ -216,8 +278,6 @@ impl DirEntry {
return false; return false;
} }
// &self.name[..3] == &[b'.', b'.', b' ']
&self.name[..2] == &[b'.', b'.'] && &self.name[2..] == &[b' '; 9] &self.name[..2] == &[b'.', b'.'] && &self.name[2..] == &[b' '; 9]
} }
@ -249,54 +309,60 @@ impl DirEntry {
std::str::from_utf8(self.extension()).ok() std::str::from_utf8(self.extension()).ok()
} }
fn short_name(&self) -> &[u8] { pub fn name_string(&self) -> CompactString {
&self.short_name // use a CompactString here to allow inlining of short names
} // maybe switch to a Cow instead? has disadvantage that we need to alloc for short names
fn short_name_str(&self) -> Option<&str> { // can't be empty
let mut short_name = self.short_name(); assert!(!self.is_empty());
// remove trailing zeros if let Some(long_filename) = self.long_name() {
while short_name.last() == Some(&0) { return long_filename.into();
short_name = &short_name[..short_name.len() - 1];
} }
std::str::from_utf8(short_name).ok() let name = &self.name[..8];
let ext = &self.name[8..];
let mut s = CompactString::const_new("");
if self.attr.contains(Attr::Hidden) {
s.push('.');
}
const VALID_SYMBOLS: &[u8] = &[
b'$', b'%', b'\'', b'-', b'_', b'@', b'~', b'`', b'!', b'(', b')', b'{', b'}', b'^',
b'#', b'&',
];
fn map_chars(c: u8) -> char {
if !c.is_ascii()
|| c < 0x20
|| !(c.is_ascii_alphanumeric() || VALID_SYMBOLS.contains(&c))
{
'?'
} else {
(c as char).to_ascii_uppercase()
}
}
s.extend(name.trim_ascii_end().iter().copied().map(map_chars));
if !ext.is_empty() {
s.push('.');
s.extend(ext.trim_ascii_end().iter().copied().map(map_chars));
}
s
} }
pub fn long_name(&self) -> Option<&str> { pub fn long_name(&self) -> Option<&str> {
self.long_name.as_deref() self.long_name.as_deref()
} }
pub fn set_long_name(&mut self, long_name: String) { pub fn set_long_name(&mut self, long_name: CompactString, n_slots: u8) {
self.long_name = Some(long_name); self.long_name = Some(long_name);
} self.n_longname_slots = n_slots;
pub fn name_str(&self) -> Option<&str> {
if let Some(long_filename) = self.long_name() {
return Some(long_filename);
}
self.short_name_str()
// let name = std::str::from_utf8(&self.name[..8]).ok()?.trim_ascii_end();
// let ext = std::str::from_utf8(&self.name[8..]).ok()?.trim_ascii_end();
// let mut s = String::new();
// if self.attr.contains(Attr::Hidden) {
// s.push('.');
// }
// s += name;
// if !ext.is_empty() {
// s.push('.');
// s += ext;
// }
// Some(s)
} }
pub fn attr(&self) -> Attr { pub fn attr(&self) -> Attr {
@ -318,6 +384,15 @@ impl DirEntry {
self.last_access_date.to_naive_date() self.last_access_date.to_naive_date()
} }
pub fn update_last_access_date(
&mut self,
time: impl Into<DateTime<Local>>,
) -> anyhow::Result<()> {
self.last_access_date = Date::from_datetime(time.into())?;
Ok(())
}
pub fn first_cluster(&self) -> u32 { pub fn first_cluster(&self) -> u32 {
self.first_cluster self.first_cluster
} }
@ -329,14 +404,27 @@ impl DirEntry {
NaiveDateTime::new(date, time) NaiveDateTime::new(date, time)
} }
pub fn update_write_time(&mut self, time: impl Into<DateTime<Local>>) -> anyhow::Result<()> {
let time = time.into();
self.write_date = Date::from_datetime(time)?;
self.write_time = Time::from_datetime(time)?;
Ok(())
}
pub fn file_size(&self) -> u32 { pub fn file_size(&self) -> u32 {
self.file_size self.file_size
} }
pub fn checksum(&self) -> u8 { pub fn update_file_size(&mut self, file_size: u32) {
self.file_size = file_size
}
pub fn checksum(name: &[u8]) -> u8 {
let mut checksum: u8 = 0; let mut checksum: u8 = 0;
for &x in self.name() { for &x in name {
checksum = checksum.rotate_right(1).wrapping_add(x); checksum = checksum.rotate_right(1).wrapping_add(x);
} }
@ -347,6 +435,7 @@ impl DirEntry {
/// long filename entry in a directory /// long filename entry in a directory
/// ///
/// this should not be exposed to end users, only for internal consumption in the DirIter /// this should not be exposed to end users, only for internal consumption in the DirIter
#[derive(Debug)]
struct LongNameDirEntry { struct LongNameDirEntry {
ordinal: u8, ordinal: u8,
is_last: bool, is_last: bool,
@ -421,13 +510,14 @@ impl LongNameDirEntry {
/// ///
/// should not be exposed publicly, end users only see DirEntries /// should not be exposed publicly, end users only see DirEntries
/// just for making the bytes -> DirEntry loading a bit easier /// just for making the bytes -> DirEntry loading a bit easier
#[derive(Debug)]
enum DirEntryWrapper { enum DirEntryWrapper {
Regular(DirEntry), Regular(DirEntry),
LongName(LongNameDirEntry), LongName(LongNameDirEntry),
} }
impl DirEntryWrapper { impl DirEntryWrapper {
pub fn load(bytes: &[u8]) -> anyhow::Result<DirEntryWrapper> { pub fn load(bytes: &[u8], offset: u64) -> anyhow::Result<DirEntryWrapper> {
assert_eq!(bytes.len(), 32); assert_eq!(bytes.len(), 32);
let attr = Attr::from_bits_truncate(bytes[11]); let attr = Attr::from_bits_truncate(bytes[11]);
@ -435,7 +525,7 @@ impl DirEntryWrapper {
let dir_entry = if attr == Attr::LongName { let dir_entry = if attr == Attr::LongName {
DirEntryWrapper::LongName(LongNameDirEntry::load(bytes)?) DirEntryWrapper::LongName(LongNameDirEntry::load(bytes)?)
} else { } else {
DirEntryWrapper::Regular(DirEntry::load(bytes)?) DirEntryWrapper::Regular(DirEntry::load(bytes, offset)?)
}; };
Ok(dir_entry) Ok(dir_entry)
@ -447,6 +537,7 @@ struct LongFilenameBuf {
rev_buf: Vec<u16>, rev_buf: Vec<u16>,
checksum: Option<u8>, checksum: Option<u8>,
last_ordinal: Option<u8>, last_ordinal: Option<u8>,
n_slots: u8,
} }
impl LongFilenameBuf { impl LongFilenameBuf {
@ -454,28 +545,43 @@ impl LongFilenameBuf {
self.rev_buf.clear(); self.rev_buf.clear();
self.checksum = None; self.checksum = None;
self.last_ordinal = None; self.last_ordinal = None;
self.n_slots = 0;
} }
pub fn next(&mut self, dir_entry: LongNameDirEntry) -> anyhow::Result<()> { pub fn next(&mut self, dir_entry: LongNameDirEntry) -> anyhow::Result<()> {
if dir_entry.is_last() { if dir_entry.is_last() {
// first/lasts entry // first/lasts entry
anyhow::ensure!(
dir_entry.ordinal() <= 20,
"can't have more than 20 long filename dir entries"
);
let mut name = dir_entry.name(); let mut name = dir_entry.name();
while name.last() == Some(&0xFFFF) { while name.last() == Some(&0xFFFF) {
name = &name[..name.len() - 1]; name = &name[..name.len() - 1];
} }
if name.last() == Some(&0) {
name = &name[..name.len() - 1];
} else {
// no null terminator only for names that are multiples of 13, i.e. perfectly fit
assert_eq!(name.len(), 13);
}
assert!(!name.is_empty()); assert!(!name.is_empty());
self.extend_name(name); self.extend_name(name);
self.checksum = Some(dir_entry.checksum()); self.checksum = Some(dir_entry.checksum());
self.last_ordinal = Some(dir_entry.ordinal()); self.last_ordinal = Some(dir_entry.ordinal());
self.n_slots += 1;
return Ok(()); return Ok(());
} }
assert!(self.checksum.is_some()); assert!(self.checksum.is_some() && self.last_ordinal.is_some());
anyhow::ensure!( anyhow::ensure!(
self.checksum == Some(dir_entry.checksum()), self.checksum == Some(dir_entry.checksum()),
@ -503,7 +609,10 @@ impl LongFilenameBuf {
self.rev_buf.extend(name.iter().rev()); self.rev_buf.extend(name.iter().rev());
} }
pub fn get_buf(&mut self, checksum: u8) -> anyhow::Result<Option<impl Iterator<Item = u16>>> { pub fn get_buf(
&mut self,
checksum: u8,
) -> anyhow::Result<Option<(impl Iterator<Item = u16>, u8)>> {
if self.checksum.is_none() { if self.checksum.is_none() {
return Ok(None); return Ok(None);
} }
@ -525,100 +634,97 @@ impl LongFilenameBuf {
self.checksum.unwrap() self.checksum.unwrap()
); );
Ok(Some(self.rev_buf.iter().copied().rev())) anyhow::ensure!(self.rev_buf.len() <= 255, "long filename too long");
Ok(Some((self.rev_buf.iter().copied().rev(), self.n_slots)))
} }
} }
pub struct DirIter<R: Read> { pub struct DirIter<'a> {
reader: R, reader: ClusterChainReader<'a>,
// long_filename_rev_buf: Vec<u16>,
// long_filename_checksum: Option<u8>,
// long_filename_last_ordinal: Option<u8>,
long_filename_buf: LongFilenameBuf, long_filename_buf: LongFilenameBuf,
} }
impl<R: Read> DirIter<R> { impl<'a> DirIter<'a> {
pub fn new(reader: R) -> DirIter<R> { pub fn new(reader: ClusterChainReader<'a>) -> Self {
DirIter { DirIter {
reader, reader,
// long_filename_rev_buf: Vec::new(),
// long_filename_checksum: None,
// long_filename_last_ordinal: None,
long_filename_buf: Default::default(), long_filename_buf: Default::default(),
} }
} }
/// inner function for iterator pub fn find_by_name(&mut self, name: &str) -> Option<DirEntry> {
fn next_impl(&mut self) -> anyhow::Result<Option<DirEntry>> { self.find(|dir_entry| &dir_entry.name_string() == name)
let mut chunk = [0; 32];
if self.reader.read_exact(&mut chunk).is_err() {
// reading failed; nothing we can do here
return Ok(None);
}
// let Ok(dir_entry) = DirEntry::load(&chunk) else {
// return self.next();
// };
let dir_entry = DirEntryWrapper::load(&chunk)
.map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?;
let mut dir_entry = match dir_entry {
DirEntryWrapper::Regular(dir_entry) => dir_entry,
DirEntryWrapper::LongName(long_name) => {
self.long_filename_buf.next(long_name).map_err(|e| {
self.long_filename_buf.reset();
anyhow::anyhow!("invalid long filename entry: {e}")
})?;
return self.next_impl();
}
};
if dir_entry.is_sentinel() {
return Ok(None);
}
if dir_entry.is_empty() {
return self.next_impl();
}
match self
.long_filename_buf
.get_buf(dir_entry.checksum())
.map_err(|e| {
anyhow::anyhow!(
"failed to get long filename for {}: {}",
dir_entry.name_str().as_deref().unwrap_or("<invalid>"),
e
)
})? {
Some(iter) => {
// attach long filename to dir_entry
let long_filename: String =
char::decode_utf16(iter).filter_map(|x| x.ok()).collect();
dir_entry.set_long_name(long_filename);
}
None => {} // no long filename -> do nothing
}
self.long_filename_buf.reset();
Ok(Some(dir_entry))
} }
} }
impl<R: Read> Iterator for DirIter<R> { impl Iterator for DirIter<'_> {
type Item = DirEntry; type Item = DirEntry;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.next_impl() { fn next_impl(me: &mut DirIter<'_>) -> anyhow::Result<Option<DirEntry>> {
let offset = me.reader.current_offset();
let mut chunk = [0; 32];
if me.reader.read_exact(&mut chunk).is_err() {
// nothing we can do here since we might be in an invalid state after a partial read
anyhow::bail!("read failed");
}
let dir_entry = DirEntryWrapper::load(&chunk, offset)
.map_err(|e| anyhow::anyhow!("failed to load dir entry: {e}"))?;
let mut dir_entry = match dir_entry {
DirEntryWrapper::Regular(dir_entry) => dir_entry,
DirEntryWrapper::LongName(long_name) => {
me.long_filename_buf.next(long_name).map_err(|e| {
me.long_filename_buf.reset();
anyhow::anyhow!("invalid long filename entry: {e}")
})?;
return next_impl(me);
}
};
if dir_entry.is_sentinel() {
return Ok(None);
}
if dir_entry.is_empty() {
return next_impl(me);
}
if let Some((iter, n_slots)) = me
.long_filename_buf
.get_buf(dir_entry.checksum)
.map_err(|e| {
anyhow::anyhow!(
"failed to get long filename for {}: {}",
dir_entry.name_string(),
e
)
})?
{
// attach long filename to dir_entry
let long_filename: CompactString =
char::decode_utf16(iter).filter_map(|x| x.ok()).collect();
dir_entry.set_long_name(long_filename, n_slots);
}
me.long_filename_buf.reset();
Ok(Some(dir_entry))
}
match next_impl(self) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
eprintln!("{}", e); // print error message, try next
debug!("{}", e);
self.next() self.next()
} }

View file

@ -1,10 +1,15 @@
use std::fmt::Display; use std::fmt::Display;
use std::io::Write as _;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use log::debug;
use crate::FatType; use crate::FatType;
use crate::subslice::SubSliceMut;
const FREE_ENTRY: u32 = 0;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum FatError { pub enum FatError {
@ -19,26 +24,24 @@ pub enum FatError {
} }
#[enum_dispatch] #[enum_dispatch]
pub trait Fatty { trait FatOps {
// get the next cluster // get the next cluster
// assumes the cluster is valid, i.e. allocated // assumes the cluster is valid, i.e. allocated
fn get_entry(&self, cluster: u32) -> u32; fn get_entry(&self, cluster: u32) -> u32;
fn set_entry(&mut self, cluster: u32, entry: u32);
fn get_valid_clusters(&self) -> RangeInclusive<u32>; fn valid_entries(&self) -> RangeInclusive<u32>;
fn get_reserved_clusters(&self) -> RangeInclusive<u32>; fn reserved_entries(&self) -> RangeInclusive<u32>;
fn get_defective_cluster(&self) -> u32; fn defective_entry(&self) -> u32;
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32>; fn reserved_eof_entries(&self) -> RangeInclusive<u32>;
fn get_eof_cluster(&self) -> u32; fn eof_entry(&self) -> u32;
fn count_free_clusters(&self) -> usize { fn write_to_disk(&self, sub_slice: SubSliceMut) -> std::io::Result<()>;
self.get_valid_clusters()
.map(|cluster| self.get_entry(cluster))
.filter(|&entry| entry == 0)
.count()
}
} }
#[enum_dispatch(Fatty)] #[enum_dispatch(FatOps)]
// others should never touch the inner FatNs directly, but instead go through the Fat type
#[allow(private_interfaces)]
pub enum Fat { pub enum Fat {
Fat12(Fat12), Fat12(Fat12),
Fat16(Fat16), Fat16(Fat16),
@ -64,24 +67,32 @@ impl Fat {
} }
} }
pub fn fat_type(&self) -> FatType {
match self {
Fat::Fat12(_) => FatType::Fat12,
Fat::Fat16(_) => FatType::Fat16,
Fat::Fat32(_) => FatType::Fat32,
}
}
pub fn get_next_cluster(&self, cluster: u32) -> Result<Option<u32>, FatError> { pub fn get_next_cluster(&self, cluster: u32) -> Result<Option<u32>, FatError> {
if cluster == 0x000 { if cluster == FREE_ENTRY {
// can't get next cluster for free cluster // can't get next cluster for free cluster
return Err(FatError::FreeCluster); return Err(FatError::FreeCluster);
} }
if self.get_reserved_clusters().contains(&cluster) { if self.reserved_entries().contains(&cluster) {
// can't get next cluster for reserved cluster // can't get next cluster for reserved cluster
return Err(FatError::ReservedCluster(cluster)); return Err(FatError::ReservedCluster(cluster));
} }
// defective cluster // defective cluster
if cluster == self.get_defective_cluster() { if cluster == self.defective_entry() {
// can't get next cluster for defective cluster // can't get next cluster for defective cluster
return Err(FatError::DefectiveCluster); return Err(FatError::DefectiveCluster);
} }
if self.get_reserved_eof_clusters().contains(&cluster) { if self.reserved_eof_entries().contains(&cluster) {
// Reserved and should not be used. May be interpreted as an allocated cluster and the // Reserved and should not be used. May be interpreted as an allocated cluster and the
// final cluster in the file (indicating end-of-file condition). // final cluster in the file (indicating end-of-file condition).
// //
@ -93,21 +104,117 @@ impl Fat {
let entry = self.get_entry(cluster); let entry = self.get_entry(cluster);
// interpret second reserved block as EOF here if entry == FREE_ENTRY {
if entry == self.get_eof_cluster() || self.get_reserved_eof_clusters().contains(&entry) { Ok(None)
return Ok(None); } else if entry == self.eof_entry() {
Ok(None)
} else if self.valid_entries().contains(&entry) {
Ok(Some(entry))
} else if self.reserved_entries().contains(&entry) || entry == 1 {
Err(FatError::ReservedCluster(entry))
} else if entry == self.defective_entry() {
Err(FatError::DefectiveCluster)
} else {
unreachable!()
}
}
/// set the next cluster of `cluster` to either some `next_cluster` or EOF
///
/// if `cluster` currently points to another cluster, that cluster MUST be the EOF and will be
/// freed
pub fn set_next_cluster(&mut self, cluster: u32, next_cluster: Option<u32>) {
assert!(self.valid_entries().contains(&cluster));
let cur_next_cluster = self.get_entry(cluster);
// can't be defective
assert_ne!(cur_next_cluster, self.defective_entry());
if self.valid_entries().contains(&cur_next_cluster) {
// cluster currently points to a valid cluster, so we free it
log::debug!("freeing chain beginning at {cluster}");
self.free_chain(cluster);
} }
// entry should be in the valid cluster range here; otherwise something went wrong if let Some(next_cluster) = next_cluster {
if !self.get_valid_clusters().contains(&entry) { assert!(self.valid_entries().contains(&next_cluster));
return Err(FatError::InvalidEntry(entry));
} }
Ok(Some(entry)) if let Some(next_cluster) = next_cluster {
log::debug!("setting {cluster} -> {next_cluster}");
} else {
log::debug!("setting {cluster} EOF");
}
self.set_entry(cluster, next_cluster.unwrap_or(self.eof_entry()));
}
/// free a cluster
///
/// must be EOF
pub fn free_cluster(&mut self, cluster: u32) {
debug!("freeing cluster {cluster}");
let entry = self.get_entry(cluster);
if entry == FREE_ENTRY {
// nothing to be done here
debug!("cluster was already free");
return;
}
// can't be reserved or defective
// can't be pointing to another cluster (we'd orphan that one)
// use free_chain to free a chain of clusters iteratively
assert!(self.is_eof(entry));
self.set_entry(cluster, FREE_ENTRY);
}
/// free `first_cluster` and all following clusters
pub fn free_chain(&mut self, mut first_cluster: u32) {
debug!("freeing chaing at {first_cluster}");
loop {
let entry = self.get_entry(first_cluster);
// assert cluster either points to another cluster of is the EOF
assert!(self.valid_entries().contains(&entry) || self.is_eof(entry));
self.set_entry(first_cluster, FREE_ENTRY);
if self.valid_entries().contains(&entry) {
first_cluster = entry;
} else {
break;
}
}
}
pub fn count_free_clusters(&self) -> u32 {
self.valid_entries()
.map(|cluster| self.get_entry(cluster))
.filter(|&entry| entry == FREE_ENTRY)
.count() as u32
}
pub fn first_free_cluster(&self) -> Option<u32> {
self.valid_entries()
.find(|&cluster| self.get_entry(cluster) == FREE_ENTRY)
}
fn is_eof(&self, entry: u32) -> bool {
entry == self.eof_entry() || self.reserved_eof_entries().contains(&entry)
}
pub fn write_back(&self, sub_slice: SubSliceMut) -> std::io::Result<()> {
self.write_to_disk(sub_slice)
} }
} }
pub struct Fat12 { struct Fat12 {
max: u32, max: u32,
next_sectors: Box<[u16]>, next_sectors: Box<[u16]>,
@ -118,7 +225,7 @@ impl Display for Fat12 {
writeln!(f, "Fat 12 {{")?; writeln!(f, "Fat 12 {{")?;
for (i, &x) in self.next_sectors.iter().enumerate() { for (i, &x) in self.next_sectors.iter().enumerate() {
if x != 0 { if x != FREE_ENTRY as u16 {
writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?; writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?;
} }
} }
@ -144,7 +251,10 @@ impl Fat12 {
// assume bytes.len() is multiple of 3 // assume bytes.len() is multiple of 3
// TODO: fix later // TODO: fix later
assert_eq!(bytes.len() % 3, 0); // assert_eq!(bytes.len() % 3, 0);
assert_eq!(max % 2, 0);
let bytes = &bytes[..next_sectors.len() / 2 * 3];
let (chunks, rem) = bytes.as_chunks::<3>(); let (chunks, rem) = bytes.as_chunks::<3>();
@ -178,7 +288,7 @@ impl Fat12 {
} }
} }
impl Fatty for Fat12 { impl FatOps for Fat12 {
fn get_entry(&self, cluster: u32) -> u32 { fn get_entry(&self, cluster: u32) -> u32 {
let cluster = cluster as usize; let cluster = cluster as usize;
assert!(cluster < self.next_sectors.len()); assert!(cluster < self.next_sectors.len());
@ -186,28 +296,74 @@ impl Fatty for Fat12 {
self.next_sectors[cluster] as u32 self.next_sectors[cluster] as u32
} }
fn get_valid_clusters(&self) -> RangeInclusive<u32> { fn set_entry(&mut self, cluster: u32, entry: u32) {
self.next_sectors[cluster as usize] = entry as u16;
}
fn valid_entries(&self) -> RangeInclusive<u32> {
2..=self.max 2..=self.max
} }
fn get_reserved_clusters(&self) -> RangeInclusive<u32> { fn reserved_entries(&self) -> RangeInclusive<u32> {
(self.max as u32 + 1)..=0xFF6 (self.max as u32 + 1)..=0xFF6
} }
fn get_defective_cluster(&self) -> u32 { fn defective_entry(&self) -> u32 {
0xFF7 0xFF7
} }
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32> { fn reserved_eof_entries(&self) -> RangeInclusive<u32> {
0xFF8..=0xFFE 0xFF8..=0xFFE
} }
fn get_eof_cluster(&self) -> u32 { fn eof_entry(&self) -> u32 {
0xFFF 0xFFF
} }
fn write_to_disk(&self, mut sub_slice: SubSliceMut) -> std::io::Result<()> {
assert!(2 * sub_slice.len() > 3 * self.next_sectors.len());
let mut iter = self.next_sectors.chunks_exact(2);
let mut buf: [u8; 3];
for chunk in &mut iter {
// first (even) entry gets truncated
// let first = u16::from_le_bytes(triple[..2].try_into().unwrap()) & 0xFFF;
// second (odd) entry gets shifted
// let second = u16::from_le_bytes(triple[1..].try_into().unwrap()) >> 4;
// assert!(idx + 1 < next_sectors.len());
// next_sectors[2 * idx] = first;
// next_sectors[2 * idx + 1] = second;
// sub_slice.write_all(&entry.to_le_bytes())?;
let first = chunk[0];
let second = chunk[1];
buf = [0; 3];
// buf[..2] |= &first.to_le_bytes();
buf[0] = first.to_le_bytes()[0];
buf[1] = first.to_le_bytes()[1] | (second << 4).to_le_bytes()[0];
buf[2] = (second << 4).to_le_bytes()[1];
sub_slice.write_all(&buf)?;
}
match iter.remainder() {
[] => {}
[x] => sub_slice.write_all(&x.to_le_bytes())?,
_ => unreachable!(),
}
Ok(())
}
} }
pub struct Fat16 { struct Fat16 {
max: u32, max: u32,
next_sectors: Box<[u16]>, next_sectors: Box<[u16]>,
@ -218,7 +374,7 @@ impl Display for Fat16 {
writeln!(f, "Fat 16 {{")?; writeln!(f, "Fat 16 {{")?;
for (i, &x) in self.next_sectors.iter().enumerate() { for (i, &x) in self.next_sectors.iter().enumerate() {
if x != 0 { if x != FREE_ENTRY as u16 {
writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?; writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?;
} }
} }
@ -262,7 +418,7 @@ impl Fat16 {
} }
} }
impl Fatty for Fat16 { impl FatOps for Fat16 {
fn get_entry(&self, cluster: u32) -> u32 { fn get_entry(&self, cluster: u32) -> u32 {
let cluster = cluster as usize; let cluster = cluster as usize;
assert!(cluster < self.next_sectors.len()); assert!(cluster < self.next_sectors.len());
@ -270,28 +426,42 @@ impl Fatty for Fat16 {
self.next_sectors[cluster] as u32 self.next_sectors[cluster] as u32
} }
fn get_valid_clusters(&self) -> RangeInclusive<u32> { fn set_entry(&mut self, cluster: u32, entry: u32) {
self.next_sectors[cluster as usize] = entry as u16;
}
fn valid_entries(&self) -> RangeInclusive<u32> {
2..=self.max 2..=self.max
} }
fn get_reserved_clusters(&self) -> RangeInclusive<u32> { fn reserved_entries(&self) -> RangeInclusive<u32> {
(self.max as u32 + 1)..=0xFFF6 (self.max as u32 + 1)..=0xFFF6
} }
fn get_defective_cluster(&self) -> u32 { fn defective_entry(&self) -> u32 {
0xFFF7 0xFFF7
} }
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32> { fn reserved_eof_entries(&self) -> RangeInclusive<u32> {
0xFFF8..=0xFFFE 0xFFF8..=0xFFFE
} }
fn get_eof_cluster(&self) -> u32 { fn eof_entry(&self) -> u32 {
0xFFFF 0xFFFF
} }
fn write_to_disk(&self, mut sub_slice: SubSliceMut) -> std::io::Result<()> {
assert_eq!(2 * sub_slice.len(), self.next_sectors.len());
for &entry in self.next_sectors.iter() {
sub_slice.write_all(&entry.to_le_bytes())?;
}
Ok(())
}
} }
pub struct Fat32 { struct Fat32 {
max: u32, max: u32,
next_sectors: Box<[u32]>, next_sectors: Box<[u32]>,
@ -302,7 +472,7 @@ impl Display for Fat32 {
writeln!(f, "Fat 32 {{")?; writeln!(f, "Fat 32 {{")?;
for (i, &x) in self.next_sectors.iter().enumerate() { for (i, &x) in self.next_sectors.iter().enumerate() {
if x != 0 { if x != FREE_ENTRY {
writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?; writeln!(f, " 0x{:03X} -> 0x{:03X}", i, x)?;
} }
} }
@ -346,7 +516,7 @@ impl Fat32 {
} }
} }
impl Fatty for Fat32 { impl FatOps for Fat32 {
fn get_entry(&self, cluster: u32) -> u32 { fn get_entry(&self, cluster: u32) -> u32 {
let cluster = cluster as usize; let cluster = cluster as usize;
assert!(cluster < self.next_sectors.len()); assert!(cluster < self.next_sectors.len());
@ -354,23 +524,37 @@ impl Fatty for Fat32 {
self.next_sectors[cluster] as u32 self.next_sectors[cluster] as u32
} }
fn get_valid_clusters(&self) -> RangeInclusive<u32> { fn set_entry(&mut self, cluster: u32, entry: u32) {
self.next_sectors[cluster as usize] = entry;
}
fn valid_entries(&self) -> RangeInclusive<u32> {
2..=self.max 2..=self.max
} }
fn get_reserved_clusters(&self) -> RangeInclusive<u32> { fn reserved_entries(&self) -> RangeInclusive<u32> {
(self.max + 1)..=0xFFFFFFF6 (self.max + 1)..=0xFFFFFFF6
} }
fn get_defective_cluster(&self) -> u32 { fn defective_entry(&self) -> u32 {
0xFFFFFFF7 0xFFFFFFF7
} }
fn get_reserved_eof_clusters(&self) -> RangeInclusive<u32> { fn reserved_eof_entries(&self) -> RangeInclusive<u32> {
0xFFFFFFF8..=0xFFFFFFFE 0xFFFFFFF8..=0xFFFFFFFE
} }
fn get_eof_cluster(&self) -> u32 { fn eof_entry(&self) -> u32 {
0xFFFFFFFF 0xFFFFFFFF
} }
fn write_to_disk(&self, mut sub_slice: SubSliceMut) -> std::io::Result<()> {
assert_eq!(4 * sub_slice.len(), self.next_sectors.len());
for &entry in self.next_sectors.iter() {
sub_slice.write_all(&entry.to_le_bytes())?;
}
Ok(())
}
} }

View file

@ -40,4 +40,12 @@ impl FsInfo {
next_free, next_free,
}) })
} }
pub fn free_count(&self) -> u32 {
self.free_count
}
pub fn next_free(&self) -> Option<u32> {
Some(self.next_free)
}
} }

View file

@ -1,48 +1,94 @@
use std::io::Read; use std::io::{Read, Write};
use crate::FatFs; use log::debug;
use crate::subslice::SubSlice;
use crate::utils::replace; use crate::subslice::{SubSlice, SubSliceMut};
use crate::{FatFs, FatType};
pub struct ClusterChainReader<'a> { pub struct ClusterChainReader<'a> {
sub_slice: SubSlice<'a>, fat_fs: &'a FatFs,
sub_slice: SubSlice,
next_cluster: Option<u32>, next_cluster: Option<u32>,
} }
impl<'a> ClusterChainReader<'a> { impl<'a> ClusterChainReader<'a> {
pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> ClusterChainReader<'a> { pub fn new(fat_fs: &'a FatFs, first_cluster: u32) -> Self {
let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None); let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None);
let sub_slice = fat_fs.cluster_as_subslice(first_cluster); let sub_slice = fat_fs.cluster_as_subslice(first_cluster);
ClusterChainReader { ClusterChainReader {
fat_fs,
sub_slice, sub_slice,
next_cluster, next_cluster,
} }
} }
fn next_cluster(&mut self) -> bool { pub fn root_dir_reader(fat_fs: &'a FatFs) -> Self {
match fat_fs.fat_type() {
FatType::Fat12 | FatType::Fat16 => {
// fixed root dir, so no need to chain
// get a single SubSlice for it and next_cluster is None
let sub_slice = fat_fs.root_dir_as_subslice();
ClusterChainReader {
fat_fs,
sub_slice,
next_cluster: None,
}
}
FatType::Fat32 => {
// FAT is directory_like, so get a real chain reader
Self::new(fat_fs, fat_fs.bpb.root_cluster().unwrap())
}
}
}
fn move_to_next_cluster(&mut self) -> bool {
let Some(next_cluster) = self.next_cluster else { let Some(next_cluster) = self.next_cluster else {
return false; return false;
}; };
replace(&mut self.sub_slice, |sub_slice| { self.next_cluster = self.fat_fs.next_cluster(next_cluster).unwrap_or(None);
let fat_fs = sub_slice.release(); self.sub_slice = self.fat_fs.cluster_as_subslice(next_cluster);
self.next_cluster = fat_fs.next_cluster(next_cluster).unwrap_or(None);
fat_fs.cluster_as_subslice(next_cluster)
});
true true
} }
pub fn skip(&mut self, n: u64) -> u64 {
let mut bytes_to_skip = n;
while bytes_to_skip > self.sub_slice.len() as u64 {
bytes_to_skip -= self.sub_slice.len() as u64;
if !self.move_to_next_cluster() {
// ran out of bytes to seek
return n - bytes_to_skip;
}
}
if bytes_to_skip != 0 {
bytes_to_skip -= self.sub_slice.skip(bytes_to_skip as usize) as u64;
}
// n should absolutely be zero here
assert_eq!(bytes_to_skip, 0);
n
}
pub fn current_offset(&self) -> u64 {
self.sub_slice.offset()
}
} }
impl<'a> Read for ClusterChainReader<'a> { impl Read for ClusterChainReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.sub_slice.is_empty() { if self.sub_slice.is_empty() {
if !self.next_cluster() { if !self.move_to_next_cluster() {
return Ok(0); return Ok(0);
} }
} }
@ -50,3 +96,127 @@ impl<'a> Read for ClusterChainReader<'a> {
self.sub_slice.read(buf) self.sub_slice.read(buf)
} }
} }
pub struct ClusterChainWriter<'a> {
fat_fs: &'a mut FatFs,
sub_slice: SubSliceMut,
// next_cluster: Option<u32>,
cur_cluster: u32,
}
impl<'a> ClusterChainWriter<'a> {
pub fn new(fat_fs: &'a mut FatFs, first_cluster: u32) -> Self {
// let next_cluster = fat_fs.next_cluster(first_cluster).unwrap_or(None);
let sub_slice = fat_fs.cluster_as_subslice_mut(first_cluster);
ClusterChainWriter {
fat_fs,
sub_slice,
cur_cluster: first_cluster,
}
}
pub fn root_dir_writer(fat_fs: &'a mut FatFs) -> Self {
match fat_fs.fat_type() {
FatType::Fat12 | FatType::Fat16 => {
// fixed root dir, so no need to chain
// get a single SubSliceMut for it and next_cluster is None
let sub_slice = fat_fs.root_dir_as_subslice_mut();
ClusterChainWriter {
fat_fs,
sub_slice,
cur_cluster: 0,
}
}
FatType::Fat32 => {
// FAT is directory_like, so get a real chain writer
Self::new(fat_fs, fat_fs.bpb.root_cluster().unwrap())
}
}
}
fn move_to_next_cluster(&mut self) -> bool {
// TODO: should allocate a new cluster here!
// let Some(next_cluster) = self.next_cluster else {
// let Some(new_cluster) = self.fat_fs.alloc_cluster() else {
// // cluster allocation failed
// return false;
// };
// return false;
// };
let Some(next_cluster) = self
.fat_fs
.next_cluster(self.cur_cluster)
.map_err(|err| {
debug!("failed to get next cluster: {err}");
err
})
.unwrap_or(None)
.or_else(|| {
debug!("allocating new cluster");
self.fat_fs.alloc_cluster(Some(self.cur_cluster))
})
else {
debug!("failed to allocate next cluster");
return false;
};
debug!("next cluster: {next_cluster}");
self.fat_fs.cluster_as_subslice_mut(next_cluster);
self.cur_cluster = next_cluster;
true
}
pub fn skip(&mut self, n: u64) -> u64 {
let mut bytes_to_skip = n;
while bytes_to_skip > self.sub_slice.len() as u64 {
bytes_to_skip -= self.sub_slice.len() as u64;
if !self.move_to_next_cluster() {
// ran out of bytes to seek
return n - bytes_to_skip;
}
}
if bytes_to_skip != 0 {
bytes_to_skip -= self.sub_slice.skip(bytes_to_skip as usize) as u64;
}
// n should absolutely be zero here
assert_eq!(bytes_to_skip, 0);
n
}
pub fn current_offset(&self) -> u64 {
self.sub_slice.offset()
}
}
impl Write for ClusterChainWriter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.sub_slice.is_empty() {
if !(self.move_to_next_cluster()) {
return Ok(0);
}
}
self.sub_slice.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

View file

@ -1,9 +1,13 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::io::{Read, Seek, SeekFrom, Write}; use std::fmt::Display;
use std::rc::Rc; use std::rc::Rc;
use log::debug;
use crate::dir::DirIter; use crate::dir::DirIter;
use crate::fat::{FatError, Fatty}; use crate::fat::FatError;
use crate::iter::ClusterChainReader;
pub use crate::slice_like::SliceLike;
use crate::subslice::{SubSlice, SubSliceMut}; use crate::subslice::{SubSlice, SubSliceMut};
pub mod bpb; pub mod bpb;
@ -11,7 +15,8 @@ mod datetime;
pub mod dir; pub mod dir;
pub mod fat; pub mod fat;
pub mod fs_info; pub mod fs_info;
mod iter; pub mod iter;
mod slice_like;
mod subslice; mod subslice;
mod utils; mod utils;
@ -22,84 +27,59 @@ pub enum FatType {
Fat32, Fat32,
} }
pub trait SliceLike {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()>;
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()>;
}
impl SliceLike for &mut [u8] {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
if offset as usize + buf.len() > self.len() {
return Err(std::io::Error::other(anyhow::anyhow!(
"reading {} bytes at offset {} is out of bounds for slice of len {}",
buf.len(),
offset,
self.len()
)));
}
buf.copy_from_slice(&self[offset as usize..][..buf.len()]);
Ok(())
}
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> {
if offset as usize + bytes.len() > self.len() {
return Err(std::io::Error::other(anyhow::anyhow!(
"writing {} bytes at offset {} is out of bounds for slice of len {}",
bytes.len(),
offset,
self.len()
)));
}
self[offset as usize..][..bytes.len()].copy_from_slice(bytes);
Ok(())
}
}
impl SliceLike for std::fs::File {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
self.seek(SeekFrom::Start(offset))?;
self.read_exact(buf)?;
Ok(())
}
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> {
self.seek(SeekFrom::Start(offset))?;
self.write_all(bytes)?;
Ok(())
}
}
#[allow(dead_code)]
pub struct FatFs { pub struct FatFs {
inner: Rc<RefCell<dyn SliceLike>>, inner: Rc<RefCell<dyn SliceLike>>,
fat_offset: u64, // fat_offset: u64,
fat_size: usize, // fat_size: usize,
root_dir_offset: Option<u64>, root_dir_offset: Option<u64>,
root_dir_size: usize, root_dir_size: usize,
pub data_offset: u64, pub data_offset: u64,
data_size: usize, // data_size: usize,
bytes_per_cluster: usize, bytes_per_cluster: usize,
bpb: bpb::Bpb, bpb: bpb::Bpb,
fat: fat::Fat, fat: fat::Fat,
next_free: Option<u32>,
free_count: u32,
}
impl Display for FatFs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.bpb)?;
writeln!(f, "")?;
writeln!(f, "{}", self.fat)?;
Ok(())
}
}
unsafe impl Send for FatFs {}
impl Drop for FatFs {
fn drop(&mut self) {
let fat_slice = SubSliceMut::new(
Rc::clone(&self.inner),
self.bpb.fat_offset(),
self.bpb.fat_len_bytes(),
);
if let Err(err) = self.fat.write_back(fat_slice) {
debug!("writing FAT back to disk failed: {err}");
}
}
} }
impl FatFs { impl FatFs {
pub fn load(data: Rc<RefCell<dyn SliceLike>>) -> anyhow::Result<FatFs> { pub fn load<S>(data: S) -> anyhow::Result<FatFs>
where
S: SliceLike + Send + 'static,
{
let data = Rc::new(RefCell::new(data));
let mut bpb_bytes = [0; 512]; let mut bpb_bytes = [0; 512];
data.borrow_mut().read_at_offset(0, &mut bpb_bytes)?; data.borrow_mut().read_at_offset(0, &mut bpb_bytes)?;
@ -128,127 +108,177 @@ impl FatFs {
// } // }
// } // }
let fat_offset = bpb.fat_offset(); // let fat_offset = bpb.fat_offset();
let fat_size = bpb.fat_len_bytes(); // let fat_size = bpb.fat_len_bytes();
let root_dir_offset = bpb.root_directory_offset(); let root_dir_offset = bpb.root_directory_offset();
let root_dir_size = bpb.root_dir_len_bytes(); let root_dir_size = bpb.root_dir_len_bytes();
let data_offset = bpb.data_offset(); let data_offset = bpb.data_offset();
let data_size = bpb.data_len_bytes(); // let data_size = bpb.data_len_bytes();
let bytes_per_cluster = bpb.bytes_per_cluster(); let bytes_per_cluster = bpb.bytes_per_cluster();
let next_free = fat.first_free_cluster();
let free_count = fat.count_free_clusters();
Ok(FatFs { Ok(FatFs {
inner: data, inner: data,
fat_offset, // fat_offset,
fat_size, // fat_size,
root_dir_offset, root_dir_offset,
root_dir_size, root_dir_size,
data_offset, data_offset,
data_size, // data_size,
bytes_per_cluster, bytes_per_cluster,
bpb, bpb,
fat, fat,
next_free,
free_count,
}) })
} }
pub fn bpb(&self) -> &bpb::Bpb { pub fn fat_type(&self) -> FatType {
&self.bpb self.fat.fat_type()
}
pub fn fat(&self) -> &fat::Fat {
&self.fat
} }
/// byte offset of data cluster /// byte offset of data cluster
pub fn data_cluster_to_offset(&self, cluster: u32) -> u64 { fn data_cluster_to_offset(&self, cluster: u32) -> u64 {
// assert!(cluster >= 2); // assert!(cluster >= 2);
assert!(self.fat().get_valid_clusters().contains(&cluster)); // assert!(self.fat.valid_entries().contains(&cluster));
self.data_offset + (cluster - 2) as u64 * self.bytes_per_cluster as u64 self.data_offset + (cluster - 2) as u64 * self.bytes_per_cluster as u64
} }
pub fn free_clusters(&self) -> u32 {
// self.fat.count_free_clusters()
self.free_count
}
pub fn alloc_cluster(&mut self, prev_cluster: Option<u32>) -> Option<u32> {
let Some(new_cluster) = self.next_free else {
// no free cluster
return None;
};
debug!("next free cluster: {new_cluster}");
// set cluster as EOF
self.fat.set_next_cluster(new_cluster, None);
if let Some(prev_cluster) = prev_cluster {
self.fat.set_next_cluster(prev_cluster, Some(new_cluster));
}
// something went terribly wrong
assert_ne!(self.free_count, 0);
self.free_count -= 1;
// find next free cluster
self.next_free = self.fat.first_free_cluster();
Some(new_cluster)
}
pub fn dealloc_cluster(&mut self, cluster: u32) {
// assert cluster is actually valid
self.fat.free_cluster(cluster);
if self.next_free.is_none() || self.next_free.unwrap() > cluster {
self.next_free = Some(cluster);
}
self.free_count += 1;
}
pub fn bytes_per_sector(&self) -> u16 {
self.bpb.bytes_per_sector()
}
pub fn sectors_per_cluster(&self) -> u8 {
self.bpb.sectors_per_cluster()
}
pub fn root_cluster(&self) -> Option<u32> {
self.bpb.root_cluster()
}
/// next data cluster or None is cluster is EOF /// next data cluster or None is cluster is EOF
/// ///
/// giving an invalid cluster (free, reserved, or defective) returns an appropriate error /// giving an invalid cluster (free, reserved, or defective) returns an appropriate error
pub fn next_cluster(&self, cluster: u32) -> Result<Option<u32>, FatError> { pub fn next_cluster(&self, cluster: u32) -> Result<Option<u32>, FatError> {
self.fat().get_next_cluster(cluster) self.fat.get_next_cluster(cluster)
} }
pub fn cluster_as_subslice_mut(&mut self, cluster: u32) -> SubSliceMut<'_> { pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice {
let offset = self.data_cluster_to_offset(cluster); if cluster == 0 {
// for cluster 0 simply return empty subslice
// this makes things a bit easier, since cluster 0 is used as a marker that a file/dir
// is empty
SubSliceMut::new(self, offset, self.bytes_per_cluster) return SubSlice::new(self.inner.clone(), 0, 0);
}
pub fn cluster_as_subslice(&self, cluster: u32) -> SubSlice<'_> {
let offset = self.data_cluster_to_offset(cluster);
SubSlice::new(self, offset, self.bytes_per_cluster)
}
pub fn root_dir_bytes(&mut self) -> std::io::Result<Vec<u8>> {
if let Some(root_dir_offset) = self.root_dir_offset {
let mut data = Vec::new();
let mut subslice = SubSliceMut::new(self, root_dir_offset, self.root_dir_size);
subslice.read_to_end(&mut data)?;
return Ok(data);
} }
let mut cluster = self.bpb().root_cluster().unwrap(); let offset = self.data_cluster_to_offset(cluster);
let mut data = vec![0; self.bytes_per_cluster]; SubSlice::new(self.inner.clone(), offset, self.bytes_per_cluster)
let mut inner = self.inner.borrow_mut();
inner.read_at_offset(self.data_cluster_to_offset(cluster), &mut data)?;
while let Ok(Some(next_cluster)) = self.next_cluster(cluster) {
cluster = next_cluster;
inner.read_at_offset(self.data_cluster_to_offset(cluster), &mut data)?;
}
Ok(data)
} }
fn chain_reader(&self, first_cluster: u32) -> impl Read { pub fn cluster_as_subslice_mut(&self, cluster: u32) -> SubSliceMut {
if cluster == 0 {
// for cluster 0 simply return empty subslice
// this makes things a bit easier, since cluster 0 is used as a marker that a file/dir
// is empty
return SubSliceMut::new(self.inner.clone(), 0, 0);
}
let offset = self.data_cluster_to_offset(cluster);
SubSliceMut::new(self.inner.clone(), offset, self.bytes_per_cluster)
}
fn root_dir_as_subslice(&self) -> SubSlice {
SubSlice::new(self.inner.clone(), self.root_dir_offset.unwrap(), self.root_dir_size)
}
fn root_dir_as_subslice_mut(&self) -> SubSliceMut {
SubSliceMut::new(self.inner.clone(), self.root_dir_offset.unwrap(), self.root_dir_size)
}
fn chain_reader(&'_ self, first_cluster: u32) -> iter::ClusterChainReader<'_> {
iter::ClusterChainReader::new(self, first_cluster) iter::ClusterChainReader::new(self, first_cluster)
} }
pub fn root_dir_iter<'a>(&'a self) -> DirIter<Box<dyn Read + 'a>> { fn chain_writer(&'_ mut self, first_cluster: u32) -> iter::ClusterChainWriter<'_> {
// Box<dyn Iterator<Item = DirEntry> + '_> iter::ClusterChainWriter::new(self, first_cluster)
// TODO: maybe wrap this in another RootDirIter enum, so we don't have to Box<dyn>
if let Some(root_dir_offset) = self.root_dir_offset {
// FAT12/FAT16
let sub_slice = SubSlice::new(self, root_dir_offset, self.root_dir_size);
return DirIter::new(Box::new(sub_slice));
}
// FAT32
// can't fail; we're in the FAT32 case
let root_cluster = self.bpb().root_cluster().unwrap();
let cluster_iter = iter::ClusterChainReader::new(self, root_cluster);
DirIter::new(Box::new(cluster_iter))
} }
pub fn dir_iter<'a>(&'a self, first_cluster: u32) -> DirIter<Box<dyn Read + 'a>> { pub fn root_dir_iter<'a>(&self) -> DirIter<'_> {
// TODO: return type must match root_dir_iter let reader = ClusterChainReader::root_dir_reader(self);
// if the Box<dyn> is changed there, update here as well
DirIter::new(reader)
}
pub fn dir_iter<'a>(&self, first_cluster: u32) -> DirIter<'_> {
let cluster_iter = self.chain_reader(first_cluster); let cluster_iter = self.chain_reader(first_cluster);
DirIter::new(Box::new(cluster_iter)) DirIter::new(cluster_iter)
}
pub fn file_reader(&self, first_cluster: u32) -> iter::ClusterChainReader<'_> {
// TODO: needs to take file size into account
assert!(first_cluster >= 2);
self.chain_reader(first_cluster)
}
pub fn file_writer(&mut self, first_cluster: u32) -> iter::ClusterChainWriter<'_> {
// TODO: needs to take file size into account
assert!(first_cluster >= 2);
self.chain_writer(first_cluster)
} }
} }

View file

@ -0,0 +1,58 @@
use std::fs::File;
use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
pub trait SliceLike {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()>;
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()>;
}
impl SliceLike for &mut [u8] {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
if offset as usize + buf.len() > self.len() {
return Err(std::io::Error::other(anyhow::anyhow!(
"reading {} bytes at offset {} is out of bounds for slice of len {}",
buf.len(),
offset,
self.len()
)));
}
buf.copy_from_slice(&self[offset as usize..][..buf.len()]);
Ok(())
}
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> {
if offset as usize + bytes.len() > self.len() {
return Err(std::io::Error::other(anyhow::anyhow!(
"writing {} bytes at offset {} is out of bounds for slice of len {}",
bytes.len(),
offset,
self.len()
)));
}
self[offset as usize..][..bytes.len()].copy_from_slice(bytes);
Ok(())
}
}
impl SliceLike for File {
fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {
self.seek(SeekFrom::Start(offset))?;
self.read_exact(buf)?;
Ok(())
}
fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> std::io::Result<()> {
self.seek(SeekFrom::Start(offset))?;
self.write_all(bytes)?;
Ok(())
}
}

View file

@ -1,16 +1,18 @@
use std::cell::RefCell;
use std::fmt::Debug; use std::fmt::Debug;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::rc::Rc;
use crate::FatFs; use crate::SliceLike;
pub struct SubSliceMut<'a> { pub struct SubSlice {
fat_fs: &'a mut FatFs, data: Rc<RefCell<dyn SliceLike>>,
offset: u64, offset: u64,
len: usize, len: usize,
} }
impl Debug for SubSliceMut<'_> { impl Debug for SubSlice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubSliceMut") f.debug_struct("SubSliceMut")
.field("offset", &self.offset) .field("offset", &self.offset)
@ -19,17 +21,15 @@ impl Debug for SubSliceMut<'_> {
} }
} }
impl SubSliceMut<'_> { impl<'a> SubSlice {
pub fn new(fat_fs: &mut FatFs, offset: u64, len: usize) -> SubSliceMut<'_> { pub fn new(data: Rc<RefCell<dyn SliceLike>>, offset: u64, len: usize) -> SubSlice {
SubSliceMut { SubSlice { data, offset, len }
fat_fs, }
offset,
len, pub fn offset(&self) -> u64 {
} self.offset
} }
}
impl SubSliceMut<'_> {
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.len self.len
} }
@ -37,14 +37,22 @@ impl SubSliceMut<'_> {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.len() == 0 self.len() == 0
} }
pub fn skip(&mut self, n: usize) -> usize {
let n = n.min(self.len());
self.offset += n as u64;
self.len -= n;
n
}
} }
impl Read for SubSliceMut<'_> { impl Read for SubSlice {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_to_read = self.len.min(buf.len()); let bytes_to_read = self.len.min(buf.len());
self.fat_fs self.data
.inner
.borrow_mut() .borrow_mut()
.read_at_offset(self.offset, &mut buf[..bytes_to_read])?; .read_at_offset(self.offset, &mut buf[..bytes_to_read])?;
@ -55,12 +63,72 @@ impl Read for SubSliceMut<'_> {
} }
} }
impl Write for SubSliceMut<'_> { pub struct SubSliceMut {
// fat_fs: &'a FatFs,
data: Rc<RefCell<dyn SliceLike>>,
offset: u64,
len: usize,
}
impl Debug for SubSliceMut {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubSliceMut")
.field("offset", &self.offset)
.field("len", &self.len)
.finish()
}
}
impl SubSliceMut {
pub fn new(data: Rc<RefCell<dyn SliceLike>>, offset: u64, len: usize) -> SubSliceMut {
SubSliceMut { data, offset, len }
}
}
impl<'a> SubSliceMut {
pub fn offset(&self) -> u64 {
self.offset
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn skip(&mut self, n: usize) -> usize {
let n = n.min(self.len());
self.offset += n as u64;
self.len -= n;
n
}
}
impl Read for SubSliceMut {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_to_read = self.len.min(buf.len());
self.data
.borrow_mut()
.read_at_offset(self.offset, &mut buf[..bytes_to_read])?;
self.offset += bytes_to_read as u64;
self.len -= bytes_to_read;
Ok(bytes_to_read)
}
}
impl Write for SubSliceMut {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let bytes_to_write = self.len.min(buf.len()); let bytes_to_write = self.len.min(buf.len());
self.fat_fs self.data
.inner
.borrow_mut() .borrow_mut()
.write_at_offset(self.offset, &buf[..bytes_to_write])?; .write_at_offset(self.offset, &buf[..bytes_to_write])?;
@ -74,67 +142,3 @@ impl Write for SubSliceMut<'_> {
Ok(()) Ok(())
} }
} }
pub struct SubSlice<'a> {
fat_fs: &'a FatFs,
offset: u64,
len: usize,
}
impl Debug for SubSlice<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubSliceMut")
.field("offset", &self.offset)
.field("len", &self.len)
.finish()
}
}
impl SubSlice<'_> {
pub fn new(fat_fs: &FatFs, offset: u64, len: usize) -> SubSlice<'_> {
SubSlice {
fat_fs,
offset,
len,
}
}
pub fn fat_fs(&self) -> &FatFs {
self.fat_fs
}
pub fn fat_fs_mut(&self) -> &FatFs {
self.fat_fs
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn len(&self) -> usize {
self.len
}
}
impl<'a> SubSlice<'a> {
/// releases the inner &FatFs, consuming self in the process
pub fn release(self) -> &'a FatFs {
self.fat_fs
}
}
impl Read for SubSlice<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_to_read = self.len.min(buf.len());
self.fat_fs
.inner
.borrow_mut()
.read_at_offset(self.offset, &mut buf[..bytes_to_read])?;
self.offset += bytes_to_read as u64;
self.len -= bytes_to_read;
Ok(bytes_to_read)
}
}

View file

@ -3,24 +3,9 @@ pub fn load_u16_le(bytes: &[u8]) -> u16 {
u16::from_le_bytes(bytes.try_into().unwrap()) u16::from_le_bytes(bytes.try_into().unwrap())
} }
pub fn load_u32_le(bytes: &[u8]) -> u32 { pub fn load_u32_le(bytes: &[u8]) -> u32 {
assert_eq!(bytes.len(), 4); assert_eq!(bytes.len(), 4);
u32::from_le_bytes(bytes.try_into().unwrap()) u32::from_le_bytes(bytes.try_into().unwrap())
} }
/// replace the value at x with f(x)
///
/// SAFETY:
/// should be safe, I guess? MIRI didn't complain about it
pub fn replace<T>(x: &mut T, f: impl FnOnce(T) -> T) {
unsafe {
let x_ptr = x as *mut T;
let old_x = std::ptr::read(x_ptr);
let new_x = f(old_x);
std::ptr::write(x_ptr, new_x);
}
}

View file

@ -1,9 +1,5 @@
use std::cell::RefCell;
use std::rc::Rc;
use fat_bits::FatFs; use fat_bits::FatFs;
use fat_bits::dir::{DirEntry, DirIter}; use fat_bits::dir::DirEntry;
use fat_bits::fat::Fatty as _;
pub fn main() -> anyhow::Result<()> { pub fn main() -> anyhow::Result<()> {
let args = std::env::args(); let args = std::env::args();
@ -22,18 +18,20 @@ pub fn main() -> anyhow::Result<()> {
// println!("{}", bpb); // println!("{}", bpb);
let fat_fs = FatFs::load(Rc::new(RefCell::new(file)))?; let fat_fs = FatFs::load(file)?;
println!("{}", fat_fs.bpb()); // println!("{}", fat_fs.bpb());
println!(); // println!();
println!("{}", fat_fs.fat()); // println!("{}", fat_fs.fat());
println!("{}", fat_fs);
println!(); println!();
println!( println!(
"free clusters: {} ({} bytes)", "free clusters: {} ({} bytes)",
fat_fs.fat().count_free_clusters(), fat_fs.free_clusters(),
fat_fs.fat().count_free_clusters() fat_fs.free_clusters() as usize
* fat_fs.bpb().bytes_per_sector() as usize * fat_fs.bytes_per_sector() as usize
* fat_fs.bpb().sectors_per_cluster() as usize * fat_fs.sectors_per_cluster() as usize
); );
println!(); println!();

View file

@ -5,10 +5,20 @@ edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.98" anyhow = "1.0.98"
chrono = { version = "0.4.41", default-features = false, features = ["alloc", "clock", "std"] } bitflags = "2.9.1"
chrono = { version = "0.4.41", default-features = false, features = [
"alloc",
"clock",
"std",
] }
compact_string = "0.1.0"
fat-bits = { version = "0.1.0", path = "../fat-bits" } fat-bits = { version = "0.1.0", path = "../fat-bits" }
fuser = "0.15.1" fuser = "0.15.1"
fxhash = "0.2.1"
libc = "0.2.174" libc = "0.2.174"
log = "0.4.27" log = "0.4.27"
rand = { version = "0.9.2", default-features = false, features = ["os_rng", "small_rng"] } rand = { version = "0.9.2", default-features = false, features = [
"os_rng",
"small_rng",
] }
thiserror = "2.0.12" thiserror = "2.0.12"

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,14 @@
use std::cell::{LazyCell, RefCell}; use std::cell::{LazyCell, RefCell};
use std::rc::Rc;
use std::time::SystemTime; use std::time::SystemTime;
use chrono::{NaiveDateTime, NaiveTime}; use chrono::{NaiveDateTime, NaiveTime};
use fat_bits::FatFs; use fat_bits::FatFs;
use fat_bits::dir::{DirEntry, DirIter}; use fat_bits::dir::{DirEntry, DirIter};
use fat_bits::iter::{ClusterChainReader, ClusterChainWriter};
use fuser::FileAttr; use fuser::FileAttr;
use libc::{EISDIR, ENOENT, ENOTDIR};
use log::debug;
use rand::{Rng, SeedableRng as _}; use rand::{Rng, SeedableRng as _};
thread_local! { thread_local! {
@ -19,13 +23,10 @@ thread_local! {
static RNG: LazyCell<RefCell<rand::rngs::SmallRng>> = LazyCell::new(|| RefCell::new(rand::rngs::SmallRng::from_os_rng())); static RNG: LazyCell<RefCell<rand::rngs::SmallRng>> = LazyCell::new(|| RefCell::new(rand::rngs::SmallRng::from_os_rng()));
} }
fn get_random_u32() -> u32 { fn get_random<T>() -> T
// RNG.with(|x| unsafe { where
// let rng = &mut (*x.get()); rand::distr::StandardUniform: rand::distr::Distribution<T>,
{
// rng.random::<u32>()
// })
RNG.with(|rng| rng.borrow_mut().random()) RNG.with(|rng| rng.borrow_mut().random())
} }
@ -44,38 +45,81 @@ impl From<Kind> for fuser::FileType {
} }
} }
const ROOT_INO: u64 = 1; pub const ROOT_INO: u64 = 1;
pub type InodeRef = Rc<RefCell<Inode>>;
// pub type InodeWeak = Weak<RefCell<Inode>>;
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Inode { pub struct Inode {
ino: u64, ino: u64,
// FUSE uses a u64 for generation, but the Linux kernel only handles u32s anyway, truncating
// the high bits, so using more is pretty pointless and possibly even detrimental
generation: u32, generation: u32,
size: u64, ref_count: u64,
uid: u32,
gid: u32,
path: Rc<str>,
parent: Option<InodeRef>,
block_size: u32, block_size: u32,
kind: Kind, kind: Kind,
read_only: bool, read_only: bool,
dirty: bool,
// these are the fields that have to get written back to the DirEntry on write_back
size: u64,
atime: SystemTime, atime: SystemTime,
mtime: SystemTime, mtime: SystemTime,
// ctime: SystemTime,
crtime: SystemTime, crtime: SystemTime,
uid: u32,
gid: u32,
first_cluster: u32, first_cluster: u32,
} }
#[allow(dead_code)] impl Drop for Inode {
fn drop(&mut self) {
// since we don't have a handle on the FatFs we can't do the write-back here
assert!(
!self.dirty,
"inode {} is dirty, but was not written back to FS before being destroyed",
self.ino()
);
}
}
impl Inode { impl Inode {
pub fn new(fat_fs: &FatFs, dir_entry: DirEntry, uid: u32, gid: u32) -> Inode { fn new_generation() -> u32 {
let rand: u16 = get_random();
let secs = SystemTime::UNIX_EPOCH
.elapsed()
.map(|dur| dur.as_secs() as u16)
.unwrap_or(0);
((secs as u32) << 16) | rand as u32
}
pub fn new(
fat_fs: &FatFs,
dir_entry: &DirEntry,
ino: u64,
uid: u32,
gid: u32,
path: impl Into<Rc<str>>,
parent: InodeRef,
) -> Inode {
assert!(dir_entry.is_file() || dir_entry.is_dir()); assert!(dir_entry.is_file() || dir_entry.is_dir());
let generation = get_random_u32(); let generation = Self::new_generation();
let kind = if dir_entry.is_dir() { let kind = if dir_entry.is_dir() {
Kind::Dir Kind::Dir
@ -95,22 +139,138 @@ impl Inode {
let mtime = datetime_to_system(dir_entry.write_time()); let mtime = datetime_to_system(dir_entry.write_time());
let crtime = datetime_to_system(dir_entry.create_time()); let crtime = datetime_to_system(dir_entry.create_time());
let path = path.into();
debug!(
"creating new inode: ino: {} name: {} path: {}",
ino,
dir_entry.name_string(),
path
);
Inode { Inode {
ino: dir_entry.first_cluster() as u64, ino,
generation, generation,
ref_count: 0,
parent: Some(parent),
size: dir_entry.file_size() as u64, size: dir_entry.file_size() as u64,
block_size: fat_fs.bpb().bytes_per_sector() as u32, block_size: fat_fs.bytes_per_sector() as u32,
kind, kind,
read_only: dir_entry.is_readonly(), read_only: dir_entry.is_readonly(),
dirty: false,
atime, atime,
mtime, mtime,
crtime, crtime,
uid, uid,
gid, gid,
first_cluster: dir_entry.first_cluster(), first_cluster: dir_entry.first_cluster(),
path,
} }
} }
pub fn root_inode(fat_fs: &FatFs, uid: u32, gid: u32) -> Inode {
let root_cluster = fat_fs.root_cluster().unwrap_or(0);
Inode {
ino: ROOT_INO,
generation: 0, // root cluster always has constant generation of 0
ref_count: 0,
parent: None, // parent is self
size: 0,
block_size: fat_fs.bytes_per_sector() as u32,
kind: Kind::Dir,
read_only: false,
dirty: false,
atime: SystemTime::UNIX_EPOCH,
mtime: SystemTime::UNIX_EPOCH,
crtime: SystemTime::UNIX_EPOCH,
uid,
gid,
first_cluster: root_cluster,
path: "/".into(),
}
}
pub fn ino(&self) -> u64 {
self.ino
}
pub fn generation(&self) -> u32 {
self.generation
}
pub fn ref_count(&self) -> u64 {
self.ref_count
}
pub fn inc_ref_count(&mut self) {
debug!(
"increasing ref_count of ino {} by 1 (new ref_count: {})",
self.ino(),
self.ref_count() + 1
);
self.ref_count += 1;
}
pub fn dec_ref_count(&mut self, n: u64) -> u64 {
debug!(
"decreasing ref_count of ino {} by {} (new ref_count: {})",
self.ino(),
n,
self.ref_count().saturating_sub(n),
);
if self.ref_count < n {
debug!(
"inode {}: tried to decrement refcount by {}, but is only {}",
self.ino(),
n,
self.ref_count
);
}
self.ref_count = self.ref_count.saturating_sub(n);
self.ref_count
}
pub fn parent(&self) -> Option<&InodeRef> {
self.parent.as_ref()
}
pub fn size(&self) -> u64 {
self.size
}
pub fn kind(&self) -> Kind {
self.kind
}
pub fn is_file(&self) -> bool {
self.kind == Kind::File
}
pub fn is_dir(&self) -> bool {
self.kind == Kind::Dir
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn first_cluster(&self) -> u32 {
self.first_cluster
}
pub fn path(&self) -> Rc<str> {
Rc::clone(&self.path)
}
pub fn is_root(&self) -> bool {
self.ino == ROOT_INO
}
pub fn file_attr(&self) -> FileAttr { pub fn file_attr(&self) -> FileAttr {
let perm = if self.read_only { 0o555 } else { 0o777 }; let perm = if self.read_only { 0o555 } else { 0o777 };
@ -133,21 +293,123 @@ impl Inode {
} }
} }
pub fn dir_iter(&self, fat_fs: &FatFs) -> anyhow::Result<impl Iterator<Item = DirEntry>> { pub fn dir_iter<'a>(&'a self, fat_fs: &'a FatFs) -> Result<DirIter<'a>, i32> {
anyhow::ensure!(self.kind == Kind::Dir, "cannot dir_iter on a file"); if self.kind != Kind::Dir {
return Err(ENOTDIR);
}
// TODO: the boxing here is not particularly pretty, but neccessary, since the DirIter for if self.is_root() {
// the root holds a
if self.ino == ROOT_INO {
// root dir // root dir
return Ok(fat_fs.root_dir_iter()); return Ok(fat_fs.root_dir_iter());
} }
let chain_reader = fat_fs.chain_reader(self.first_cluster); Ok(fat_fs.dir_iter(self.first_cluster))
}
// TODO: get rid of this Box if the boxing is removed from root_dir_iter pub fn find_child_by_name(&self, fat_fs: &FatFs, name: &str) -> Result<DirEntry, i32> {
Ok(DirIter::new(Box::new(chain_reader))) self.dir_iter(fat_fs)
.and_then(|mut dir_iter| dir_iter.find_by_name(name).ok_or(ENOENT))
}
pub fn file_reader<'a>(&'a self, fat_fs: &'a FatFs) -> Result<ClusterChainReader<'a>, i32> {
if self.is_dir() {
return Err(EISDIR);
}
Ok(fat_fs.file_reader(self.first_cluster()))
}
pub fn file_writer<'a>(&'a self, fat_fs: &'a mut FatFs) -> Result<ClusterChainWriter<'a>, i32> {
if self.is_dir() {
return Err(EISDIR);
}
Ok(fat_fs.file_writer(self.first_cluster()))
}
pub fn update_size(&mut self, new_size: u64) {
debug!("updating size to {new_size}");
if new_size == self.size {
return;
}
self.size = new_size;
self.dirty = true;
}
pub fn update_atime(&mut self, atime: SystemTime) {
if self.atime == atime {
return;
}
self.atime = atime;
self.dirty = true;
}
pub fn update_mtime(&mut self, mtime: SystemTime) {
if self.mtime == mtime {
return;
}
self.mtime = mtime;
self.dirty = true;
}
pub fn write_back(&mut self, fat_fs: &FatFs) -> anyhow::Result<()> {
if !self.dirty {
return Ok(());
}
if self.is_root() {
// root dir has no attributes
self.dirty = false;
return Ok(());
}
let Some(parent_inode) = self.parent() else {
anyhow::bail!("parent inode of {} does not exist", self.ino);
};
let parent_inode = parent_inode.borrow();
// since we just wrote to the file with this inode, first cluster should not be zero
let Some(mut dir_entry) = parent_inode
.dir_iter(fat_fs)
.unwrap()
.find(|dir_entry| dir_entry.first_cluster() == self.first_cluster())
else {
anyhow::bail!("could not find dir_entry corresponding to self in parent inode");
};
drop(parent_inode);
// update stats on DirEntry
assert!(self.size <= u32::MAX as u64);
dir_entry.update_file_size(self.size as u32);
dir_entry
.update_last_access_date(self.atime)
.map_err(|err| {
anyhow::anyhow!("failed to update atime for inode {}: {err}", self.ino)
})?;
dir_entry.update_write_time(self.mtime).map_err(|err| {
anyhow::anyhow!("failed to update mtime for inode {}: {err}", self.ino)
})?;
// update cr_time as well?
// write DirEntry back to device
dir_entry.write_back(fat_fs).map_err(|err| {
anyhow::anyhow!("failed to write back dir_entry for inode {}: {err}", self.ino)
})?;
self.dirty = false;
Ok(())
} }
} }

View file

@ -5,9 +5,12 @@ use std::cell::RefCell;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::rc::Rc; use std::rc::Rc;
use fat_bits::dir::DirEntry;
use fat_bits::{FatFs, SliceLike}; use fat_bits::{FatFs, SliceLike};
use fxhash::FxHashMap;
use log::{debug, error};
use crate::inode::Inode; use crate::inode::{Inode, InodeRef};
#[allow(dead_code)] #[allow(dead_code)]
pub struct FatFuse { pub struct FatFuse {
@ -16,32 +19,279 @@ pub struct FatFuse {
uid: u32, uid: u32,
gid: u32, gid: u32,
next_fd: u32, next_ino: u64,
next_fh: u64,
inode_table: BTreeMap<u64, Inode>, inode_table: BTreeMap<u64, InodeRef>,
ino_by_first_cluster: BTreeMap<u32, u64>,
ino_by_fh: BTreeMap<u64, u64>,
ino_by_path: FxHashMap<Rc<str>, u64>,
} }
/// SAFETY
///
/// do NOT leak Rc<str> from this type
unsafe impl Send for FatFuse {}
impl FatFuse { impl FatFuse {
pub fn new(data: Rc<RefCell<dyn SliceLike>>) -> anyhow::Result<FatFuse> { pub fn new<S>(data: S) -> anyhow::Result<FatFuse>
where
S: SliceLike + Send + 'static,
{
let uid = unsafe { libc::getuid() }; let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() }; let gid = unsafe { libc::getgid() };
let fat_fs = FatFs::load(data)?; let fat_fs = FatFs::load(data)?;
Ok(FatFuse { let mut fat_fuse = FatFuse {
fat_fs, fat_fs,
uid, uid,
gid, gid,
next_fd: 0, next_ino: 2, // 0 is reserved and 1 is root
next_fh: 0,
inode_table: BTreeMap::new(), inode_table: BTreeMap::new(),
}) ino_by_first_cluster: BTreeMap::new(),
ino_by_fh: BTreeMap::new(),
ino_by_path: FxHashMap::default(),
};
// TODO: build and insert root dir inode
let root_inode = Inode::root_inode(&fat_fuse.fat_fs, uid, gid);
fat_fuse.insert_inode(root_inode);
Ok(fat_fuse)
} }
fn get_inode(&self, ino: u64) -> Option<&Inode> { fn next_ino(&mut self) -> u64 {
let ino = self.next_ino;
assert!(!self.inode_table.contains_key(&ino));
self.next_ino += 1;
ino
}
fn next_fh(&mut self) -> u64 {
let fh = self.next_fh;
assert!(!self.ino_by_fh.contains_key(&fh));
self.next_fh += 1;
fh
}
fn insert_inode(&mut self, inode: Inode) -> InodeRef {
let ino = inode.ino();
let generation = inode.generation();
let first_cluster = inode.first_cluster();
// let old_inode = self.inode_table.insert(ino, inode);
let inode = Rc::new(RefCell::new(inode));
let entry = self.inode_table.entry(ino);
let (new_inode, old_inode) = match entry {
std::collections::btree_map::Entry::Vacant(vacant_entry) => {
let new_inode = vacant_entry.insert(inode);
(Rc::clone(new_inode), None)
}
std::collections::btree_map::Entry::Occupied(occupied_entry) => {
let entry_ref = occupied_entry.into_mut();
let old_inode = std::mem::replace(entry_ref, inode);
(Rc::clone(entry_ref), Some(old_inode))
}
};
debug!(
"inserted new inode with ino {} and generation {} (first cluster: {})",
ino, generation, first_cluster
);
if let Some(old_inode) = old_inode {
let old_inode = old_inode.borrow();
debug!("ejected inode {} {}", old_inode.ino(), old_inode.generation());
}
if first_cluster != 0 {
if let Some(old_ino) = self.ino_by_first_cluster.insert(first_cluster, ino) {
debug!("ejected old {} -> {} cluster to ino mapping", first_cluster, old_ino);
}
}
let path = new_inode.borrow().path();
if let Some(old_ino) = self.ino_by_path.insert(Rc::clone(&path), ino) {
debug!("ejected old {} -> {} path to ino mapping", path, old_ino);
}
new_inode
}
fn drop_inode(&mut self, inode: InodeRef) {
let inode = inode.borrow();
let ino = inode.ino();
debug!("dropping inode {}", ino);
if self.inode_table.remove(&ino).is_none() {
error!("tried to drop inode with ino {}, but was not in table", ino);
return;
};
let first_cluster = inode.first_cluster();
if first_cluster != 0 {
let entry = self.ino_by_first_cluster.entry(first_cluster);
match entry {
std::collections::btree_map::Entry::Vacant(_) => debug!(
"removed inode with ino {} from table, but it's first cluster did not point to any ino",
ino
),
std::collections::btree_map::Entry::Occupied(occupied_entry) => {
let found_ino = *occupied_entry.get();
if found_ino == ino {
// matches our inode, remove it
occupied_entry.remove();
} else {
// does not match removed inode, leave it as is
debug!(
"removed inode with ino {} from table, but its first cluster pointed to ino {} instead",
ino, found_ino
);
}
}
}
}
{
let entry = self.ino_by_path.entry(inode.path());
match entry {
std::collections::hash_map::Entry::Vacant(_) => debug!(
"removed inode with ino {} from table, but it's path did not point to any ino",
ino
),
std::collections::hash_map::Entry::Occupied(occupied_entry) => {
let found_ino = *occupied_entry.get();
if found_ino == ino {
// matches our inode, remove it
occupied_entry.remove();
} else {
// does not match removed inode, leave it as is
debug!(
"removed inode with ino {} from table, but its path pointed to ino {} instead",
ino, found_ino
);
}
}
}
}
}
fn get_inode(&self, ino: u64) -> Option<&InodeRef> {
self.inode_table.get(&ino) self.inode_table.get(&ino)
} }
fn get_inode_mut(&mut self, ino: u64) -> Option<&mut Inode> { fn get_or_make_inode(&mut self, dir_entry: &DirEntry, parent: &Inode) -> InodeRef {
self.inode_table.get_mut(&ino) // let parent = parent.borrow();
// try to find inode by first cluster first
if dir_entry.first_cluster() != 0
&& let Some(inode) = self.get_inode_by_first_cluster(dir_entry.first_cluster())
{
return inode;
}
// try to find inode by path
// mostly for empty files/directories which have a first cluster of 0
let path = {
let mut path = parent.path().as_ref().to_owned();
if parent.ino() != inode::ROOT_INO {
// root inode already has trailing slash
path.push('/');
}
path += &dir_entry.name_string();
path
};
if let Some(inode) = self.get_inode_by_path(&path) {
return inode;
}
// no inode found, make a new one
let ino = self.next_ino();
let Some(parent_inode) = self.get_inode(parent.ino()).cloned() else {
// TODO: what do we do here? should not happen
panic!("parent_ino {} does not lead to inode", parent.ino());
};
let inode =
Inode::new(&self.fat_fs, dir_entry, ino, self.uid, self.gid, path, parent_inode);
self.insert_inode(inode)
}
pub fn get_inode_by_first_cluster(&self, first_cluster: u32) -> Option<InodeRef> {
if first_cluster == 0 {
debug!("trying to get inode by first cluster 0");
return None;
}
let ino = self.ino_by_first_cluster.get(&first_cluster)?;
if let Some(inode) = self.inode_table.get(ino) {
Some(Rc::clone(inode))
} else {
debug!(
"first cluster {} is mapped to ino {}, but inode is not in table",
first_cluster, ino
);
None
}
}
pub fn get_inode_by_fh(&self, fh: u64) -> Option<&InodeRef> {
let ino = *self.ino_by_fh.get(&fh)?;
if let Some(inode) = self.get_inode(ino) {
Some(inode)
} else {
debug!("fh {} is mapped to ino {}, but inode is not in table", fh, ino);
None
}
}
pub fn get_inode_by_path(&self, path: &str) -> Option<InodeRef> {
let ino = *self.ino_by_path.get(path)?;
if let Some(inode) = self.get_inode(ino).cloned() {
Some(inode)
} else {
debug!("path {} is mapped to ino {}, but inode is not in table", path, ino);
None
}
} }
} }

11
fat-mount/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "fat-mount"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.98"
ctrlc = "3.4.7"
env_logger = "0.11.8"
fat-fuse = { version = "0.1.0", path = "../fat-fuse" }
fuser = "0.15.1"

41
fat-mount/src/main.rs Normal file
View file

@ -0,0 +1,41 @@
use std::fs::OpenOptions;
use std::sync::mpsc::channel;
use fat_fuse::FatFuse;
use fuser::MountOption;
fn main() -> anyhow::Result<()> {
env_logger::init();
let mut args = std::env::args();
let _prog_name = args.next().unwrap();
let path = args.next().ok_or(anyhow::anyhow!("missing fs path"))?;
let mountpoint = args.next().ok_or(anyhow::anyhow!("missing mount point"))?;
// let file = File::open(path)?;
let file = OpenOptions::new().read(true).write(true).open(path)?;
let fat_fuse = FatFuse::new(file)?;
let options = vec![
// MountOption::RO,
MountOption::FSName("fat-fuse".to_owned()),
MountOption::AutoUnmount,
];
let (tx, rx) = channel();
ctrlc::set_handler(move || {
tx.send(()).unwrap();
})
.unwrap();
let handle = fuser::spawn_mount2(fat_fuse, mountpoint, &options)?;
rx.recv().unwrap();
handle.join();
Ok(())
}