use crate::error::Error;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use std::fmt;
pub(crate) const BYTES_PER_BLOCK: usize = 254;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CbmFileType {
PRG,
SEQ,
USR,
REL,
Unknown,
}
impl CbmFileType {
pub fn _to_suffix(&self) -> &'static str {
match self {
CbmFileType::PRG => ",P",
CbmFileType::SEQ => ",S",
CbmFileType::USR => ",U",
CbmFileType::REL => ",R",
CbmFileType::Unknown => "",
}
}
}
impl fmt::Display for CbmFileType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let output = match self {
CbmFileType::PRG => "prg",
CbmFileType::SEQ => "seq",
CbmFileType::USR => "usr",
CbmFileType::REL => "rel",
CbmFileType::Unknown => "",
};
write!(f, "{}", output)?;
Ok(())
}
}
impl From<&str> for CbmFileType {
fn from(s: &str) -> Self {
match s.to_uppercase().as_str() {
"PRG" => CbmFileType::PRG,
"SEQ" => CbmFileType::SEQ,
"USR" => CbmFileType::USR,
"REL" => CbmFileType::REL,
_ => CbmFileType::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CbmFileMode {
Read,
Write,
Append,
}
impl CbmFileMode {
fn _to_suffix(&self) -> &'static str {
match self {
CbmFileMode::Read => "",
CbmFileMode::Write => ",W",
CbmFileMode::Append => ",A",
}
}
}
#[derive(Debug, Clone)]
pub enum CbmFileEntry {
ValidFile {
blocks: u16,
filename: String,
file_type: CbmFileType,
},
InvalidFile {
raw_line: String,
error: String, partial_blocks: Option<u16>, partial_filename: Option<String>, },
}
impl CbmFileEntry {
pub fn max_size(&self) -> Option<u64> {
match self {
CbmFileEntry::ValidFile { blocks, .. } => {
Some((*blocks as u64) * (BYTES_PER_BLOCK as u64))
}
CbmFileEntry::InvalidFile { partial_blocks, .. } => {
if let Some(blocks) = partial_blocks {
Some((*blocks as u64) * (BYTES_PER_BLOCK as u64))
} else {
None
}
}
}
}
}
impl fmt::Display for CbmFileEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CbmFileEntry::ValidFile {
blocks,
filename,
file_type,
} => {
write!(
f,
"Filename: \"{}.{}\"{:width$}Blocks: {:>3}",
filename,
file_type,
"", blocks,
width = 25 - (filename.len() + 3 + 1) )
}
CbmFileEntry::InvalidFile {
raw_line,
error,
partial_blocks,
partial_filename,
} => {
write!(f, "Invalid entry: {} ({})", raw_line, error)?;
if let Some(filename) = partial_filename {
write!(f, " [Filename: \"{}\"]", filename)?;
}
if let Some(blocks) = partial_blocks {
write!(f, " [Blocks: {}]", blocks)?;
}
Ok(())
}
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CbmDiskHeader {
pub drive_number: u8,
pub name: String,
pub id: String,
}
impl CbmDiskHeader {
pub const MAX_NAME_LENGTH: usize = 16;
pub const ID_LENGTH: usize = 2;
}
impl fmt::Display for CbmDiskHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Drive {} Header: \"{}\" ID: {}",
self.drive_number, self.name, self.id
)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CbmDirListing {
pub header: CbmDiskHeader,
pub files: Vec<CbmFileEntry>,
pub blocks_free: u16,
}
impl fmt::Display for CbmDirListing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.header)?;
for entry in &self.files {
writeln!(f, "{}", entry)?;
}
writeln!(f, "Free blocks: {}", self.blocks_free)
}
}
impl CbmDirListing {
pub fn parse(input: &str) -> Result<Self, Error> {
trace!("CbmDirListing::parse input.len() {}", input.len());
trace!("Input:\n{}", input);
let mut lines = input.lines();
let header = Self::parse_header(lines.next().ok_or_else(|| {
debug!("CbmDirListing::parse Missing header line");
Error::Parse {
message: "Missing header line".to_string(),
}
})?)?;
let mut files = Vec::new();
let mut blocks_free = None;
for line in lines {
if line.contains("blocks free") {
blocks_free = Some(Self::parse_blocks_free(line)?);
break;
} else {
files.push(Self::parse_file_entry(line));
}
}
let blocks_free = blocks_free.ok_or_else(|| {
debug!("CbmDirListing::parse Missing blocks free line");
Error::Parse {
message: "Missing blocks free line".to_string(),
}
})?;
Ok(CbmDirListing {
header,
files,
blocks_free,
})
}
fn parse_header(line: &str) -> Result<CbmDiskHeader, Error> {
let re =
regex::Regex::new(r#"^\s*(\d+)\s+\."([^"]*)" ([a-zA-Z0-9]{2})"#).map_err(|_| {
Error::Parse {
message: "Invalid header regex".to_string(),
}
})?;
let caps = re.captures(line).ok_or_else(|| Error::Parse {
message: format!("Invalid header format: {}", line),
})?;
Ok(CbmDiskHeader {
drive_number: caps[1].parse().map_err(|_| Error::Parse {
message: format!("Invalid drive number: {}", &caps[1]),
})?,
name: caps[2].trim_end().to_string(), id: caps[3].to_string(),
})
}
fn parse_file_entry(line: &str) -> CbmFileEntry {
let re = regex::Regex::new(r#"^\s*(\d+)\s+"([^"]+)"\s+(\w+)\s*$"#).expect("Invalid regex");
match re.captures(line) {
Some(caps) => {
let blocks = match caps[1].trim().parse() {
Ok(b) => b,
Err(_) => {
return CbmFileEntry::InvalidFile {
raw_line: line.to_string(),
error: "Invalid block count".to_string(),
partial_blocks: None,
partial_filename: Some(caps[2].to_string()),
}
}
};
let filetype = CbmFileType::from(&caps[3]);
CbmFileEntry::ValidFile {
blocks,
filename: caps[2].to_string(), file_type: filetype,
}
}
None => CbmFileEntry::InvalidFile {
raw_line: line.to_string(),
error: "Could not parse line format".to_string(),
partial_blocks: None,
partial_filename: None,
},
}
}
fn parse_blocks_free(line: &str) -> Result<u16, Error> {
let re = regex::Regex::new(r"^\s*(\d+)\s+blocks free").map_err(|_| Error::Parse {
message: "Invalid blocks free regex".to_string(),
})?;
let caps = re.captures(line).ok_or_else(|| Error::Parse {
message: format!("Invalid blocks free format: {}", line),
})?;
caps[1].parse().map_err(|_| Error::Parse {
message: format!("Invalid blocks free number: {}", &caps[1]),
})
}
pub fn num_files(&self) -> usize {
self.files.len()
}
pub fn num_blocks_used_valid(&self) -> u16 {
self.files
.iter()
.map(|entry| match entry {
CbmFileEntry::ValidFile { blocks, .. } => *blocks,
_ => 0,
})
.sum()
}
pub fn total_blocks(&self) -> u16 {
self.num_blocks_used_valid() + self.blocks_free
}
}