use clap;
use regex::Regex;
use std::str::FromStr;
use num_traits::FromPrimitive;
use std::path::PathBuf;
use std::sync::LazyLock;
use super::CommandError;
use crate::fs::{DiskFS,FileImage,UnpackedData,dos3x,prodos};
use crate::img::tracks::Method;
use crate::{DYNERR,STDRESULT};
use crate::lang::{applesoft,integer,merlin,is_lang};
static CPM_COLON: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(^|\/|\\)([0-9][0-9]?)(:)").unwrap());
static CPM_UNDER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(^|\/|\\)([0-9][0-9]?)(_)").unwrap());
static DRIVE_PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-zA-Z]:").unwrap());
static CIDERPRESS_SUFFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$").unwrap());
struct Source {
pub fimg: FileImage,
pub fused_path: String
}
enum Destination {
Dimg(String,Box<dyn DiskFS>,String,String),
Host(String)
}
fn parse_cp_suffix(suffix: &str) -> (u8,usize) {
let typ = hex::decode(&suffix[1..3]).expect("bad suffix");
let aux = hex::decode(&suffix[3..7]).expect("bad suffix");
(typ[0],u16::from_be_bytes([aux[0],aux[1]]) as usize)
}
fn swap_cpm_delimiter(path: &str,old: &LazyLock<Regex>,new: &str) -> String {
let rep = r"${1}${2}x".replace('x',new);
old.replace(path, &rep).to_string()
}
fn extract_ordinary_filename(path: &str) -> Result<String,DYNERR> {
let pbuf = PathBuf::from(path);
let Some(os_fname) = pbuf.file_name() else {
log::error!("could not extract filename from {}",path);
return Err(Box::new(CommandError::FileNotFound));
};
let Some(fname) = os_fname.to_str() else {
return Err(Box::new(CommandError::UnsupportedFormat));
};
Ok(fname.to_string())
}
fn parse_fused_path(fused: &str,dimg_patt: &Regex) -> Result<(String,String),DYNERR> {
let mut locs = dimg_patt.capture_locations();
dimg_patt.captures_read(&mut locs,fused);
let (_,end) = locs.get(0).unwrap();
match fused[end-1..end].as_ref() {
"/" => Ok((fused[0..end-1].to_owned(),fused[end..].to_owned())),
_ => Ok((fused.to_owned(),String::new()))
}
}
fn finalize_destination_path(src_path: &str, dst_path: &str, multi_src: bool, dst_is_dir: bool, dst_is_img: bool, cpm: bool) -> Result<String,DYNERR> {
match (multi_src,dst_is_dir,dst_path.is_empty()) {
(false,false,false) => Ok(dst_path.to_owned()),
_ => {
let mut fname = src_path.to_string();
if cpm {
fname = swap_cpm_delimiter(&fname, &CPM_COLON, "_");
}
fname = extract_ordinary_filename(&fname)?;
if cpm && dst_is_img {
fname = swap_cpm_delimiter(&fname, &CPM_UNDER, ":");
}
match dst_is_img {
true => match dst_path.len() {
0 => Ok(fname.to_owned()),
_ => Ok([dst_path,"/",fname.as_str()].concat())
},
false => match PathBuf::from(dst_path).join(fname).as_os_str().to_str() {
Some(s) => Ok(s.to_owned()),
None => Err(Box::new(CommandError::UnsupportedFormat))
}
}
}
}
}
fn create_fimg_path(src_path: &str, dst_path: &str, multi_src: bool, dst_is_dir: bool, strip: Vec<&str>, cpm: bool) -> Result<String,DYNERR> {
match (multi_src, dst_is_dir, dst_path.is_empty()) {
(false,false,false) => Ok(dst_path.to_string()),
_ => {
let mut ans = src_path.to_string();
if cpm {
ans = swap_cpm_delimiter(&ans, &CPM_COLON, "_");
}
ans = extract_ordinary_filename(&ans)?;
if cpm {
ans = swap_cpm_delimiter(&ans, &CPM_UNDER, ":");
}
let l = ans.len();
for x in strip {
if l > x.len() && ans.to_lowercase().ends_with(x) {
ans.truncate(l-x.len());
break;
}
}
if ans.len() > 7 && CIDERPRESS_SUFFIX.is_match(&ans) {
ans.truncate(l-7)
}
Ok(ans)
}
}
}
fn smart_pack(fimg: &mut FileImage, dat: &[u8], load_addr0: Option<usize>, cp_suffix: Option<String>) -> STDRESULT {
let load_addr = match (load_addr0,&cp_suffix) {
(Some(addr),_) => Some(addr),
(None,Some(s)) => Some(parse_cp_suffix(&s).1),
_ => None
};
let typ = match &cp_suffix {
Some(s) => Some(parse_cp_suffix(&s).0),
None => None
};
let is_apple = match fimg.file_system.as_str() {
"prodos" | "a2 dos" => true,
_ => false
};
log::debug!("load={}, type={}",load_addr.unwrap_or(0),typ.unwrap_or(0));
match str::from_utf8(dat) {
Ok(program) => {
if is_apple && is_lang(tree_sitter_applesoft::LANGUAGE.into(),program) {
log::info!("detected Applesoft");
let start_addr = match load_addr {
Some(addr) => u16::try_from(addr)?,
None => 2049
};
let mut tokenizer = applesoft::tokenizer::Tokenizer::new();
let tok = tokenizer.tokenize(&program,start_addr)?;
fimg.pack_tok(&tok,super::ItemType::ApplesoftTokens,None)?;
} else if is_apple && is_lang(tree_sitter_integerbasic::LANGUAGE.into(), program) {
log::info!("detected Integer BASIC");
let mut tokenizer = integer::tokenizer::Tokenizer::new();
let tok = tokenizer.tokenize(program.to_string())?;
fimg.pack_tok(&tok,super::ItemType::IntegerTokens,None)?;
} else if is_apple && is_lang(tree_sitter_merlin6502::LANGUAGE.into(), program) {
log::info!("detected Merlin");
let mut tokenizer = merlin::tokenizer::Tokenizer::new();
let tok = tokenizer.tokenize(program.to_string())?;
fimg.pack_raw(&tok)?;
} else {
fimg.pack(&dat,load_addr)?;
}
},
Err(_) => {
fimg.pack(&dat,load_addr)?;
}
};
if let Some(t) = typ {
match fimg.file_system.as_str() {
"prodos" => {
fimg.fs_type = vec![t];
},
"a2 dos" => {
match prodos::types::FileType::from_u8(t) {
Some(prodos::types::FileType::Text) => fimg.fs_type = vec![dos3x::types::FileType::Text as u8],
Some(prodos::types::FileType::ApplesoftCode) => fimg.fs_type = vec![dos3x::types::FileType::Applesoft as u8],
Some(prodos::types::FileType::IntegerCode) => fimg.fs_type = vec![dos3x::types::FileType::Integer as u8],
_ => fimg.fs_type = vec![dos3x::types::FileType::Binary as u8]
}
},
_ => {}
}
}
Ok(())
}
fn smart_unpack(fimg: &FileImage,dst_path: &str,add_suffix: bool) -> Result<(UnpackedData,String),DYNERR> {
let maybe_file_type = match fimg.file_system.as_str() {
"prodos" => prodos::Packer::get_prodos_type(fimg),
"a2 dos" => match dos3x::Packer::get_dos3x_type(fimg) {
Some(dos3x::types::FileType::Applesoft) => Some(prodos::types::FileType::ApplesoftCode),
Some(dos3x::types::FileType::Integer) => Some(prodos::types::FileType::IntegerCode),
Some(dos3x::types::FileType::Text) => Some(prodos::types::FileType::Text),
Some(dos3x::types::FileType::Binary) => Some(prodos::types::FileType::Binary),
_ => None
},
_ => None
};
let cp_suffix = match fimg.file_system.as_str() {
"prodos" => ["#".to_string(),hex::encode(&fimg.fs_type),hex::encode(&fimg.aux.iter().rev().map(|x| *x).collect::<Vec<u8>>())].concat(),
"a2 dos" => {
let typ = match &maybe_file_type {
Some(t) => t.clone() as u8, None => fimg.fs_type[0] };
["#".to_string(),hex::encode(vec![typ]),hex::encode(u16::to_be_bytes(u16::try_from(fimg.get_load_address())?))].concat()
},
_ => "".to_string()
};
let update_dst_path = |s: &str| -> String {
match dst_path.to_lowercase().ends_with(s) || !add_suffix {
true => dst_path.to_string(),
false => [dst_path,s].concat()
}
};
match maybe_file_type {
Some(prodos::types::FileType::ApplesoftCode) => {
log::info!("detected Applesoft");
let toks = fimg.unpack_tok()?;
let tokenizer = applesoft::tokenizer::Tokenizer::new();
return Ok((UnpackedData::Text(tokenizer.detokenize(&toks)?),update_dst_path(".abas")))
},
Some(prodos::types::FileType::IntegerCode) => {
log::info!("detected Integer BASIC");
let toks = fimg.unpack_tok()?;
let tokenizer = integer::tokenizer::Tokenizer::new();
return Ok((UnpackedData::Text(tokenizer.detokenize(&toks)?),update_dst_path(".ibas")))
},
Some(prodos::types::FileType::Text) => {
let merlin_code = fimg.unpack_raw(true)?;
let mut tokenizer = merlin::tokenizer::Tokenizer::new();
tokenizer.set_err_log(false);
if let Ok(src) = tokenizer.detokenize(&merlin_code) {
if is_lang(tree_sitter_merlin6502::LANGUAGE.into(), &src) {
log::info!("detected Merlin");
return Ok((UnpackedData::Text(src),update_dst_path(".S")));
}
}
},
_ => {}
}
match fimg.unpack() {
Ok(UnpackedData::Text(t)) => Ok((UnpackedData::Text(t),update_dst_path(".txt"))),
Ok(UnpackedData::Records(r)) => Ok((UnpackedData::Records(r),update_dst_path(".json"))),
Ok(ud) => Ok((ud,update_dst_path(&cp_suffix))),
Err(e) => Err(e)
}
}
fn gather(src: Vec<String>,dst: &Destination,dst_is_dir: bool,dimg_patt: &Regex,cmd: &clap::ArgMatches) -> Result<Vec<Source>,DYNERR> {
let mut ans = Vec::new();
let fmt = super::get_fmt(cmd)?;
let load_addr: Option<usize> = match cmd.get_one::<String>("addr") {
Some(a) => Some(usize::from_str(a)?),
_ => None
};
let add_suffix = cmd.get_flag("suffix");
let mut partial_count = 0;
for fused_path in &src {
match dimg_patt.is_match(fused_path) {
true => {
let (path_to,path_in) = parse_fused_path(&fused_path,dimg_patt)?;
let mut src_disk = crate::create_fs_from_file(&path_to,fmt.as_ref())?;
src_disk.get_img().change_method(Method::from_str(cmd.get_one::<String>("method").unwrap())?);
match src_disk.glob(&path_in,false) {
Ok(glob_matches) => partial_count += glob_matches.len(),
Err(_) => {}
}
},
false => partial_count += 1
}
if partial_count > 1 {
break;
}
};
let multi_src = partial_count > 1;
let (dst_flat, dst_empty_path) = match dst {
Destination::Host(dst_path) => {
(false, dst_path.is_empty())
}
Destination::Dimg(fs, _, _, dst_path) => {
(["cpm","a2 dos","a2 pascal"].contains(&fs.as_str()), dst_path.is_empty())
}
};
let joining_dst_src = multi_src || dst_is_dir || dst_empty_path;
for fused_path in src {
match dimg_patt.is_match(&fused_path) {
true => {
let (path_to,path_in) = parse_fused_path(&fused_path,dimg_patt)?;
let mut src_disk = crate::create_fs_from_file(&path_to,fmt.as_ref())?;
src_disk.get_img().change_method(Method::from_str(cmd.get_one::<String>("method").unwrap())?);
let src_flat = match src_disk.stat() {
Ok(stat) if ["cpm","a2 dos","a2 pascal"].contains(&stat.fs_name.as_str()) => true,
_ => false
};
let forbid_delims_in_src = src_flat && !dst_flat && joining_dst_src;
match src_disk.glob(&path_in,false) {
Ok(glob_matches) => {
if glob_matches.len() == 0 {
log::error!("no matches to source path {}",path_in);
return Err(Box::new(CommandError::FileNotFound));
}
for raw_match in glob_matches {
if forbid_delims_in_src && (raw_match.contains("/") || raw_match.contains("\\")) {
log::warn!("skipping {} due to path delimiter in filename",raw_match);
continue;
}
let m = match src_flat {
true => ["/",&raw_match].concat(),
false => raw_match.clone()
};
ans.push(Source {fimg: src_disk.get(&raw_match)?, fused_path: [path_to.as_str(),m.as_str()].concat()});
}
},
Err(_) => ans.push(Source {fimg: src_disk.get(&path_in)?, fused_path})
}
},
false => {
match (dst,std::fs::read(&fused_path)) {
(Destination::Host(_),_) => {
log::error!("refusing host-to-host copy");
return Err(Box::new(CommandError::InvalidCommand))
},
(Destination::Dimg(fs, dst_disk, _, raw_dst_path),Ok(dat)) => {
let cpm = fs.as_str()=="cpm";
let strip = match fs.as_str() {
"prodos" | "a2 dos" => vec![".json",".txt",".bas",".abas",".ibas"],
_ => vec![".json"]
};
let cp_suffix = match (add_suffix,CIDERPRESS_SUFFIX.is_match(&fused_path)) {
(true,true) => Some(fused_path[fused_path.len()-7..].to_string()),
_ => None
};
let dst_path = create_fimg_path(&fused_path,raw_dst_path,multi_src,dst_is_dir,strip,cpm)?;
let mut fimg = dst_disk.new_fimg(None,true,&dst_path)?;
smart_pack(&mut fimg,&dat,load_addr,cp_suffix)?;
ans.push(Source {fimg,fused_path});
},
(_,Err(e)) => return Err(Box::new(e))
}
}
}
}
Ok(ans)
}
pub fn ezcopy(cmd: &clap::ArgMatches) -> STDRESULT {
let add_suffix = cmd.get_flag("suffix");
let dimg_patt = Regex::new(r"(?i)\.(2mg|d13|dsk|do|dsk|ima|imd|img|nib|po|td0|woz)($|/)").expect("failed to parse regex");
let mut path_list: Vec<String> = cmd.get_many::<String>("paths").expect("no paths").map(|x| x.to_owned()).collect();
let fused = path_list.pop().unwrap();
let fmt = super::get_fmt(cmd)?;
let mut dst = match dimg_patt.is_match(&fused) {
true => {
let (path_to,path_inside) = parse_fused_path(&fused,&dimg_patt)?;
let mut dimg = crate::create_fs_from_file(&path_to,fmt.as_ref())?;
dimg.get_img().change_method(Method::from_str(cmd.get_one::<String>("method").unwrap())?);
Destination::Dimg(dimg.stat()?.fs_name,dimg,path_to,path_inside)
},
false => {
Destination::Host(fused.clone())
}
};
let dst_is_dir = match &mut dst {
Destination::Dimg(_, disk, path_to, path_inside) => {
log::info!("image destination {}",path_to);
match disk.catalog_to_vec(path_inside) {
Ok(_) => true,
Err(_) => false
}
},
Destination::Host(target_path) => {
log::info!("host destination {}",target_path);
if PathBuf::from(target_path.as_str()).is_file() {
log::error!("destination already exists as a file");
return Err(Box::new(CommandError::InvalidCommand));
}
PathBuf::from(target_path.as_str()).is_dir()
}
};
let mut src_list = gather(path_list,&dst,dst_is_dir,&dimg_patt,cmd)?;
let multi_src = src_list.len() > 1;
for src in &mut src_list {
let cpm = src.fimg.file_system.as_str() == "cpm";
match &mut dst {
Destination::Dimg(_, dst_disk, _, raw_dst_path) => {
let dst_path = finalize_destination_path(&src.fimg.full_path, &raw_dst_path, multi_src, dst_is_dir, true, cpm)?;
log::info!("copy {} -> {}",src.fused_path,dst_path);
dst_disk.put_at(&dst_path,&mut src.fimg)?;
},
Destination::Host(raw_dst_path) => {
let dst_path = finalize_destination_path(&src.fimg.full_path, raw_dst_path, multi_src, dst_is_dir, false, cpm)?;
if cfg!(windows) {
let start = match DRIVE_PREFIX.is_match(&dst_path) {
true => 2,
false => 0
};
if dst_path[start..].contains(':') {
log::warn!("skipping {} due to colon",dst_path);
break;
}
}
if PathBuf::from(dst_path.as_str()).is_file() {
log::error!("destination {} already exists as a file",dst_path);
return Err(Box::new(CommandError::InvalidCommand));
}
log::info!("copy {} -> {}",src.fused_path,dst_path);
match smart_unpack(&src.fimg,&dst_path,add_suffix) {
Ok((UnpackedData::Binary(dat),final_dst)) => std::fs::write(&final_dst,&dat).expect("host file system error"),
Ok((UnpackedData::Text(s),final_dst)) => std::fs::write(&final_dst,s.as_bytes()).expect("host file system error"),
Ok((UnpackedData::Records(r),final_dst)) => {
let rec_str = r.to_json(None);
std::fs::write(&final_dst,rec_str.as_bytes()).expect("host file system error")
},
_ => {
log::info!("not unpacking {}",dst_path);
let fimg_str = src.fimg.to_json(Some(2));
std::fs::write(&[&dst_path,".json"].concat(),fimg_str.as_bytes()).expect("host file system error")
}
}
}
}
}
match &mut dst {
Destination::Dimg(_, dst_disk,dimg_path,_) => crate::save_img(dst_disk,&dimg_path),
Destination::Host(_) => Ok(())
}
}