cpclib_crunch/
lib.rs

1use clap::{Parser, ValueEnum};
2use cpclib_common::camino::{Utf8Path, Utf8PathBuf};
3use cpclib_common::clap;
4use cpclib_common::clap::CommandFactory;
5use cpclib_crunchers::CompressMethod;
6use cpclib_crunchers::lzsa::LzsaVersion;
7use cpclib_disc::amsdos::AmsdosAddBehavior;
8use cpclib_files::FileAndSupport;
9
10pub mod built_info {
11    include!(concat!(env!("OUT_DIR"), "/built.rs"));
12}
13
14#[derive(Parser, Debug)]
15#[command(version, about, long_about = None)]
16pub struct CrunchArgs {
17    #[arg(short, long, help = "Cruncher of interest")]
18    cruncher: Cruncher,
19
20    #[arg(
21        short,
22        long,
23        help = "Input file to compress. Can be a binary (my_file.o), an Amsdos file (FILE.BIN), a file in a disc (my_disc.dsk#FILE.BIN"
24    )]
25    input: Option<Utf8PathBuf>,
26
27    #[arg(
28        short,
29        long,
30        help = "Also crunch the header. This is useful for binary files where the first bytes still correspond to a valid amsdos header",
31        default_value_t = false
32    )]
33    keep_header: bool,
34
35    #[arg(
36        short,
37        long,
38        help = "Compressed output file. Can be a binary, an Amsdos file, a file in a disc",
39        requires = "input"
40    )]
41    output: Option<Utf8PathBuf>,
42
43    #[arg(
44        short = 'H',
45        long,
46        help = "Add a header when storing the file on the host",
47        default_value_t = false
48    )]
49    header: bool,
50
51    #[arg(
52        short,
53        long,
54        help = "Show the z80 decompression source",
55        default_value_t = false,
56        conflicts_with = "input"
57    )]
58    z80: bool
59}
60
61#[derive(Debug, ValueEnum, Clone)]
62pub enum Cruncher {
63    Apultra,
64    Exomizer,
65    Lz4,
66    Lz48,
67    Lz49,
68    Lzsa1,
69    Lzsa2,
70    Shrinkler,
71    Upkr,
72    Zx0
73}
74
75impl Cruncher {
76    pub fn z80(&self) -> &Utf8Path {
77        let fname = match self {
78            Cruncher::Apultra => "inner://unaplib.asm",
79            Cruncher::Exomizer => "inner://deexo.asm",
80            Cruncher::Lz4 => "inner://lz4_docent.asm",
81            Cruncher::Lz48 => "inner://lz48decrunch.asm",
82            Cruncher::Lz49 => "inner://lz49decrunch.asm",
83            Cruncher::Lzsa1 => "inner://unlzsa1_fast.asm",
84            Cruncher::Lzsa2 => "inner://unlzsa1_fast.asm",
85            Cruncher::Shrinkler => "inner://deshrink.asm",
86            Cruncher::Zx0 => "inner://dzx0_fast.asm",
87            Cruncher::Upkr => "inner://uncrunch/upkr.asm"
88        };
89        fname.into()
90    }
91}
92
93pub fn command() -> clap::Command {
94    CrunchArgs::command()
95}
96
97pub fn process(args: CrunchArgs) -> Result<(), String> {
98    if args.z80 {
99        let fname = args.cruncher.z80();
100        let content = cpclib_asm::file::load_file(fname, &Default::default())
101            .unwrap()
102            .0;
103        let content = Vec::from(content);
104        let content = String::from_utf8(content).unwrap();
105        println!("; Import \"{fname}\" in basm or include the following content:\n{content}");
106        return Ok(());
107    }
108
109    // TODO move loading code in the disc crate
110    let (data, header) =
111        cpclib_asm::file::load_file(args.input.as_ref().unwrap().as_path(), &Default::default())
112            .map_err(|e| {
113                format!(
114                    "Unable to load the input file {}.\n{}",
115                    args.input.unwrap(),
116                    e
117                )
118            })
119            .map_err(|e| format!("Error while loading the file. {e}"))?;
120
121    // keep header if needed
122    let data = if args.keep_header {
123        if let Some(header) = header {
124            let mut header = header.as_bytes().to_vec();
125            let data: Vec<u8> = data.into();
126            header.extend_from_slice(&data);
127            header
128        }
129        else {
130            eprintln!("There is no header in the input file");
131            data.into()
132        }
133    }
134    else {
135        data.into()
136    };
137
138    // TODO eventually get additional options to properly parametrize them
139    let cruncher = match args.cruncher {
140        Cruncher::Apultra => CompressMethod::Apultra,
141        Cruncher::Exomizer => CompressMethod::Exomizer,
142        Cruncher::Lz4 => CompressMethod::Lz4,
143        Cruncher::Lz48 => CompressMethod::Lz48,
144        Cruncher::Lz49 => CompressMethod::Lz49,
145        Cruncher::Lzsa1 => CompressMethod::Lzsa(LzsaVersion::V1, None),
146        Cruncher::Lzsa2 => CompressMethod::Lzsa(LzsaVersion::V1, None),
147        Cruncher::Shrinkler => CompressMethod::Shrinkler(Default::default()),
148        Cruncher::Zx0 => CompressMethod::Zx0,
149        Cruncher::Upkr => CompressMethod::Upkr
150    };
151
152    let crunched = cruncher
153        .compress(&data)
154        .map_err(|e| "Error when crunching file.".to_string())?;
155
156    let file_and_support = FileAndSupport::new_auto(args.output.unwrap(), args.header);
157
158    file_and_support
159        .save(
160            &crunched,
161            Some(0xC000),
162            None,
163            Some(AmsdosAddBehavior::ReplaceAndEraseIfPresent)
164        )
165        .map_err(|e| format!("Error when saving the file. {e}"))?;
166
167    Ok(())
168}