diff --git a/src/main.rs b/src/main.rs index ccc7513..6696dd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,22 +30,36 @@ fn exfat_timestamp_to_system_time( ) -> Result { 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);