fix: parse exFAT UtcOffset per spec, tolerate bad timestamps

This commit is contained in:
jujuforce
2026-04-20 11:37:53 +02:00
parent b0e97f8b00
commit 558c0ecec0
+44 -25
View File
@@ -30,22 +30,36 @@ fn exfat_timestamp_to_system_time(
) -> Result<SystemTime> {
let exfat_date = timestamp.date();
let exfat_time = timestamp.time();
// exFAT UTC offset is in 15-minute intervals, so 1 = UTC+00:15, 2 = UTC+00:30, etc.
let exfat_utc_offset = timestamp.utc_offset() as i32 * 15 * 60;
let chrono_date_time = FixedOffset::east_opt(exfat_utc_offset)
.ok_or_else(|| anyhow!("invaid utc offset: {}", timestamp.utc_offset()))?
.with_ymd_and_hms(
exfat_date.year as i32,
exfat_date.month as u32,
exfat_date.day as u32,
exfat_time.hour as u32,
exfat_time.minute as u32,
exfat_time.second as u32,
)
.unwrap();
return Ok(SystemTime::UNIX_EPOCH
+ Duration::from_micros(chrono_date_time.timestamp_micros().try_into()?));
// The exFAT UtcOffset byte packs an OffsetValid flag (bit 7) with a 7-bit
// two's-complement OffsetFromUtc in 15-minute units. When OffsetValid is 0
// the timestamp has no timezone info and the offset bits must be ignored.
let raw = timestamp.utc_offset() as u8;
let offset_seconds = if raw & 0x80 == 0 {
0
} else {
let offset_quarters = (((raw & 0x7F) << 1) as i8) >> 1;
offset_quarters as i32 * 15 * 60
};
let fixed_offset = FixedOffset::east_opt(offset_seconds).unwrap_or_else(|| FixedOffset::east_opt(0).unwrap());
let chrono_date_time = match fixed_offset.with_ymd_and_hms(
exfat_date.year as i32,
exfat_date.month as u32,
exfat_date.day as u32,
exfat_time.hour as u32,
exfat_time.minute as u32,
exfat_time.second as u32,
) {
chrono::LocalResult::Single(dt) => dt,
_ => return Ok(SystemTime::UNIX_EPOCH),
};
let micros: u64 = chrono_date_time
.timestamp_micros()
.try_into()
.unwrap_or(0);
Ok(SystemTime::UNIX_EPOCH + Duration::from_micros(micros))
}
fn extract_exfat_contents(exfat_path: &Path) -> Result<()> {
@@ -96,17 +110,22 @@ fn extract_exfat_elements(
match element {
FsElement::F(ref mut file) => {
let dest_path = output_dir.join(file.name());
let mut dest = File::create(dest_path)?;
let mut dest = File::create(&dest_path)?;
dest.set_times(
FileTimes::new()
.set_accessed(exfat_timestamp_to_system_time(
file.timestamps().accessed(),
)?)
.set_modified(exfat_timestamp_to_system_time(
file.timestamps().modified(),
)?),
)?;
let accessed = exfat_timestamp_to_system_time(file.timestamps().accessed());
let modified = exfat_timestamp_to_system_time(file.timestamps().modified());
if let (Ok(accessed), Ok(modified)) = (accessed, modified) {
if let Err(e) = dest.set_times(
FileTimes::new()
.set_accessed(accessed)
.set_modified(modified),
) {
println!(
"WARNING: Failed to set times on {}: {e}",
dest_path.display()
);
}
}
let mut writer = BufWriter::with_capacity(256 * 1024, &mut dest);