Skip to content

Commit

Permalink
FAT refinements and repairs
Browse files Browse the repository at this point in the history
  • Loading branch information
dfgordon committed Oct 21, 2023
1 parent b952e6a commit d340009
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "a2kit"
version = "2.4.0"
version = "2.4.1"
edition = "2021"
readme = "README.md"
license = "MIT"
Expand Down
36 changes: 30 additions & 6 deletions src/bios/bpb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crate::{STDRESULT,DYNERR};
use a2kit_macro::DiskStruct;
use a2kit_macro_derive::DiskStruct;

use super::fat::FIRST_DATA_CLUSTER;

const JMP_BOOT: [u8;3] = [0xeb,0x58,0x90];
const OEM_NAME: [u8;8] = *b"A2KITX.X";
const BOOT_SIGNATURE: [u8;2] = [0x55,0xaa]; // goes in boot[510..512]
Expand Down Expand Up @@ -342,7 +344,7 @@ impl BootSector {
ans = false;
}
if ans {
debug!("BPB counts: {}({}) FAT, {} tot, {} res, {} root",fat_secs,bpb.num_fats,bpb.tot_sec(),bpb.res_secs(),bpb.root_dir_secs());
debug!("BPB counts: {} tot, {} res, {}x{} FAT, {} root",bpb.tot_sec(),fat_secs,bpb.num_fats,bpb.res_secs(),bpb.root_dir_secs());
}
ans
}
Expand Down Expand Up @@ -408,14 +410,23 @@ impl BootSector {
pub fn data_rgn_secs(&self) -> u64 {
self.tot_sec() - (self.res_secs() as u64 + (self.foundation.num_fats as u64 * self.fat_secs()) + self.root_dir_secs())
}
/// total clusters used, rounding down (remainder partial-cluster is not used)
pub fn cluster_count(&self) -> u64 {
/// number of clusters possible in the data region rounding down,
/// this is used to determine the FAT type.
pub fn cluster_count_abstract(&self) -> u64 {
self.data_rgn_secs()/self.foundation.sec_per_clus() as u64
}
/// smaller of (clusters possible in the data region , clusters possible in the FAT)
pub fn cluster_count_usable(&self) -> u64 {
let typ = self.fat_type() as u64;
u64::min(
self.data_rgn_secs()/self.foundation.sec_per_clus() as u64,
self.fat_secs() * self.sec_size() * 8 / typ - FIRST_DATA_CLUSTER as u64
)
}
/// FAT type determination based on the cluster count.
/// These peculiar cutoffs are correct according to MS.
pub fn fat_type(&self) -> usize {
match self.cluster_count() {
match self.cluster_count_abstract() {
x if x < 4085 => 12,
x if x < 65525 => 16,
_ => 32
Expand Down Expand Up @@ -450,6 +461,19 @@ impl BootSector {
pub fn first_cluster_sec(&self,n: u64) -> u64 {
(n-2)*self.foundation.sec_per_clus() as u64 + self.first_data_sec()
}
pub fn create_tail(&mut self,drv_num: u8,id: [u8;4],label: [u8;11]) {
self.tail.boot_sig = 0x29;
self.tail.drv_num = drv_num;
self.tail.fil_sys_type = match self.fat_type() {
12 => *b"FAT12 ",
16 => *b"FAT16 ",
32 => *b"FAT32 ",
_ => panic!("unexpected FAT type")
};
self.tail.reserved1 = 0x00;
self.tail.vol_id = id;
self.tail.vol_lab = label;
}
}

const SSDD_525_8: BPBFoundation = BPBFoundation {
Expand Down Expand Up @@ -492,7 +516,7 @@ const DSDD_525_8: BPBFoundation = BPBFoundation {
media: 0xff,
fat_size_16: [1,0],
sec_per_trk: [8,0],
num_heads: [1,0],
num_heads: [2,0],
hidd_sec: [0,0,0,0],
tot_sec_32: [0,0,0,0]
};
Expand All @@ -507,7 +531,7 @@ const DSDD_525_9: BPBFoundation = BPBFoundation {
media: 0xfd,
fat_size_16: [2,0],
sec_per_trk: [9,0],
num_heads: [1,0],
num_heads: [2,0],
hidd_sec: [0,0,0,0],
tot_sec_32: [0,0,0,0]
};
Expand Down
36 changes: 25 additions & 11 deletions src/fs/fat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,13 @@ impl Disk {
Block::FAT((self.boot_sector.first_cluster_sec(block as u64),self.boot_sector.secs_per_clus()))
}
fn clus_in_rng(&self,block: usize) -> bool {
block >= fat::FIRST_DATA_CLUSTER as usize && block < fat::FIRST_DATA_CLUSTER as usize + self.boot_sector.cluster_count() as usize
block >= fat::FIRST_DATA_CLUSTER as usize && block < fat::FIRST_DATA_CLUSTER as usize + self.boot_sector.cluster_count_usable() as usize
}
/// Open buffer if not already present. Will usually be called indirectly.
/// The backup FAT will be written when the buffer is written back.
fn open_fat_buffer(&mut self) -> STDRESULT {
if self.maybe_fat==None {
trace!("buffering first FAT");
let fat_secs = self.boot_sector.fat_secs();
let mut ans = Vec::new();
let sec1 = self.boot_sector.res_secs() as u64;
Expand Down Expand Up @@ -176,7 +177,7 @@ impl Disk {
fn num_free_blocks(&mut self) -> Result<usize,DYNERR> {
let mut free: usize = 0;
let beg = fat::FIRST_DATA_CLUSTER as usize;
for i in beg..beg+self.boot_sector.cluster_count() as usize {
for i in beg..beg+self.boot_sector.cluster_count_usable() as usize {
if self.is_block_free(i)? {
free += 1;
}
Expand Down Expand Up @@ -242,14 +243,15 @@ impl Disk {
self.img.write_block(cluster, &data[offset..offset+actual_len].to_vec())
}
fn get_available_block(&mut self) -> Result<Option<usize>,DYNERR> {
for block in fat::FIRST_DATA_CLUSTER as usize..self.boot_sector.cluster_count() as usize {
for block in fat::FIRST_DATA_CLUSTER as usize..self.boot_sector.cluster_count_usable() as usize {
if self.is_block_free(block)? {
return Ok(Some(block));
}
}
return Ok(None);
}
/// Format a disk with the FAT file system, by this point the boot sector is presumed to be buffered.
/// Format a disk with the FAT file system, by this point the boot sector is presumed to be buffered,
/// and must at least contain a valid BPB foundation. If there is a BPB tail it is overwritten.
pub fn format(&mut self, vol_name: &str, time: Option<chrono::NaiveDateTime>) -> STDRESULT {
if !pack::is_label_valid(vol_name) && vol_name.len()>0 {
error!("FAT volume name invalid");
Expand All @@ -272,6 +274,18 @@ impl Disk {
self.img.write_sector(c,h,s,&f6)?;
}
}
// Create a BPB tail
let boot_label: [u8;11] = match vol_name.len()>0 {
true => {
let (nm,x) = pack::string_to_label_name(vol_name);
[nm.to_vec(),x.to_vec()].concat().try_into().expect("label mismatch")
}
false => *b"NO NAME "
};
// Using nanos gives us about 30 bits of resolution.
// This avoids issues with the FAT datestamp after the year 2107.
let id = u32::to_le_bytes(chrono::Local::now().naive_local().timestamp_subsec_nanos());
self.boot_sector.create_tail(0x00, id, boot_label);
// write the boot sector (perhaps rewriting).
// may be written again if FAT32.
trace!("write boot sector");
Expand Down Expand Up @@ -348,7 +362,7 @@ impl Disk {
/// given any cluster, return the last cluster in its chain.
fn last_cluster(&mut self,initial: &Ptr) -> Result<Ptr,DYNERR> {
let mut curr = *initial;
let max_clusters = self.boot_sector.cluster_count() as usize;
let max_clusters = self.boot_sector.cluster_count_usable() as usize;
for _i in 0..max_clusters {
curr = match self.next_cluster(&curr)? {
None => return Ok(curr),
Expand All @@ -370,7 +384,7 @@ impl Disk {
}
}
let mut curr = *initial;
let max_clusters = self.boot_sector.cluster_count() as usize;
let max_clusters = self.boot_sector.cluster_count_usable() as usize;
for _i in 0..max_clusters {
let mut data: Vec<u8> = vec![0;self.boot_sector.block_size() as usize];
self.read_block(&mut data, curr.unwrap(), 0)?;
Expand All @@ -393,7 +407,7 @@ impl Disk {
}
}
let mut curr = *initial;
let max_clusters = self.boot_sector.cluster_count() as usize;
let max_clusters = self.boot_sector.cluster_count_usable() as usize;
for _i in 0..max_clusters {
let maybe_next = self.deallocate_block(curr.unwrap())?;
curr = match maybe_next {
Expand Down Expand Up @@ -631,9 +645,9 @@ impl Disk {
// TODO: eliminate redundancy, by this time the directory as already been read at least once
let dir = self.get_directory(&parent.cluster1)?;
let entry = dir.get_entry(&Ptr::Entry(finfo.idx));
entry.metadata_to_fimg(&mut fimg);
let all_data = self.get_cluster_chain_data(&finfo.cluster1.unwrap())?;
fimg.desequence(&all_data);
entry.metadata_to_fimg(&mut fimg); // must come after desequence or eof is spoiled
Ok(fimg)
}
/// If the new name does not already exist, return an EntryLocation with entry pointer set to existing file.
Expand Down Expand Up @@ -973,7 +987,7 @@ impl super::DiskFS for Disk {
match usize::from_str(num) {
Ok(block) => {
let mut buf: Vec<u8> = vec![0;self.boot_sector.block_size() as usize];
if block >= 2 + self.boot_sector.cluster_count() as usize {
if block >= 2 + self.boot_sector.cluster_count_usable() as usize {
return Err(Box::new(Error::SectorNotFound));
}
self.read_block(&mut buf,block,0)?;
Expand All @@ -985,7 +999,7 @@ impl super::DiskFS for Disk {
fn write_block(&mut self, num: &str, dat: &[u8]) -> Result<usize,DYNERR> {
match usize::from_str(num) {
Ok(block) => {
if dat.len() > self.boot_sector.block_size() as usize || block >= 2 + self.boot_sector.cluster_count() as usize {
if dat.len() > self.boot_sector.block_size() as usize || block >= 2 + self.boot_sector.cluster_count_usable() as usize {
return Err(Box::new(Error::SectorNotFound));
}
self.zap_block(dat,block,0)?;
Expand Down Expand Up @@ -1109,7 +1123,7 @@ impl super::DiskFS for Disk {
assert_eq!(actual,expected," at sector {}",lsec)
}
// compare clusters in the data region
for block in 2..2+self.boot_sector.cluster_count() {
for block in 2..2+self.boot_sector.cluster_count_usable() {
let addr = self.export_clus(block as usize);
let mut actual = self.img.read_block(addr).expect("bad sector access");
let mut expected = emulator_disk.get_img().read_block(addr).expect("bad sector access");
Expand Down
3 changes: 2 additions & 1 deletion src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ impl FileImage {
}
return ans;
}
/// use any byte stream as the file image data; internally this organizes the data into chunks
/// Use any byte stream as the file image data. The eof is set to the length of the data.
/// The last chunk is not padded.
pub fn desequence(&mut self, dat: &[u8]) {
let mut mark = 0;
let mut idx = 0;
Expand Down
12 changes: 6 additions & 6 deletions src/img/dsk_img.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,21 @@ impl img::DiskImage for Img {
}
fn read_sector(&mut self,cyl: usize,head: usize,sec: usize) -> Result<Vec<u8>,DYNERR> {
let track = cyl*self.heads + head;
if track>=self.track_count() || head>=self.heads || sec<1 || sec>self.sectors as usize {
error!("chs range should be 0-{}/0-{}/1-{}",self.track_count()-1,self.heads-1,self.sectors);
trace!("reading {}/{}/{}",cyl,head,sec);
if track>=self.track_count() || sec<1 || sec>self.sectors as usize {
error!("track/sector range should be 0-{}/1-{}",self.track_count()-1,self.sectors);
return Err(Box::new(img::Error::SectorAccess));
}
trace!("reading {}/{}/{}",cyl,head,sec);
let offset = (track*self.sectors as usize + sec - 1)*self.sec_size;
Ok(self.data[offset..offset+self.sec_size].to_vec())
}
fn write_sector(&mut self,cyl: usize,head: usize,sec: usize,dat: &[u8]) -> STDRESULT {
let track = cyl*self.heads + head;
if track>=self.track_count() || head>=self.heads || sec<1 || sec>self.sectors as usize {
error!("chs range should be 0-{}/0-{}/1-{}",self.track_count()-1,self.heads-1,self.sectors);
trace!("writing {}/{}/{}",cyl,head,sec);
if track>=self.track_count() || sec<1 || sec>self.sectors as usize {
error!("track/sector range should be 0-{}/1-{}",self.track_count()-1,self.sectors);
return Err(Box::new(img::Error::SectorAccess));
}
trace!("writing {}/{}/{}",cyl,head,sec);
let offset = (track*self.sectors as usize + sec - 1)*self.sec_size;
let padded = super::quantize_block(dat, self.sec_size);
self.data[offset..offset+self.sec_size].copy_from_slice(&padded);
Expand Down
49 changes: 37 additions & 12 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,27 @@
//! In order to manipulate files, `a2kit` must understand the file system it finds on the disk image.
//! As of this writing `a2kit` supports
//! * CP/M 1,2,3
//! * DOS 3.x
//! * Apple DOS 3.x
//! * FAT (e.g. MS-DOS)
//! * ProDOS
//! * Pascal File System
//!
//! ## Disk Images
//!
//! In order to manipulate tracks and sectors, `a2kit` must understand the way the track data is packed
//! into a disk image. As of this writing `a2kit` supports
//! * 2MG
//! * DSK, D13, DO, PO
//! * IMD
//! * NIB
//! * TD0
//! * WOZ (1 and 2)
//!
//! format | platforms | aliases
//! -------|-----------|--------
//! 2MG | Apple II |
//! D13 | Apple II |
//! DO | Apple II | DSK
//! PO | Apple II | DSK
//! IMD | CP/M, FAT |
//! IMG | FAT | DSK, IMA
//! NIB | Apple II |
//! TD0 | CP/M, FAT |
//! WOZ | Apple II |
//!
//! ## Disk Kinds
//!
Expand All @@ -50,9 +57,11 @@
//! * Logical ProDOS volumes
//! * 3 inch CP/M formats (Amstrad 184K)
//! * 3.5 inch Apple formats (400K/800K)
//! * 3.5 inch IBM formats(720K through 2880K)
//! * 5.25 inch Apple formats (114K/140K)
//! * 8 inch CP/M formats (IBM 250K, Nabu 1M, TRS-80 600K)
//! * 5.25 inch IBM formats (160K through 1200K)
//! * 5.25 inch CP/M formats (Osborne 100K/200K, Kaypro 200K/400K)
//! * 8 inch CP/M formats (IBM 250K, Nabu 1M, TRS-80 600K)

pub mod fs;
pub mod lang;
Expand All @@ -72,6 +81,7 @@ type DYNERR = Box<dyn std::error::Error>;
type STDRESULT = Result<(),Box<dyn std::error::Error>>;

const KNOWN_FILE_EXTENSIONS: &str = "2mg,2img,dsk,d13,do,nib,po,woz,imd,td0,img,ima";
const MAX_FILE_SIZE: u64 = 1 << 26;

/// Save the image file (make changes permanent)
pub fn save_img(disk: &mut Box<dyn DiskFS>,img_path: &str) -> STDRESULT {
Expand Down Expand Up @@ -285,12 +295,27 @@ pub fn create_img_from_bytestream(disk_img_data: &Vec<u8>,maybe_ext: Option<&str
return Err(Box::new(img::Error::ImageTypeMismatch));
}

/// buffer a file if its EOF < `max`, otherwise return an error
fn buffer_file(path: &str,max: u64) -> Result<Vec<u8>,DYNERR> {
match std::fs::OpenOptions::new().read(true).open(path) {
Ok(mut f) => match f.metadata()?.len() <= max {
true => {
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(buf)
},
false => Err(Box::new(img::Error::ImageSizeMismatch))
},
Err(e) => Err(Box::new(e))
}
}

/// Calls `create_img_from_bytestream` getting the bytes from a file.
/// The pathname must already be in the right format for the file system.
/// File extension will be used to restrict image types that are tried,
/// unless the extension is unknown, in which case all will be tried.
pub fn create_img_from_file(img_path: &str) -> Result<Box<dyn DiskImage>,DYNERR> {
match std::fs::read(img_path) {
match buffer_file(img_path,MAX_FILE_SIZE) {
Ok(disk_img_data) => {
let mut maybe_ext = img_path.split('.').last();
if let Some(ext) = maybe_ext {
Expand All @@ -300,7 +325,7 @@ pub fn create_img_from_file(img_path: &str) -> Result<Box<dyn DiskImage>,DYNERR>
}
create_img_from_bytestream(&disk_img_data,maybe_ext)
},
Err(e) => Err(Box::new(e))
Err(e) => Err(e)
}
}

Expand All @@ -319,7 +344,7 @@ pub fn create_fs_from_stdin() -> Result<Box<dyn DiskFS>,DYNERR> {
/// File extension will be used to restrict image types that are tried,
/// unless the extension is unknown, in which case all will be tried.
pub fn create_fs_from_file(img_path: &str) -> Result<Box<dyn DiskFS>,DYNERR> {
match std::fs::read(img_path) {
match buffer_file(img_path,MAX_FILE_SIZE) {
Ok(disk_img_data) => {
let mut maybe_ext = img_path.split('.').last();
if let Some(ext) = maybe_ext {
Expand All @@ -329,7 +354,7 @@ pub fn create_fs_from_file(img_path: &str) -> Result<Box<dyn DiskFS>,DYNERR> {
}
create_fs_from_bytestream(&disk_img_data,maybe_ext)
},
Err(e) => Err(Box::new(e))
Err(e) => Err(e)
}
}

Expand Down

0 comments on commit d340009

Please sign in to comment.