diff --git a/.gitignore b/.gitignore index ea8c4bf..055d792 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,36 @@ -/target +# Generated by Cargo +# will have compiled files and executables +/target/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +# Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# IDE specific files +.idea/ +*.iml +.vscode/ +*.swp +*.swo +*~ + +# OS specific files +.DS_Store +Thumbs.db + +# Test artifacts +*.img +esp.img +test.img + +# Temporary files +*.tmp +*.log +runlog*.txt diff --git a/Cargo.lock b/Cargo.lock index 8eeca41..15e26f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,13 +2,75 @@ # It is not intended for manual editing. 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]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "fat32-raw" -version = "0.3.0" +version = "1.0.0" dependencies = [ + "anyhow", + "env_logger", "glob", "log", + "uuid", "winapi", + "windows-sys", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", ] [[package]] @@ -17,12 +79,226 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[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]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -39,8 +315,99 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +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]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] diff --git a/Cargo.toml b/Cargo.toml index 6cf09ea..ebeba1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,33 @@ [package] name = "fat32-raw" -version = "0.3.0" -edition = "2024" +version = "1.0.0" +edition = "2021" authors = ["DIMFLIX "] -description = "Lightweight and safe Rust library for working with FAT32 partitions and images" +description = "Cross-platform Rust library for direct FAT32 partition manipulation with ESP support" license = "GPL-3.0-or-later" repository = "https://github.com/meowrch/fat32-raw" readme = "README.md" -keywords = ["fat32", "filesystem", "esp", "rust", "raw"] -categories = ["filesystem"] +keywords = ["fat32", "filesystem", "esp", "efi", "raw"] +categories = ["filesystem", "os", "hardware-support"] homepage = "https://github.com/meowrch/fat32-raw" +documentation = "https://docs.rs/fat32-raw" +rust-version = "1.70.0" [lib] name = "fat32_raw" path = "src/lib.rs" +[dependencies] +log = "0.4.27" +uuid = { version = "1", features = ["v4"] } +anyhow = "1.0" + [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["winbase", "fileapi"] } +windows-sys = { version = "0.52", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Threading", "Win32_System_SystemServices", "Win32_System_WindowsProgramming", "Win32_System_Ioctl"] } +winapi = { version = "0.3", features = ["fileapi", "winbase", "winioctl", "ioapiset", "handleapi", "winnt"] } [target.'cfg(unix)'.dependencies] glob = "0.3" -[dependencies] -log = "0.4.27" +[dev-dependencies] +env_logger = "0.10" diff --git a/README.md b/README.md index 4994edc..b11e003 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,180 @@ # fat32-raw 🚀 -Лёгкая и безопасная Rust-библиотека для работы с FAT32-разделами и образами, с поддержкой чтения, записи и автодетектом параметров. - -## ✨ Особенности -- 💾 Работа с raw-образами и raw-дисками FAT32 (ESP, SD-карты, флешки) -- 🔍 Автоматическое определение параметров раздела (BPB) -- 📖 Чтение и ✍️ запись файлов с поддержкой изменения размера -- 📝 Поддержка длинных имён файлов (LFN) -- 🔒 Минимум unsafe, максимум безопасности и стабильности -- ⚙️ Простое и понятное API для интеграции в проекты -- 🔄 Идеально подходит для синхронизации данных между системами (например, Bluetooth keys между Windows и Linux) - -## 🚀 Пример использования с образом + +[![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge&logo=rust&logoColor=white)](https://www.rust-lang.org/) +[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux-blue?style=for-the-badge)]() +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg?style=for-the-badge)](https://www.gnu.org/licenses/gpl-3.0) + +Полнофункциональная Rust-библиотека для прямой работы с FAT32-разделами и образами. Обеспечивает низкоуровневый доступ к файловой системе FAT32 с поддержкой чтения, записи, создания и удаления файлов и директорий. + +## ✨ Ключевые особенности + +### 🎯 Основные возможности +- **Прямая работа с разделами**: Нативная поддержка ESP (EFI System Partition), SD-карт, USB-флешек +- **Кроссплатформенность**: Полная поддержка Windows и Linux с обработкой специфичных для ОС особенностей +- **Полный функционал FAT32**: Чтение, запись, создание, удаление файлов и директорий +- **Вложенные директории**: Поддержка создания и навигации по глубоко вложенным структурам каталогов +- **Длинные имена файлов (LFN)**: Полная поддержка Unicode-имён до 255 символов +- **Автоопределение параметров**: Автоматический разбор BPB (BIOS Parameter Block) + +### 🔧 Технические преимущества +- **Безопасность**: Минимальное использование `unsafe` кода, строгая типизация +- **Производительность**: Оптимизированные операции чтения/записи с буферизацией +- **Надёжность**: Корректная обработка ошибок, защита от повреждения данных +- **Windows-специфика**: Решение проблем с правами доступа (OS Error 5) через специальные флаги открытия файлов + +## 🚀 Быстрый старт + +### Работа с образом диска ```rust -use fat32_raw::fat32::Fat32Volume; +use fat32_raw::Fat32Volume; fn main() -> std::io::Result<()> { - // Открываем FAT32-образ (рекомендуется для тестов) - let image_path = "esp.img"; - let mut volume = Fat32Volume::open_esp(Some(image_path))? + // Открываем FAT32-образ + let mut volume = Fat32Volume::open_esp(Some("esp.img"))? .expect("Не удалось открыть FAT32-образ"); - // Создаём файл с длинным именем - let filename = "test.conf"; - if volume.create_file_lfn(filename)? { - println!("Файл '{}' создан", filename); - } else { - println!("Файл '{}' уже существует", filename); + // Создаём директории + volume.create_dir_lfn("config")?; + + // Создаём и записываем файл + volume.create_file_lfn("test.txt")?; + let content = b"Hello from fat32-raw!"; + volume.write_file("test.txt", content)?; + + // Читаем файл обратно + if let Some(data) = volume.read_file("test.txt")? { + println!("Содержимое: {}", String::from_utf8_lossy(&data)); } + + // Удаляем файл + volume.delete_file_lfn("test.txt")?; + + Ok(()) +} +``` - // Записываем данные в файл - let content = b"Привет из fat32-raw!"; - volume.write_file(filename, content)?; - println!("Данные записаны в '{}'", filename); - - // Читаем данные из файла - if let Some(data) = volume.read_file(filename)? { - println!("Содержимое '{}': {}", filename, String::from_utf8_lossy(&data)); - } +### Работа с реальным ESP-разделом +```rust +use fat32_raw::Fat32Volume; - // Удаляем файл - if volume.delete_file_lfn(filename)? { - println!("Файл '{}' удалён", filename); +fn main() -> std::io::Result<()> { + // Автоматический поиск и открытие ESP-раздела + // На Windows требуются права администратора + // На Linux может потребоваться sudo + let mut volume = Fat32Volume::open_esp(None::<&str>)? + .expect("ESP-раздел не найден"); + + // Работаем с разделом так же, как с образом + volume.create_dir_lfn("MyApp")?; + volume.create_file_lfn("MyApp_config.txt")?; + volume.write_file("MyApp_config.txt", b"Configuration")?; + + // Перечисляем файлы в корне + let entries = volume.list_root()?; + for entry in entries { + println!("{} - {}", + entry.name, + if entry.is_directory { "DIR" } else { "FILE" } + ); } - + Ok(()) } ``` -> [!tip] -> Полный пример использования находится в `./src/bin/main.rs` -> Для запуска используйте команду `cargo run --bin main` - ## 📦 Установка + Добавьте в `Cargo.toml`: -```ini +```toml [dependencies] -fat32-raw = "0.1" +fat32-raw = "1.0" +``` + +## 🧪 Тестирование + +Проект включает набор тестов для проверки всех операций: + +```bash +# Запуск обычных тестов +cargo test + +# Запуск тестов на реальном ESP (требует sudo/Administrator) +# ⨏️ ОСТОРОЖНО: этот тест работает с реальным ESP разделом! +sudo cargo test --test real_esp_test +``` + +## 🏗️ Структура проекта + +``` +fat32-raw/ +├── src/ +│ ├── lib.rs # Главный модуль библиотеки +│ ├── error.rs # Обработка ошибок +│ ├── fat32/ +│ │ ├── mod.rs # Модуль FAT32 +│ │ ├── volume.rs # Основная логика работы с томом +│ │ ├── directory.rs # Работа с директориями +│ │ ├── file.rs # Работа с файлами +│ │ ├── fat_table.rs # Работа с FAT-таблицей +│ │ ├── lfn.rs # Поддержка длинных имён файлов +│ │ └── utils.rs # Вспомогательные функции +│ └── platform/ +│ ├── mod.rs # Платформенные абстракции +│ ├── windows/ # Windows-специфичный код +│ └── unix/ # Unix/Linux-специфичный код +└── tests/ + └── real_esp_test.rs # Комплексные тесты с реальным ESP ``` -## 🚧 Планы на будущее +## 🔄 Версия 1.0.0 - Мажорный релиз 🎉 + +### 🐛 Исправления для Windows +- **Решена критическая проблема OS Error 5**: При записи файлов на ESP-раздел в Windows возникала ошибка доступа +- **Реализовано решение**: Использование специальных флагов `FILE_SHARE_READ | FILE_SHARE_WRITE` при открытии разделов +- **Улучшена совместимость**: Корректная работа с Windows-специфичными путями (`\\.\PhysicalDriveN`) + +### 🏗️ Рефакторинг структуры +- Реорганизована структура проекта для лучшей модульности +- Разделен код на платформенно-зависимые и независимые части +- Улучшена структура модуля FAT32 для большей ясности + +### ✅ Улучшения тестирования +- Создан комплексный тест `real_esp_test.rs` для реального ESP +- Добавлены тесты для вложенных директорий и больших файлов +- Реализована полная проверка всех операций файловой системы + +## 🚧 Планы развития + - [X] Поддержка создания и удаления файлов и директорий - [X] Автоматический поиск ESP раздела на дисках -- [ ] Работа с поддиректориями -- [X] Интеграция с реальными дисками Windows и Linux -- [ ] Поддержка MBR -- [ ] Тесты и CI +- [X] Работа с вложенными директориями +- [X] Полная интеграция с Windows и Linux +- [X] Решение проблем с правами доступа в Windows +- [ ] ⏳ Поддержка MBR-разделов +- [ ] ⏳ Дефрагментация и оптимизация +- [ ] ⏳ Поддержка FAT12/FAT16 +- [X] ⏳ Интеграция с GitHub Actions CI/CD + +## 🤝 Вклад в проект + +Мы приветствуем вклад в развитие проекта! Пожалуйста: +1. Форкните репозиторий +2. Создайте ветку для ваших изменений +3. Убедитесь, что все тесты проходят +4. Отправьте pull request ## 📄 Лицензия + Проект распространяется под лицензией [GPLv3](./LICENSE). + +## 🙏 Благодарности + +- Сообществу Rust за отличные инструменты и документацию +- Авторам спецификации FAT32 от Microsoft +- Всем контрибьюторам и пользователям проекта + +--- + +
+Сделано с ❤️ используя Rust +
diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index d39b723..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Пример использования библиотеки для работы с FAT32-томом или образом ESP-раздела. -//! -//! **ВНИМАНИЕ:** Для тестов рекомендуется использовать файл-образ (например, esp.img), -//! а не реальный раздел диска, чтобы избежать потери данных! - -use fat32_raw::fat32::{Fat32Volume}; -use std::io::Result; - -/// Основная функция: демонстрирует работу с образом и с реальным ESP-разделом. -fn main() -> Result<()> { - // === Рекомендуемый вариант: работа с образом === - let image_path = "esp.img"; - println!("Открытие FAT32-тома по образу '{}'", image_path); - if let Some(mut volume) = Fat32Volume::open_esp(Some(image_path))? { - run_demo(&mut volume)?; - } else { - println!("Не удалось открыть FAT32-образ '{}'", image_path); - } - - // === Альтернативный вариант: автоматический поиск ESP-раздела === - // - // ВНИМАНИЕ! - // При разработке и тестировании библиотеки рекомендуется использовать только образы дисков (например, esp.img), - // чтобы избежать риска повреждения данных на реальных разделах. - // - // Если вы используете библиотеку в реальных приложениях и уверены в стабильности, - // автоматический поиск и работа с настоящим ESP-разделом возможны. - // Однако, вся ответственность за сохранность данных лежит на вас: - // библиотека не гарантирует 100% корректность работы с каждым конкретным диском. - // - /* - println!("Автоматический поиск и открытие ESP-раздела на реальном диске..."); - if let Some(mut volume) = Fat32Volume::open_esp::<&str>(None)? { - run_demo(&mut volume)?; - } else { - println!("ESP раздел не найден!"); - } - */ - - Ok(()) -} - -/// Демонстрирует создание, запись, чтение и удаление файлов и директорий. -fn run_demo(volume: &mut Fat32Volume) -> Result<()> { - println!("Содержимое корня до операций:"); - print_dir(volume)?; - - test_file_workflow(volume)?; - test_dir_workflow(volume)?; - - println!("Содержимое корня после всех операций:"); - print_dir(volume)?; - - Ok(()) -} - - -/// Демонстрирует создание, запись, чтение и удаление файла. -fn test_file_workflow(volume: &mut Fat32Volume) -> std::io::Result<()> { - let filename = "test.conf"; - if volume.create_file_lfn(filename)? { - println!("Файл '{}' создан.", filename); - } else { - println!("Файл '{}' уже существует.", filename); - } - - println!("Содержимое корня после создания файла:"); - print_dir(volume)?; - - let content = b"hello from rust & fat32!"; - if volume.write_file(filename, content)? { - println!("Файл '{}' записан.", filename); - } else { - println!("Не удалось записать в файл '{}'.", filename); - } - - match volume.read_file(filename)? { - Some(data) => println!( - "Содержимое '{}': {}", - filename, - String::from_utf8_lossy(&data) - ), - None => println!("Файл '{}' не найден после записи!", filename), - } - - if volume.delete_file_lfn(filename)? { - println!("Файл '{}' удалён.", filename); - } else { - println!("Не удалось удалить файл '{}'.", filename); - } - - println!("Содержимое корня после удаления файла:"); - print_dir(volume)?; - - Ok(()) -} - -/// Демонстрирует создание, проверку и удаление директории. -fn test_dir_workflow(volume: &mut Fat32Volume) -> std::io::Result<()> { - let dirname = "testdir"; - if volume.create_dir_lfn(dirname)? { - println!("Директория '{}' создана.", dirname); - } else { - println!("Директория '{}' уже существует.", dirname); - } - - println!("Содержимое корня после создания директории:"); - print_dir(volume)?; - - println!("Содержимое папки testdir:"); - print_dir_contents(volume, dirname)?; - - // Проверяем, пуста ли директория - let dir_cluster = { - let entries = volume.list_root()?; - entries - .iter() - .find(|e| e.name.eq_ignore_ascii_case(dirname) && e.is_directory) - .map(|e| e.start_cluster) - }; - if let Some(cluster) = dir_cluster { - if volume.is_dir_empty(cluster)? { - println!("Директория '{}' пуста.", dirname); - } else { - println!("Директория '{}' не пуста!", dirname); - } - } else { - println!("Директория '{}' не найдена!", dirname); - } - - if volume.delete_dir_lfn(dirname)? { - println!("Директория '{}' удалена.", dirname); - } else { - println!( - "Не удалось удалить директорию '{}'. Возможно, она не пуста.", - dirname - ); - } - - println!("Содержимое корня после удаления директории:"); - print_dir(volume)?; - - Ok(()) -} - -/// Красиво выводит содержимое корневой директории. -fn print_dir(volume: &mut Fat32Volume) -> std::io::Result<()> { - let entries = volume.list_root()?; - if entries.is_empty() { - println!("(пусто)"); - } else { - for e in entries { - let typ = if e.is_directory { "" } else { " " }; - println!( - "{:20} {} кластер={} размер={}", - e.name, typ, e.start_cluster, e.size - ); - } - } - Ok(()) -} - -/// Красиво выводит содержимое указанной директории. -fn print_dir_contents(volume: &mut Fat32Volume, dirname: &str) -> std::io::Result<()> { - let entries = list_directory_by_name(volume, dirname)?; - if entries.is_empty() { - println!("(пусто)"); - } else { - for e in entries { - let typ = if e.is_directory { "" } else { " " }; - println!( - "{:20} {} кластер={} размер={}", - e.name, typ, e.start_cluster, e.size - ); - } - } - Ok(()) -} - -/// Получает содержимое директории по имени. -fn list_directory_by_name( - volume: &mut Fat32Volume, - dirname: &str, -) -> std::io::Result> { - let entries = volume.list_root()?; - let dir = entries - .iter() - .find(|e| e.name.eq_ignore_ascii_case(dirname) && e.is_directory); - if let Some(dir_entry) = dir { - let all = volume.list_directory(dir_entry.start_cluster)?; - Ok(all.into_iter().filter(|e| !e.name.is_empty()).collect()) - } else { - Ok(Vec::new()) - } -} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..9c7d29b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,133 @@ +//! Error types for the fat32-raw library + +use std::fmt; +use std::io; + +/// Result type for fat32-raw operations +pub type Result = std::result::Result; + +/// Main error type for fat32-raw operations +#[derive(Debug)] +pub enum Fat32Error { + /// I/O error from underlying file operations + Io(io::Error), + + /// Invalid FAT32 structure or parameters + InvalidFat32 { message: String }, + + /// File or directory not found + NotFound { path: String }, + + /// File or directory already exists + AlreadyExists { path: String }, + + /// No free clusters available + NoFreeSpace, + + /// Invalid file name + InvalidFileName { name: String, reason: String }, + + /// Platform-specific error + PlatformError { + message: String, + #[cfg(windows)] + code: Option, + }, + + /// Access denied (typically on Windows ESP partitions) + AccessDenied { + path: String, + tried_strategies: Vec, + }, + + /// ESP partition not found + EspNotFound, +} + +impl fmt::Display for Fat32Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(err) => write!(f, "I/O error: {}", err), + Self::InvalidFat32 { message } => write!(f, "Invalid FAT32: {}", message), + Self::NotFound { path } => write!(f, "Not found: {}", path), + Self::AlreadyExists { path } => write!(f, "Already exists: {}", path), + Self::NoFreeSpace => write!(f, "No free space available"), + Self::InvalidFileName { name, reason } => { + write!(f, "Invalid file name '{}': {}", name, reason) + } + Self::PlatformError { message, .. } => write!(f, "Platform error: {}", message), + Self::AccessDenied { + path, + tried_strategies, + } => { + write!( + f, + "Access denied for '{}'. Tried strategies: {:?}", + path, tried_strategies + ) + } + Self::EspNotFound => write!(f, "ESP partition not found"), + } + } +} + +impl std::error::Error for Fat32Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Io(err) => Some(err), + _ => None, + } + } +} + +impl From for Fat32Error { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +// Convenience constructors +impl Fat32Error { + pub fn invalid_fat32(message: impl Into) -> Self { + Self::InvalidFat32 { + message: message.into(), + } + } + + pub fn not_found(path: impl Into) -> Self { + Self::NotFound { path: path.into() } + } + + pub fn already_exists(path: impl Into) -> Self { + Self::AlreadyExists { path: path.into() } + } + + pub fn invalid_file_name(name: impl Into, reason: impl Into) -> Self { + Self::InvalidFileName { + name: name.into(), + reason: reason.into(), + } + } + + #[cfg(windows)] + pub fn platform_error(message: impl Into, code: Option) -> Self { + Self::PlatformError { + message: message.into(), + code, + } + } + + #[cfg(not(windows))] + pub fn platform_error(message: impl Into) -> Self { + Self::PlatformError { + message: message.into(), + } + } + + pub fn access_denied(path: impl Into, tried_strategies: Vec) -> Self { + Self::AccessDenied { + path: path.into(), + tried_strategies, + } + } +} diff --git a/src/fat32.rs b/src/fat32.rs deleted file mode 100644 index 6befab7..0000000 --- a/src/fat32.rs +++ /dev/null @@ -1,1158 +0,0 @@ -#[cfg(target_os = "linux")] -use glob::glob; - -use log; -use std::path::Path; - -use std::fs::File; -use std::io::{self, Read, Seek, SeekFrom, Write}; -use std::string::String; - -const DIR_ENTRY_SIZE: usize = 32; -const LFN_ATTRIBUTE: u8 = 0x0F; - -#[derive(Debug)] -pub struct Fat32FileEntry { - pub name: String, - pub start_cluster: u32, - pub size: u32, - pub is_directory: bool, -} - -#[derive(Debug)] -pub struct Fat32Params { - pub bytes_per_sector: u16, - pub sectors_per_cluster: u8, - pub reserved_sectors: u16, - pub num_fats: u8, - pub sectors_per_fat: u32, - pub root_cluster: u32, -} - -pub fn read_bpb(file: &mut std::fs::File, offset: u64) -> std::io::Result { - use std::io::Seek; - file.seek(std::io::SeekFrom::Start(offset))?; - let mut bpb = [0u8; 512]; - file.read_exact(&mut bpb)?; - Ok(Fat32Params { - bytes_per_sector: u16::from_le_bytes([bpb[0x0B], bpb[0x0C]]), - sectors_per_cluster: bpb[0x0D], - reserved_sectors: u16::from_le_bytes([bpb[0x0E], bpb[0x0F]]), - num_fats: bpb[0x10], - sectors_per_fat: u32::from_le_bytes([bpb[0x24], bpb[0x25], bpb[0x26], bpb[0x27]]), - root_cluster: u32::from_le_bytes([bpb[0x2C], bpb[0x2D], bpb[0x2E], bpb[0x2F]]), - }) -} - -pub struct Fat32Volume { - sync_on_write: bool, - file: File, - fat_offset: u64, - data_offset: u64, - bytes_per_sector: u16, - sectors_per_cluster: u32, - root_cluster: u32, - fat: Vec, -} - -impl Fat32Volume { - pub fn open( - sync_on_write: bool, - device_path: &str, - esp_start_lba: u64, - bytes_per_sector: u16, - sectors_per_cluster: u32, - reserved_sectors: u32, - num_fats: u32, - sectors_per_fat: u32, - root_cluster: u32, - ) -> io::Result { - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(device_path)?; - - // Преобразуем в u64 перед умножением - let bytes_per_sector_u64 = bytes_per_sector as u64; - let esp_offset = esp_start_lba * bytes_per_sector_u64; - - // Остальные вычисления - let fat_offset = esp_offset + (reserved_sectors as u64 * bytes_per_sector_u64); - let fat_size_bytes = (sectors_per_fat as u64 * bytes_per_sector_u64) as usize; - - file.seek(SeekFrom::Start(fat_offset))?; - let mut fat = vec![0u8; fat_size_bytes]; - file.read_exact(&mut fat)?; - - let data_offset = esp_offset - + (reserved_sectors as u64 + (num_fats as u64) * (sectors_per_fat as u64)) - * bytes_per_sector_u64; - - Ok(Fat32Volume { - sync_on_write, - file, - fat_offset, - data_offset, - bytes_per_sector, - sectors_per_cluster, - root_cluster, - fat, - }) - } - - fn read_cluster(&mut self, cluster_num: u32) -> io::Result> { - let cluster_size = self.sectors_per_cluster as u64 * self.bytes_per_sector as u64; - let cluster_offset = self.data_offset + (cluster_num as u64 - 2) * cluster_size; - self.file.seek(SeekFrom::Start(cluster_offset))?; - let mut buf = vec![0u8; cluster_size as usize]; - self.file.read_exact(&mut buf)?; - Ok(buf) - } - - fn get_fat_entry(&self, cluster_num: u32) -> u32 { - let offset = (cluster_num * 4) as usize; - u32::from_le_bytes(self.fat[offset..offset + 4].try_into().unwrap()) & 0x0FFFFFFF - } - - pub fn list_root(&mut self) -> io::Result> { - self.list_directory(self.root_cluster) - } - - pub fn list_directory(&mut self, start_cluster: u32) -> io::Result> { - let mut entries = Vec::new(); - let mut current_cluster = start_cluster; - loop { - let cluster_data = self.read_cluster(current_cluster)?; - let parsed_entries = parse_directory_entries(&cluster_data); - for (name, cluster, size, attr) in parsed_entries { - let is_dir = (attr & 0x10) != 0; - entries.push(Fat32FileEntry { - name, - start_cluster: cluster, - size, - is_directory: is_dir, - }); - } - current_cluster = self.get_fat_entry(current_cluster); - if current_cluster >= 0x0FFFFFF8 { - break; - } - } - Ok(entries) - } - - pub fn read_file(&mut self, filename: &str) -> io::Result>> { - let entries = self.list_root()?; - for entry in entries { - if entry.name.trim().eq_ignore_ascii_case(filename) && !entry.is_directory { - let mut cluster = entry.start_cluster; - let mut remaining = entry.size; - let mut content = Vec::new(); - while cluster < 0x0FFFFFF8 { - let data = self.read_cluster(cluster)?; - let to_take = remaining.min(data.len() as u32) as usize; - content.extend_from_slice(&data[..to_take]); - remaining -= to_take as u32; - if remaining == 0 { - break; - } - cluster = self.get_fat_entry(cluster); - } - return Ok(Some(content)); - } - } - Ok(None) - } - - pub fn write_file(&mut self, filename: &str, new_content: &[u8]) -> io::Result { - // 1. Находим файл в корне - let entries = self.list_root()?; - let mut entry_opt = None; - for entry in &entries { - if entry.name.trim().eq_ignore_ascii_case(filename) && !entry.is_directory { - entry_opt = Some(entry); - break; - } - } - let entry = match entry_opt { - Some(e) => e, - None => return Ok(false), - }; - let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; - let needed_clusters = (new_content.len() + cluster_size - 1) / cluster_size; - // 2. Собираем текущую цепочку кластеров - let mut clusters = Vec::new(); - let mut cluster = entry.start_cluster; - while cluster < 0x0FFFFFF8 { - clusters.push(cluster); - cluster = self.get_fat_entry(cluster); - } - // 3. Если не хватает кластеров — выделяем новые - while clusters.len() < needed_clusters { - let free = self - .find_free_cluster() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных кластеров"))?; - self.set_fat_entry(*clusters.last().unwrap(), free); - clusters.push(free); - } - // 4. Если лишние — освобождаем - while clusters.len() > needed_clusters { - let last = clusters.pop().unwrap(); - self.set_fat_entry(last, 0); - } - // 5. Завершаем цепочку - if let Some(&last) = clusters.last() { - self.set_fat_entry(last, 0x0FFFFFFF); - } - // 6. Записываем новые данные по кластерам - let mut offset = 0; - for &cl in &clusters { - let cluster_offset = self.data_offset + (cl as u64 - 2) * cluster_size as u64; - self.file.seek(SeekFrom::Start(cluster_offset))?; - let to_write = (new_content.len() - offset).min(cluster_size); - self.file - .write_all(&new_content[offset..offset + to_write])?; - if to_write < cluster_size { - let zeroes = vec![0u8; cluster_size - to_write]; - self.file.write_all(&zeroes)?; - } - offset += to_write; - } - // 7. Обновляем размер файла в директории - self.update_file_size_in_dir(entry.start_cluster, new_content.len() as u32)?; - // 8. Сохраняем FAT на диск - self.flush_fat()?; - Ok(true) - } - - fn find_free_cluster(&self) -> Option { - for i in 2..(self.fat.len() as u32 / 4) { - if self.get_fat_entry(i) == 0 { - return Some(i); - } - } - None - } - - fn set_fat_entry(&mut self, cluster: u32, value: u32) { - let offset = (cluster * 4) as usize; - self.fat[offset..offset + 4].copy_from_slice(&(value & 0x0FFFFFFF).to_le_bytes()); - } - - fn flush_fat(&mut self) -> io::Result<()> { - self.file.seek(SeekFrom::Start(self.fat_offset))?; - self.file.write_all(&self.fat)?; - if self.sync_on_write { - self.file.sync_all()?; - } - Ok(()) - } - - fn update_file_size_in_dir(&mut self, start_cluster: u32, new_size: u32) -> io::Result<()> { - // Находим запись в директории и обновляем размер (4 байта) - let mut dir_cluster = self.root_cluster; - loop { - let cluster_offset = self.data_offset - + (dir_cluster as u64 - 2) - * self.sectors_per_cluster as u64 - * self.bytes_per_sector as u64; - self.file.seek(SeekFrom::Start(cluster_offset))?; - let mut buf = - vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; - self.file.read_exact(&mut buf)?; - for i in 0..(buf.len() / 32) { - let entry = &mut buf[i * 32..(i + 1) * 32]; - let high = u16::from_le_bytes([entry[20], entry[21]]) as u32; - let low = u16::from_le_bytes([entry[26], entry[27]]) as u32; - let cl = (high << 16) | low; - if cl == start_cluster { - entry[28..32].copy_from_slice(&new_size.to_le_bytes()); - // Записываем обратно - self.file - .seek(SeekFrom::Start(cluster_offset + (i * 32) as u64))?; - self.file.write_all(entry)?; - return Ok(()); - } - } - dir_cluster = self.get_fat_entry(dir_cluster); - if dir_cluster >= 0x0FFFFFF8 { - break; - } - } - Err(io::Error::new( - io::ErrorKind::NotFound, - "dir entry not found", - )) - } - - fn create_entry_lfn( - &mut self, - name: &str, - attr: u8, - parent_cluster: u32, // кластер родителя (обычно root_cluster для корня) - ) -> io::Result> { - let entries = self.list_directory(parent_cluster)?; - if entries.iter().any(|e| e.name.eq_ignore_ascii_case(name)) { - return Ok(None); - } - let (base, ext) = generate_short_name( - name, - &entries.iter().map(|e| e.name.clone()).collect::>(), - ); - let mut short_name = [b' '; 11]; - for (i, b) in base.as_bytes().iter().take(8).enumerate() { - short_name[i] = *b; - } - for (i, b) in ext.as_bytes().iter().take(3).enumerate() { - short_name[8 + i] = *b; - } - let checksum = lfn_checksum(&short_name); - - let new_cluster = self - .find_free_cluster() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных кластеров"))?; - self.set_fat_entry(new_cluster, 0x0FFFFFFF); - - let lfn_entries = make_lfn_entries(name, checksum); - - let mut dir_entry = [0u8; 32]; - dir_entry[0..8].copy_from_slice(&short_name[0..8]); - dir_entry[8..11].copy_from_slice(&short_name[8..11]); - dir_entry[11] = attr; // 0x20 файл, 0x10 директория - dir_entry[20..22].copy_from_slice(&((new_cluster >> 16) as u16).to_le_bytes()); - dir_entry[26..28].copy_from_slice(&(new_cluster as u16).to_le_bytes()); - dir_entry[28..32].copy_from_slice(&0u32.to_le_bytes()); - - self.write_lfn_and_short_entry(parent_cluster, lfn_entries, dir_entry)?; - self.flush_fat()?; - Ok(Some(new_cluster)) - } - - pub fn create_file_lfn(&mut self, filename: &str) -> io::Result { - Ok(self - .create_entry_lfn(filename, 0x20, self.root_cluster)? - .is_some()) - } - - pub fn delete_file_lfn(&mut self, filename: &str) -> io::Result { - let mut dir_cluster = self.root_cluster; - loop { - let cluster_offset = self.data_offset - + (dir_cluster as u64 - 2) - * self.sectors_per_cluster as u64 - * self.bytes_per_sector as u64; - self.file.seek(SeekFrom::Start(cluster_offset))?; - let mut buf = - vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; - self.file.read_exact(&mut buf)?; - - let mut i = 0; - while i < buf.len() / 32 { - // Собираем LFN-цепочку - let mut lfn_stack = Vec::new(); - let mut j = i; - while j < buf.len() / 32 - && buf[j * 32 + 11] == LFN_ATTRIBUTE - && buf[j * 32] != 0xE5 - && buf[j * 32] != 0x00 - { - lfn_stack.push(j); - j += 1; - } - if j >= buf.len() / 32 || buf[j * 32] == 0x00 { - break; - } - // Проверяем короткую запись - if let Some((_short_name, start_cluster, _file_size)) = - parse_dir_entry(&buf[j * 32..(j + 1) * 32]) - { - let full_name = if !lfn_stack.is_empty() { - let mut name_parts = Vec::new(); - for &lfn_idx in lfn_stack.iter().rev() { - if let Some(part) = - parse_lfn_entry(&buf[lfn_idx * 32..(lfn_idx + 1) * 32]) - { - name_parts.push(part); - } - } - name_parts.concat() - } else { - _short_name.clone() - }; - if full_name.eq_ignore_ascii_case(filename) { - // 1. Освобождаем цепочку кластеров - let mut cl = start_cluster; - while cl < 0x0FFFFFF8 && cl != 0 { - let next = self.get_fat_entry(cl); - self.set_fat_entry(cl, 0); - cl = next; - } - // 2. Помечаем LFN-записи как удалённые - for &lfn_idx in &lfn_stack { - buf[lfn_idx * 32] = 0xE5; - } - // 3. Помечаем короткую запись как удалённую - buf[j * 32] = 0xE5; - self.file.seek(SeekFrom::Start(cluster_offset))?; - self.file.write_all(&buf)?; - self.flush_fat()?; - return Ok(true); - } - } - i = j + 1; - } - dir_cluster = self.get_fat_entry(dir_cluster); - if dir_cluster >= 0x0FFFFFF8 { - break; - } - } - Ok(false) - } - - pub fn create_dir_lfn(&mut self, dirname: &str) -> io::Result { - if let Some(new_cluster) = self.create_entry_lfn(dirname, 0x10, self.root_cluster)? { - let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; - let mut buf = vec![0u8; cluster_size]; - - // Запись "." - let mut dot_entry = [b' '; 32]; - dot_entry[0] = b'.'; - dot_entry[11] = 0x10; // атрибут директории - dot_entry[20..22].copy_from_slice(&((new_cluster >> 16) as u16).to_le_bytes()); - dot_entry[26..28].copy_from_slice(&(new_cluster as u16).to_le_bytes()); - - // Запись ".." - let mut dotdot_entry = [b' '; 32]; - dotdot_entry[0] = b'.'; - dotdot_entry[1] = b'.'; - dotdot_entry[11] = 0x10; // атрибут директории - dotdot_entry[20..22].copy_from_slice(&((self.root_cluster >> 16) as u16).to_le_bytes()); - dotdot_entry[26..28].copy_from_slice(&(self.root_cluster as u16).to_le_bytes()); - - // Копируем записи в буфер - buf[0..32].copy_from_slice(&dot_entry); - buf[32..64].copy_from_slice(&dotdot_entry); - - let cluster_offset = self.data_offset + (new_cluster as u64 - 2) * cluster_size as u64; - self.file.seek(SeekFrom::Start(cluster_offset))?; - self.file.write_all(&buf)?; - self.flush_fat()?; - Ok(true) - } else { - Ok(false) - } - } - - pub fn delete_dir_lfn(&mut self, dirname: &str) -> io::Result { - let mut dir_cluster = self.root_cluster; - loop { - let cluster_offset = self.data_offset - + (dir_cluster as u64 - 2) - * self.sectors_per_cluster as u64 - * self.bytes_per_sector as u64; - self.file.seek(SeekFrom::Start(cluster_offset))?; - let mut buf = - vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; - self.file.read_exact(&mut buf)?; - - let mut i = 0; - while i < buf.len() / 32 { - let mut lfn_stack = Vec::new(); - let mut j = i; - while j < buf.len() / 32 - && buf[j * 32 + 11] == LFN_ATTRIBUTE - && buf[j * 32] != 0xE5 - && buf[j * 32] != 0x00 - { - lfn_stack.push(j); - j += 1; - } - if j >= buf.len() / 32 || buf[j * 32] == 0x00 { - break; - } - if let Some((_short_name, start_cluster, _file_size)) = - parse_dir_entry(&buf[j * 32..(j + 1) * 32]) - { - let full_name = if !lfn_stack.is_empty() { - let mut name_parts = Vec::new(); - for &lfn_idx in lfn_stack.iter().rev() { - if let Some(part) = - parse_lfn_entry(&buf[lfn_idx * 32..(lfn_idx + 1) * 32]) - { - name_parts.push(part); - } - } - name_parts.concat() - } else { - _short_name.clone() - }; - let attr = buf[j * 32 + 11]; - if full_name.eq_ignore_ascii_case(dirname) && (attr & 0x10) != 0 { - // Проверяем, пуста ли директория - let entries = self.list_directory(start_cluster)?; - let only_dot = entries.iter().all(|e| e.name == "." || e.name == ".."); - if !only_dot { - return Ok(false); // не пуста! - } - // Освободить кластер - let mut cl = start_cluster; - while cl < 0x0FFFFFF8 && cl != 0 { - let next = self.get_fat_entry(cl); - self.set_fat_entry(cl, 0); - cl = next; - } - // Пометить LFN и короткую запись как удалённые - for &lfn_idx in &lfn_stack { - buf[lfn_idx * 32] = 0xE5; - } - buf[j * 32] = 0xE5; - self.file.seek(SeekFrom::Start(cluster_offset))?; - self.file.write_all(&buf)?; - self.flush_fat()?; - return Ok(true); - } - } - i = j + 1; - } - dir_cluster = self.get_fat_entry(dir_cluster); - if dir_cluster >= 0x0FFFFFF8 { - break; - } - } - Ok(false) - } - - fn write_lfn_and_short_entry( - &mut self, - dir_cluster: u32, - lfn_entries: Vec<[u8; 32]>, - short_entry: [u8; 32], - ) -> io::Result<()> { - // Находим подряд N+1 свободных записей в директории - let cluster_offset = self.data_offset - + (dir_cluster as u64 - 2) - * self.sectors_per_cluster as u64 - * self.bytes_per_sector as u64; - self.file.seek(SeekFrom::Start(cluster_offset))?; - let mut buf = vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; - self.file.read_exact(&mut buf)?; - - let total = lfn_entries.len() + 1; - let mut free_idx = None; - let mut count = 0; - for i in 0..(buf.len() / 32) { - let entry = &buf[i * 32..(i + 1) * 32]; - if entry[0] == 0x00 || entry[0] == 0xE5 { - count += 1; - if count == total { - free_idx = Some(i + 1 - total); - break; - } - } else { - count = 0; - } - } - let idx = free_idx - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных записей"))?; - // Записываем LFN - for (j, lfn) in lfn_entries.iter().enumerate() { - buf[(idx + j) * 32..(idx + j + 1) * 32].copy_from_slice(lfn); - } - // Записываем короткую запись - buf[(idx + lfn_entries.len()) * 32..(idx + lfn_entries.len() + 1) * 32] - .copy_from_slice(&short_entry); - - // Записываем обратно - self.file.seek(SeekFrom::Start(cluster_offset))?; - self.file.write_all(&buf)?; - - Ok(()) - } - - pub fn is_dir_empty(&mut self, dir_cluster: u32) -> std::io::Result { - let mut current_cluster = dir_cluster; - loop { - let cluster_data = self.read_cluster(current_cluster)?; - for i in 0..(cluster_data.len() / 32) { - let entry = &cluster_data[i * 32..(i + 1) * 32]; - if entry[0] == 0x00 { - // Все последующие записи свободны - break; - } - if entry[0] == 0xE5 || entry[11] == LFN_ATTRIBUTE { - continue; - } - // Это короткая запись, не удалённая, не LFN - let name_raw = &entry[0..8]; - let ext_raw = &entry[8..11]; - let name = String::from_utf8_lossy(name_raw).trim_end().to_string(); - let ext = String::from_utf8_lossy(ext_raw).trim_end().to_string(); - let filename = if ext.is_empty() { - name.clone() - } else { - format!("{}.{}", name, ext) - }; - if filename != "." && filename != ".." { - return Ok(false); - } - } - current_cluster = self.get_fat_entry(current_cluster); - if current_cluster >= 0x0FFFFFF8 { - break; - } - } - Ok(true) - } - - pub fn open_esp>(path: Option

) -> io::Result> { - if let Some(p) = path { - // Открываем указанный образ или устройство - let path_str = p.as_ref(); - let mut file = std::fs::File::open(path_str)?; - // BPB обычно в самом начале - let params = read_bpb(&mut file, 0)?; - - // Валидация параметров BPB - if params.bytes_per_sector < 512 || params.bytes_per_sector > 4096 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "bytes_per_sector out of range", - )); - } - if params.sectors_per_cluster == 0 || params.sectors_per_cluster > 128 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "sectors_per_cluster out of range", - )); - } - if params.num_fats == 0 || params.num_fats > 4 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "num_fats out of range", - )); - } - if params.sectors_per_fat == 0 || params.sectors_per_fat > 1_000_000 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "sectors_per_fat out of range", - )); - } - - Fat32Volume::open( - false, - path_str, - 0, // lba = 0 для образа - params.bytes_per_sector, - params.sectors_per_cluster as u32, - params.reserved_sectors as u32, - params.num_fats as u32, - params.sectors_per_fat, - params.root_cluster, - ) - .map(Some) - } else { - match find_esp_device()? { - Some((path, lba)) => { - let mut file = File::open(&path)?; - - let bpb_offset = lba * 512; - let params = read_bpb(&mut file, bpb_offset)?; - - log::info!( - "BPB: bytes_per_sector={}, sectors_per_cluster={}, reserved_sectors={}, num_fats={}, sectors_per_fat={}, root_cluster={}", - params.bytes_per_sector, - params.sectors_per_cluster, - params.reserved_sectors, - params.num_fats, - params.sectors_per_fat, - params.root_cluster - ); - - if params.bytes_per_sector < 512 || params.bytes_per_sector > 4096 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "bytes_per_sector out of range", - )); - } - if params.sectors_per_cluster == 0 || params.sectors_per_cluster > 128 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "sectors_per_cluster out of range", - )); - } - if params.num_fats == 0 || params.num_fats > 4 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "num_fats out of range", - )); - } - if params.sectors_per_fat == 0 || params.sectors_per_fat > 1_000_000 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "sectors_per_fat out of range", - )); - } - - Fat32Volume::open( - true, // sync_on_write = true для реальных устройств (безопаснее) - &path, - lba, // LBA - params.bytes_per_sector, - params.sectors_per_cluster as u32, - params.reserved_sectors as u32, - params.num_fats as u32, - params.sectors_per_fat, - params.root_cluster, - ) - .map(Some) - } - None => Ok(None), - } - } - } -} - -fn generate_short_name(long_name: &str, existing: &[String]) -> (String, String) { - let mut base = String::new(); - let mut ext = String::new(); - let parts: Vec<&str> = long_name.split('.').collect(); - let name_part = parts.get(0).unwrap_or(&""); - let ext_part = parts.get(1).unwrap_or(&""); - for c in name_part.chars() { - if base.len() >= 6 { - break; - } - if c.is_ascii_alphanumeric() { - base.push(c.to_ascii_uppercase()); - } - } - if base.is_empty() { - base.push('X'); - } - let mut num = 1; - let mut candidate = format!("{}~{}", base, num); - while existing.iter().any(|n| n.starts_with(&candidate)) { - num += 1; - candidate = format!("{}~{}", base, num); - } - base = candidate; - for c in ext_part.chars() { - if ext.len() >= 3 { - break; - } - if c.is_ascii_alphanumeric() { - ext.push(c.to_ascii_uppercase()); - } - } - (base, ext) -} - -fn lfn_checksum(short_name: &[u8; 11]) -> u8 { - let mut sum = 0u8; - for &b in short_name { - sum = sum.rotate_right(1).wrapping_add(b); - } - sum -} - -fn make_lfn_entries(long_name: &str, checksum: u8) -> Vec<[u8; 32]> { - let utf16: Vec = long_name.encode_utf16().collect(); - let lfn_count = (utf16.len() + 12) / 13; - let mut entries = Vec::new(); - for i in 0..lfn_count { - let mut entry = [0u8; 32]; - entry[11] = 0x0F; - entry[13] = checksum; - let seq = (lfn_count - i) as u8; // номер LFN-записи (от lfn_count до 1) - entry[0] = if i == 0 { seq | 0x40 } else { seq }; - let start = i * 13; - let end = ((i + 1) * 13).min(utf16.len()); - let chunk = &utf16[start..end]; - for (j, &c) in chunk.iter().enumerate() { - let pos = match j { - 0..=4 => 1 + j * 2, - 5..=10 => 14 + (j - 5) * 2, - 11..=12 => 28 + (j - 11) * 2, - _ => 0, - }; - if pos > 0 { - entry[pos..pos + 2].copy_from_slice(&c.to_le_bytes()); - } - } - // Остальные байты - 0xFF - for pos in [1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30] { - if entry[pos] == 0 && entry[pos + 1] == 0 { - entry[pos] = 0xFF; - entry[pos + 1] = 0xFF; - } - } - entries.push(entry); - } - entries.reverse(); - entries -} - -fn parse_dir_entry(entry: &[u8]) -> Option<(String, u32, u32)> { - if entry[0] == 0x00 || entry[0] == 0xE5 { - return None; - } - let attr = entry[11]; - if attr == LFN_ATTRIBUTE { - return None; - } - let name_raw = &entry[0..8]; - let ext_raw = &entry[8..11]; - let name = String::from_utf8_lossy(name_raw).trim_end().to_string(); - let ext = String::from_utf8_lossy(ext_raw).trim_end().to_string(); - let filename = if ext.is_empty() { - name - } else { - format!("{}.{}", name, ext) - }; - let high = u16::from_le_bytes([entry[20], entry[21]]) as u32; - let low = u16::from_le_bytes([entry[26], entry[27]]) as u32; - let start_cluster = (high << 16) | low; - let file_size = u32::from_le_bytes([entry[28], entry[29], entry[30], entry[31]]); - Some((filename, start_cluster, file_size)) -} - -fn parse_lfn_entry(entry: &[u8]) -> Option { - if entry[11] != LFN_ATTRIBUTE { - return None; - } - let mut name_utf16 = Vec::new(); - let read_u16 = |b: &[u8]| u16::from_le_bytes([b[0], b[1]]); - for i in (1..=10).step_by(2) { - let c = read_u16(&entry[i..i + 2]); - if c == 0x0000 || c == 0xFFFF { - break; - } - name_utf16.push(c); - } - for i in (14..=25).step_by(2) { - let c = read_u16(&entry[i..i + 2]); - if c == 0x0000 || c == 0xFFFF { - break; - } - name_utf16.push(c); - } - for i in (28..=31).step_by(2) { - let c = read_u16(&entry[i..i + 2]); - if c == 0x0000 || c == 0xFFFF { - break; - } - name_utf16.push(c); - } - String::from_utf16(&name_utf16).ok() -} - -fn parse_directory_entries(cluster_data: &[u8]) -> Vec<(String, u32, u32, u8)> { - let mut results = Vec::new(); - let mut lfn_stack = Vec::new(); - for i in 0..(cluster_data.len() / DIR_ENTRY_SIZE) { - let entry = &cluster_data[i * DIR_ENTRY_SIZE..(i + 1) * DIR_ENTRY_SIZE]; - if entry[0] == 0x00 { - break; - } - if entry[11] == LFN_ATTRIBUTE { - if let Some(name_part) = parse_lfn_entry(entry) { - lfn_stack.insert(0, name_part); - } - continue; - } - if entry[0] == 0xE5 { - lfn_stack.clear(); - continue; - } - if let Some((_short_name, start_cluster, file_size)) = parse_dir_entry(entry) { - let full_name = if !lfn_stack.is_empty() { - let name = lfn_stack.join(""); - lfn_stack.clear(); - name - } else { - _short_name - }; - let attr = entry[11]; - if !full_name.is_empty() { - results.push((full_name, start_cluster, file_size, attr)); - } - } else { - lfn_stack.clear(); - } - } - results -} - -pub fn find_esp_device() -> io::Result> { - #[cfg(target_os = "linux")] - { - use std::fs; - - // Проверка стандартных путей ESP - let known_paths = [ - "/boot/efi", - "/efi", - ]; - - // 1. Поиск устройства, на котором смонтирован ESP - for mount_point in &known_paths { - let path = Path::new(mount_point); - if path.exists() { - if let Ok(mounts) = fs::read_to_string("/proc/mounts") { - for line in mounts.lines() { - let fields: Vec<&str> = line.split_whitespace().collect(); - if fields.len() >= 2 && fields[1] == *mount_point { - // fields[0] — это устройство, например /dev/sda1 - return Ok(Some((fields[0].to_string(), 0))); - } - } - } - } - } - - // 2. Проверка по /dev/disk/by-* - let known_dev_paths = [ - "/dev/disk/by-label/ESP", - "/dev/disk/by-label/EFI", - "/dev/disk/by-partlabel/ESP", - "/dev/disk/by-partlabel/EFI", - ]; - for path in &known_dev_paths { - let path = Path::new(path); - if path.exists() { - if let Ok(real_path) = fs::canonicalize(path) { - if let Some(path_str) = real_path.to_str() { - return Ok(Some((path_str.to_string(), 0))); - } - } - } - } - - // 3. Сканирование физических устройств - let sys_block = Path::new("/sys/block"); - for entry in fs::read_dir(sys_block)? { - let entry = entry?; - let dev_name = entry.file_name(); - let dev_path = entry.path(); - let dev_name_str = dev_name.to_string_lossy(); - - // Пропуск виртуальных устройств - if dev_name_str.starts_with("loop") - || dev_name_str.starts_with("ram") - || dev_name_str.starts_with("sr") - { - continue; - } - - // Поиск разделов - let pattern = format!("{}/{}*", dev_path.display(), dev_name_str); - let entries = match glob(&pattern) { - Ok(e) => e, - Err(_) => continue, - }; - - for entry in entries.flatten() { - // Проверка что это раздел - if !entry.join("partition").exists() { - continue; - } - - // Читаем PARTLABEL или PARTUUID для проверки, что это ESP - let partlabel_path = entry.join("partlabel"); - let is_esp = if partlabel_path.exists() { - if let Ok(label) = fs::read_to_string(&partlabel_path) { - label.trim().eq_ignore_ascii_case("EFI System Partition") || - label.trim().eq_ignore_ascii_case("ESP") || - label.trim().eq_ignore_ascii_case("EFI") - } else { - false - } - } else { - false - }; - - if !is_esp { - // Альтернативно: можно проверить type_guid, если есть - let type_path = entry.join("type"); - if type_path.exists() { - if let Ok(type_guid) = fs::read_to_string(&type_path) { - // GUID ESP: c12a7328-f81f-11d2-ba4b-00a0c93ec93b - if !type_guid.trim().eq_ignore_ascii_case("c12a7328-f81f-11d2-ba4b-00a0c93ec93b") { - continue; - } - } else { - continue; - } - } else { - continue; - } - } - - let part_name = entry.file_name().unwrap().to_str().unwrap(); - let dev_file = format!("/dev/{}", part_name); - - // Получаем LBA начала раздела - let start_lba_path = entry.join("start"); - let start_lba = if start_lba_path.exists() { - fs::read_to_string(&start_lba_path) - .ok() - .and_then(|s| s.trim().parse::().ok()) - .unwrap_or(0) - } else { - 0 - }; - - // Проверка сигнатуры FAT - if let Ok(mut f) = File::open(&dev_file) { - let mut header = [0u8; 3]; - if f.read_exact(&mut header).is_ok() { - if &header == b"FAT" || &header == b"MSD" { - // Это FAT-раздел - return Ok(Some((dev_file, start_lba))); - } - } - } - } - } - - Ok(None) - } - - #[cfg(target_os = "windows")] - { - use std::fs::OpenOptions; - use std::io::Seek; - use std::os::windows::fs::OpenOptionsExt; - use std::path::PathBuf; - use winapi::um::fileapi::{GetDriveTypeW, GetLogicalDrives}; - use winapi::um::winbase::{DRIVE_FIXED, DRIVE_NO_ROOT_DIR}; - - // 1. Поиск по буквам дисков через файловую систему - let drives = unsafe { GetLogicalDrives() }; - log::debug!("Logical drives bitmap: {:b}", drives); - - for i in 0..26 { - if (drives & (1 << i)) != 0 { - let drive_letter_char = (b'A' + i) as char; - let drive_letter = format!(r"{}:\", drive_letter_char); - log::debug!("Checking drive: {}", drive_letter); - - let wide_path: Vec = drive_letter.encode_utf16().chain(Some(0)).collect(); - - let drive_type = unsafe { GetDriveTypeW(wide_path.as_ptr()) }; - log::debug!("Drive type: {}", drive_type); - - if drive_type == DRIVE_FIXED || drive_type == DRIVE_NO_ROOT_DIR { - // Проверяем наличие стандартных EFI путей - let test_paths = [ - PathBuf::from(&drive_letter).join(r"EFI\BOOT\BOOTX64.EFI"), - PathBuf::from(&drive_letter).join(r"EFI\MICROSOFT\BOOT\BOOTMGFW.EFI"), - ]; - - for test_path in &test_paths { - log::debug!("Checking path: {}", test_path.display()); - if test_path.exists() { - log::debug!("Found bootloader at: {}", test_path.display()); - return Ok(Some((format!(r"\\.\{}:", drive_letter_char), 0))); - } - } - } - } - } - - log::debug!("No drives with bootloader found, checking physical drives..."); - - // 2. Поиск по GPT разделам - for disk_num in 0..4 { - let device_path = format!(r"\\.\PhysicalDrive{}", disk_num); - log::debug!("Checking physical drive: {}", device_path); - - match OpenOptions::new() - .read(true) - .share_mode(0) - .open(&device_path) - { - Ok(mut f) => { - // Проверяем GPT сигнатуру - let mut header = [0u8; 512]; - if let Err(e) = f.read_exact(&mut header) { - log::debug!("Failed to read header: {}", e); - continue; - } - - // Проверка защитного MBR (тип 0xEE) - let mbr_valid = header[510] == 0x55 && header[511] == 0xAA; - let protective_mbr = mbr_valid && header[450] == 0xEE; - - if protective_mbr { - log::debug!("Found protective MBR (GPT disk)"); - - // Читаем GPT header - let mut gpt_header = [0u8; 512]; - f.seek(SeekFrom::Start(512))?; - if let Err(e) = f.read_exact(&mut gpt_header) { - log::debug!("Failed to read GPT header: {}", e); - continue; - } - - // Сигнатура GPT - if &gpt_header[0..8] != b"EFI PART" { - log::debug!("Invalid GPT signature"); - continue; - } - - // Ищем раздел с типом ESP - let part_entry_start = - u64::from_le_bytes(gpt_header[72..80].try_into().unwrap()); - let part_entry_size = - u32::from_le_bytes(gpt_header[84..88].try_into().unwrap()); - let num_part_entries = - u32::from_le_bytes(gpt_header[80..84].try_into().unwrap()); - - log::debug!( - "Partition entries start: {} size: {} count: {}", - part_entry_start, part_entry_size, num_part_entries - ); - - // Читаем таблицу разделов - f.seek(SeekFrom::Start(part_entry_start * 512))?; - let mut part_table = - vec![0u8; (part_entry_size * num_part_entries) as usize]; - f.read_exact(&mut part_table)?; - - for i in 0..num_part_entries { - let offset = (i * part_entry_size) as usize; - let part_type = &part_table[offset..offset + 16]; - - // GUID для ESP раздела: C12A7328-F81F-11D2-BA4B-00A0C93EC93B - if part_type - == [ - 0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, - 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B, - ] - { - let part_start = u64::from_le_bytes( - part_table[offset + 32..offset + 40].try_into().unwrap(), - ); - log::debug!("Found ESP partition at LBA: {}", part_start); - - // Возвращаем путь к физическому диску - return Ok(Some((device_path, part_start))); - } - } - } - } - Err(e) => { - log::debug!("Error opening device: {}", e); - } - } - } - - log::debug!("No ESP partition found after full scan"); - Ok(None) - } - - // Для других ОС или если ничего не найдено - #[cfg(not(any(target_os = "linux", target_os = "windows")))] - Ok(None) -} diff --git a/src/fat32/directory.rs b/src/fat32/directory.rs new file mode 100644 index 0000000..879f8e8 --- /dev/null +++ b/src/fat32/directory.rs @@ -0,0 +1,39 @@ +use super::*; +use crate::fat32::file::{parse_dir_entry, parse_lfn_entry}; + +pub fn parse_directory_entries(cluster_data: &[u8]) -> Vec<(String, u32, u32, u8)> { + let mut results = Vec::new(); + let mut lfn_stack = Vec::new(); + for i in 0..(cluster_data.len() / DIR_ENTRY_SIZE) { + let entry = &cluster_data[i * DIR_ENTRY_SIZE..(i + 1) * DIR_ENTRY_SIZE]; + if entry[0] == 0x00 { + break; + } + if entry[11] == LFN_ATTRIBUTE { + if let Some(name_part) = parse_lfn_entry(entry) { + lfn_stack.insert(0, name_part); + } + continue; + } + if entry[0] == 0xE5 { + lfn_stack.clear(); + continue; + } + if let Some((_short_name, start_cluster, file_size)) = parse_dir_entry(entry) { + let full_name = if !lfn_stack.is_empty() { + let name = lfn_stack.join(""); + lfn_stack.clear(); + name + } else { + _short_name + }; + let attr = entry[11]; + if !full_name.is_empty() { + results.push((full_name, start_cluster, file_size, attr)); + } + } else { + lfn_stack.clear(); + } + } + results +} diff --git a/src/fat32/fat_table.rs b/src/fat32/fat_table.rs new file mode 100644 index 0000000..0d156ff --- /dev/null +++ b/src/fat32/fat_table.rs @@ -0,0 +1,189 @@ +//! FAT table management for FAT32 + +use std::io::{self, Read, Seek, SeekFrom, Write}; + +/// FAT32 end-of-chain marker +pub const FAT32_EOC: u32 = 0x0FFFFFF8; + +/// FAT32 bad cluster marker +pub const FAT32_BAD_CLUSTER: u32 = 0x0FFFFFF7; + +/// FAT32 free cluster marker +pub const FAT32_FREE_CLUSTER: u32 = 0x00000000; + +/// FAT table manager +pub struct FatTable { + /// FAT data in memory + data: Vec, + /// Offset of FAT in the file/device + offset: u64, + /// Whether to sync on write + sync_on_write: bool, +} + +impl FatTable { + /// Create a new FAT table manager + pub fn new(data: Vec, offset: u64, sync_on_write: bool) -> Self { + Self { + data, + offset, + sync_on_write, + } + } + + /// Load FAT table from file + pub fn load(file: &mut F, offset: u64, size: usize) -> io::Result { + file.seek(SeekFrom::Start(offset))?; + let mut data = vec![0u8; size]; + file.read_exact(&mut data)?; + Ok(Self::new(data, offset, false)) + } + + /// Get a FAT entry value + pub fn get_entry(&self, cluster: u32) -> u32 { + let offset = (cluster * 4) as usize; + if offset + 4 > self.data.len() { + return FAT32_EOC; + } + u32::from_le_bytes(self.data[offset..offset + 4].try_into().unwrap()) & 0x0FFFFFFF + } + + /// Set a FAT entry value + pub fn set_entry(&mut self, cluster: u32, value: u32) { + let offset = (cluster * 4) as usize; + if offset + 4 > self.data.len() { + return; + } + let bytes = (value & 0x0FFFFFFF).to_le_bytes(); + self.data[offset..offset + 4].copy_from_slice(&bytes); + } + + /// Find a free cluster + pub fn find_free_cluster(&self, start_hint: u32) -> Option { + let max_clusters = (self.data.len() / 4) as u32; + + // Try from hint first + for cluster in start_hint..max_clusters { + if cluster < 2 { + continue; // Skip reserved clusters + } + if self.get_entry(cluster) == FAT32_FREE_CLUSTER { + return Some(cluster); + } + } + + // Try from beginning + for cluster in 2..start_hint.min(max_clusters) { + if self.get_entry(cluster) == FAT32_FREE_CLUSTER { + return Some(cluster); + } + } + + None + } + + /// Allocate a chain of clusters + pub fn allocate_chain(&mut self, count: usize) -> Option> { + let mut clusters = Vec::with_capacity(count); + let mut last_allocated = 2; + + for _ in 0..count { + if let Some(free) = self.find_free_cluster(last_allocated) { + clusters.push(free); + last_allocated = free + 1; + + // Mark as end-of-chain for now + self.set_entry(free, FAT32_EOC); + } else { + // Not enough free clusters, rollback + for &cluster in &clusters { + self.set_entry(cluster, FAT32_FREE_CLUSTER); + } + return None; + } + } + + // Link the chain + for i in 0..clusters.len() - 1 { + self.set_entry(clusters[i], clusters[i + 1]); + } + + Some(clusters) + } + + /// Free a cluster chain + pub fn free_chain(&mut self, start_cluster: u32) { + let mut current = start_cluster; + + while current < FAT32_EOC && current != 0 { + let next = self.get_entry(current); + self.set_entry(current, FAT32_FREE_CLUSTER); + current = next; + } + } + + /// Extend a cluster chain + pub fn extend_chain(&mut self, last_cluster: u32, additional_count: usize) -> Option> { + let new_clusters = self.allocate_chain(additional_count)?; + + if !new_clusters.is_empty() { + // Link the old chain to the new clusters + self.set_entry(last_cluster, new_clusters[0]); + } + + Some(new_clusters) + } + + /// Get the last cluster in a chain + pub fn get_last_cluster(&self, start_cluster: u32) -> u32 { + let mut current = start_cluster; + + loop { + let next = self.get_entry(current); + if next >= FAT32_EOC || next == 0 { + return current; + } + current = next; + } + } + + /// Count clusters in a chain + pub fn count_chain(&self, start_cluster: u32) -> usize { + let mut count = 0; + let mut current = start_cluster; + + while current < FAT32_EOC && current != 0 { + count += 1; + current = self.get_entry(current); + } + + count + } + + /// Write FAT table back to file + pub fn write(&self, file: &mut F) -> io::Result<()> { + file.seek(SeekFrom::Start(self.offset))?; + file.write_all(&self.data)?; + if self.sync_on_write { + file.flush()?; + } + Ok(()) + } + + /// Refresh FAT table from file + pub fn refresh(&mut self, file: &mut F) -> io::Result<()> { + file.seek(SeekFrom::Start(self.offset))?; + file.read_exact(&mut self.data)?; + Ok(()) + } + + /// Get reference to raw FAT data + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Get mutable reference to raw FAT data + pub fn data_mut(&mut self) -> &mut [u8] { + &mut self.data + } +} diff --git a/src/fat32/file.rs b/src/fat32/file.rs new file mode 100644 index 0000000..f43a70f --- /dev/null +++ b/src/fat32/file.rs @@ -0,0 +1,79 @@ +//! File operations for FAT32 + +use super::LFN_ATTRIBUTE; +use crate::fat32::lfn; + +// Re-export LFN functions for backward compatibility +pub use crate::fat32::lfn::{lfn_checksum, make_lfn_entries}; + +/// Generate a short name from a long name +/// Returns (base, extension) tuple +pub fn generate_short_name(long_name: &str, existing: &[String]) -> (String, String) { + let mut base = String::new(); + let mut ext = String::new(); + let parts: Vec<&str> = long_name.split('.').collect(); + let name_part = parts.get(0).unwrap_or(&""); + let ext_part = parts.get(1).unwrap_or(&""); + + for c in name_part.chars() { + if base.len() >= 6 { + break; + } + if c.is_ascii_alphanumeric() { + base.push(c.to_ascii_uppercase()); + } + } + + if base.is_empty() { + base.push('X'); + } + + let mut num = 1; + let mut candidate = format!("{}~{}", base, num); + while existing.iter().any(|n| n.starts_with(&candidate)) { + num += 1; + candidate = format!("{}~{}", base, num); + } + base = candidate; + + for c in ext_part.chars() { + if ext.len() >= 3 { + break; + } + if c.is_ascii_alphanumeric() { + ext.push(c.to_ascii_uppercase()); + } + } + + (base, ext) +} + +pub fn parse_dir_entry(entry: &[u8]) -> Option<(String, u32, u32)> { + if entry[0] == 0x00 || entry[0] == 0xE5 { + return None; + } + let attr = entry[11]; + if attr == LFN_ATTRIBUTE { + return None; + } + let name_raw = &entry[0..8]; + let ext_raw = &entry[8..11]; + let name = String::from_utf8_lossy(name_raw).trim_end().to_string(); + let ext = String::from_utf8_lossy(ext_raw).trim_end().to_string(); + let filename = if ext.is_empty() { + name + } else { + format!("{}.{}", name, ext) + }; + let high = u16::from_le_bytes([entry[20], entry[21]]) as u32; + let low = u16::from_le_bytes([entry[26], entry[27]]) as u32; + let start_cluster = (high << 16) | low; + let file_size = u32::from_le_bytes([entry[28], entry[29], entry[30], entry[31]]); + Some((filename, start_cluster, file_size)) +} + +/// Parse a LFN directory entry +pub fn parse_lfn_entry(entry: &[u8]) -> Option { + lfn::parse_lfn_entry(entry) + .map(|lfn_entry| String::from_utf16(&lfn_entry.chars).unwrap_or_default()) +} diff --git a/src/fat32/lfn.rs b/src/fat32/lfn.rs new file mode 100644 index 0000000..4e12346 --- /dev/null +++ b/src/fat32/lfn.rs @@ -0,0 +1,243 @@ +//! Long File Name (LFN) support for FAT32 + +use super::DIR_ENTRY_SIZE; + +/// LFN entry attribute marker +pub const LFN_ATTRIBUTE: u8 = 0x0F; + +/// Maximum number of characters in a LFN entry +const LFN_CHARS_PER_ENTRY: usize = 13; + +/// LFN entry structure +#[derive(Debug, Clone)] +pub struct LfnEntry { + pub sequence: u8, + pub chars: Vec, + pub checksum: u8, +} + +/// Parse a LFN directory entry +pub fn parse_lfn_entry(entry: &[u8]) -> Option { + if entry.len() < DIR_ENTRY_SIZE { + return None; + } + + // Check if it's a LFN entry + if entry[0x0B] != LFN_ATTRIBUTE { + return None; + } + + let sequence = entry[0]; + let checksum = entry[0x0D]; + + // Extract characters from LFN entry + let mut chars = Vec::with_capacity(LFN_CHARS_PER_ENTRY); + + // First 5 characters (bytes 1-10) + for i in 0..5 { + let c = u16::from_le_bytes([entry[1 + i * 2], entry[2 + i * 2]]); + if c == 0 || c == 0xFFFF { + break; + } + chars.push(c); + } + + // Next 6 characters (bytes 14-25) + for i in 0..6 { + let c = u16::from_le_bytes([entry[14 + i * 2], entry[15 + i * 2]]); + if c == 0 || c == 0xFFFF { + break; + } + chars.push(c); + } + + // Last 2 characters (bytes 28-31) + for i in 0..2 { + let c = u16::from_le_bytes([entry[28 + i * 2], entry[29 + i * 2]]); + if c == 0 || c == 0xFFFF { + break; + } + chars.push(c); + } + + Some(LfnEntry { + sequence, + chars, + checksum, + }) +} + +/// Calculate checksum for a short name +pub fn lfn_checksum(name: &[u8; 11]) -> u8 { + let mut sum: u8 = 0; + for &byte in name { + sum = (((sum & 1) << 7) | ((sum & 0xFE) >> 1)).wrapping_add(byte); + } + sum +} + +/// Generate short name from a long name +pub fn generate_short_name(long_name: &str, existing_names: &[String]) -> [u8; 11] { + let mut short_name = [b' '; 11]; + + // Extract base name and extension + let (base, ext) = if let Some(dot_pos) = long_name.rfind('.') { + (&long_name[..dot_pos], &long_name[dot_pos + 1..]) + } else { + (long_name.as_ref(), "") + }; + + // Convert base name to uppercase and truncate to 8 characters + let base_upper = base.to_uppercase(); + let base_clean: String = base_upper + .chars() + .filter(|c| c.is_ascii_alphanumeric() || *c == '_') + .take(6) // Leave room for ~N suffix + .collect(); + + // Convert extension to uppercase and truncate to 3 characters + let ext_upper = ext.to_uppercase(); + let ext_clean: String = ext_upper + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .take(3) + .collect(); + + // Find a unique name with ~N suffix + for n in 1..=999 { + // Create the short name with suffix + let suffix = format!("~{}", n); + let name_len = (8 - suffix.len()).min(base_clean.len()); + + // Fill in the base name + for (i, c) in base_clean.chars().take(name_len).enumerate() { + short_name[i] = c as u8; + } + + // Add the suffix + for (i, c) in suffix.chars().enumerate() { + short_name[name_len + i] = c as u8; + } + + // Fill in the extension + for (i, c) in ext_clean.chars().take(3).enumerate() { + short_name[8 + i] = c as u8; + } + + // Check if this name already exists + let test_name = String::from_utf8_lossy(&short_name).trim().to_string(); + + if !existing_names.contains(&test_name) { + return short_name; + } + } + + // If we can't find a unique name, use a fallback + short_name +} + +/// Create LFN entries for a long filename +pub fn make_lfn_entries(long_name: &str, short_name: &[u8; 11]) -> Vec<[u8; 32]> { + let mut entries = Vec::new(); + let checksum = lfn_checksum(short_name); + + // Convert long name to UTF-16 + let utf16: Vec = long_name.encode_utf16().collect(); + + // Calculate number of LFN entries needed + let num_entries = (utf16.len() + LFN_CHARS_PER_ENTRY - 1) / LFN_CHARS_PER_ENTRY; + + // Create LFN entries in reverse order (last fragment first) + for i in (0..num_entries).rev() { + let mut entry = [0u8; 32]; + + // Set sequence number (0x40 marks the last entry) + entry[0] = (i + 1) as u8; + if i == num_entries - 1 { + entry[0] |= 0x40; + } + + // Set attribute to LFN + entry[0x0B] = LFN_ATTRIBUTE; + + // Set checksum + entry[0x0D] = checksum; + + // Fill in characters for this entry + let start = i * LFN_CHARS_PER_ENTRY; + let chars_in_entry = (utf16.len() - start).min(LFN_CHARS_PER_ENTRY); + + for j in 0..chars_in_entry { + let c = utf16[start + j]; + + if j < 5 { + // First 5 chars (bytes 1-10) + entry[1 + j * 2] = (c & 0xFF) as u8; + entry[2 + j * 2] = (c >> 8) as u8; + } else if j < 11 { + // Next 6 chars (bytes 14-25) + let idx = j - 5; + entry[14 + idx * 2] = (c & 0xFF) as u8; + entry[15 + idx * 2] = (c >> 8) as u8; + } else { + // Last 2 chars (bytes 28-31) + let idx = j - 11; + entry[28 + idx * 2] = (c & 0xFF) as u8; + entry[29 + idx * 2] = (c >> 8) as u8; + } + } + + // Add null terminator if this is the last entry and we have space + if i == num_entries - 1 && chars_in_entry < LFN_CHARS_PER_ENTRY { + // Add 0x0000 after the last character + if chars_in_entry < 5 { + entry[1 + chars_in_entry * 2] = 0x00; + entry[2 + chars_in_entry * 2] = 0x00; + } else if chars_in_entry < 11 { + let idx = chars_in_entry - 5; + entry[14 + idx * 2] = 0x00; + entry[15 + idx * 2] = 0x00; + } else { + let idx = chars_in_entry - 11; + entry[28 + idx * 2] = 0x00; + entry[29 + idx * 2] = 0x00; + } + + // Pad the rest with 0xFFFF + for j in (chars_in_entry + 1)..LFN_CHARS_PER_ENTRY { + if j < 5 { + entry[1 + j * 2] = 0xFF; + entry[2 + j * 2] = 0xFF; + } else if j < 11 { + let idx = j - 5; + entry[14 + idx * 2] = 0xFF; + entry[15 + idx * 2] = 0xFF; + } else { + let idx = j - 11; + entry[28 + idx * 2] = 0xFF; + entry[29 + idx * 2] = 0xFF; + } + } + } else { + // Not the last entry or no space for terminator - pad with 0xFFFF + for j in chars_in_entry..LFN_CHARS_PER_ENTRY { + if j < 5 { + entry[1 + j * 2] = 0xFF; + entry[2 + j * 2] = 0xFF; + } else if j < 11 { + let idx = j - 5; + entry[14 + idx * 2] = 0xFF; + entry[15 + idx * 2] = 0xFF; + } else { + let idx = j - 11; + entry[28 + idx * 2] = 0xFF; + entry[29 + idx * 2] = 0xFF; + } + } + } + + entries.push(entry); + } + + entries +} diff --git a/src/fat32/mod.rs b/src/fat32/mod.rs new file mode 100644 index 0000000..f8285d7 --- /dev/null +++ b/src/fat32/mod.rs @@ -0,0 +1,33 @@ +pub mod directory; +pub mod fat_table; +pub mod file; +pub mod lfn; +pub mod utils; +pub mod volume; + +use log; + +use std::fs::File; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::string::String; + +pub(crate) const DIR_ENTRY_SIZE: usize = 32; +pub(crate) const LFN_ATTRIBUTE: u8 = 0x0F; + +#[derive(Debug, Clone)] +pub struct Fat32FileEntry { + pub name: String, + pub start_cluster: u32, + pub size: u32, + pub is_directory: bool, +} + +#[derive(Debug)] +pub struct Fat32Params { + pub bytes_per_sector: u16, + pub sectors_per_cluster: u8, + pub reserved_sectors: u16, + pub num_fats: u8, + pub sectors_per_fat: u32, + pub root_cluster: u32, +} diff --git a/src/fat32/utils.rs b/src/fat32/utils.rs new file mode 100644 index 0000000..f5219d3 --- /dev/null +++ b/src/fat32/utils.rs @@ -0,0 +1,124 @@ +#[cfg(windows)] +use log::info; +use log::warn; +#[cfg(windows)] +use std::ffi::OsStr; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; +#[cfg(windows)] +use std::ptr; + +#[cfg(windows)] +use windows_sys::core::GUID; +#[cfg(windows)] +use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; +#[cfg(windows)] +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, + FILE_SHARE_WRITE, OPEN_EXISTING, +}; +#[cfg(windows)] +use windows_sys::Win32::System::Ioctl::{ + DRIVE_LAYOUT_INFORMATION_EX, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, PARTITION_STYLE_GPT, +}; +#[cfg(windows)] +use windows_sys::Win32::System::IO::DeviceIoControl; + +#[cfg(windows)] +#[allow(dead_code)] +const IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS: u32 = 0x00560000; + +#[cfg(windows)] +fn esp_guid() -> GUID { + GUID { + data1: 0xc12a7328, + data2: 0xf81f, + data3: 0x11d2, + data4: [0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b], + } +} + +#[cfg(windows)] +fn is_esp_guid(guid: &GUID) -> bool { + let esp = esp_guid(); + guid.data1 == esp.data1 + && guid.data2 == esp.data2 + && guid.data3 == esp.data3 + && guid.data4 == esp.data4 +} + +#[cfg(windows)] +fn open_handle(path_w: &[u16]) -> Option { + unsafe { + let h = CreateFileW( + path_w.as_ptr(), + (FILE_GENERIC_READ | FILE_GENERIC_WRITE) as u32, + (FILE_SHARE_READ | FILE_SHARE_WRITE) as u32, + ptr::null_mut(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + 0, + ); + if h == INVALID_HANDLE_VALUE || h == 0 { + None + } else { + Some(h) + } + } +} + +#[cfg(windows)] +pub fn find_esp_device() -> std::io::Result> { + info!("Scanning for ESP device using Windows API..."); + unsafe { + for disk_num in 0..16 { + let phys_path_str = format!("\\\\.\\PhysicalDrive{}", disk_num); + let phys_path: Vec = OsStr::new(&phys_path_str) + .encode_wide() + .chain(Some(0)) + .collect(); + + if let Some(h_phys) = open_handle(&phys_path) { + let mut layout_buf = vec![0u8; 8192]; + let mut bytes: u32 = 0; + + if DeviceIoControl( + h_phys, + IOCTL_DISK_GET_DRIVE_LAYOUT_EX, + ptr::null_mut(), + 0, + layout_buf.as_mut_ptr().cast(), + layout_buf.len() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + let layout = &*(layout_buf.as_ptr() as *const DRIVE_LAYOUT_INFORMATION_EX); + + if layout.PartitionStyle == PARTITION_STYLE_GPT as u32 { + let part_entry_ptr = layout.PartitionEntry.as_ptr(); + for i in 0..layout.PartitionCount { + let part_info = &*part_entry_ptr.add(i as usize); + if part_info.PartitionStyle == PARTITION_STYLE_GPT + && is_esp_guid(&part_info.Anonymous.Gpt.PartitionType) + { + let lba = (part_info.StartingOffset / 512) as u64; // Assuming 512 bytes per sector + CloseHandle(h_phys); + return Ok(Some((phys_path_str, lba))); + } + } + } + } + CloseHandle(h_phys); + } + } + } + warn!("ESP device not found after scanning all physical drives."); + Ok(None) +} + +#[cfg(not(windows))] +pub fn find_esp_device() -> std::io::Result> { + warn!("find_esp_device is only implemented on Windows; returning None"); + Ok(None) +} diff --git a/src/fat32/volume.rs b/src/fat32/volume.rs new file mode 100644 index 0000000..27b4533 --- /dev/null +++ b/src/fat32/volume.rs @@ -0,0 +1,1590 @@ +use super::*; +use crate::fat32::directory::parse_directory_entries; +use crate::fat32::file::{ + lfn_checksum, make_lfn_entries, parse_dir_entry, parse_lfn_entry, +}; +use crate::fat32::lfn::generate_short_name; + +// Используем платформозависимую функцию поиска ESP +use crate::platform::find_esp_device; + +pub fn read_bpb(file: &mut std::fs::File, offset: u64) -> std::io::Result { + use std::io::Seek; + file.seek(std::io::SeekFrom::Start(offset))?; + let mut bpb = [0u8; 512]; + file.read_exact(&mut bpb)?; + Ok(Fat32Params { + bytes_per_sector: u16::from_le_bytes([bpb[0x0B], bpb[0x0C]]), + sectors_per_cluster: bpb[0x0D], + reserved_sectors: u16::from_le_bytes([bpb[0x0E], bpb[0x0F]]), + num_fats: bpb[0x10], + sectors_per_fat: u32::from_le_bytes([bpb[0x24], bpb[0x25], bpb[0x26], bpb[0x27]]), + root_cluster: u32::from_le_bytes([bpb[0x2C], bpb[0x2D], bpb[0x2E], bpb[0x2F]]), + }) +} + +pub struct Fat32Volume { + sync_on_write: bool, + file: File, + fat_offset: u64, + data_offset: u64, + bytes_per_sector: u16, + sectors_per_cluster: u32, + root_cluster: u32, + fat: Vec, + #[cfg(windows)] + volume_path: Option, // Путь к Volume для Windows (\\?\Volume{GUID}\) +} + +impl Fat32Volume { + pub fn open( + sync_on_write: bool, + device_path: &str, + esp_start_lba: u64, + bytes_per_sector: u16, + sectors_per_cluster: u32, + reserved_sectors: u32, + num_fats: u32, + sectors_per_fat: u32, + root_cluster: u32, + ) -> io::Result { + // На Windows открываем PhysicalDrive без специальных флагов, чтобы избежать требований выравнивания буферов + #[cfg(windows)] + let mut file = { + std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(device_path)? + }; + + #[cfg(not(windows))] + let mut file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(device_path)?; + + // Преобразуем в u64 перед умножением + let bytes_per_sector_u64 = bytes_per_sector as u64; + let esp_offset = esp_start_lba * bytes_per_sector_u64; + + // Остальные вычисления + let fat_offset = esp_offset + (reserved_sectors as u64 * bytes_per_sector_u64); + let fat_size_bytes = (sectors_per_fat as u64 * bytes_per_sector_u64) as usize; + + file.seek(SeekFrom::Start(fat_offset))?; + let mut fat = vec![0u8; fat_size_bytes]; + file.read_exact(&mut fat)?; + + let data_offset = esp_offset + + (reserved_sectors as u64 + (num_fats as u64) * (sectors_per_fat as u64)) + * bytes_per_sector_u64; + + Ok(Fat32Volume { + sync_on_write, + file, + fat_offset, + data_offset, + bytes_per_sector, + sectors_per_cluster, + root_cluster, + fat, + #[cfg(windows)] + volume_path: None, // Will be set in open_esp if needed + }) + } + + /// Обновляет кеш FAT из файловой системы (перечитывает FAT таблицу с диска) + pub fn refresh_fat_cache(&mut self) -> io::Result<()> { + self.file.seek(SeekFrom::Start(self.fat_offset))?; + self.file.read_exact(&mut self.fat)?; + log::debug!("FAT cache refreshed from disk"); + Ok(()) + } + + /// Полностью сбрасывает все кеши и синхронизирует с диском + pub fn refresh_all_caches(&mut self) -> io::Result<()> { + // Синхронизируем все записи на диск + self.file.sync_all()?; + + // На Windows: если знаем GUID-путь тома, открываем именно том и выполняем Flush + LOCK/UNLOCK + #[cfg(windows)] + { + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + use winapi::um::fileapi::{CreateFileW, FlushFileBuffers, OPEN_EXISTING}; + use winapi::um::handleapi::INVALID_HANDLE_VALUE; + use winapi::um::ioapiset::DeviceIoControl; + use winapi::um::winioctl::{FSCTL_LOCK_VOLUME, FSCTL_UNLOCK_VOLUME}; + use winapi::um::winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE}; + + if let Some(ref vol_path) = self.volume_path { + // Удаляем завершающий обратный слэш, чтобы CreateFileW открыл том + let mut s = vol_path.as_os_str().to_string_lossy().to_string(); + if s.ends_with('\\') { + s.pop(); + } + let wide: Vec = OsStr::new(&s).encode_wide().chain(Some(0)).collect(); + unsafe { + let h = CreateFileW( + wide.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null_mut(), + OPEN_EXISTING, + 0, + std::ptr::null_mut(), + ); + if h != INVALID_HANDLE_VALUE { + // Сброс буферов тома + let _ = FlushFileBuffers(h); + // Лёгкая последовательность LOCK/UNLOCK, чтобы заставить систему синхронизировать состояние + let mut br: u32 = 0; + let _ = DeviceIoControl( + h, + FSCTL_LOCK_VOLUME, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + 0, + &mut br, + std::ptr::null_mut(), + ); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = DeviceIoControl( + h, + FSCTL_UNLOCK_VOLUME, + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + 0, + &mut br, + std::ptr::null_mut(), + ); + // Закрываем дескриптор + winapi::um::handleapi::CloseHandle(h); + } + } + } + } + + // Обновляем FAT кеш после всех операций + self.refresh_fat_cache()?; + + log::info!("All caches refreshed and synced with disk"); + Ok(()) + } + + fn read_cluster(&mut self, cluster_num: u32) -> io::Result> { + // Validate cluster number + if cluster_num < 2 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid cluster number {} (must be >= 2)", cluster_num), + )); + } + + let cluster_size = self.sectors_per_cluster as u64 * self.bytes_per_sector as u64; + let cluster_offset = self.data_offset + (cluster_num as u64 - 2) * cluster_size; + + // Всегда делаем seek перед чтением, чтобы гарантировать чтение с диска + self.file.seek(SeekFrom::Start(cluster_offset))?; + + let mut buf = vec![0u8; cluster_size as usize]; + self.file.read_exact(&mut buf)?; + Ok(buf) + } + + /// Читает цепочку кластеров начиная с указанного + fn read_chain(&mut self, start_cluster: u32) -> io::Result> { + let mut data = Vec::new(); + let mut current_cluster = start_cluster; + + while current_cluster < 0x0FFFFFF8 && current_cluster != 0 { + let cluster_data = self.read_cluster(current_cluster)?; + data.extend_from_slice(&cluster_data); + current_cluster = self.get_fat_entry(current_cluster); + } + + Ok(data) + } + + /// Записывает данные в цепочку кластеров начиная с указанного + fn write_chain(&mut self, start_cluster: u32, data: &[u8]) -> io::Result<()> { + let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; + let mut current_cluster = start_cluster; + let mut offset = 0; + + while current_cluster < 0x0FFFFFF8 && current_cluster != 0 && offset < data.len() { + let cluster_offset = + self.data_offset + (current_cluster as u64 - 2) * cluster_size as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + + let to_write = (data.len() - offset).min(cluster_size); + self.file.write_all(&data[offset..offset + to_write])?; + + // Если данные не помещаются полностью в кластер, заполняем остаток нулями + if to_write < cluster_size { + let zeros = vec![0u8; cluster_size - to_write]; + self.file.write_all(&zeros)?; + } + + offset += to_write; + current_cluster = self.get_fat_entry(current_cluster); + } + + if self.sync_on_write { + self.file.sync_all()?; + } + + Ok(()) + } + + fn get_fat_entry(&self, cluster_num: u32) -> u32 { + let offset = (cluster_num * 4) as usize; + u32::from_le_bytes(self.fat[offset..offset + 4].try_into().unwrap()) & 0x0FFFFFFF + } + + pub fn list_root(&mut self) -> io::Result> { + self.list_directory(self.root_cluster) + } + + pub fn list_directory(&mut self, start_cluster: u32) -> io::Result> { + let mut entries = Vec::new(); + let mut current_cluster = start_cluster; + loop { + let cluster_data = self.read_cluster(current_cluster)?; + let parsed_entries = parse_directory_entries(&cluster_data); + for (name, cluster, size, attr) in parsed_entries { + let is_dir = (attr & 0x10) != 0; + entries.push(Fat32FileEntry { + name, + start_cluster: cluster, + size, + is_directory: is_dir, + }); + } + current_cluster = self.get_fat_entry(current_cluster); + if current_cluster >= 0x0FFFFFF8 { + break; + } + } + Ok(entries) + } + + pub fn read_file(&mut self, path: &str) -> io::Result>> { + log::debug!("read_file: Reading file at path: {}", path); + + // Normalize path + let path_normalized = path.replace('\\', "/"); + let parts: Vec<&str> = path_normalized + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + log::debug!("read_file: Path parts: {:?}", parts); + + if parts.is_empty() { + return Ok(None); + } + + // Determine parent directory and filename + let filename = parts.last().unwrap(); + let parent_cluster = if parts.len() > 1 { + // Find the parent directory + let dir_path = parts[..parts.len() - 1].join("/"); + log::debug!("read_file: Looking for parent directory: {}", dir_path); + match self.find_directory(&dir_path)? { + Some(cluster) => { + log::debug!("read_file: Parent directory found at cluster {}", cluster); + cluster + }, + None => { + log::debug!("read_file: Parent directory not found"); + return Ok(None); + } + } + } else { + // File is in root directory + log::debug!("read_file: File is in root directory (cluster {})", self.root_cluster); + self.root_cluster + }; + + // List files in parent directory + log::debug!("read_file: Listing directory at cluster {}", parent_cluster); + let entries = self.list_directory(parent_cluster)?; + log::debug!("read_file: Found {} entries in directory", entries.len()); + + // Find the file + for entry in entries { + log::debug!("read_file: Checking entry: name='{}', is_dir={}, size={}, cluster={}", + entry.name.trim(), entry.is_directory, entry.size, entry.start_cluster); + if entry.name.trim().eq_ignore_ascii_case(filename) && !entry.is_directory { + log::debug!("read_file: Found matching file, reading {} bytes from cluster {}", + entry.size, entry.start_cluster); + + let mut cluster = entry.start_cluster; + let mut remaining = entry.size; + let mut content = Vec::new(); + + while cluster < 0x0FFFFFF8 && cluster != 0 { + log::debug!("read_file: Reading cluster {}, {} bytes remaining", cluster, remaining); + let data = self.read_cluster(cluster)?; + let to_take = remaining.min(data.len() as u32) as usize; + content.extend_from_slice(&data[..to_take]); + remaining -= to_take as u32; + if remaining == 0 { + break; + } + cluster = self.get_fat_entry(cluster); + } + + log::debug!("read_file: Successfully read {} bytes", content.len()); + return Ok(Some(content)); + } + } + + log::debug!("read_file: File '{}' not found in directory", filename); + Ok(None) + } + + /// Записывает файл по пути (с созданием директорий если необходимо) + pub fn write_file_with_path(&mut self, path: &str, new_content: &[u8]) -> io::Result { + // На Windows пытаемся сначала использовать filesystem-based write + #[cfg(windows)] + { + use std::path::Path; + + // Создаём директории если нужно + let path_normalized = path.replace('\\', "/"); + let parts: Vec<&str> = path_normalized + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + if parts.len() > 1 { + let dir_path = parts[..parts.len() - 1].join("/"); + self.create_directory_path(&dir_path)?; + } + + if let Err(e) = crate::platform::windows::write_file_to_esp(Path::new(path), new_content) { + log::warn!( + "write_file_to_esp failed for {}, falling back to raw write: {}", + path, + e + ); + } else { + // После успешной записи через Windows API максимально синхронизируемся + if let Err(e) = self.refresh_all_caches() { + log::warn!("Failed to refresh caches after write: {}", e); + } + + // Подождём, пока файловая система отразит изменения (ESP часто кешируется) + let path_normalized = path.replace('\\', "/"); + let parts: Vec<&str> = path_normalized + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + let filename = parts.last().copied().unwrap_or(""); + let parent_cluster = if parts.len() > 1 { + let dir_path = parts[..parts.len() - 1].join("/"); + match self.find_directory(&dir_path)? { + Some(cluster) => cluster, + None => self.root_cluster, + } + } else { + self.root_cluster + }; + + let mut visible = false; + for attempt in 0..30 { + let entries = self.list_directory(parent_cluster)?; + if entries.iter().any(|e| !e.is_directory && e.name.trim().eq_ignore_ascii_case(filename)) { + visible = true; + log::info!("File '{}' visible after {} attempt(s)", filename, attempt + 1); + break; + } + log::debug!("File '{}' not yet visible (attempt {}/30), sleeping...", filename, attempt + 1); + std::thread::sleep(std::time::Duration::from_millis(200)); + // Попробуем так же обновить FAT кеш между попытками + let _ = self.refresh_fat_cache(); + } + if !visible { + log::warn!("File '{}' not visible after write via Windows API, continuing anyway", filename); + } + + return Ok(true); + } + } + + // Разделяем путь на директории и имя файла + let path_normalized = path.replace('\\', "/"); + let parts: Vec<&str> = path_normalized + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + if parts.is_empty() { + return Ok(false); + } + + let filename = parts.last().unwrap(); + let dir_path = if parts.len() > 1 { + parts[..parts.len() - 1].join("/") + } else { + String::new() + }; + + // Создаём директории если нужно + if !dir_path.is_empty() { + self.create_directory_path(&dir_path)?; + } + + // Находим родительскую директорию + let parent_cluster = if dir_path.is_empty() { + self.root_cluster + } else { + self.find_directory(&dir_path)?.ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "Parent directory not found") + })? + }; + + // Ищем файл в родительской директории + let entries = self.list_directory(parent_cluster)?; + let mut existing_entry = None; + for entry in &entries { + if entry.name.trim().eq_ignore_ascii_case(filename) && !entry.is_directory { + existing_entry = Some(entry.clone()); + break; + } + } + + // Если файл не существует, создаём его + let entry = match existing_entry { + Some(e) => e, + None => { + // Создаём новый файл + if let Some(new_cluster) = self.create_entry_lfn(filename, 0x20, parent_cluster)? { + // Создаём новую структуру для нового файла + Fat32FileEntry { + name: filename.to_string(), + start_cluster: new_cluster, + size: 0, + is_directory: false, + } + } else { + return Ok(false); + } + } + }; + + // Далее логика записи содержимого (как в оригинале) + self.write_file_content(&entry, new_content, parent_cluster) + } + + pub fn write_file(&mut self, filename: &str, new_content: &[u8]) -> io::Result { + self.write_file_with_path(filename, new_content) + } + + // Вспомогательный метод для записи содержимого файла + fn write_file_content( + &mut self, + entry: &Fat32FileEntry, + new_content: &[u8], + parent_cluster: u32, + ) -> io::Result { + let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; + let needed_clusters = (new_content.len() + cluster_size - 1) / cluster_size; + + // Собираем текущую цепочку кластеров + let mut clusters = Vec::new(); + let mut cluster = entry.start_cluster; + + // Если это новый файл с кластером 0, выделяем первый кластер + if cluster == 0 || cluster >= 0x0FFFFFF8 { + let free = self + .find_free_cluster() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных кластеров"))?; + // Сразу помечаем как занятый (EOC), чтобы не выбрать повторно + self.set_fat_entry(free, 0x0FFFFFFF); + clusters.push(free); + // Обновляем запись в директории с новым start_cluster + self.update_file_start_cluster_in_dir(parent_cluster, &entry.name, free)?; + } else { + while cluster < 0x0FFFFFF8 { + clusters.push(cluster); + cluster = self.get_fat_entry(cluster); + } + } + + // Если не хватает кластеров — выделяем новые + while clusters.len() < needed_clusters { + let free = self + .find_free_cluster() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных кластеров"))?; + // Сразу помечаем новый кластер как EOC (занят), затем свяжем предыдущий на него + self.set_fat_entry(free, 0x0FFFFFFF); + self.set_fat_entry(*clusters.last().unwrap(), free); + clusters.push(free); + } + + // Если лишние — освобождаем + while clusters.len() > needed_clusters { + let last = clusters.pop().unwrap(); + self.set_fat_entry(last, 0); + } + + // Завершаем цепочку + if let Some(&last) = clusters.last() { + self.set_fat_entry(last, 0x0FFFFFFF); + } + + // Записываем новые данные по кластерам + let mut offset = 0; + for &cl in &clusters { + let cluster_offset = self.data_offset + (cl as u64 - 2) * cluster_size as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + let to_write = (new_content.len() - offset).min(cluster_size); + + #[cfg(windows)] + { + // На Windows используем улучшенную стратегию записи для ESP + match self.write_data_with_retry(&new_content[offset..offset + to_write]) { + Ok(_) => {} + Err(e) => return Err(e), + } + if to_write < cluster_size { + let zeroes = vec![0u8; cluster_size - to_write]; + match self.write_data_with_retry(&zeroes) { + Ok(_) => {} + Err(e) => return Err(e), + } + } + } + #[cfg(not(windows))] + { + self.file + .write_all(&new_content[offset..offset + to_write])?; + if to_write < cluster_size { + let zeroes = vec![0u8; cluster_size - to_write]; + self.file.write_all(&zeroes)?; + } + } + + offset += to_write; + } + + // Обновляем размер файла в директории + let first_cluster = clusters.first().copied(); + self.update_file_size_in_dir_by_name( + parent_cluster, + &entry.name, + new_content.len() as u32, + first_cluster, + )?; + + // Сохраняем FAT на диск + self.flush_fat()?; + Ok(true) + } + + fn find_free_cluster(&self) -> Option { + for i in 2..(self.fat.len() as u32 / 4) { + if self.get_fat_entry(i) == 0 { + return Some(i); + } + } + None + } + + fn set_fat_entry(&mut self, cluster: u32, value: u32) { + let offset = (cluster * 4) as usize; + self.fat[offset..offset + 4].copy_from_slice(&(value & 0x0FFFFFFF).to_le_bytes()); + } + + fn flush_fat(&mut self) -> io::Result<()> { + self.file.seek(SeekFrom::Start(self.fat_offset))?; + self.file.write_all(&self.fat)?; + if self.sync_on_write { + self.file.sync_all()?; + } + Ok(()) + } + + fn create_entry_lfn( + &mut self, + name: &str, + attr: u8, + parent_cluster: u32, // кластер родителя (обычно root_cluster для корня) + ) -> io::Result> { + let entries = self.list_directory(parent_cluster)?; + if entries.iter().any(|e| e.name.eq_ignore_ascii_case(name)) { + return Ok(None); + } + let short_name = generate_short_name( + name, + &entries.iter().map(|e| e.name.clone()).collect::>(), + ); + let _checksum = lfn_checksum(&short_name); + + // Для файлов НЕ выделяем кластер сразу, он будет выделен при записи + // Для директорий выделяем кластер сразу, так как нужно инициализировать . и .. + let new_cluster = if attr == 0x10 { + // Директория - выделяем кластер + let cluster = self + .find_free_cluster() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных кластеров"))?; + self.set_fat_entry(cluster, 0x0FFFFFFF); + cluster + } else { + // Файл - не выделяем кластер, используем 0 + 0 + }; + + let lfn_entries = make_lfn_entries(name, &short_name); + + let mut dir_entry = [0u8; 32]; + dir_entry[0..8].copy_from_slice(&short_name[0..8]); + dir_entry[8..11].copy_from_slice(&short_name[8..11]); + dir_entry[11] = attr; // 0x20 файл, 0x10 директория + dir_entry[20..22].copy_from_slice(&((new_cluster >> 16) as u16).to_le_bytes()); + dir_entry[26..28].copy_from_slice(&(new_cluster as u16).to_le_bytes()); + dir_entry[28..32].copy_from_slice(&0u32.to_le_bytes()); + + self.write_lfn_and_short_entry(parent_cluster, lfn_entries, dir_entry)?; + self.flush_fat()?; + Ok(Some(new_cluster)) + } + + /// Находит директорию по пути и возвращает её кластер + pub fn find_directory(&mut self, path: &str) -> io::Result> { + // Нормализуем путь + let path_normalized = path.replace('\\', "/"); + let parts: Vec<&str> = path_normalized + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + if parts.is_empty() { + return Ok(Some(self.root_cluster)); + } + + let mut current_cluster = self.root_cluster; + + for part in parts { + let entries = self.list_directory(current_cluster)?; + let mut found = false; + + // Пробуем найти точное совпадение или совпадение без учёта регистра + for entry in &entries { + let entry_name = entry.name.trim(); + + // Проверяем разные варианты имени: + // 1. Точное совпадение без учёта регистра + // 2. Совпадение с подчёркиванием вместо пробелов + // 3. Совпадение коротких имён 8.3 + if entry.is_directory + && (entry_name.eq_ignore_ascii_case(part) || + entry_name.replace('_', " ").eq_ignore_ascii_case(part) || + entry_name.replace(' ', "_").eq_ignore_ascii_case(part) || + // Проверяем короткое имя 8.3 (если длинное имя обрезано) + (part.len() > 8 && entry_name.eq_ignore_ascii_case(&part[..8.min(part.len())]))) + { + current_cluster = entry.start_cluster; + found = true; + log::debug!("Found directory '{}' as '{}'", part, entry_name); + break; + } + } + + if !found { + log::debug!( + "Directory '{}' not found in cluster {}", + part, + current_cluster + ); + log::debug!( + "Available entries: {:?}", + entries + .iter() + .filter(|e| e.is_directory) + .map(|e| e.name.clone()) + .collect::>() + ); + return Ok(None); + } + } + + Ok(Some(current_cluster)) + } + + /// Создаёт директории по пути (mkdir -p) + pub fn create_directory_path(&mut self, path: &str) -> io::Result { + let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + + if parts.is_empty() { + return Ok(true); // Корневая директория уже существует + } + + // На Windows пытаемся сначала использовать Windows API для создания директорий + #[cfg(windows)] + { + use std::path::Path; + + // Пробуем создать всю структуру директорий через Windows API + if let Err(e) = crate::platform::windows::create_directory_on_esp(Path::new(path)) { + log::warn!( + "create_directory_on_esp failed for {}, falling back to raw write: {}", + path, + e + ); + } else { + log::info!("Created directory path {} via Windows API", path); + // После успешного создания через Windows API обновляем все кеши + if let Err(e) = self.refresh_all_caches() { + log::warn!("Failed to refresh caches after directory creation: {}", e); + } + return Ok(true); + } + } + + // Fallback на raw метод для не-Windows или если Windows API не работает + let mut current_cluster = self.root_cluster; + + for part in parts { + let entries = self.list_directory(current_cluster)?; + let mut found = false; + + for entry in entries { + if entry.name.trim().eq_ignore_ascii_case(part) && entry.is_directory { + current_cluster = entry.start_cluster; + found = true; + break; + } + } + + if !found { + // Создаём директорию + if let Some(new_cluster) = self.create_entry_lfn(part, 0x10, current_cluster)? { + // Инициализируем новую директорию с . и .. + let cluster_size = + self.sectors_per_cluster as usize * self.bytes_per_sector as usize; + let mut buf = vec![0u8; cluster_size]; + + // Запись "." + let mut dot_entry = [b' '; 32]; + dot_entry[0] = b'.'; + dot_entry[11] = 0x10; // атрибут директории + dot_entry[20..22].copy_from_slice(&((new_cluster >> 16) as u16).to_le_bytes()); + dot_entry[26..28].copy_from_slice(&(new_cluster as u16).to_le_bytes()); + + // Запись ".." + let mut dotdot_entry = [b' '; 32]; + dotdot_entry[0] = b'.'; + dotdot_entry[1] = b'.'; + dotdot_entry[11] = 0x10; // атрибут директории + dotdot_entry[20..22] + .copy_from_slice(&((current_cluster >> 16) as u16).to_le_bytes()); + dotdot_entry[26..28].copy_from_slice(&(current_cluster as u16).to_le_bytes()); + + // Копируем записи в буфер + buf[0..32].copy_from_slice(&dot_entry); + buf[32..64].copy_from_slice(&dotdot_entry); + + let cluster_offset = + self.data_offset + (new_cluster as u64 - 2) * cluster_size as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + self.file.write_all(&buf)?; + self.flush_fat()?; + + current_cluster = new_cluster; + } else { + return Ok(false); // Не удалось создать директорию + } + } + } + + Ok(true) + } + + /// Создаёт файл по пути (с созданием директорий если необходимо) + pub fn create_file_with_path(&mut self, path: &str) -> io::Result { + // Разделяем путь на директории и имя файла + let path_normalized = path.replace('\\', "/"); + let parts: Vec<&str> = path_normalized + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + + if parts.is_empty() { + return Ok(false); + } + + let filename = parts.last().unwrap(); + let dir_path = if parts.len() > 1 { + parts[..parts.len() - 1].join("/") + } else { + String::new() + }; + + // Создаём директории если нужно + if !dir_path.is_empty() { + self.create_directory_path(&dir_path)?; + } + + // Находим родительскую директорию + let parent_cluster = if dir_path.is_empty() { + self.root_cluster + } else { + self.find_directory(&dir_path)?.ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "Parent directory not found") + })? + }; + + // На Windows используем write_file_to_esp для создания пустого файла + #[cfg(windows)] + { + use std::path::Path; + + // Проверяем, существует ли файл + let entries = self.list_directory(parent_cluster)?; + for entry in entries { + if entry.name.trim().eq_ignore_ascii_case(filename) { + return Ok(false); // Файл уже существует + } + } + + // Создаём пустой файл через write_file_to_esp + match crate::platform::windows::write_file_to_esp(Path::new(path), b"") { + Ok(_) => { + log::info!("Created file {} via write_file_to_esp", path); + // После успешной записи через Windows API обновляем все кеши + if let Err(e) = self.refresh_all_caches() { + log::warn!("Failed to refresh caches after file creation: {}", e); + } + return Ok(true); + } + Err(e) => { + log::warn!( + "Failed to create file via write_file_to_esp: {}, falling back to raw", + e + ); + // Падаем на raw метод + } + } + } + + // Fallback на raw метод для не-Windows или если write_file_to_esp не работает + Ok(self + .create_entry_lfn(filename, 0x20, parent_cluster)? + .is_some()) + } + + pub fn create_file_lfn(&mut self, filename: &str) -> io::Result { + self.create_file_with_path(filename) + } + + pub fn delete_file_lfn(&mut self, filename: &str) -> io::Result { + // На Windows пытаемся сначала использовать Windows API для удаления + #[cfg(windows)] + { + use std::path::Path; + + // Пробуем удалить через Windows API + if let Err(e) = crate::platform::windows::delete_file_from_esp(Path::new(filename)) { + log::warn!( + "delete_file_from_esp failed for {}, falling back to raw delete: {}", + filename, + e + ); + } else { + log::info!("Deleted file {} via Windows API", filename); + // После успешного удаления через Windows API обновляем все кеши + if let Err(e) = self.refresh_all_caches() { + log::warn!("Failed to refresh caches after file deletion: {}", e); + } + return Ok(true); + } + } + + // Нормализуем путь и вычисляем директорию-родителя и имя файла + let path_normalized = filename.replace('\\', "/"); + let parts: Vec<&str> = path_normalized.split('/').filter(|s| !s.is_empty()).collect(); + if parts.is_empty() { + return Ok(false); + } + let target_name = parts.last().unwrap().trim().to_string(); + let mut dir_cluster = if parts.len() > 1 { + let dir_path = parts[..parts.len() - 1].join("/"); + match self.find_directory(&dir_path)? { + Some(c) => c, + None => return Ok(false), + } + } else { + self.root_cluster + }; + + // Raw удаление в пределах найденной директории + loop { + let cluster_offset = self.data_offset + + (dir_cluster as u64 - 2) + * self.sectors_per_cluster as u64 + * self.bytes_per_sector as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + let mut buf = + vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; + self.file.read_exact(&mut buf)?; + + let mut i = 0; + while i < buf.len() / 32 { + // Собираем LFN-цепочку + let mut lfn_stack = Vec::new(); + let mut j = i; + while j < buf.len() / 32 + && buf[j * 32 + 11] == LFN_ATTRIBUTE + && buf[j * 32] != 0xE5 + && buf[j * 32] != 0x00 + { + lfn_stack.push(j); + j += 1; + } + if j >= buf.len() / 32 || buf[j * 32] == 0x00 { + break; + } + // Проверяем короткую запись + if let Some((_short_name, start_cluster, _file_size)) = + parse_dir_entry(&buf[j * 32..(j + 1) * 32]) + { + let full_name = if !lfn_stack.is_empty() { + let mut name_parts = Vec::new(); + for &lfn_idx in lfn_stack.iter().rev() { + if let Some(part) = + parse_lfn_entry(&buf[lfn_idx * 32..(lfn_idx + 1) * 32]) + { + name_parts.push(part); + } + } + name_parts.concat() + } else { + _short_name.clone() + }; + if full_name.trim().eq_ignore_ascii_case(&target_name) { + // 1. Освобождаем цепочку кластеров + let mut cl = start_cluster; + while cl < 0x0FFFFFF8 && cl != 0 { + let next = self.get_fat_entry(cl); + self.set_fat_entry(cl, 0); + cl = next; + } + // 2. Помечаем LFN-записи как удалённые + for &lfn_idx in &lfn_stack { + buf[lfn_idx * 32] = 0xE5; + } + // 3. Помечаем короткую запись как удалённую + buf[j * 32] = 0xE5; + self.file.seek(SeekFrom::Start(cluster_offset))?; + self.file.write_all(&buf)?; + self.flush_fat()?; + return Ok(true); + } + } + i = j + 1; + } + dir_cluster = self.get_fat_entry(dir_cluster); + if dir_cluster >= 0x0FFFFFF8 { + break; + } + } + Ok(false) + } + + pub fn create_dir_lfn(&mut self, dirname: &str) -> io::Result { + if let Some(new_cluster) = self.create_entry_lfn(dirname, 0x10, self.root_cluster)? { + let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; + let mut buf = vec![0u8; cluster_size]; + + // Запись "." + let mut dot_entry = [b' '; 32]; + dot_entry[0] = b'.'; + dot_entry[11] = 0x10; // атрибут директории + dot_entry[20..22].copy_from_slice(&((new_cluster >> 16) as u16).to_le_bytes()); + dot_entry[26..28].copy_from_slice(&(new_cluster as u16).to_le_bytes()); + + // Запись ".." + let mut dotdot_entry = [b' '; 32]; + dotdot_entry[0] = b'.'; + dotdot_entry[1] = b'.'; + dotdot_entry[11] = 0x10; // атрибут директории + dotdot_entry[20..22].copy_from_slice(&((self.root_cluster >> 16) as u16).to_le_bytes()); + dotdot_entry[26..28].copy_from_slice(&(self.root_cluster as u16).to_le_bytes()); + + // Копируем записи в буфер + buf[0..32].copy_from_slice(&dot_entry); + buf[32..64].copy_from_slice(&dotdot_entry); + + let cluster_offset = self.data_offset + (new_cluster as u64 - 2) * cluster_size as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + self.file.write_all(&buf)?; + self.flush_fat()?; + Ok(true) + } else { + Ok(false) + } + } + + pub fn delete_dir_lfn(&mut self, dirname: &str) -> io::Result { + // На Windows пытаемся сначала использовать Windows API для удаления + #[cfg(windows)] + { + use std::path::Path; + + // Пробуем удалить через Windows API + if let Err(e) = crate::platform::windows::delete_directory_from_esp(Path::new(dirname)) { + log::warn!( + "delete_directory_from_esp failed for {}, falling back to raw delete: {}", + dirname, + e + ); + } else { + log::info!("Deleted directory {} via Windows API", dirname); + // После успешного удаления через Windows API обновляем все кеши + if let Err(e) = self.refresh_all_caches() { + log::warn!("Failed to refresh caches after directory deletion: {}", e); + } + return Ok(true); + } + } + + // Нормализуем путь, определяем родителя и целевое имя каталога + let path_normalized = dirname.replace('\\', "/"); + let parts: Vec<&str> = path_normalized.split('/').filter(|s| !s.is_empty()).collect(); + if parts.is_empty() { + return Ok(false); + } + let target_name = parts.last().unwrap().trim().to_string(); + let mut dir_cluster = if parts.len() > 1 { + let parent_path = parts[..parts.len() - 1].join("/"); + match self.find_directory(&parent_path)? { + Some(c) => c, + None => return Ok(false), + } + } else { + self.root_cluster + }; + + // Raw удаление каталога в пределах найденной директории + loop { + let cluster_offset = self.data_offset + + (dir_cluster as u64 - 2) + * self.sectors_per_cluster as u64 + * self.bytes_per_sector as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + let mut buf = + vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; + self.file.read_exact(&mut buf)?; + + let mut i = 0; + while i < buf.len() / 32 { + let mut lfn_stack = Vec::new(); + let mut j = i; + while j < buf.len() / 32 + && buf[j * 32 + 11] == LFN_ATTRIBUTE + && buf[j * 32] != 0xE5 + && buf[j * 32] != 0x00 + { + lfn_stack.push(j); + j += 1; + } + if j >= buf.len() / 32 || buf[j * 32] == 0x00 { + break; + } + if let Some((_short_name, start_cluster, _file_size)) = + parse_dir_entry(&buf[j * 32..(j + 1) * 32]) + { + let full_name = if !lfn_stack.is_empty() { + let mut name_parts = Vec::new(); + for &lfn_idx in lfn_stack.iter().rev() { + if let Some(part) = + parse_lfn_entry(&buf[lfn_idx * 32..(lfn_idx + 1) * 32]) + { + name_parts.push(part); + } + } + name_parts.concat() + } else { + _short_name.clone() + }; + let attr = buf[j * 32 + 11]; + if full_name.trim().eq_ignore_ascii_case(&target_name) && (attr & 0x10) != 0 { + // Проверяем, пуста ли директория + let entries = self.list_directory(start_cluster)?; + let only_dot = entries.iter().all(|e| e.name == "." || e.name == ".."); + if !only_dot { + return Ok(false); // не пуста! + } + // Освободить кластер + let mut cl = start_cluster; + while cl < 0x0FFFFFF8 && cl != 0 { + let next = self.get_fat_entry(cl); + self.set_fat_entry(cl, 0); + cl = next; + } + // Пометить LFN и короткую запись как удалённые + for &lfn_idx in &lfn_stack { + buf[lfn_idx * 32] = 0xE5; + } + buf[j * 32] = 0xE5; + self.file.seek(SeekFrom::Start(cluster_offset))?; + self.file.write_all(&buf)?; + self.flush_fat()?; + return Ok(true); + } + } + i = j + 1; + } + dir_cluster = self.get_fat_entry(dir_cluster); + if dir_cluster >= 0x0FFFFFF8 { + break; + } + } + Ok(false) + } + + fn write_lfn_and_short_entry( + &mut self, + dir_cluster: u32, + lfn_entries: Vec<[u8; 32]>, + short_entry: [u8; 32], + ) -> io::Result<()> { + // Находим подряд N+1 свободных записей в директории + let cluster_offset = self.data_offset + + (dir_cluster as u64 - 2) + * self.sectors_per_cluster as u64 + * self.bytes_per_sector as u64; + self.file.seek(SeekFrom::Start(cluster_offset))?; + let mut buf = vec![0u8; self.sectors_per_cluster as usize * self.bytes_per_sector as usize]; + self.file.read_exact(&mut buf)?; + + let total = lfn_entries.len() + 1; + let mut free_idx = None; + let mut count = 0; + for i in 0..(buf.len() / 32) { + let entry = &buf[i * 32..(i + 1) * 32]; + if entry[0] == 0x00 || entry[0] == 0xE5 { + count += 1; + if count == total { + free_idx = Some(i + 1 - total); + break; + } + } else { + count = 0; + } + } + let idx = free_idx + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Нет свободных записей"))?; + // Записываем LFN + for (j, lfn) in lfn_entries.iter().enumerate() { + buf[(idx + j) * 32..(idx + j + 1) * 32].copy_from_slice(lfn); + } + // Записываем короткую запись + buf[(idx + lfn_entries.len()) * 32..(idx + lfn_entries.len() + 1) * 32] + .copy_from_slice(&short_entry); + + // Записываем обратно + self.file.seek(SeekFrom::Start(cluster_offset))?; + self.file.write_all(&buf)?; + + Ok(()) + } + + pub fn is_dir_empty(&mut self, dir_cluster: u32) -> std::io::Result { + let mut current_cluster = dir_cluster; + loop { + let cluster_data = self.read_cluster(current_cluster)?; + for i in 0..(cluster_data.len() / 32) { + let entry = &cluster_data[i * 32..(i + 1) * 32]; + if entry[0] == 0x00 { + // Все последующие записи свободны + break; + } + if entry[0] == 0xE5 || entry[11] == LFN_ATTRIBUTE { + continue; + } + // Это короткая запись, не удалённая, не LFN + let name_raw = &entry[0..8]; + let ext_raw = &entry[8..11]; + let name = String::from_utf8_lossy(name_raw).trim_end().to_string(); + let ext = String::from_utf8_lossy(ext_raw).trim_end().to_string(); + let filename = if ext.is_empty() { + name.clone() + } else { + format!("{}.{}", name, ext) + }; + if filename != "." && filename != ".." { + return Ok(false); + } + } + current_cluster = self.get_fat_entry(current_cluster); + if current_cluster >= 0x0FFFFFF8 { + break; + } + } + Ok(true) + } + + #[cfg(windows)] + fn write_data_with_retry(&mut self, data: &[u8]) -> io::Result<()> { + // Сначала пробуем обычную запись + match self.file.write_all(data) { + Ok(_) => return Ok(()), + Err(ref e) if e.kind() == io::ErrorKind::PermissionDenied => { + // Если получили ACCESS_DENIED, применяем стратегии из прототипа + log::warn!("Got ACCESS_DENIED, trying advanced ESP writing strategies"); + } + Err(e) => return Err(e), + } + + // Стратегии записи для ESP на Windows (из прототипа) + use crate::platform::windows::write_file_to_esp; + use std::path::Path; + use uuid::Uuid; + + // Создаем временный файл для данных + let tmp_filename = format!("tmp-{}.bin", Uuid::new_v4()); + let tmp_path = Path::new(&tmp_filename); + + // Используем write_file_to_esp для записи временных данных + match write_file_to_esp(tmp_path, data) { + Ok(_) => { + log::info!("Successfully wrote data using ESP writing strategies"); + Ok(()) + } + Err(e) => { + log::error!("Failed to write data using ESP strategies: {}", e); + Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!("ESP write failed: {}", e), + )) + } + } + } + + pub fn open_esp>(path: Option

) -> io::Result> { + if let Some(p) = path { + // Открываем указанный образ или устройство + let path_str = p.as_ref(); + let mut file = std::fs::File::open(path_str)?; + // BPB обычно в самом начале + let params = read_bpb(&mut file, 0)?; + + // Валидация параметров BPB + if params.bytes_per_sector < 512 || params.bytes_per_sector > 4096 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "bytes_per_sector out of range", + )); + } + if params.sectors_per_cluster == 0 || params.sectors_per_cluster > 128 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "sectors_per_cluster out of range", + )); + } + if params.num_fats == 0 || params.num_fats > 4 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "num_fats out of range", + )); + } + if params.sectors_per_fat == 0 || params.sectors_per_fat > 1_000_000 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "sectors_per_fat out of range", + )); + } + + Fat32Volume::open( + false, + path_str, + 0, // lba = 0 для образа + params.bytes_per_sector, + params.sectors_per_cluster as u32, + params.reserved_sectors as u32, + params.num_fats as u32, + params.sectors_per_fat, + params.root_cluster, + ) + .map(Some) + } else { + match find_esp_device().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? { + Some((path, lba)) => { + let mut file = File::open(&path)?; + + let bpb_offset = lba * 512; + let params = read_bpb(&mut file, bpb_offset)?; + + log::info!( + "BPB: bytes_per_sector={}, sectors_per_cluster={}, reserved_sectors={}, num_fats={}, sectors_per_fat={}, root_cluster={}", + params.bytes_per_sector, + params.sectors_per_cluster, + params.reserved_sectors, + params.num_fats, + params.sectors_per_fat, + params.root_cluster + ); + + if params.bytes_per_sector < 512 || params.bytes_per_sector > 4096 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "bytes_per_sector out of range", + )); + } + if params.sectors_per_cluster == 0 || params.sectors_per_cluster > 128 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "sectors_per_cluster out of range", + )); + } + if params.num_fats == 0 || params.num_fats > 4 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "num_fats out of range", + )); + } + if params.sectors_per_fat == 0 || params.sectors_per_fat > 1_000_000 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "sectors_per_fat out of range", + )); + } + + #[cfg_attr(not(windows), allow(unused_mut))] + let mut volume = Fat32Volume::open( + true, // sync_on_write = true для реальных устройств (безопаснее) + &path, + lba, // LBA + params.bytes_per_sector, + params.sectors_per_cluster as u32, + params.reserved_sectors as u32, + params.num_fats as u32, + params.sectors_per_fat, + params.root_cluster, + )?; + + // На Windows пытаемся получить Volume path для ESP + #[cfg(windows)] + { + if let Some(vol_path) = crate::platform::windows::find_esp_volume_path() { + volume.volume_path = Some(vol_path); + log::info!("ESP volume path set: {:?}", volume.volume_path); + } + } + + Ok(Some(volume)) + } + None => Ok(None), + } + } + } + + // Вспомогательный метод для обновления размера файла в директории по стартовому кластеру + #[allow(dead_code)] + fn update_file_size_in_dir( + &mut self, + dir_cluster: u32, + start_cluster: u32, + new_size: u32, + ) -> io::Result<()> { + let mut dir_data = self.read_chain(dir_cluster)?; + let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; + let entries_per_cluster = cluster_size / 32; + + for cluster_idx in 0..dir_data.len() / cluster_size { + let cluster_offset = cluster_idx * cluster_size; + + for i in 0..entries_per_cluster { + let offset = cluster_offset + i * 32; + if offset + 32 > dir_data.len() { + break; + } + + let entry = &dir_data[offset..offset + 32]; + if entry[0] == 0x00 { + break; // Конец директории + } + if entry[0] == 0xE5 || entry[11] == 0x0F { + continue; // Удаленная запись или LFN + } + + // Проверяем стартовый кластер (FAT32: high 16 bits at 20..21, low 16 bits at 26..27) + let high = u16::from_le_bytes([entry[20], entry[21]]) as u32; + let low = u16::from_le_bytes([entry[26], entry[27]]) as u32; + let entry_start_cluster = (high << 16) | low; + + if entry_start_cluster == start_cluster { + // Обновляем размер файла + dir_data[offset + 28] = (new_size & 0xFF) as u8; + dir_data[offset + 29] = ((new_size >> 8) & 0xFF) as u8; + dir_data[offset + 30] = ((new_size >> 16) & 0xFF) as u8; + dir_data[offset + 31] = ((new_size >> 24) & 0xFF) as u8; + + // Записываем обновленные данные обратно + self.write_chain(dir_cluster, &dir_data)?; + return Ok(()); + } + } + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + "File entry not found in directory", + )) + } + + // Вспомогательный метод для обновления размера и кластера файла по имени + fn update_file_size_in_dir_by_name( + &mut self, + dir_cluster: u32, + filename: &str, + new_size: u32, + new_start_cluster: Option, + ) -> io::Result<()> { + let mut dir_data = self.read_chain(dir_cluster)?; + let cluster_size = self.sectors_per_cluster as usize * self.bytes_per_sector as usize; + let entries_per_cluster = cluster_size / 32; + + let filename_upper = filename.to_uppercase(); + let mut lfn_entries = Vec::new(); + + for cluster_idx in 0..dir_data.len() / cluster_size { + let cluster_offset = cluster_idx * cluster_size; + + for i in 0..entries_per_cluster { + let offset = cluster_offset + i * 32; + if offset + 32 > dir_data.len() { + break; + } + + let entry = &dir_data[offset..offset + 32]; + if entry[0] == 0x00 { + break; // Конец директории + } + if entry[0] == 0xE5 { + lfn_entries.clear(); + continue; // Удаленная запись + } + + if entry[11] == 0x0F { + // LFN запись + lfn_entries.push(entry.to_vec()); + } else { + // Обычная запись - проверяем имя + let mut short_name = String::new(); + + // Имя (первые 8 байт) + for j in 0..8 { + if entry[j] != 0x20 { + short_name.push(entry[j] as char); + } + } + + // Расширение (байты 8-10) + let mut has_ext = false; + for j in 8..11 { + if entry[j] != 0x20 { + if !has_ext { + short_name.push('.'); + has_ext = true; + } + short_name.push(entry[j] as char); + } + } + + // Проверяем LFN если есть + let mut full_name = String::new(); + if !lfn_entries.is_empty() { + // Собираем LFN из записей + lfn_entries.reverse(); + for lfn_entry in &lfn_entries { + // Извлекаем символы из LFN записи + for j in 0..5 { + let ch = (lfn_entry[1 + j * 2] as u16) + | ((lfn_entry[2 + j * 2] as u16) << 8); + if ch != 0 && ch != 0xFFFF { + if let Some(c) = char::from_u32(ch as u32) { + full_name.push(c); + } + } + } + for j in 0..6 { + let ch = (lfn_entry[14 + j * 2] as u16) + | ((lfn_entry[15 + j * 2] as u16) << 8); + if ch != 0 && ch != 0xFFFF { + if let Some(c) = char::from_u32(ch as u32) { + full_name.push(c); + } + } + } + for j in 0..2 { + let ch = (lfn_entry[28 + j * 2] as u16) + | ((lfn_entry[29 + j * 2] as u16) << 8); + if ch != 0 && ch != 0xFFFF { + if let Some(c) = char::from_u32(ch as u32) { + full_name.push(c); + } + } + } + } + } else { + full_name = short_name.clone(); + } + + // Сравниваем имена + if full_name.to_uppercase() == filename_upper || short_name == filename_upper { + // Обновляем размер файла только если передано не u32::MAX + if new_size != u32::MAX { + dir_data[offset + 28] = (new_size & 0xFF) as u8; + dir_data[offset + 29] = ((new_size >> 8) & 0xFF) as u8; + dir_data[offset + 30] = ((new_size >> 16) & 0xFF) as u8; + dir_data[offset + 31] = ((new_size >> 24) & 0xFF) as u8; + } + + // Обновляем стартовый кластер если нужно (FAT32: high 16 bits at 20..21, low 16 bits at 26..27) + if let Some(cluster) = new_start_cluster { + let high: u16 = (cluster >> 16) as u16; + let low: u16 = (cluster & 0xFFFF) as u16; + let high_bytes = high.to_le_bytes(); + let low_bytes = low.to_le_bytes(); + dir_data[offset + 20] = high_bytes[0]; + dir_data[offset + 21] = high_bytes[1]; + dir_data[offset + 26] = low_bytes[0]; + dir_data[offset + 27] = low_bytes[1]; + } + + // Записываем обновленные данные обратно + self.write_chain(dir_cluster, &dir_data)?; + return Ok(()); + } + + lfn_entries.clear(); + } + } + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("File '{}' not found in directory", filename), + )) + } + + // Вспомогательный метод для обновления стартового кластера файла + fn update_file_start_cluster_in_dir( + &mut self, + dir_cluster: u32, + filename: &str, + new_start_cluster: u32, + ) -> io::Result<()> { + // Передаём u32::MAX как специальное значение, чтобы не обновлять размер + self.update_file_size_in_dir_by_name(dir_cluster, filename, u32::MAX, Some(new_start_cluster))?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 43475dd..3de5fb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,16 @@ +//! FAT32-raw: A lightweight Rust library for working with FAT32 partitions and images + +pub mod error; pub mod fat32; +pub mod platform; + +// Re-export main types +pub use error::{Fat32Error, Result}; +pub use fat32::{volume::Fat32Volume, Fat32FileEntry, Fat32Params}; + +// Platform-specific re-exports +#[cfg(windows)] +pub use platform::windows::{ + create_directory_on_esp, delete_directory_from_esp, delete_file_from_esp, write_file_to_esp, +}; + diff --git a/src/platform/mod.rs b/src/platform/mod.rs new file mode 100644 index 0000000..fc36448 --- /dev/null +++ b/src/platform/mod.rs @@ -0,0 +1,29 @@ +//! Platform-specific implementations + +#[cfg(windows)] +pub mod windows; + +#[cfg(unix)] +pub mod unix; + +use crate::error::Result; + +/// Find ESP device for raw access +/// Returns device path and LBA offset +pub fn find_esp_device() -> Result> { + #[cfg(windows)] + { + windows::esp::find_esp_device() + } + + #[cfg(unix)] + { + unix::esp::find_esp_device() + } + + #[cfg(not(any(windows, unix)))] + { + log::warn!("ESP detection not implemented for this platform"); + Ok(None) + } +} diff --git a/src/platform/unix/esp.rs b/src/platform/unix/esp.rs new file mode 100644 index 0000000..cead7ca --- /dev/null +++ b/src/platform/unix/esp.rs @@ -0,0 +1,109 @@ +//! ESP (EFI System Partition) detection on Unix systems + +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; + +use crate::error::Result; + +/// Find ESP device on Unix/Linux systems +pub fn find_esp_device() -> Result> { + // Try to find ESP partition using various methods + + // Method 1: Check /boot/efi mount point + if let Ok(contents) = std::fs::read_to_string("/proc/mounts") { + for line in contents.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 && (parts[1] == "/boot/efi" || parts[1] == "/efi") { + let device = parts[0]; + log::info!("Found ESP mounted at {} on device {}", parts[1], device); + + // Try to get partition offset + if let Some(lba) = get_partition_offset(device) { + return Ok(Some((device.to_string(), lba))); + } + } + } + } + + // Method 2: Check for GPT partitions with ESP type GUID + // This requires parsing GPT headers, which is complex + // For now, we'll try common device paths + for disk_num in 0..4 { + for part_num in 1..5 { + let device = format!("/dev/nvme{}n{}p{}", disk_num, 1, part_num); + if is_esp_partition(&device) { + if let Some(lba) = get_partition_offset(&device) { + log::info!("Found ESP at {} with LBA {}", device, lba); + return Ok(Some((device, lba))); + } + } + + let device = format!("/dev/sda{}", part_num); + if is_esp_partition(&device) { + if let Some(lba) = get_partition_offset(&device) { + log::info!("Found ESP at {} with LBA {}", device, lba); + return Ok(Some((device, lba))); + } + } + } + } + + log::warn!("ESP device not found on Unix system"); + Ok(None) +} + +/// Check if a device is an ESP partition by checking filesystem type +fn is_esp_partition(device: &str) -> bool { + // Try to read the partition and check for FAT32 signature + if let Ok(mut file) = File::open(device) { + let mut buffer = [0u8; 512]; + if file.read_exact(&mut buffer).is_ok() { + // Check for FAT32 signature + if buffer[0x52] == b'F' && buffer[0x53] == b'A' && buffer[0x54] == b'T' { + return true; + } + // Check for FAT16 signature + if buffer[0x36] == b'F' && buffer[0x37] == b'A' && buffer[0x38] == b'T' { + return true; + } + } + } + false +} + +/// Get partition offset in LBA for a device +fn get_partition_offset(_device: &str) -> Option { + // When we open a partition device directly (e.g., /dev/sda1, /dev/nvme0n1p1), + // the kernel already handles the offset, so we use offset 0. + // + // LBA offset would only be needed if we were accessing the raw disk device + // and needed to skip to the partition start ourselves. + + // For partition devices, the offset is always 0 + Some(0) +} + +/// Find ESP mount point on Unix systems +pub fn find_esp_mount_point() -> Option { + // Check common ESP mount points + let mount_points = ["/boot/efi", "/efi", "/boot/EFI"]; + + for mount_point in &mount_points { + let path = PathBuf::from(mount_point); + if path.exists() && path.is_dir() { + // Check if it's actually mounted + if let Ok(contents) = std::fs::read_to_string("/proc/mounts") { + for line in contents.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 && parts[1] == *mount_point { + log::info!("Found ESP mount point at {}", mount_point); + return Some(path); + } + } + } + } + } + + None +} diff --git a/src/platform/unix/mod.rs b/src/platform/unix/mod.rs new file mode 100644 index 0000000..6cdebda --- /dev/null +++ b/src/platform/unix/mod.rs @@ -0,0 +1,5 @@ +//! Unix-specific platform implementation + +pub mod esp; + +pub use esp::{find_esp_device, find_esp_mount_point}; diff --git a/src/platform/windows/esp.rs b/src/platform/windows/esp.rs new file mode 100644 index 0000000..ccc6a46 --- /dev/null +++ b/src/platform/windows/esp.rs @@ -0,0 +1,374 @@ +//! ESP (EFI System Partition) detection and access on Windows + +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::path::PathBuf; +use std::ptr; + +use windows_sys::core::GUID; +use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE}; +use windows_sys::Win32::Storage::FileSystem::{FindFirstVolumeW, FindNextVolumeW, FindVolumeClose}; +use windows_sys::Win32::System::Ioctl::{ + DISK_EXTENT, DRIVE_LAYOUT_INFORMATION_EX, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, PARTITION_STYLE_GPT, + VOLUME_DISK_EXTENTS, +}; +use windows_sys::Win32::System::IO::DeviceIoControl; + +use super::volume::{is_fat_fs, open_handle}; +use crate::error::Result; + +// IOCTL codes +const IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS: u32 = 0x00560000; +const IOCTL_DISK_GET_PARTITION_INFO_EX: u32 = 0x00070048; + +/// ESP GUID as defined by UEFI specification +fn esp_guid() -> GUID { + GUID { + data1: 0xc12a7328, + data2: 0xf81f, + data3: 0x11d2, + data4: [0xba, 0x4b, 0x00, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b], + } +} + +/// Check if a GUID matches the ESP GUID +fn is_esp_guid(guid: &GUID) -> bool { + let esp = esp_guid(); + guid.data1 == esp.data1 + && guid.data2 == esp.data2 + && guid.data3 == esp.data3 + && guid.data4 == esp.data4 +} + +/// Partition information for getting partition number +#[repr(C)] +#[allow(non_snake_case)] +struct PARTITION_INFORMATION_EX { + PartitionStyle: u32, + StartingOffset: i64, + PartitionLength: i64, + PartitionNumber: u32, + RewritePartition: u32, + // Other fields omitted for simplicity +} + +/// Get partition number for a volume handle +unsafe fn get_partition_number(h_vol: *mut std::ffi::c_void) -> u32 { + let mut part_info: PARTITION_INFORMATION_EX = std::mem::zeroed(); + let mut bytes: u32 = 0; + + if DeviceIoControl( + h_vol as isize, + IOCTL_DISK_GET_PARTITION_INFO_EX, + ptr::null_mut(), + 0, + &mut part_info as *mut _ as *mut std::ffi::c_void, + std::mem::size_of::() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + part_info.PartitionNumber + } else { + 0 // Fallback + } +} + +/// Find ESP by scanning physical disks directly +unsafe fn find_esp_by_scanning_disks() -> Option { + // Scan physical drives 0..15 to find GPT ESP partition + for disk_num in 0..16 { + let phys_path_str = format!("\\\\.\\PhysicalDrive{}", disk_num); + let phys_path: Vec = OsStr::new(&phys_path_str) + .encode_wide() + .chain(Some(0)) + .collect(); + + if let Some(h_phys) = open_handle(&phys_path) { + let mut layout_buf = vec![0u8; 8192]; + let mut bytes: u32 = 0; + + if DeviceIoControl( + h_phys, + IOCTL_DISK_GET_DRIVE_LAYOUT_EX, + ptr::null_mut(), + 0, + layout_buf.as_mut_ptr().cast(), + layout_buf.len() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + let layout = &*(layout_buf.as_ptr() as *const DRIVE_LAYOUT_INFORMATION_EX); + + if layout.PartitionStyle == PARTITION_STYLE_GPT as u32 { + let part_entry_ptr = layout.PartitionEntry.as_ptr(); + + for i in 0..layout.PartitionCount { + let part_info = &*part_entry_ptr.add(i as usize); + + if part_info.PartitionStyle == PARTITION_STYLE_GPT + && is_esp_guid(&part_info.Anonymous.Gpt.PartitionType) + { + CloseHandle(h_phys); + + // Try to find corresponding volume + if let Some(volume_path) = + find_volume_for_partition(disk_num, part_info.PartitionNumber) + { + return Some(volume_path); + } + + // Fallback: direct harddisk partition path + return Some(PathBuf::from(format!( + "\\\\.\\Harddisk{}Partition{}", + disk_num, part_info.PartitionNumber + ))); + } + } + } + } + CloseHandle(h_phys); + } + } + None +} + +/// Find Volume GUID path for a specific partition +unsafe fn find_volume_for_partition(disk_num: u32, partition_num: u32) -> Option { + let mut name_buf = vec![0u16; 128]; + let h_find = FindFirstVolumeW(name_buf.as_mut_ptr(), name_buf.len() as u32); + + if h_find == INVALID_HANDLE_VALUE { + return None; + } + + loop { + let len = name_buf.iter().position(|&c| c == 0).unwrap_or(0); + if len > 0 { + let vol_path_slice = &name_buf[..len + 1]; + let mut vol_no_slash = vol_path_slice.to_vec(); + vol_no_slash.pop(); // Remove trailing null + + if vol_no_slash.last() == Some(&('\\' as u16)) { + vol_no_slash.pop(); // Remove trailing slash + } + + if let Some(h_vol) = open_handle(&vol_no_slash) { + let mut ext_buf: Vec = vec![ + 0; + std::mem::size_of::() + + 8 * std::mem::size_of::() + ]; + let mut bytes: u32 = 0; + + if DeviceIoControl( + h_vol, + IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, + ptr::null(), + 0, + ext_buf.as_mut_ptr().cast(), + ext_buf.len() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + let extents = &*(ext_buf.as_ptr() as *const VOLUME_DISK_EXTENTS); + if extents.NumberOfDiskExtents > 0 { + let first_extent = extents.Extents[0]; + + // Get partition number through additional IOCTL + let part_num = get_partition_number(h_vol as *mut std::ffi::c_void); + + if first_extent.DiskNumber == disk_num && part_num == partition_num { + CloseHandle(h_vol); + FindVolumeClose(h_find); + let s = String::from_utf16_lossy(&name_buf[..len]); + return Some(PathBuf::from(s)); + } + } + } + CloseHandle(h_vol); + } + } + + if FindNextVolumeW(h_find, name_buf.as_mut_ptr(), name_buf.len() as u32) == 0 { + break; + } + } + + FindVolumeClose(h_find); + None +} + +/// Find ESP volume path by scanning volumes +unsafe fn find_esp_volume_path_by_volumes() -> Option { + let mut name_buf = vec![0u16; 128]; + let h_find = FindFirstVolumeW(name_buf.as_mut_ptr(), name_buf.len() as u32); + + if h_find == INVALID_HANDLE_VALUE { + return None; + } + + loop { + let len = name_buf.iter().position(|&c| c == 0).unwrap_or(0); + if len > 0 { + // Got a volume GUID path like \\?\Volume{...}\ + let vol_path_slice = &name_buf[..len + 1]; + + // Check if it's a FAT filesystem, common for ESPs + if is_fat_fs(vol_path_slice) { + // To be sure, check partition type via IOCTL + let mut vol_no_slash = vol_path_slice.to_vec(); + vol_no_slash.pop(); // Remove trailing null + if vol_no_slash.last() == Some(&('\\' as u16)) { + vol_no_slash.pop(); // Remove trailing slash for CreateFileW + } + + if let Some(h_vol) = open_handle(&vol_no_slash) { + let mut ext_buf: Vec = vec![ + 0; + std::mem::size_of::() + + 8 * std::mem::size_of::() + ]; + let mut bytes: u32 = 0; + + if DeviceIoControl( + h_vol, + IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, + ptr::null(), + 0, + ext_buf.as_mut_ptr().cast(), + ext_buf.len() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + let extents = &*(ext_buf.as_ptr() as *const VOLUME_DISK_EXTENTS); + if extents.NumberOfDiskExtents > 0 { + let first_extent = extents.Extents[0]; + let phys_path_str = + format!("\\\\.\\PhysicalDrive{}", first_extent.DiskNumber); + let phys_path: Vec = OsStr::new(&phys_path_str) + .encode_wide() + .chain(Some(0)) + .collect(); + + if let Some(h_phys) = open_handle(&phys_path) { + let mut layout_buf = vec![0u8; 4096]; + if DeviceIoControl( + h_phys, + IOCTL_DISK_GET_DRIVE_LAYOUT_EX, + ptr::null_mut(), + 0, + layout_buf.as_mut_ptr().cast(), + layout_buf.len() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + let layout = &*(layout_buf.as_ptr() + as *const DRIVE_LAYOUT_INFORMATION_EX); + if layout.PartitionStyle == PARTITION_STYLE_GPT as u32 { + let part_entry_ptr = layout.PartitionEntry.as_ptr(); + for i in 0..layout.PartitionCount { + let part_info = &*part_entry_ptr.add(i as usize); + if part_info.PartitionStyle == PARTITION_STYLE_GPT + && is_esp_guid( + &part_info.Anonymous.Gpt.PartitionType, + ) + { + // Found it + CloseHandle(h_phys); + CloseHandle(h_vol); + FindVolumeClose(h_find); + let s = String::from_utf16_lossy(&name_buf[..len]); + return Some(PathBuf::from(s)); + } + } + } + } + CloseHandle(h_phys); + } + } + } + CloseHandle(h_vol); + } + } + } + + if FindNextVolumeW(h_find, name_buf.as_mut_ptr(), name_buf.len() as u32) == 0 { + break; + } + } + + FindVolumeClose(h_find); + None +} + +/// Find ESP volume path on Windows +pub fn find_esp_volume_path() -> Option { + unsafe { + // First try the new method of direct physical disk scanning + if let Some(path) = find_esp_by_scanning_disks() { + return Some(path); + } + + // Fallback to scanning volumes + find_esp_volume_path_by_volumes() + } +} + +/// Find ESP device for raw access +pub fn find_esp_device() -> Result> { + unsafe { + // Scan physical drives to find ESP + for disk_num in 0..16 { + let phys_path_str = format!("\\\\.\\PhysicalDrive{}", disk_num); + let phys_path: Vec = OsStr::new(&phys_path_str) + .encode_wide() + .chain(Some(0)) + .collect(); + + if let Some(h_phys) = open_handle(&phys_path) { + let mut layout_buf = vec![0u8; 8192]; + let mut bytes: u32 = 0; + + if DeviceIoControl( + h_phys, + IOCTL_DISK_GET_DRIVE_LAYOUT_EX, + ptr::null_mut(), + 0, + layout_buf.as_mut_ptr().cast(), + layout_buf.len() as u32, + &mut bytes, + ptr::null_mut(), + ) != 0 + { + let layout = &*(layout_buf.as_ptr() as *const DRIVE_LAYOUT_INFORMATION_EX); + + if layout.PartitionStyle == PARTITION_STYLE_GPT as u32 { + let part_entry_ptr = layout.PartitionEntry.as_ptr(); + + for i in 0..layout.PartitionCount { + let part_info = &*part_entry_ptr.add(i as usize); + + if part_info.PartitionStyle == PARTITION_STYLE_GPT + && is_esp_guid(&part_info.Anonymous.Gpt.PartitionType) + { + let lba = (part_info.StartingOffset / 512) as u64; + log::info!("Found ESP on PhysicalDrive{} at LBA {}", disk_num, lba); + CloseHandle(h_phys); + return Ok(Some((phys_path_str, lba))); + } + } + } + } + CloseHandle(h_phys); + } + } + } + + log::warn!("ESP device not found after scanning all physical drives"); + Ok(None) +} diff --git a/src/platform/windows/io.rs b/src/platform/windows/io.rs new file mode 100644 index 0000000..c13b0da --- /dev/null +++ b/src/platform/windows/io.rs @@ -0,0 +1,389 @@ +//! Windows I/O operations with OS Error 5 bypass strategies + +use std::fs::{self, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +use super::esp::find_esp_volume_path; +use super::privileges::try_enable_esp_privileges; +use super::volume::{ + dismount_locked_volume, guid_volume_root_from_drive_root, try_lock_volume, unlock_and_close, +}; +use crate::error::{Fat32Error, Result}; + +/// Write file with atomic operation (using temp file and rename) +fn write_file_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> { + // Create parent directories if needed + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + + // Create temp file with unique name + let tmp = path.with_file_name(format!(".tmp-{}.tmp", Uuid::new_v4())); + + // Try multiple times in case of transient errors + let mut last_err: Option = None; + for _ in 0..5 { + match (|| -> std::io::Result<()> { + let mut f = File::create(&tmp)?; + f.write_all(data)?; + f.flush()?; + fs::rename(&tmp, path)?; + Ok(()) + })() { + Ok(()) => return Ok(()), + Err(e) => { + last_err = Some(e); + std::thread::sleep(std::time::Duration::from_millis(500)); + } + } + } + + Err(last_err.unwrap_or_else(|| { + std::io::Error::new(std::io::ErrorKind::Other, "write retry loop failed") + })) +} + +/// Get ESP root and relative path from absolute path +fn get_esp_root_and_relative_path(path: &Path) -> Option<(PathBuf, PathBuf)> { + // If the path already looks like an ESP volume path, use it directly + let path_str = path.to_string_lossy(); + + // Check if it's already a volume path (\\?\Volume{...}\...) + if path_str.starts_with("\\\\?\\Volume{") { + // Extract the volume part and the relative part + if let Some(end_idx) = path_str.find("}\\").map(|i| i + 2) { + let volume = PathBuf::from(&path_str[..end_idx]); + let relative = PathBuf::from(&path_str[end_idx..]); + return Some((volume, relative)); + } + } + + // Otherwise try to find ESP volume path + if let Some(esp_root) = find_esp_volume_path() { + // Try to extract relative path + if path.is_absolute() { + // If it's an absolute path like C:\test_dir\file.txt, + // convert to relative path test_dir\file.txt + let components: Vec<_> = path.components().collect(); + if components.len() > 1 { + let relative = PathBuf::from_iter(components[1..].iter()); + return Some((esp_root, relative)); + } + } else { + // It's already a relative path + return Some((esp_root, path.to_path_buf())); + } + } + + None +} + +/// Write file to ESP with multiple strategies to bypass OS Error 5 +pub fn write_file_to_esp(rel_path: &Path, data: &[u8]) -> Result<()> { + let root = find_esp_volume_path().ok_or_else(|| Fat32Error::EspNotFound)?; + let dst = root.join(rel_path); + + let mut tried_strategies = Vec::new(); + + // Enable privileges first + try_enable_esp_privileges(); + + // Strategy 1: Try direct write + tried_strategies.push("direct write".to_string()); + if write_file_atomic(&dst, data).is_ok() { + // Try to ensure data is flushed to disk for raw reader visibility + if let Ok(f) = File::open(&dst) { + let _ = f.sync_all(); + } + log::info!("Wrote {} bytes to {} (plain)", data.len(), dst.display()); + return Ok(()); + } + + // Strategy 2: Try with volume lock/unlock + tried_strategies.push("volume lock/unlock".to_string()); + unsafe { + if let Some(h) = try_lock_volume(&root) { + let _ = dismount_locked_volume(h); + unlock_and_close(h); + std::thread::sleep(std::time::Duration::from_millis(800)); + } + } + + if write_file_atomic(&dst, data).is_ok() { + if let Ok(f) = File::open(&dst) { + let _ = f.sync_all(); + } + log::info!( + "Wrote {} bytes to {} (after remount)", + data.len(), + dst.display() + ); + return Ok(()); + } + + // Strategy 3: Try GUID path + tried_strategies.push("GUID volume path".to_string()); + if let Some(guid_root) = guid_volume_root_from_drive_root(&root) { + let alt_dst = guid_root.join(rel_path); + if write_file_atomic(&alt_dst, data).is_ok() { + if let Ok(f) = File::open(&alt_dst) { + let _ = f.sync_all(); + } + log::info!( + "Wrote {} bytes to {} (GUID path)", + data.len(), + alt_dst.display() + ); + return Ok(()); + } + } + + Err(Fat32Error::access_denied( + dst.display().to_string(), + tried_strategies, + )) +} + +/// Create directory on ESP with multiple strategies +pub fn create_directory_on_esp(rel_path: &Path) -> Result<()> { + let root = find_esp_volume_path().ok_or_else(|| Fat32Error::EspNotFound)?; + let dst = root.join(rel_path); + + let mut tried_strategies = Vec::new(); + + // Enable privileges first + try_enable_esp_privileges(); + + // Strategy 1: Try simple directory creation + tried_strategies.push("direct creation".to_string()); + if fs::create_dir_all(&dst).is_ok() { + log::info!("Created directory {} (plain)", dst.display()); + return Ok(()); + } + + // Strategy 2: Try with volume lock/unlock + tried_strategies.push("volume lock/unlock".to_string()); + unsafe { + if let Some(h) = try_lock_volume(&root) { + let _ = dismount_locked_volume(h); + unlock_and_close(h); + std::thread::sleep(std::time::Duration::from_millis(800)); + } + } + + if fs::create_dir_all(&dst).is_ok() { + log::info!("Created directory {} (after remount)", dst.display()); + return Ok(()); + } + + // Strategy 3: Try GUID path + tried_strategies.push("GUID volume path".to_string()); + if let Some(guid_root) = guid_volume_root_from_drive_root(&root) { + let alt_dst = guid_root.join(rel_path); + if fs::create_dir_all(&alt_dst).is_ok() { + log::info!("Created directory {} (GUID path)", alt_dst.display()); + return Ok(()); + } + } + + Err(Fat32Error::access_denied( + dst.display().to_string(), + tried_strategies, + )) +} + +/// Delete file from ESP with multiple strategies +pub fn delete_file_from_esp(path: &Path) -> Result<()> { + log::info!("Attempting to delete file from ESP: {:?}", path); + + // Try to get ESP root and relative path + let (root, rel_path) = match get_esp_root_and_relative_path(path) { + Some((r, p)) => (r, p), + None => { + // Fallback to direct deletion + return fs::remove_file(path).map_err(|e| e.into()); + } + }; + + let dst = root.join(&rel_path); + let mut tried_strategies = Vec::new(); + + // Strategy 1: Try direct deletion + tried_strategies.push("direct deletion".to_string()); + match fs::remove_file(&dst) { + Ok(()) => { + log::info!( + "Successfully deleted file via direct method: {}", + dst.display() + ); + return Ok(()); + } + Err(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + log::warn!("Direct deletion failed with permission denied, trying other strategies"); + } + Err(e) => return Err(e.into()), + } + + // Strategy 2: Try with privileges + tried_strategies.push("with privileges".to_string()); + try_enable_esp_privileges(); + match fs::remove_file(&dst) { + Ok(()) => { + log::info!( + "Successfully deleted file with elevated privileges: {}", + dst.display() + ); + return Ok(()); + } + Err(e) => { + log::warn!("Deletion with privileges failed: {}", e); + } + } + + // Strategy 3: Try with volume lock/dismount + tried_strategies.push("volume lock/dismount".to_string()); + unsafe { + if let Some(h) = try_lock_volume(&root) { + let _dismounted = dismount_locked_volume(h); + unlock_and_close(h); + + // Wait for volume to be accessible again + std::thread::sleep(std::time::Duration::from_millis(800)); + + match fs::remove_file(&dst) { + Ok(()) => { + log::info!( + "Successfully deleted file after volume remount: {}", + dst.display() + ); + return Ok(()); + } + Err(e) => { + log::warn!("Deletion after remount failed: {}", e); + } + } + } + } + + // Strategy 4: Try GUID path + tried_strategies.push("GUID volume path".to_string()); + if let Some(guid_root) = guid_volume_root_from_drive_root(&root) { + let alt_dst = guid_root.join(&rel_path); + match fs::remove_file(&alt_dst) { + Ok(()) => { + log::info!( + "Successfully deleted file via GUID path: {}", + alt_dst.display() + ); + return Ok(()); + } + Err(e) => { + log::warn!("Deletion via GUID path failed: {}", e); + } + } + } + + Err(Fat32Error::access_denied( + dst.display().to_string(), + tried_strategies, + )) +} + +/// Delete directory from ESP with multiple strategies +pub fn delete_directory_from_esp(path: &Path) -> Result<()> { + log::info!("Attempting to delete directory from ESP: {:?}", path); + + // Try to get ESP root and relative path + let (root, rel_path) = match get_esp_root_and_relative_path(path) { + Some((r, p)) => (r, p), + None => { + // Fallback to direct deletion - use remove_dir for empty dirs only + return fs::remove_dir(path).map_err(|e| e.into()); + } + }; + + let dst = root.join(&rel_path); + let mut tried_strategies = Vec::new(); + + // Strategy 1: Try direct deletion (remove_dir for empty directories only) + tried_strategies.push("direct deletion".to_string()); + match fs::remove_dir(&dst) { + Ok(()) => { + log::info!( + "Successfully deleted directory via direct method: {}", + dst.display() + ); + return Ok(()); + } + Err(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + log::warn!("Direct deletion failed with permission denied, trying other strategies"); + } + Err(e) => return Err(e.into()), + } + + // Strategy 2: Try with privileges + tried_strategies.push("with privileges".to_string()); + try_enable_esp_privileges(); + match fs::remove_dir(&dst) { + Ok(()) => { + log::info!( + "Successfully deleted directory with elevated privileges: {}", + dst.display() + ); + return Ok(()); + } + Err(e) => { + log::warn!("Deletion with privileges failed: {}", e); + } + } + + // Strategy 3: Try with volume lock/dismount + tried_strategies.push("volume lock/dismount".to_string()); + unsafe { + if let Some(h) = try_lock_volume(&root) { + let _dismounted = dismount_locked_volume(h); + unlock_and_close(h); + + // Wait for volume to be accessible again + std::thread::sleep(std::time::Duration::from_millis(800)); + + match fs::remove_dir(&dst) { + Ok(()) => { + log::info!( + "Successfully deleted directory after volume remount: {}", + dst.display() + ); + return Ok(()); + } + Err(e) => { + log::warn!("Deletion after remount failed: {}", e); + } + } + } + } + + // Strategy 4: Try GUID path + tried_strategies.push("GUID volume path".to_string()); + if let Some(guid_root) = guid_volume_root_from_drive_root(&root) { + let alt_dst = guid_root.join(&rel_path); + match fs::remove_dir(&alt_dst) { + Ok(()) => { + log::info!( + "Successfully deleted directory via GUID path: {}", + alt_dst.display() + ); + return Ok(()); + } + Err(e) => { + log::warn!("Deletion via GUID path failed: {}", e); + } + } + } + + Err(Fat32Error::access_denied( + dst.display().to_string(), + tried_strategies, + )) +} diff --git a/src/platform/windows/mod.rs b/src/platform/windows/mod.rs new file mode 100644 index 0000000..036da95 --- /dev/null +++ b/src/platform/windows/mod.rs @@ -0,0 +1,14 @@ +//! Windows-specific platform implementation + +pub mod esp; +pub mod io; +pub mod privileges; +pub mod volume; + +// Re-export commonly used functions +pub use esp::{find_esp_device, find_esp_volume_path}; +pub use io::{ + create_directory_on_esp, delete_directory_from_esp, delete_file_from_esp, write_file_to_esp, +}; +pub use privileges::{enable_esp_privileges, try_enable_esp_privileges}; +pub use volume::VolumeLock; diff --git a/src/platform/windows/privileges.rs b/src/platform/windows/privileges.rs new file mode 100644 index 0000000..125b0b1 --- /dev/null +++ b/src/platform/windows/privileges.rs @@ -0,0 +1,123 @@ +//! Windows privilege management for accessing ESP partitions + +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::ptr; + +use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, LUID}; +use windows_sys::Win32::Security::{ + AdjustTokenPrivileges, LookupPrivilegeValueW, LUID_AND_ATTRIBUTES, SE_PRIVILEGE_ENABLED, + TOKEN_ADJUST_PRIVILEGES, TOKEN_PRIVILEGES, TOKEN_QUERY, +}; +use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; + +use crate::error::{Fat32Error, Result}; + +/// Enable a specific Windows privilege by name +pub fn enable_privilege(name: &str) -> Result<()> { + unsafe { + let mut token: HANDLE = 0; + + // Open process token + if OpenProcessToken( + GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + &mut token, + ) == 0 + { + let error_code = GetLastError(); + return Err(Fat32Error::platform_error( + format!("OpenProcessToken failed: {}", error_code), + Some(error_code), + )); + } + + // Convert privilege name to wide string + let wname: Vec = OsStr::new(name).encode_wide().chain(Some(0)).collect(); + + // Look up privilege value + let mut luid = LUID { + LowPart: 0, + HighPart: 0, + }; + + if LookupPrivilegeValueW(ptr::null(), wname.as_ptr(), &mut luid) == 0 { + let error_code = GetLastError(); + let _ = CloseHandle(token); + return Err(Fat32Error::platform_error( + format!("LookupPrivilegeValueW failed for {}: {}", name, error_code), + Some(error_code), + )); + } + + // Prepare privilege structure + let tp = TOKEN_PRIVILEGES { + PrivilegeCount: 1, + Privileges: [LUID_AND_ATTRIBUTES { + Luid: luid, + Attributes: SE_PRIVILEGE_ENABLED, + }], + }; + + // Adjust token privileges + if AdjustTokenPrivileges(token, 0, &tp, 0, ptr::null_mut(), ptr::null_mut()) == 0 { + let error_code = GetLastError(); + let _ = CloseHandle(token); + return Err(Fat32Error::platform_error( + format!("AdjustTokenPrivileges failed for {}: {}", name, error_code), + Some(error_code), + )); + } + + let _ = CloseHandle(token); + + log::debug!("Enabled Windows privilege: {}", name); + Ok(()) + } +} + +/// Enable core privileges needed for ESP access +/// These privileges help bypass OS Error 5 (Access Denied) +pub fn enable_esp_privileges() -> Result<()> { + // Try to enable each privilege, but don't fail if some can't be enabled + // (user might not have permission for all of them) + let privileges = [ + "SeBackupPrivilege", // Allows reading files without access checks + "SeRestorePrivilege", // Allows writing files without access checks + "SeTakeOwnershipPrivilege", // Allows taking ownership of files + ]; + + let mut any_enabled = false; + let mut last_error = None; + + for privilege in &privileges { + match enable_privilege(privilege) { + Ok(()) => { + any_enabled = true; + log::info!("Enabled privilege: {}", privilege); + } + Err(e) => { + log::warn!("Failed to enable privilege {}: {:?}", privilege, e); + last_error = Some(e); + } + } + } + + if any_enabled { + Ok(()) + } else { + Err(last_error.unwrap_or_else(|| { + Fat32Error::platform_error("Failed to enable any ESP privileges", None) + })) + } +} + +/// Try to enable ESP privileges without failing if unsuccessful +pub fn try_enable_esp_privileges() { + if let Err(e) = enable_esp_privileges() { + log::debug!( + "Could not enable ESP privileges (might not be needed): {:?}", + e + ); + } +} diff --git a/src/platform/windows/volume.rs b/src/platform/windows/volume.rs new file mode 100644 index 0000000..069531c --- /dev/null +++ b/src/platform/windows/volume.rs @@ -0,0 +1,189 @@ +//! Windows volume management (lock/unlock for ESP access) + +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::ptr; + +use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; +use windows_sys::Win32::Storage::FileSystem::{ + CreateFileW, GetVolumeInformationW, GetVolumeNameForVolumeMountPointW, FILE_ATTRIBUTE_NORMAL, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, +}; +use windows_sys::Win32::System::IO::DeviceIoControl; + + +// IOCTL codes for volume operations +const FSCTL_LOCK_VOLUME: u32 = 0x00090018; +const FSCTL_DISMOUNT_VOLUME: u32 = 0x00090020; + +/// Convert a root path to a volume path for CreateFileW +/// For example: "C:\" -> "\\.\C:" +pub fn volume_path_from_root(root: &Path) -> Option> { + let s = root.as_os_str().to_string_lossy(); + let drive = s.chars().next()?; + + if !drive.is_ascii_alphabetic() { + return None; + } + + // Use \\.\C: style path (with colon) to open a volume handle correctly + let path = format!("\\\\.\\{}: ", drive.to_ascii_uppercase()); + let mut w: Vec = OsStr::new(&path).encode_wide().collect(); + w.push(0); + Some(w) +} + +/// Get the GUID volume path from a drive root +/// For example: "C:\" -> "\\?\Volume{GUID}\" +pub fn guid_volume_root_from_drive_root(root: &Path) -> Option { + let mut drive = root.as_os_str().encode_wide().collect::>(); + if !drive.ends_with(&[0]) { + drive.push(0); + } + + let mut buf = vec![0u16; 64]; + let ok = unsafe { + GetVolumeNameForVolumeMountPointW(drive.as_ptr(), buf.as_mut_ptr(), buf.len() as u32) + }; + + if ok == 0 { + return None; + } + + let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len()); + let s = String::from_utf16_lossy(&buf[..len]); + Some(PathBuf::from(s)) +} + +/// Check if a volume uses FAT/FAT32 filesystem +pub fn is_fat_fs(volume_path: &[u16]) -> bool { + unsafe { + let mut fs_name = [0u16; 32]; + let ok = GetVolumeInformationW( + volume_path.as_ptr(), + ptr::null_mut(), + 0, + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + fs_name.as_mut_ptr(), + fs_name.len() as u32, + ); + + if ok == 0 { + return false; + } + + let len = fs_name + .iter() + .position(|&c| c == 0) + .unwrap_or(fs_name.len()); + let name = String::from_utf16_lossy(&fs_name[..len]).to_uppercase(); + name == "FAT" || name == "FAT32" + } +} + +/// Open a handle to a device or volume +pub fn open_handle(path_w: &[u16]) -> Option { + unsafe { + let h = CreateFileW( + path_w.as_ptr(), + (FILE_GENERIC_READ | FILE_GENERIC_WRITE) as u32, + (FILE_SHARE_READ | FILE_SHARE_WRITE) as u32, + ptr::null_mut(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + 0, + ); + + if h == INVALID_HANDLE_VALUE || h == 0 { + None + } else { + Some(h) + } + } +} + +/// Try to lock a volume for exclusive access +/// Returns a handle that must be closed with unlock_and_close +pub unsafe fn try_lock_volume(root: &Path) -> Option { + if let Some(wpath) = volume_path_from_root(root) { + let h = CreateFileW( + wpath.as_ptr(), + (FILE_GENERIC_READ | FILE_GENERIC_WRITE) as u32, + (FILE_SHARE_READ | FILE_SHARE_WRITE) as u32, + ptr::null_mut(), + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + 0, + ); + + if h == INVALID_HANDLE_VALUE || h == 0 { + return None; + } + + let mut bytes: u32 = 0; + let ok = DeviceIoControl( + h, + FSCTL_LOCK_VOLUME, + ptr::null_mut(), + 0, + ptr::null_mut(), + 0, + &mut bytes as *mut u32, + ptr::null_mut(), + ); + + if ok == 0 { + let _ = CloseHandle(h); + return None; + } + + return Some(h); + } + None +} + +/// Dismount a locked volume +pub unsafe fn dismount_locked_volume(h: HANDLE) -> bool { + let mut bytes: u32 = 0; + DeviceIoControl( + h, + FSCTL_DISMOUNT_VOLUME, + ptr::null_mut(), + 0, + ptr::null_mut(), + 0, + &mut bytes as *mut u32, + ptr::null_mut(), + ) != 0 +} + +/// Unlock and close a volume handle +pub unsafe fn unlock_and_close(h: HANDLE) { + let _ = CloseHandle(h); +} + +/// Volume lock guard for RAII pattern +pub struct VolumeLock { + handle: HANDLE, +} + +impl VolumeLock { + /// Try to lock a volume + pub fn lock(root: &Path) -> Option { + unsafe { try_lock_volume(root).map(|handle| Self { handle }) } + } + + /// Dismount the locked volume + pub fn dismount(&self) -> bool { + unsafe { dismount_locked_volume(self.handle) } + } +} + +impl Drop for VolumeLock { + fn drop(&mut self) { + unsafe { unlock_and_close(self.handle) } + } +} diff --git a/tests/real_esp_test.rs b/tests/real_esp_test.rs new file mode 100644 index 0000000..a57c9ef --- /dev/null +++ b/tests/real_esp_test.rs @@ -0,0 +1,424 @@ +use fat32_raw::Fat32Volume; +use std::io::Result; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn run_comprehensive_test() -> Result<()> { + println!("\n=== COMPREHENSIVE FAT32 TEST ==="); + println!("Attempting to open real ESP partition..."); + let mut volume = match Fat32Volume::open_esp::<&str>(None)? { + Some(v) => { + println!("✅ Successfully opened ESP partition."); + v + } + None => { + println!("⚠️ ESP partition not found. Skipping test."); + return Ok(()); + } + }; + + // Create a unique root directory for all tests + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let root_test_dir = format!("fat32test{}", timestamp % 1000000); // Use shorter name + + println!("\n📁 Test root directory: {}", root_test_dir); + + // Cleanup function + let cleanup = |volume: &mut Fat32Volume| { + println!("\n🧹 Cleaning up test directory..."); + let _ = volume.delete_dir_lfn(&root_test_dir); + }; + + // TEST 1: Basic directory operations + println!("\n--- TEST 1: Basic Directory Operations ---"); + + println!("1.1 Creating root test directory: '{}'", root_test_dir); + match volume.create_directory_path(&root_test_dir) { + Ok(true) => println!(" ✅ Directory created."), + Ok(false) => { + println!(" ⚠️ Directory already exists, cleaning up..."); + cleanup(&mut volume); + volume.create_directory_path(&root_test_dir)?; + println!(" ✅ Directory recreated."); + } + Err(e) => { + eprintln!(" ❌ Failed to create directory: {}", e); + return Err(e.into()); + } + } + + // TEST 2: Nested directory creation + println!("\n--- TEST 2: Nested Directory Creation ---"); + + let nested_dir1 = format!("{}/level1", root_test_dir); + let nested_dir2 = format!("{}/level1/level2", root_test_dir); + let nested_dir3 = format!("{}/level1/level2/level3", root_test_dir); + + println!("2.1 Creating nested directories..."); + for (idx, dir) in [&nested_dir1, &nested_dir2, &nested_dir3].iter().enumerate() { + println!(" Creating level {}: '{}'", idx + 1, dir); + match volume.create_directory_path(dir) { + Ok(_) => println!(" ✅ Created"), + Err(e) => { + eprintln!(" ❌ Failed: {}", e); + cleanup(&mut volume); + return Err(e.into()); + } + } + } + + // TEST 3: File operations in different directories + println!("\n--- TEST 3: File Operations ---"); + + // 3.1 Small file + let small_file = format!("{}/small.txt", root_test_dir); + let small_content = b"Small file content"; + + println!("3.1 Writing small file: '{}'", small_file); + volume.write_file_with_path(&small_file, small_content)?; + println!(" ✅ Written {} bytes", small_content.len()); + + volume.refresh_all_caches()?; + + println!(" Reading back..."); + match volume.read_file(&small_file)? { + Some(content) => { + assert_eq!(content, small_content); + println!(" ✅ Content verified"); + } + None => { + eprintln!(" ❌ File not found after write"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Small file not found", + )); + } + } + + // 3.2 Medium file (1KB) + let medium_file = format!("{}/medium.dat", nested_dir1); + let medium_content: Vec = (0..1024).map(|i| (i % 256) as u8).collect(); + + println!("\n3.2 Writing medium file (1KB): '{}'", medium_file); + volume.write_file_with_path(&medium_file, &medium_content)?; + println!(" ✅ Written {} bytes", medium_content.len()); + + volume.refresh_all_caches()?; + + println!(" Reading back..."); + match volume.read_file(&medium_file)? { + Some(content) => { + assert_eq!(content, medium_content); + println!(" ✅ Content verified"); + } + None => { + eprintln!(" ❌ File not found after write"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Medium file not found", + )); + } + } + + // 3.3 Large file (64KB) + let large_file = format!("{}/large.bin", nested_dir2); + let large_content: Vec = (0..65536).map(|i| ((i * 7) % 256) as u8).collect(); + + println!("\n3.3 Writing large file (64KB): '{}'", large_file); + volume.write_file_with_path(&large_file, &large_content)?; + println!(" ✅ Written {} bytes", large_content.len()); + + volume.refresh_all_caches()?; + + println!(" Reading back..."); + match volume.read_file(&large_file)? { + Some(content) => { + assert_eq!(content.len(), large_content.len()); + // Verify first and last 1KB + assert_eq!(&content[..1024], &large_content[..1024]); + assert_eq!(&content[content.len()-1024..], &large_content[large_content.len()-1024..]); + println!(" ✅ Content verified (size and samples)"); + } + None => { + eprintln!(" ❌ File not found after write"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Large file not found", + )); + } + } + + // TEST 4: Multiple files in same directory + println!("\n--- TEST 4: Multiple Files in Same Directory ---"); + + let multi_dir = format!("{}/multifile", root_test_dir); + volume.create_directory_path(&multi_dir)?; + + println!("4.1 Creating 10 files in '{}'", multi_dir); + for i in 0..10 { + let filename = format!("{}/file{:02}.txt", multi_dir, i); + let content = format!("This is file number {}", i); + volume.write_file_with_path(&filename, content.as_bytes())?; + print!("."); + } + println!(" ✅ Created 10 files"); + + volume.refresh_all_caches()?; + + println!("4.2 Verifying all files..."); + for i in 0..10 { + let filename = format!("{}/file{:02}.txt", multi_dir, i); + let expected = format!("This is file number {}", i); + match volume.read_file(&filename)? { + Some(content) => { + assert_eq!(content, expected.as_bytes()); + print!("."); + } + None => { + eprintln!("\n ❌ File {} not found", filename); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("File {} not found", filename), + )); + } + } + } + println!(" ✅ All files verified"); + + // TEST 5: File overwrite + println!("\n--- TEST 5: File Overwrite ---"); + + let overwrite_file = format!("{}/overwrite.txt", root_test_dir); + let original_content = b"Original content"; + let new_content = b"This is the new content that replaces the original"; + + println!("5.1 Writing original file: '{}'", overwrite_file); + volume.write_file_with_path(&overwrite_file, original_content)?; + println!(" ✅ Written {} bytes", original_content.len()); + + volume.refresh_all_caches()?; + + println!("5.2 Overwriting with new content..."); + volume.write_file_with_path(&overwrite_file, new_content)?; + println!(" ✅ Written {} bytes", new_content.len()); + + volume.refresh_all_caches()?; + + println!("5.3 Verifying new content..."); + match volume.read_file(&overwrite_file)? { + Some(content) => { + assert_eq!(content, new_content); + assert_ne!(content, original_content); + println!(" ✅ Overwrite successful"); + } + None => { + eprintln!(" ❌ File not found after overwrite"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Overwritten file not found", + )); + } + } + + // TEST 6: Empty file + println!("\n--- TEST 6: Empty File ---"); + + let empty_file = format!("{}/empty.txt", root_test_dir); + + println!("6.1 Creating empty file: '{}'", empty_file); + volume.write_file_with_path(&empty_file, b"")?; + println!(" ✅ Created"); + + volume.refresh_all_caches()?; + + println!("6.2 Reading empty file..."); + match volume.read_file(&empty_file)? { + Some(content) => { + assert_eq!(content.len(), 0); + println!(" ✅ Empty file verified"); + } + None => { + eprintln!(" ❌ Empty file not found"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Empty file not found", + )); + } + } + + // TEST 7: File deletion + println!("\n--- TEST 7: File Deletion ---"); + + println!("7.1 Deleting small file: '{}'", small_file); + match volume.delete_file_lfn(&small_file) { + Ok(true) => println!(" ✅ Deleted"), + Ok(false) => println!(" ⚠️ File not found"), + Err(e) => { + eprintln!(" ❌ Failed: {}", e); + cleanup(&mut volume); + return Err(e.into()); + } + } + + volume.refresh_all_caches()?; + + println!("7.2 Verifying deletion..."); + match volume.read_file(&small_file)? { + Some(_) => { + eprintln!(" ❌ File still exists after deletion"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "File not deleted", + )); + } + None => println!(" ✅ File successfully deleted"), + } + + println!("7.3 Deleting all files in multifile directory..."); + for i in 0..10 { + let filename = format!("{}/file{:02}.txt", multi_dir, i); + volume.delete_file_lfn(&filename)?; + print!("."); + } + println!(" ✅ All files deleted"); + + // TEST 8: Directory deletion + println!("\n--- TEST 8: Directory Deletion ---"); + + println!("8.1 Attempting to delete non-empty directory (should fail)..."); + match volume.delete_dir_lfn(&nested_dir1) { + Ok(false) | Err(_) => println!(" ✅ Correctly refused to delete non-empty directory"), + Ok(true) => { + eprintln!(" ❌ Incorrectly deleted non-empty directory"); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Non-empty directory was deleted", + )); + } + } + + println!("8.2 Deleting files from nested directories..."); + volume.delete_file_lfn(&medium_file)?; + println!(" ✅ Deleted medium file"); + volume.delete_file_lfn(&large_file)?; + println!(" ✅ Deleted large file"); + + println!("8.3 Deleting empty nested directories (bottom-up)..."); + volume.delete_dir_lfn(&nested_dir3)?; + println!(" ✅ Deleted level3"); + volume.delete_dir_lfn(&nested_dir2)?; + println!(" ✅ Deleted level2"); + volume.delete_dir_lfn(&nested_dir1)?; + println!(" ✅ Deleted level1"); + + println!("8.4 Deleting empty multifile directory..."); + volume.delete_dir_lfn(&multi_dir)?; + println!(" ✅ Deleted multifile directory"); + + // TEST 9: Special characters and edge cases + println!("\n--- TEST 9: Special Characters and Edge Cases ---"); + + let special_dir = format!("{}/special", root_test_dir); + volume.create_directory_path(&special_dir)?; + + // Test files with various names + let test_names = vec![ + ("test.txt", b"normal name" as &[u8]), + ("test-123.txt", b"with numbers" as &[u8]), + ("test_file.txt", b"with underscore" as &[u8]), + ("upper.txt", b"uppercase" as &[u8]), + ("a.txt", b"single char" as &[u8]), + ("123.txt", b"starts with number" as &[u8]), + ]; + + println!("9.1 Creating files with various names..."); + for (name, content) in &test_names { + let filepath = format!("{}/{}", special_dir, name); + volume.write_file_with_path(&filepath, content)?; + println!(" ✅ Created: {}", name); + } + + volume.refresh_all_caches()?; + + println!("9.2 Verifying all special files..."); + for (name, expected_content) in &test_names { + let filepath = format!("{}/{}", special_dir, name); + match volume.read_file(&filepath)? { + Some(content) => { + assert_eq!(content, *expected_content); + println!(" ✅ Verified: {}", name); + } + None => { + eprintln!(" ❌ Not found: {}", name); + cleanup(&mut volume); + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Special file {} not found", name), + )); + } + } + } + + // TEST 10: Final cleanup + println!("\n--- TEST 10: Final Cleanup ---"); + + println!("10.1 Deleting remaining test files..."); + volume.delete_file_lfn(&overwrite_file)?; + println!(" ✅ Deleted overwrite test file"); + volume.delete_file_lfn(&empty_file)?; + println!(" ✅ Deleted empty file"); + + for (name, _) in &test_names { + let filepath = format!("{}/{}", special_dir, name); + volume.delete_file_lfn(&filepath)?; + } + println!(" ✅ Deleted all special test files"); + + println!("10.2 Deleting special directory..."); + volume.delete_dir_lfn(&special_dir)?; + println!(" ✅ Deleted special directory"); + + println!("10.3 Deleting root test directory: '{}'", root_test_dir); + match volume.delete_dir_lfn(&root_test_dir) { + Ok(true) => println!(" ✅ Successfully deleted root test directory"), + Ok(false) => { + eprintln!(" ⚠️ Directory not found or not empty"); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not delete root test directory", + )); + } + Err(e) => { + eprintln!(" ❌ Failed to delete: {}", e); + return Err(e.into()); + } + } + + println!("\n✅✅✅ ALL TESTS PASSED SUCCESSFULLY! ✅✅✅"); + Ok(()) +} + +#[test] +#[ignore] +fn comprehensive_fat32_test() { + println!("\n🚀 Starting comprehensive FAT32 test."); + println!("⚠️ This test requires elevated privileges (sudo/Administrator)."); + println!("⚠️ It will create and delete test files on your ESP partition.\n"); + + if let Err(e) = run_comprehensive_test() { + eprintln!("\n❌❌❌ COMPREHENSIVE TEST FAILED ❌❌❌"); + eprintln!("Error: {}", e); + panic!("Comprehensive FAT32 test failed: {}", e); + } +} +