use super::types::*;
use super::pack::*;
use crate::bios::dpb::DiskParameterBlock;
use std::collections::BTreeMap;
use crate::fs::Attributes;
use crate::{STDRESULT,DYNERR};
use a2kit_macro::{DiskStructError,DiskStruct};
use a2kit_macro_derive::DiskStruct;
const RCH: &str = "unreachable was reached";
const LABEL_EXISTS: u8 = 0x01;
const CREATE: u8 = 0x10;
const UPDATE: u8 = 0x20;
const ACCESS: u8 = 0x40;
const PROTECT_READ: u8 = 0x80;
const PROTECT_WRITE: u8 = 0x40;
const PROTECT_DELETE: u8 = 0x20;
const FILES_PROTECTED: u8 = 0x80;
#[derive(Clone)]
pub struct FileInfo {
pub user: u8,
pub name: String,
pub typ: String,
pub read_only: bool,
pub system: bool,
pub archived: bool,
pub f1: bool,
pub f2: bool,
pub f3: bool,
pub f4: bool,
pub encrypted_password: [u8;8],
pub read_pass: bool,
pub write_pass: bool,
pub del_pass: bool,
pub decoder: u8,
pub update_time: Option<[u8;4]>,
pub create_time: Option<[u8;4]>,
pub access_time: Option<[u8;4]>,
pub blocks_allocated: usize,
pub entries: BTreeMap<Ptr,Ptr>
}
pub trait DirectoryEntry {
fn stat_range() -> [u8;2];
}
#[derive(DiskStruct,Copy,Clone,PartialEq)]
pub struct Extent {
pub user: u8,
name: [u8;8],
typ: [u8;3],
idx_low: u8,
last_bytes: u8,
idx_high: u8,
last_records: u8,
pub block_list: [u8;16]
}
#[derive(DiskStruct)]
pub struct Password {
pub user: u8, pub name: [u8;8], pub typ: [u8;3], mode: u8,
decoder: u8,
pad1: [u8;2],
password: [u8;8],
pad2: [u8;8]
}
#[derive(DiskStruct)]
pub struct Label {
status: u8, name: [u8;8],
typ: [u8;3],
mode: u8,
decoder: u8,
pad: [u8;2],
password: [u8;8],
create_time: [u8;4],
update_time: [u8;4]
}
#[derive(DiskStruct)]
pub struct Timestamp {
status: u8, create_access1: [u8;4],
update1: [u8;4],
pass1: u8, pad1: u8,
create_access2: [u8;4],
update2: [u8;4],
pass2: u8,
pad2: u8,
create_access3: [u8;4],
update3: [u8;4],
pass3: u8,
pad3: u8,
pad4: u8
}
pub struct Directory {
entries: Vec<[u8;DIR_ENTRY_SIZE]>
}
impl Label {
pub fn create() -> Self {
let mut ans = Label::new();
ans.status = LABEL;
ans.mode |= LABEL_EXISTS;
ans.name = b"LABEL ".clone();
ans.typ = b" ".clone();
ans
}
pub fn set(&mut self,name: [u8;8],typ: [u8;3]) {
self.name = name;
self.typ = typ;
}
pub fn set_timestamp_for_label(&mut self,create: Option<chrono::NaiveDateTime>,update: Option<chrono::NaiveDateTime>) {
if create.is_some() {
self.create_time = pack_date(create);
}
if update.is_some() {
self.update_time = pack_date(update);
}
}
pub fn get_split_string(&self) -> (String,String) {
file_name_to_split_string(self.name, self.typ)
}
pub fn get_create_time(&self) -> [u8;4] {
self.create_time
}
pub fn get_update_time(&self) -> [u8;4] {
self.update_time
}
pub fn protect(&mut self,yes: bool) {
if yes {
self.mode |= FILES_PROTECTED;
} else {
self.mode &= FILES_PROTECTED ^ u8::MAX;
}
}
pub fn timestamp_update(&mut self,yes: bool) {
if yes {
self.mode |= UPDATE;
} else {
self.mode &= UPDATE ^ u8::MAX;
}
}
pub fn timestamp_creation(&mut self,yes: bool) {
if yes {
self.mode |= CREATE;
self.mode &= ACCESS ^ u8::MAX;
} else {
self.mode &= CREATE ^ u8::MAX;
}
}
pub fn is_protected(&self) -> bool {
self.mode & FILES_PROTECTED > 0
}
pub fn is_timestamped(&self ) -> bool {
self.is_timestamped_access() || self.is_timestamped_creation() || self.is_timestamped_update()
}
pub fn is_timestamped_access(&self) -> bool {
self.mode & ACCESS > 0
}
pub fn is_timestamped_update(&self) -> bool {
self.mode & UPDATE > 0
}
pub fn is_timestamped_creation(&self) -> bool {
self.mode & CREATE > 0
}
}
impl DirectoryEntry for Label {
fn stat_range() -> [u8;2] {
[0x20,0x21]
}
}
impl Extent {
pub fn set_name(&mut self,name: [u8;8],typ: [u8;3]) {
for i in 0..8 {
self.name[i] = (name[i] & 0x7f) + (self.name[i] & 0x80);
}
for i in 0..3 {
self.typ[i] = (typ[i] & 0x7f) + (self.typ[i] & 0x80);
}
}
pub fn set_flags(&mut self,name: [u8;8],typ: [u8;3]) {
for i in 0..8 {
self.name[i] = (name[i] & 0x80) + (self.name[i] & 0x7f);
}
for i in 0..3 {
self.typ[i] = (typ[i] & 0x80) + (self.typ[i] & 0x7f);
}
}
pub fn get_name_and_flags(&self) -> [u8;11] {
let mut ans: [u8;11] = [0;11];
for i in 0..8 {
ans[i] = self.name[i];
}
for i in 0..3 {
ans[i+8] = self.typ[i];
}
return ans;
}
pub fn get_flags(&self) -> [u8;11] {
let mut ans: [u8;11] = [0;11];
for i in 0..8 {
ans[i] = self.name[i] & 0x80;
}
for i in 0..3 {
ans[i+8] = self.typ[i] & 0x80;
}
return ans;
}
pub fn get_string(&self) -> String {
file_name_to_string(self.name, self.typ)
}
pub fn get_string_escaped(&self) -> String {
file_name_to_string_escaped(self.name, self.typ)
}
pub fn get_split_string(&self) -> (String,String) {
file_name_to_split_string(self.name, self.typ)
}
pub fn get_data_ptr(&self) -> Ptr {
Ptr::ExtentData((self.idx_low as u16 + ((self.idx_high as u16) << 5)) as usize)
}
pub fn set_data_ptr(&mut self,ptr: Ptr) {
match ptr {
Ptr::ExtentData(i) => {
self.idx_low = (i & 0b11111) as u8;
self.idx_high = ((i & 0b11111100000) >> 5) as u8;
},
_ => panic!("wrong pointer type")
}
}
pub fn get_eof(&self) -> usize {
let logical_ext_idx = self.get_data_ptr().unwrap();
let rec_idx = match self.last_records {
0 => return logical_ext_idx*LOGICAL_EXTENT_SIZE,
rc if rc < 0x80 => rc as usize - 1,
_ => 0x7f
};
let bytes = match self.last_bytes {
0 => RECORD_SIZE as usize,
x => x as usize
};
logical_ext_idx*LOGICAL_EXTENT_SIZE + rec_idx*RECORD_SIZE + bytes
}
pub fn set_eof(&mut self,x_bytes: usize,vers: [u8;3]) {
let mut total_records = x_bytes/RECORD_SIZE;
self.last_bytes = (x_bytes%RECORD_SIZE) as u8;
if self.last_bytes>0 {
total_records += 1;
}
let recs_per_lx = LOGICAL_EXTENT_SIZE / RECORD_SIZE;
self.last_records = (total_records % recs_per_lx) as u8;
if self.last_records==0 && total_records>0 {
self.last_records = recs_per_lx as u8;
}
if vers[0]<3 {
self.last_bytes = 0;
}
}
pub fn set_block_ptr(&mut self,slot: usize,lx: usize,iblock: u16,dpb: &DiskParameterBlock) {
let lx_per_x = dpb.exm as usize + 1;
match dpb.ptr_size() {
1 => self.block_list[lx*16/lx_per_x + slot] = iblock as u8,
2 => {
self.block_list[2*(lx*8/lx_per_x + slot)] = u16::to_le_bytes(iblock)[0];
self.block_list[2*(lx*8/lx_per_x + slot)+1] = u16::to_le_bytes(iblock)[1];
},
_ => panic!("invalid block pointer size")
}
}
pub fn get_block_list(&self,dpb: &DiskParameterBlock) -> Vec<u16> {
match dpb.ptr_size() {
1 => self.block_list.iter().map(|x| *x as u16).collect::<Vec<u16>>(),
2 => {
let mut ans: Vec<u16> = Vec::new();
for i in 0..8 {
ans.push(u16::from_le_bytes([self.block_list[i*2],self.block_list[i*2+1]]));
}
ans
},
_ => panic!("invalid block pointer size")
}
}
}
impl DirectoryEntry for Extent {
fn stat_range() -> [u8;2] {
[0,USER_END]
}
}
impl Password {
pub fn create(password: &str,user: u8,name_string: &str,permissions: Attributes) -> Self {
let (name,typ) = string_to_file_name(&name_string);
let (decoder,encrypted) = string_to_password(password);
let mut mode = 0;
mode += match permissions.read {
Some(false) => PROTECT_READ,
_ => 0
};
mode += match permissions.write {
Some(false) => PROTECT_WRITE,
_ => 0
};
mode += match permissions.destroy {
Some(false) => PROTECT_DELETE,
_ => 0
};
Self {
user: user + 16,
name,
typ,
mode,
decoder,
pad1: [0,0],
password: encrypted,
pad2: [0;8]
}
}
pub fn merge(&mut self, password: &str, permissions: Attributes) {
let read = match permissions.read {
Some(setting) => !setting,
None => self.mode & PROTECT_READ > 0
};
let write = match permissions.write {
Some(setting) => !setting,
None => self.mode & PROTECT_WRITE > 0
};
let delete = match permissions.destroy {
Some(setting) => !setting,
None => self.mode & PROTECT_DELETE > 0
};
self.mode = (read as u8 * PROTECT_READ) | (write as u8 * PROTECT_WRITE) | (delete as u8 * PROTECT_DELETE);
let (decoder,encrypted) = string_to_password(password);
self.decoder = decoder;
self.password = encrypted;
}
pub fn get_string(&self) -> String {
file_name_to_string(self.name, self.typ)
}
}
impl DirectoryEntry for Password {
fn stat_range() -> [u8;2] {
[USER_END,2*USER_END-1]
}
}
impl Timestamp {
fn create() -> Self {
let mut bytes = vec![0;32];
bytes[0] = TIMESTAMP;
Self::from_bytes(&bytes).expect(RCH)
}
fn get(dir: &Directory,lab: &Label,lx0: &Ptr,info: &mut FileInfo) -> STDRESULT {
if !lab.is_timestamped() {
return Ok(());
}
let expected_idx = 4*(1+lx0.unwrap()/4) - 1;
let sub_idx = lx0.unwrap()%4 + 1;
if let Some(ts) = dir.get_entry::<Timestamp>(&Ptr::ExtentEntry(expected_idx)) {
let (update,create_access) = match sub_idx {
1 => (ts.update1,ts.create_access1),
2 => (ts.update2,ts.create_access2),
3 => (ts.update3,ts.create_access3),
_ => {
return Err(Box::new(Error::BadFormat))
}
};
if lab.is_timestamped_update() {
info.update_time = Some(update);
} else {
info.update_time = None;
}
if lab.is_timestamped_creation() {
info.create_time = Some(create_access);
} else {
info.create_time = None;
}
if lab.is_timestamped_access() {
info.access_time = Some(create_access);
} else {
info.access_time = None;
}
} else {
log::error!("timestamp entry not in expected slot");
return Err(Box::new(Error::BadFormat));
}
Ok(())
}
fn maybe_set(dir: &mut Directory,lab: &Label,lx0: &Ptr,time: Option<chrono::NaiveDateTime>,flags: u8) -> STDRESULT {
if (flags == CREATE) && !lab.is_timestamped_creation() && !lab.is_timestamped_access() {
return Ok(());
}
if (flags == UPDATE) && !lab.is_timestamped_update() {
return Ok(());
}
if (flags == ACCESS) && !lab.is_timestamped_access() {
return Ok(());
}
let expected_idx = 4*(1+lx0.unwrap()/4) - 1;
let sub_idx = lx0.unwrap()%4 + 1;
if let Some(mut ts) = dir.get_entry::<Timestamp>(&Ptr::ExtentEntry(expected_idx)) {
match (sub_idx,flags) {
(1,CREATE) | (1,ACCESS) => ts.create_access1 = pack_date(time),
(2,CREATE) | (2,ACCESS) => ts.create_access2 = pack_date(time),
(3,CREATE) | (3,ACCESS) => ts.create_access3 = pack_date(time),
(1,UPDATE) => ts.update1 = pack_date(time),
(2,UPDATE) => ts.update2 = pack_date(time),
(3,UPDATE) => ts.update3 = pack_date(time),
_ => {
return Err(Box::new(Error::BadFormat))
}
};
dir.set_entry(&Ptr::ExtentEntry(expected_idx), &ts);
} else {
log::error!("timestamp entry not in expected slot");
return Err(Box::new(Error::BadFormat));
}
Ok(())
}
pub fn maybe_set_create(dir: &mut Directory,lab: &Label,lx0: &Ptr,time: Option<chrono::NaiveDateTime>) -> STDRESULT {
Self::maybe_set(dir,lab,lx0,time,CREATE)?;
Self::maybe_set(dir,lab,lx0,time,UPDATE)
}
pub fn maybe_set_access(dir: &mut Directory,lab: &Label,lx0: &Ptr,time: Option<chrono::NaiveDateTime>) -> STDRESULT {
Self::maybe_set(dir,lab,lx0,time,ACCESS)
}
pub fn maybe_set_update(dir: &mut Directory,lab: &Label,lx0: &Ptr,time: Option<chrono::NaiveDateTime>) -> STDRESULT {
Self::maybe_set(dir,lab,lx0,time,UPDATE)
}
}
impl DirectoryEntry for Timestamp {
fn stat_range() -> [u8;2] {
[0x21,0x22]
}
}
impl DiskStruct for Directory {
fn new() -> Self {
let entries: Vec<[u8;DIR_ENTRY_SIZE]> = Vec::new();
Self {
entries
}
}
fn to_bytes(&self) -> Vec<u8> {
let mut ans: Vec<u8> = Vec::new();
for x in &self.entries {
ans.append(&mut x.to_vec());
}
return ans;
}
fn update_from_bytes(&mut self,bytes: &[u8]) -> Result<(),DiskStructError> {
self.entries = Vec::new();
let num_extents = bytes.len()/DIR_ENTRY_SIZE;
if bytes.len()%DIR_ENTRY_SIZE!=0 {
log::warn!("directory buffer wrong size");
}
for i in 0..num_extents {
match bytes[i*DIR_ENTRY_SIZE..(i+1)*DIR_ENTRY_SIZE].try_into() {
Ok(x) => self.entries.push(x),
Err(_) => return Err(DiskStructError::OutOfData)
}
}
Ok(())
}
fn from_bytes(bytes: &[u8]) -> Result<Self,DiskStructError> {
let mut ans = Self::new();
ans.update_from_bytes(bytes)?;
Ok(ans)
}
fn len(&self) -> usize {
return DIR_ENTRY_SIZE*(self.entries.len());
}
}
impl Directory {
pub fn num_entries(&self) -> usize {
self.entries.len()
}
pub fn get_type(&self,ptr: &Ptr) -> EntryType {
let (idx,xstat) = match ptr {
Ptr::ExtentEntry(i) => (*i,self.entries[*i][0]),
_ => panic!("wrong pointer type")
};
log::trace!("entry {} has type {}",idx,xstat);
match xstat {
x if x<USER_END => EntryType::FileExtent,
x if x<USER_END*2 => EntryType::Password,
LABEL => EntryType::Label,
TIMESTAMP => EntryType::Timestamp,
DELETED => EntryType::Deleted,
x => {
log::debug!("unknown entry type {}",x);
EntryType::Unknown
}
}
}
pub fn get_raw_entry(&self,ptr: &Ptr) -> [u8;DIR_ENTRY_SIZE] {
match ptr {
Ptr::ExtentEntry(i) => self.entries[*i].clone(),
_ => panic!("wrong pointer type")
}
}
pub fn get_entry<EntryObject: DiskStruct + DirectoryEntry>(&self,ptr: &Ptr) -> Option<EntryObject> {
let rng = EntryObject::stat_range();
match ptr {
Ptr::ExtentEntry(idx) => match self.entries[*idx][0] {
x if x>=rng[0] && x<rng[1] => Some(
EntryObject::from_bytes(&self.entries[*idx]).expect(RCH)
),
_ => None
},
_ => panic!("wrong pointer type")
}
}
pub fn set_entry<EntryObject: DiskStruct>(&mut self,ptr: &Ptr,x: &EntryObject) {
match ptr {
Ptr::ExtentEntry(idx) => {
self.entries[*idx] = x.to_bytes().try_into().expect("unexpected size")
},
_ => panic!("wrong pointer type")
}
}
pub fn find_label(&self) -> Option<Label> {
for i in 0..self.num_entries() {
if let Some(label) = self.get_entry::<Label>(&Ptr::ExtentEntry(i)) {
return Some(label);
}
}
None
}
pub fn add_timestamps(&self) -> Result<Directory,DYNERR> {
let mut ans = Directory::new();
let mut timestamp = Timestamp::create();
let empty_entry: [u8;32] = [vec![DELETED],vec![0;31]].concat().try_into().expect(RCH);
for i in 0..self.num_entries() {
if ans.entries.len()%4 == 3 {
ans.entries.push(timestamp.to_bytes().try_into().expect(RCH));
timestamp = Timestamp::create();
}
if self.entries[i][0]==TIMESTAMP {
log::error!("directory already has timestamps");
return Err(Box::new(Error::BadFormat));
}
if self.entries[i][0]==LABEL {
let lab = self.get_entry::<Label>(&Ptr::ExtentEntry(i)).unwrap();
match ans.entries.len()%4 + 1 {
1 => {
timestamp.create_access1 = lab.create_time;
timestamp.update1 = lab.update_time;
},
2 => {
timestamp.create_access2 = lab.create_time;
timestamp.update2 = lab.update_time;
},
3 => {
timestamp.create_access3 = lab.create_time;
timestamp.update3 = lab.update_time;
},
_ => {
log::error!("unexpected non-timestamp");
return Err(Box::new(Error::BadFormat));
}
};
}
if self.entries[i][0]!=DELETED {
let bytes = self.get_raw_entry(&Ptr::ExtentEntry(i));
ans.entries.push(bytes);
}
}
if ans.num_entries() > self.num_entries() {
return Err(Box::new(Error::DirectoryFull));
}
for _i in ans.num_entries()..self.num_entries() {
if ans.entries.len()%4 == 3 {
ans.entries.push(timestamp.to_bytes().try_into().expect(RCH));
timestamp = Timestamp::create();
} else {
ans.entries.push(empty_entry);
}
}
Ok(ans)
}
pub fn build_files(&self,dpb: &DiskParameterBlock,cpm_vers: [u8;3]) -> Result<BTreeMap<String,FileInfo>,DYNERR> {
let mut bad_names = 0;
let mut ans = BTreeMap::new();
let maybe_lab = self.find_label();
for i in 0..self.num_entries() {
let xtype = self.get_type(&Ptr::ExtentEntry(i));
if xtype==EntryType::Unknown {
log::debug!("unknown entry type in entry {}",i);
return Err(Box::new(Error::BadFormat));
}
if cpm_vers[0]<3 && (xtype==EntryType::Label || xtype==EntryType::Timestamp || xtype==EntryType::Password) {
log::debug!("rejecting CP/M v3 entry type at {}",i);
return Err(Box::new(Error::BadFormat));
}
if let Some(fx) = self.get_entry::<Extent>(&Ptr::ExtentEntry(i)) {
let key = fx.user.to_string() + ":" + &fx.get_string();
let (name,typ) = fx.get_split_string();
let flags = fx.get_flags();
if flags[4]>0x7f || flags[5]>0x7f || flags[6]>0x7f || flags[7]>0x7f {
log::debug!("unexpected high bits in file name");
return Err(Box::new(Error::BadFormat));
}
if !is_name_valid(&fx.get_string()) {
bad_names += 1;
}
if bad_names > 2 {
log::debug!("after {} bad file names rejecting disk",bad_names);
return Err(Box::new(Error::BadFormat));
}
log::trace!("found file {}:{}",fx.user,fx.get_string_escaped());
let finfo = match ans.get_mut(&key) {
Some(f) => f,
None => {
let v = FileInfo {
user: fx.user,
name,
typ,
read_only: flags[8] > 0,
system: flags[9] > 0,
archived: flags[10] > 0,
f1: flags[0] > 0,
f2: flags[1] > 0,
f3: flags[2] > 0,
f4: flags[3] > 0,
encrypted_password: [0;8],
read_pass: false,
write_pass: false,
del_pass: false,
decoder: 0,
update_time: None,
create_time: None,
access_time: None,
blocks_allocated: 0,
entries: BTreeMap::new()
};
ans.insert(key.clone(),v);
ans.get_mut(&key).unwrap()
}
};
finfo.entries.insert(fx.get_data_ptr(),Ptr::ExtentEntry(i));
for b in fx.get_block_list(dpb) {
finfo.blocks_allocated += match b>0 { true => 1, false => 0};
}
if fx.get_data_ptr() <= Ptr::ExtentData(dpb.exm as usize) {
if let Some(lab) = &maybe_lab {
if lab.is_timestamped() {
Timestamp::get(&self,lab, &Ptr::ExtentEntry(i), finfo)?;
}
}
}
}
}
for i in 0..self.num_entries() {
if let Some(px) = self.get_entry::<Password>(&Ptr::ExtentEntry(i)) {
let key = (px.user-16).to_string() + ":" + &px.get_string();
match ans.get_mut(&key) {
Some(finfo) => {
finfo.read_pass = px.mode & 0b10000000 > 0;
finfo.write_pass = px.mode & 0b01000000 > 0;
finfo.del_pass = px.mode & 0b00100000 > 0;
finfo.decoder = px.decoder;
finfo.encrypted_password = px.password;
},
None => {
log::warn!("detached password for `{}`",key);
}
}
}
}
Ok(ans)
}
pub fn get_users(&self) -> Vec<u8> {
let mut ans = Vec::new();
for i in 0..self.num_entries() {
if let Some(fx) = self.get_entry::<Extent>(&Ptr::ExtentEntry(i)) {
if !ans.contains(&fx.user) {
ans.push(fx.user);
}
}
}
ans.sort();
ans
}
pub fn sort_on_entry_index(&self,files: &BTreeMap<String,FileInfo>) -> BTreeMap<usize,FileInfo> {
let mut ans = BTreeMap::new();
for f in files.values() {
let min_idx = f.entries.values().min().unwrap().unwrap();
ans.insert(min_idx,f.clone());
}
ans
}
}
pub fn get_file<'a>(xname: &str,files: &'a BTreeMap<String,FileInfo>) -> Option<&'a FileInfo> {
let mut trimmed = xname.trim_end().to_string();
if !xname.contains(".") {
trimmed += ".";
}
if let Some(finfo) = files.get(&trimmed) {
return Some(finfo);
}
if let Some(finfo) = files.get(&trimmed.to_uppercase()) {
return Some(finfo);
}
if trimmed.contains(":") {
return None;
}
if let Some(finfo) = files.get(&("0:".to_string()+&trimmed)) {
return Some(finfo);
}
if let Some(finfo) = files.get(&("0:".to_string()+&trimmed.to_uppercase())) {
return Some(finfo);
}
return None;
}