flasher: configure the boot partition inside the image, before writing

The user's diagnostic log proved the macOS write and verify complete
successfully and only the post-flash mount of the boot partition fails:
the ArchR boot FAT is a below-spec-cluster-count FAT32 (the geometry
the vendor U-Boots require), and the macOS msdos driver refuses to
mount it, misreading it as FAT16.

Stop mounting it at all. The panel overlay, variant marker and the
soysauce extlinux switch are now written into the image itself before
flashing, through a vendored fatfs whose FAT type detection trusts the
BPB layout instead of the cluster-count heuristic (see
vendor/fatfs/VENDORED.md); the Linux kernel and mtools make the same
call. A raw .img supplied by the user is copied to temp first so the
original file is never modified, and the verify pass now covers the
configuration bytes too. The post-write mount path and its err:mount_boot
failure mode are gone.
This commit is contained in:
Douglas Teles
2026-07-08 16:35:17 -03:00
parent 9dc5a8d8b4
commit 23457691ca
21 changed files with 6547 additions and 86 deletions
+23
View File
@@ -61,8 +61,10 @@ name = "archr-flasher"
version = "1.3.6"
dependencies = [
"cc",
"fatfs",
"flate2",
"fs2",
"fscommon",
"libc",
"md-5",
"reqwest 0.12.28",
@@ -325,8 +327,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link 0.2.1",
]
@@ -736,6 +740,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fatfs"
version = "0.3.6"
dependencies = [
"bitflags 1.3.2",
"byteorder",
"chrono",
"log",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -855,6 +869,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "fscommon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "315ce685aca5ddcc5a3e7e436ef47d4a5d0064462849b6f0f628c28140103531"
dependencies = [
"log",
]
[[package]]
name = "futf"
version = "0.1.5"
+5
View File
@@ -35,6 +35,11 @@ sha2 = "0.10"
fs2 = "0.4"
md-5 = "0.10"
libc = "0.2"
# Vendored with a relaxed FAT type check: the ArchR boot partition is a
# below-minimum-cluster-count FAT32 the vendor U-Boots require, which
# stock fatfs (and the macOS msdos driver) refuse. See vendor/fatfs/VENDORED.md.
fatfs = { path = "vendor/fatfs" }
fscommon = "0.1"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
+30 -62
View File
@@ -20,7 +20,7 @@ use std::time::Duration;
use tauri::{AppHandle, Emitter};
use serde::Serialize;
use crate::flash::{emit_progress, validate_device, check_temp_space, poll_flash_progress, decompress_xz, decompress_gz};
use crate::rawwrite_unix::{receive_fd_from_helper, write_image_to_raw_fd, configure_boot_volume, FdHelperError};
use crate::rawwrite_unix::{receive_fd_from_helper, write_image_to_raw_fd, patch_boot_partition, FdHelperError};
use crate::diaglog::DiagLog;
#[cfg(target_os = "macos")]
@@ -64,19 +64,39 @@ fn flash_core(
check_temp_space(image_path)?;
}
// Step 1: decompress in user space (with progress)
let img_to_flash = if needs_decompress {
// Step 1: materialize a private working copy of the image. macOS
// cannot mount the ArchR boot FAT (below-spec FAT32 the vendor
// U-Boots require), so the panel overlay and variant marker are
// patched into the image itself before writing; the user's original
// file is never modified.
let temp_img = std::env::temp_dir().join("archr-flash-temp.img");
if needs_decompress {
emit_progress(app, 0.0, "decompressing");
let temp_img = std::env::temp_dir().join("archr-flash-temp.img");
if is_xz {
decompress_xz(app, image_path, &temp_img)?;
} else {
decompress_gz(app, image_path, &temp_img)?;
}
temp_img
} else {
PathBuf::from(image_path)
};
diag.push_str("raw image given, copying to temp for the boot patch\n");
check_temp_space(image_path)?;
fs::copy(image_path, &temp_img)
.map_err(|e| format!("err:image_patch copying image to temp: {}", e))?;
}
let img_to_flash = temp_img;
// Step 1b: install the overlay, variant and extlinux switch inside
// the image. The later verify pass then covers these bytes too.
diag.push_str(&format!(
"patching boot partition (dtbo: {}, variant: {})\n",
custom_dtbo_path, variant
));
patch_boot_partition(&img_to_flash, Path::new(custom_dtbo_path), variant)
.map_err(|e| {
diag.push_str(&format!("boot patch failed: {}\n", e));
e
})?;
diag.push_str("boot partition patched\n");
let image_size = fs::metadata(&img_to_flash).map(|m| m.len()).unwrap_or(0);
@@ -167,34 +187,13 @@ fn flash_core(
stop.store(true, Ordering::Relaxed);
let _ = poll_handle.join();
let _ = fs::remove_file(&progress_file);
if needs_decompress {
let _ = fs::remove_file(&img_to_flash);
}
let _ = fs::remove_file(&img_to_flash);
write_result?;
verify_result?;
// Step 6: remount and configure the boot partition as the plain user.
emit_progress(app, 92.0, "configuring");
let boot_vol = match mount_boot_volume(device, diag) {
Some(v) => v,
None => {
let _ = Command::new("diskutil").args(["eject", device]).status();
return Err(format!(
"err:mount_boot could not mount {}s1 after flashing (image was written)",
device
));
}
};
diag.push_str(&format!("boot volume: {}\n", boot_vol.display()));
let cfg = configure_boot_volume(&boot_vol, Path::new(custom_dtbo_path), variant);
if let Err(e) = &cfg {
diag.push_str(&format!("configure failed: {}\n", e));
let _ = Command::new("diskutil").args(["eject", device]).status();
}
cfg?;
// The boot partition was configured inside the image before the
// write, so nothing needs the quirky FAT mounted here. Just eject.
emit_progress(app, 95.0, "syncing");
let _ = Command::new("sync").status();
let _ = Command::new("diskutil").args(["eject", device]).status();
@@ -222,34 +221,3 @@ fn diskutil_device_bytes(device: &str) -> Option<u64> {
None
}
/// Remount the freshly written disk and resolve the boot (s1) mount point.
/// mountDisk mounts every volume; the explicit s1 mount covers the case
/// where diskutil skips the boot FAT on the first pass.
#[cfg(target_os = "macos")]
fn mount_boot_volume(device: &str, diag: &mut DiagLog) -> Option<PathBuf> {
use std::process::Command;
let part = format!("{}s1", device);
for attempt in 1..=5 {
thread::sleep(Duration::from_secs(2));
let _ = Command::new("diskutil").args(["mountDisk", device]).output();
let _ = Command::new("diskutil").args(["mount", &part]).output();
thread::sleep(Duration::from_secs(1));
if let Ok(out) = Command::new("diskutil").args(["info", &part]).output() {
let text = String::from_utf8_lossy(&out.stdout).to_string();
for line in text.lines() {
if line.contains("Mount Point:") {
if let Some(mp) = line.splitn(2, ':').nth(1) {
let mp = mp.trim();
if !mp.is_empty() && Path::new(mp).is_dir() {
return Some(PathBuf::from(mp));
}
}
}
}
if attempt == 5 {
diag.push_str(&format!("diskutil info {}:\n{}\n", part, text));
}
}
}
None
}
+115 -21
View File
@@ -16,7 +16,7 @@
// this file verbatim inside a mod block, where they are not allowed;
// the allow(dead_code) lives on the mod declaration in main.rs.
use std::fs::{self, File};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
use std::path::Path;
@@ -259,29 +259,123 @@ fn full_sync(f: &File) -> std::io::Result<()> {
f.sync_all()
}
/// Post-flash boot partition setup, done as the regular user on the
/// mounted FAT volume: install the panel overlay, record the variant and
/// switch the soysauce extlinux config, mirroring the Linux flash path.
pub fn configure_boot_volume(boot: &Path, dtbo: &Path, variant: &str) -> Result<(), String> {
/// Install the panel overlay, variant marker and (for soysauce) the
/// extlinux switch INSIDE the image file, before anything touches the
/// card. This is how the macOS path configures the boot partition: the
/// ArchR boot FAT is a below-spec-cluster-count FAT32 that the macOS
/// msdos driver refuses to mount, so post-flash configuration through
/// the OS is impossible there. Patching the image up front also means a
/// subsequent verify pass covers the configuration bytes too.
pub fn patch_boot_partition(image: &Path, dtbo: &Path, variant: &str) -> Result<(), String> {
use std::io::Read;
if !dtbo.is_file() {
return Err(format!("Custom DTBO not found at {}", dtbo.display()));
return Err(format!("err:image_patch custom DTBO not found at {}", dtbo.display()));
}
let overlays = boot.join("overlays");
fs::create_dir_all(&overlays)
.map_err(|e| format!("Cannot create overlays dir: {}", e))?;
fs::copy(dtbo, overlays.join("mipi-panel.dtbo"))
.map_err(|e| format!("Cannot copy overlay: {}", e))?;
fs::write(boot.join("variant"), variant)
.map_err(|e| format!("Cannot write variant: {}", e))?;
if variant == "soysauce" {
let conf = boot.join("extlinux/extlinux.conf");
let soy = boot.join("extlinux/extlinux.conf.soysauce");
if soy.is_file() {
fs::copy(&conf, boot.join("extlinux/extlinux.conf.bak"))
.map_err(|e| format!("Cannot back up extlinux.conf: {}", e))?;
fs::copy(&soy, &conf)
.map_err(|e| format!("Cannot switch extlinux.conf: {}", e))?;
let mut dtbo_bytes = Vec::new();
File::open(dtbo)
.and_then(|mut f| f.read_to_end(&mut dtbo_bytes))
.map_err(|e| format!("err:image_patch reading dtbo: {}", e))?;
let mut img = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(image)
.map_err(|e| format!("err:image_patch opening image: {}", e))?;
// First MBR partition entry: status byte at 446, type at 446+4,
// start LBA (little endian u32) at 446+8, sector count at 446+12.
let mut mbr = [0u8; 512];
img.read_exact(&mut mbr)
.map_err(|e| format!("err:image_patch reading MBR: {}", e))?;
if mbr[510] != 0x55 || mbr[511] != 0xAA {
return Err("err:image_patch image has no MBR signature".into());
}
let entry = &mbr[446..462];
let ptype = entry[4];
// FAT32 LBA (0x0c) or FAT32 CHS (0x0b); everything ArchR ships uses 0x0c.
if ptype != 0x0c && ptype != 0x0b {
return Err(format!(
"err:image_patch first partition is not FAT32 (type 0x{:02x})",
ptype
));
}
let start_lba = u32::from_le_bytes(entry[8..12].try_into().unwrap()) as u64;
let num_sectors = u32::from_le_bytes(entry[12..16].try_into().unwrap()) as u64;
if start_lba == 0 || num_sectors == 0 {
return Err("err:image_patch empty boot partition entry".into());
}
let part_start = start_lba * 512;
let part_end = part_start + num_sectors * 512;
let slice = fscommon::StreamSlice::new(img, part_start, part_end)
.map_err(|e| format!("err:image_patch slicing partition: {}", e))?;
let fs = fatfs::FileSystem::new(slice, fatfs::FsOptions::new())
.map_err(|e| format!("err:image_patch opening FAT: {}", e))?;
{
let root = fs.root_dir();
let overlays = root
.create_dir("overlays")
.map_err(|e| format!("err:image_patch overlays dir: {}", e))?;
let mut f = overlays
.create_file("mipi-panel.dtbo")
.map_err(|e| format!("err:image_patch creating overlay: {}", e))?;
f.truncate()
.map_err(|e| format!("err:image_patch truncating overlay: {}", e))?;
f.write_all(&dtbo_bytes)
.map_err(|e| format!("err:image_patch writing overlay: {}", e))?;
drop(f);
let mut v = root
.create_file("variant")
.map_err(|e| format!("err:image_patch creating variant: {}", e))?;
v.truncate()
.map_err(|e| format!("err:image_patch truncating variant: {}", e))?;
v.write_all(variant.as_bytes())
.map_err(|e| format!("err:image_patch writing variant: {}", e))?;
drop(v);
if variant == "soysauce" {
let extlinux = root
.open_dir("extlinux")
.map_err(|e| format!("err:image_patch extlinux dir: {}", e))?;
let mut soys = Vec::new();
match extlinux.open_file("extlinux.conf.soysauce") {
Ok(mut f) => {
f.read_to_end(&mut soys)
.map_err(|e| format!("err:image_patch reading soysauce conf: {}", e))?;
}
// Older images without the alternate config keep the default.
Err(_) => {}
}
if !soys.is_empty() {
let mut cur = Vec::new();
extlinux
.open_file("extlinux.conf")
.and_then(|mut f| f.read_to_end(&mut cur).map(|_| ()))
.map_err(|e| format!("err:image_patch reading extlinux.conf: {}", e))?;
let mut bak = extlinux
.create_file("extlinux.conf.bak")
.map_err(|e| format!("err:image_patch creating conf backup: {}", e))?;
bak.truncate()
.map_err(|e| format!("err:image_patch truncating conf backup: {}", e))?;
bak.write_all(&cur)
.map_err(|e| format!("err:image_patch writing conf backup: {}", e))?;
drop(bak);
let mut conf = extlinux
.open_file("extlinux.conf")
.map_err(|e| format!("err:image_patch opening extlinux.conf: {}", e))?;
conf.truncate()
.map_err(|e| format!("err:image_patch truncating extlinux.conf: {}", e))?;
conf.write_all(&soys)
.map_err(|e| format!("err:image_patch writing extlinux.conf: {}", e))?;
drop(conf);
}
}
}
fs.unmount()
.map_err(|e| format!("err:image_patch unmounting FAT: {}", e))?;
Ok(())
}
+3 -3
View File
@@ -14,9 +14,9 @@
{
"title": "Arch R Flasher",
"width": 680,
"height": 480,
"minWidth": 600,
"minHeight": 400,
"height": 500,
"minWidth": 680,
"minHeight": 490,
"resizable": true,
"center": true
}
+14
View File
@@ -0,0 +1,14 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# http://editorconfig.org
root = true
[*.rs]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
max_line_length = 120
+3
View File
@@ -0,0 +1,3 @@
target
tmp
Cargo.lock
+67
View File
@@ -0,0 +1,67 @@
Changelog
=========
0.3.6 (2023-01-17)
------------------------
Bug fixes:
* Create directory entry with `VOLUME_ID` attribute when formatting if volume label was set in `FormatVolumeOptions`
* Fix `format_volume` function panicking in debug build for FAT12 volumes with size below 1 MB
0.3.5 (2021-01-23)
------------------------
Bug fixes:
* Fix file-system corruption that occurs when creating multiple directory entries in a non-root directory (directory
size must be greater or equal to the cluster size for the corruption to happen)
0.3.4 (2020-07-20)
------------------
Bug fixes:
* Fix time encoding and decoding in directory entries
0.3.3 (2019-11-10)
------------------
Bug fixes:
* Add missing characters to the whitelist for long file name (`^`, `#`, `&`)
* Fix invalid short file names for `.` and `..` entries when creating a new directory
* Fix `no_std` build
Misc changes:
* Fix compiler warnings
* Improve documentation
0.3.2 (2018-12-29)
------------------
New features:
* Add `format_volume` function for initializing a FAT filesystem on a partition
* Add more checks of filesystem correctness when mounting
Bug fixes:
* Clear directory returned from `create_dir` method - upgrade ASAP if this method is used
* Fix handling of FSInfo sector on FAT32 volumes with sector size different than 512 - upgrade ASAP if such sector size is used
* Use `write_all` in `serialize` method for FSInfo sector - previously it could have been improperly updated
0.3.1 (2018-10-20)
------------------
New features:
* Increased file creation time resolution from 2s to 1/100s
* Added oem_cp_converter filesystem option allowing to provide custom short name decoder
* Added time_provider filesystem option allowing to provide time used when modifying directory entries
* Added marking volume as dirty on first write and not-dirty on unmount
* Added support for reading volume label from root directory
Bug fixes:
* Fixed handling of short names with spaces in the middle - all characters after first space in 8.3 components were
stripped before
* Fixed decoding 0xE5 character in first byte of short name - if first character of short name is equal to 0xE5,
it was read as 0x05
* Preserve 4 most significant bits in FAT32 entries - it is required by FAT specification, but previous behavior
should not cause any compatibility problem because all known implementations ignore those bits
* Fixed warnings for file entries without LFN entries - they were handled properly, but caused warnings in run-time
Misc changes:
* Deprecated set_created. set_accessed, set_modified methods on File - those fields are updated automatically using
information provided by TimeProvider
* Fixed size formatting in ls.rs example
* Added more filesystem checks causing errors or warnings when incompatibility is detected
* Removed unnecessary clone() calls
* Code formatting and docs fixes
+35
View File
@@ -0,0 +1,35 @@
[package]
name = "fatfs"
version = "0.3.6"
authors = ["Rafał Harabień <rafalh92@outlook.com>"]
repository = "https://github.com/rafalh/rust-fatfs"
readme = "README.md"
keywords = ["fat", "filesystem", "no_std"]
categories = ["filesystem"]
license = "MIT"
description = """
FAT filesystem library.
"""
exclude = [
"resources/*",
]
[badges]
travis-ci = { repository = "rafalh/rust-fatfs" }
[features]
# Use Rust std library
std = ["byteorder/std"]
# Use dynamic allocation - required for LFN support. When used without std please enable core_io/collections
alloc = []
# Default features
default = ["chrono", "std", "alloc"]
[dependencies]
byteorder = { version = "1", default-features = false }
bitflags = "1.0"
log = "0.4"
chrono = { version = "0.4", optional = true }
core_io = { version = "0.1", optional = true }
# dev-dependencies removed: tests and examples are not vendored
+19
View File
@@ -0,0 +1,19 @@
Copyright 2017 Rafał Harabień
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+73
View File
@@ -0,0 +1,73 @@
Rust FAT FS
===========
[![CI Status](https://github.com/rafalh/rust-fatfs/actions/workflows/ci.yml/badge.svg)](https://github.com/rafalh/rust-fatfs/actions/workflows/ci.yml)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.txt)
[![crates.io](https://img.shields.io/crates/v/fatfs)](https://crates.io/crates/fatfs)
[![Documentation](https://docs.rs/fatfs/badge.svg)](https://docs.rs/fatfs)
[![Minimum rustc version](https://img.shields.io/badge/rustc-1.24+-yellow.svg)](https://blog.rust-lang.org/2018/02/15/Rust-1.24.html)
A FAT filesystem library implemented in Rust.
Features:
* read/write file using standard Read/Write traits
* read directory contents
* create/remove file or directory
* rename/move file or directory
* read/write file timestamps (updated automatically if `chrono` feature is enabled)
* format volume
* FAT12, FAT16, FAT32 compatibility
* LFN (Long File Names) extension is supported
* Basic no_std environment support
Usage
-----
Add this to your `Cargo.toml`:
[dependencies]
fatfs = "0.3"
and this to your crate root:
extern crate fatfs;
You can start using the `fatfs` library now:
let img_file = File::open("fat.img")?;
let fs = fatfs::FileSystem::new(img_file, fatfs::FsOptions::new())?;
let root_dir = fs.root_dir();
let mut file = root_dir.create_file("hello.txt")?;
file.write_all(b"Hello World!")?;
Note: it is recommended to wrap the underlying file struct in a buffering/caching object like `BufStream` from `fscommon` crate. For example:
extern crate fscommon;
let buf_stream = BufStream::new(img_file);
let fs = fatfs::FileSystem::new(buf_stream, fatfs::FsOptions::new())?;
See more examples in the `examples` subdirectory.
no_std usage
------------
Add this to your `Cargo.toml`:
[dependencies]
fatfs = { version = "0.3", features = ["core_io"], default-features = false }
For building in `no_std` mode a Rust compiler version compatible with `core_io` crate is required.
For now `core_io` supports only nightly Rust channel. See a date suffix in latest `core_io` crate version for exact
compiler version.
Additional features:
* `alloc` - use `alloc` crate for dynamic allocation. Required for LFN (long file name) support and API which uses
`String` type. You may have to provide a memory allocator implementation.
Note: above feature is enabled by default.
License
-------
The MIT license. See `LICENSE.txt`.
+18
View File
@@ -0,0 +1,18 @@
# Vendored fatfs 0.3.6
Vendored copy of https://github.com/rafalh/rust-fatfs (MIT, see
LICENSE.txt) with one behavioral change, marked with `ArchR:` comments
in `src/boot_sector.rs` and `src/fs.rs`:
The ArchR boot partition is FAT32 formatted with a cluster count below
the spec minimum of 65525. The vendor U-Boots shipped on R36S boards
only boot from exactly this geometry, so the image cannot be
"corrected". Upstream fatfs (like the macOS msdos driver) refuses to
touch such a volume because the FAT type inferred from the cluster
count disagrees with the BPB layout. The patch makes the BPB layout
signal (sectors_per_fat_16 == 0 means FAT32) authoritative, which is
what the Linux kernel driver and mtools do in practice.
Used by the flasher to install the panel overlay and variant marker
inside the image before writing, so no OS-level mount of the quirky
FAT is ever needed.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1076
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+370
View File
@@ -0,0 +1,370 @@
use core;
use core::cmp;
use io;
use io::prelude::*;
use io::{ErrorKind, SeekFrom};
use dir_entry::DirEntryEditor;
use fs::{FileSystem, ReadWriteSeek};
use time::{Date, DateTime};
const MAX_FILE_SIZE: u32 = core::u32::MAX;
/// A FAT filesystem file object used for reading and writing data.
///
/// This struct is created by the `open_file` or `create_file` methods on `Dir`.
pub struct File<'a, T: ReadWriteSeek + 'a> {
// Note first_cluster is None if file is empty
first_cluster: Option<u32>,
// Note: if offset points between clusters current_cluster is the previous cluster
current_cluster: Option<u32>,
// current position in this file
offset: u32,
// file dir entry editor - None for root dir
entry: Option<DirEntryEditor>,
// file-system reference
fs: &'a FileSystem<T>,
}
impl<'a, T: ReadWriteSeek> File<'a, T> {
pub(crate) fn new(first_cluster: Option<u32>, entry: Option<DirEntryEditor>, fs: &'a FileSystem<T>) -> Self {
File {
first_cluster,
entry,
fs,
current_cluster: None, // cluster before first one
offset: 0,
}
}
fn update_dir_entry_after_write(&mut self) {
let offset = self.offset;
if let Some(ref mut e) = self.entry {
let now = self.fs.options.time_provider.get_current_date_time();
e.set_modified(now);
if e.inner().size().map_or(false, |s| offset > s) {
e.set_size(offset);
}
}
}
/// Truncate file in current position.
pub fn truncate(&mut self) -> io::Result<()> {
if let Some(ref mut e) = self.entry {
e.set_size(self.offset);
if self.offset == 0 {
e.set_first_cluster(None, self.fs.fat_type());
}
}
if self.offset > 0 {
debug_assert!(self.current_cluster.is_some());
// if offset is not 0 current cluster cannot be empty
self.fs.truncate_cluster_chain(self.current_cluster.unwrap()) // SAFE
} else {
debug_assert!(self.current_cluster.is_none());
if let Some(n) = self.first_cluster {
self.fs.free_cluster_chain(n)?;
self.first_cluster = None;
}
Ok(())
}
}
pub(crate) fn abs_pos(&self) -> Option<u64> {
// Returns current position relative to filesystem start
// Note: when between clusters it returns position after previous cluster
match self.current_cluster {
Some(n) => {
let cluster_size = self.fs.cluster_size();
let offset_mod_cluster_size = self.offset % cluster_size;
let offset_in_cluster = if offset_mod_cluster_size == 0 {
// position points between clusters - we are returning previous cluster so
// offset must be set to the cluster size
cluster_size
} else {
offset_mod_cluster_size
};
let offset_in_fs = self.fs.offset_from_cluster(n) + u64::from(offset_in_cluster);
Some(offset_in_fs)
},
None => None,
}
}
fn flush_dir_entry(&mut self) -> io::Result<()> {
if let Some(ref mut e) = self.entry {
e.flush(self.fs)?;
}
Ok(())
}
/// Sets date and time of creation for this file.
///
/// Note: it is set to a value from the `TimeProvider` when creating a file.
/// Deprecated: if needed implement a custom `TimeProvider`.
#[deprecated]
pub fn set_created(&mut self, date_time: DateTime) {
if let Some(ref mut e) = self.entry {
e.set_created(date_time);
}
}
/// Sets date of last access for this file.
///
/// Note: it is overwritten by a value from the `TimeProvider` on every file read operation.
/// Deprecated: if needed implement a custom `TimeProvider`.
#[deprecated]
pub fn set_accessed(&mut self, date: Date) {
if let Some(ref mut e) = self.entry {
e.set_accessed(date);
}
}
/// Sets date and time of last modification for this file.
///
/// Note: it is overwritten by a value from the `TimeProvider` on every file write operation.
/// Deprecated: if needed implement a custom `TimeProvider`.
#[deprecated]
pub fn set_modified(&mut self, date_time: DateTime) {
if let Some(ref mut e) = self.entry {
e.set_modified(date_time);
}
}
fn size(&self) -> Option<u32> {
match self.entry {
Some(ref e) => e.inner().size(),
None => None,
}
}
fn is_dir(&self) -> bool {
match self.entry {
Some(ref e) => e.inner().is_dir(),
None => false,
}
}
fn bytes_left_in_file(&self) -> Option<usize> {
// Note: seeking beyond end of file is not allowed so overflow is impossible
self.size().map(|s| (s - self.offset) as usize)
}
fn set_first_cluster(&mut self, cluster: u32) {
self.first_cluster = Some(cluster);
if let Some(ref mut e) = self.entry {
e.set_first_cluster(self.first_cluster, self.fs.fat_type());
}
}
pub(crate) fn first_cluster(&self) -> Option<u32> {
self.first_cluster
}
}
impl<'a, T: ReadWriteSeek> Drop for File<'a, T> {
fn drop(&mut self) {
if let Err(err) = self.flush() {
error!("flush failed {}", err);
}
}
}
// Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925
impl<'a, T: ReadWriteSeek> Clone for File<'a, T> {
fn clone(&self) -> Self {
File {
first_cluster: self.first_cluster,
current_cluster: self.current_cluster,
offset: self.offset,
entry: self.entry.clone(),
fs: self.fs,
}
}
}
impl<'a, T: ReadWriteSeek> Read for File<'a, T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let cluster_size = self.fs.cluster_size();
let current_cluster_opt = if self.offset % cluster_size == 0 {
// next cluster
match self.current_cluster {
None => self.first_cluster,
Some(n) => {
let r = self.fs.cluster_iter(n).next();
match r {
Some(Err(err)) => return Err(err),
Some(Ok(n)) => Some(n),
None => None,
}
},
}
} else {
self.current_cluster
};
let current_cluster = match current_cluster_opt {
Some(n) => n,
None => return Ok(0),
};
let offset_in_cluster = self.offset % cluster_size;
let bytes_left_in_cluster = (cluster_size - offset_in_cluster) as usize;
let bytes_left_in_file = self.bytes_left_in_file().unwrap_or(bytes_left_in_cluster);
let read_size = cmp::min(cmp::min(buf.len(), bytes_left_in_cluster), bytes_left_in_file);
if read_size == 0 {
return Ok(0);
}
trace!("read {} bytes in cluster {}", read_size, current_cluster);
let offset_in_fs = self.fs.offset_from_cluster(current_cluster) + (offset_in_cluster as u64);
let read_bytes = {
let mut disk = self.fs.disk.borrow_mut();
disk.seek(SeekFrom::Start(offset_in_fs))?;
disk.read(&mut buf[..read_size])?
};
if read_bytes == 0 {
return Ok(0);
}
self.offset += read_bytes as u32;
self.current_cluster = Some(current_cluster);
if let Some(ref mut e) = self.entry {
if self.fs.options.update_accessed_date {
let now = self.fs.options.time_provider.get_current_date();
e.set_accessed(now);
}
}
Ok(read_bytes)
}
}
impl<'a, T: ReadWriteSeek> Write for File<'a, T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let cluster_size = self.fs.cluster_size();
let offset_in_cluster = self.offset % cluster_size;
let bytes_left_in_cluster = (cluster_size - offset_in_cluster) as usize;
let bytes_left_until_max_file_size = (MAX_FILE_SIZE - self.offset) as usize;
let write_size = cmp::min(buf.len(), bytes_left_in_cluster);
let write_size = cmp::min(write_size, bytes_left_until_max_file_size);
// Exit early if we are going to write no data
if write_size == 0 {
return Ok(0);
}
// Mark the volume 'dirty'
self.fs.set_dirty_flag(true)?;
// Get cluster for write possibly allocating new one
let current_cluster = if self.offset % cluster_size == 0 {
// next cluster
let next_cluster = match self.current_cluster {
None => self.first_cluster,
Some(n) => {
let r = self.fs.cluster_iter(n).next();
match r {
Some(Err(err)) => return Err(err),
Some(Ok(n)) => Some(n),
None => None,
}
},
};
match next_cluster {
Some(n) => n,
None => {
// end of chain reached - allocate new cluster
let new_cluster = self.fs.alloc_cluster(self.current_cluster, self.is_dir())?;
trace!("allocated cluser {}", new_cluster);
if self.first_cluster.is_none() {
self.set_first_cluster(new_cluster);
}
new_cluster
},
}
} else {
// self.current_cluster should be a valid cluster
match self.current_cluster {
Some(n) => n,
None => panic!("Offset inside cluster but no cluster allocated"),
}
};
trace!("write {} bytes in cluster {}", write_size, current_cluster);
let offset_in_fs = self.fs.offset_from_cluster(current_cluster) + (offset_in_cluster as u64);
let written_bytes = {
let mut disk = self.fs.disk.borrow_mut();
disk.seek(SeekFrom::Start(offset_in_fs))?;
disk.write(&buf[..write_size])?
};
if written_bytes == 0 {
return Ok(0);
}
// some bytes were writter - update position and optionally size
self.offset += written_bytes as u32;
self.current_cluster = Some(current_cluster);
self.update_dir_entry_after_write();
Ok(written_bytes)
}
fn flush(&mut self) -> io::Result<()> {
self.flush_dir_entry()?;
let mut disk = self.fs.disk.borrow_mut();
disk.flush()
}
}
impl<'a, T: ReadWriteSeek> Seek for File<'a, T> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let mut new_pos = match pos {
SeekFrom::Current(x) => self.offset as i64 + x,
SeekFrom::Start(x) => x as i64,
SeekFrom::End(x) => {
let size = self.size().expect("cannot seek from end if size is unknown") as i64;
size + x
},
};
if new_pos < 0 {
return Err(io::Error::new(ErrorKind::InvalidInput, "Seek to a negative offset"));
}
if let Some(s) = self.size() {
if new_pos > s as i64 {
info!("seek beyond end of file");
new_pos = s as i64;
}
}
let mut new_pos = new_pos as u32;
trace!("file seek {} -> {} - entry {:?}", self.offset, new_pos, self.entry);
if new_pos == self.offset {
// position is the same - nothing to do
return Ok(self.offset as u64);
}
// get number of clusters to seek (favoring previous cluster in corner case)
let cluster_count = (self.fs.clusters_from_bytes(new_pos as u64) as i32 - 1) as isize;
let old_cluster_count = (self.fs.clusters_from_bytes(self.offset as u64) as i32 - 1) as isize;
let new_cluster = if new_pos == 0 {
None
} else if cluster_count == old_cluster_count {
self.current_cluster
} else {
match self.first_cluster {
Some(n) => {
let mut cluster = n;
let mut iter = self.fs.cluster_iter(n);
for i in 0..cluster_count {
cluster = match iter.next() {
Some(r) => r?,
None => {
// chain ends before new position - seek to end of last cluster
new_pos = self.fs.bytes_from_clusters((i + 1) as u32) as u32;
break;
},
};
}
Some(cluster)
},
None => {
// empty file - always seek to 0
new_pos = 0;
None
},
}
};
self.offset = new_pos as u32;
self.current_cluster = new_cluster;
Ok(self.offset as u64)
}
}
+1013
View File
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
//! A FAT filesystem library implemented in Rust.
//!
//! # Usage
//!
//! This crate is [on crates.io](https://crates.io/crates/fatfs) and can be
//! used by adding `fatfs` to the dependencies in your project's `Cargo.toml`.
//!
//! ```toml
//! [dependencies]
//! fatfs = "0.3"
//! ```
//!
//! And this in your crate root:
//!
//! ```rust
//! extern crate fatfs;
//! ```
//!
//! # Examples
//!
//! ```rust
//! // Declare external crates
//! // Note: `fscommon` crate is used to speedup IO operations
//! extern crate fatfs;
//! extern crate fscommon;
//!
//! use std::io::prelude::*;
//!
//! fn main() -> std::io::Result<()> {
//! # std::fs::copy("resources/fat16.img", "tmp/fat.img")?;
//! // Initialize a filesystem object
//! let img_file = std::fs::OpenOptions::new().read(true).write(true)
//! .open("tmp/fat.img")?;
//! let buf_stream = fscommon::BufStream::new(img_file);
//! let fs = fatfs::FileSystem::new(buf_stream, fatfs::FsOptions::new())?;
//! let root_dir = fs.root_dir();
//!
//! // Write a file
//! root_dir.create_dir("foo")?;
//! let mut file = root_dir.create_file("foo/hello.txt")?;
//! file.truncate()?;
//! file.write_all(b"Hello World!")?;
//!
//! // Read a directory
//! let dir = root_dir.open_dir("foo")?;
//! for r in dir.iter() {
//! let entry = r?;
//! println!("{}", entry.file_name());
//! }
//! # std::fs::remove_file("tmp/fat.img")?;
//! # Ok(())
//! }
//! ```
#![crate_type = "lib"]
#![crate_name = "fatfs"]
#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(all(not(feature = "std"), feature = "alloc"), feature(alloc))]
// Disable warnings to not clutter code with cfg too much
#![cfg_attr(not(feature = "alloc"), allow(dead_code, unused_imports))]
// Inclusive ranges requires Rust 1.26.0
#![allow(ellipsis_inclusive_range_patterns)]
// `dyn` syntax requires Rust 1.27.0
#![allow(bare_trait_objects)]
// `alloc` compiler feature is needed in Rust before 1.36
#![cfg_attr(all(not(feature = "std"), feature = "alloc"), allow(stable_features))]
extern crate byteorder;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate log;
#[cfg(feature = "chrono")]
extern crate chrono;
#[cfg(not(feature = "std"))]
extern crate core_io;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
extern crate alloc;
mod boot_sector;
mod dir;
mod dir_entry;
mod file;
mod fs;
mod table;
mod time;
#[cfg(not(feature = "std"))]
mod byteorder_core_io;
#[cfg(feature = "std")]
use byteorder as byteorder_ext;
#[cfg(not(feature = "std"))]
use byteorder_core_io as byteorder_ext;
#[cfg(not(feature = "std"))]
use core_io as io;
#[cfg(feature = "std")]
use std as core;
#[cfg(feature = "std")]
use std::io;
pub use dir::*;
pub use dir_entry::*;
pub use file::*;
pub use fs::*;
pub use time::*;
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More