mc_repack_core/min/
mod.rs

1use crate::{cfg, ext::KnownFmt};
2
3/// Minifier for JSON files
4pub mod json;
5
6/// Optimizer for PNG files
7pub mod png;
8
9/// Minifier for TOML files
10pub mod toml;
11
12/// Optimizer for NBT files
13pub mod nbt;
14
15/// Minifier for OGG files
16pub mod ogg;
17
18/// Optimizer for JAR archives
19pub mod jar;
20
21#[inline]
22const fn strip_bom(b: &[u8]) -> &[u8] {
23    if let [239, 187, 191, x @ ..] = b { x } else { b }
24}
25
26#[inline]
27fn brackets(b: &[u8]) -> Option<&[u8]> {
28    let i = b.iter().position(|&b| b == b'{' || b == b'[')?;
29    let endb = match b[i] {
30        b'{' => b'}',
31        b'[' => b']',
32        _ => { return None; }
33    };
34    let j = b.iter().rposition(|&b| b == endb)?;
35    Some(&b[i..=j])
36}
37
38/// A type to determine a minifying method and minimum compress size for file data.
39#[derive(Clone)]
40pub enum Minifier {
41    /// A PNG minifier using `oxipng`.
42    #[cfg(feature = "png")] PNG,
43    /// A JSON minifier using `serde_json`.
44    JSON,
45    /// A TOML minifier using `toml`.
46    #[cfg(feature = "toml")] TOML,
47    /// A customized NBT minifier
48    #[cfg(feature = "nbt")] NBT,
49    /// An OGG minifier using `optivorbis`.
50    #[cfg(feature = "ogg")] OGG,
51    /// A simple repacker for embedded JAR archives
52    #[cfg(feature = "jar")] JAR,
53    /// A minifier that removes hash (`#`) comment lines (and empty lines)
54    Hash,
55    /// A minifier that removes double-slash (`//`) comment lines (and empty lines)
56    Slash,
57    /// A simple Unix line checker
58    UnixLine
59}
60impl Minifier {
61    /// Return a Minifier based on file extension.
62    #[must_use]
63    pub fn by_extension(ftype: &str) -> Option<Self> {
64        Some(match ftype {
65            #[cfg(feature = "png")] "png" => Self::PNG,
66            "json" | "mcmeta" => Self::JSON,
67            #[cfg(feature = "toml")] "toml" => Self::TOML,
68            #[cfg(feature = "nbt")] "nbt" | "blueprint" => Self::NBT,
69            #[cfg(feature = "ogg")] "ogg" => Self::OGG,
70            #[cfg(feature = "jar")] "jar" => Self::JAR,
71            "cfg" | "obj" | "mtl" => Self::Hash,
72            "zs" | "js" | "fsh" | "vsh" => Self::Slash,
73            "mf" => Self::UnixLine,
74            _ => return None
75        })
76    }
77
78    /// Return a Minifier based on known (by this library) file format.
79    pub const fn by_file_format(f: KnownFmt) -> Option<Self> {
80        Some(match f {
81            #[cfg(feature = "png")] KnownFmt::Png => Self::PNG,
82            KnownFmt::Json => Self::JSON,
83            #[cfg(feature = "toml")] KnownFmt::Toml => Self::TOML,
84            #[cfg(feature = "nbt")] KnownFmt::Nbt => Self::NBT,
85            #[cfg(feature = "ogg")] KnownFmt::Ogg => Self::OGG,
86            #[cfg(feature = "jar")] KnownFmt::Jar => Self::JAR,
87            KnownFmt::Cfg | KnownFmt::Obj | KnownFmt::Mtl => Self::Hash,
88            KnownFmt::Fsh | KnownFmt::Vsh | KnownFmt::Js | KnownFmt::Zs => Self::Slash,
89            KnownFmt::Mf => Self::UnixLine,
90            _ => return None
91        })
92    }
93
94    /// Minifies file data and writes the result in provided vec.
95    /// # Errors
96    /// Returns an error if minifying fails, depending on file type
97    pub fn minify(&self, cfgmap: &cfg::ConfigMap, v: &[u8], vout: &mut Vec<u8>) -> Result_ {
98        match self {
99            #[cfg(feature = "png")] Self::PNG => cfgmap.fetch::<png::MinifierPNG>().minify(v, vout),
100            Self::JSON => cfgmap.fetch::<json::MinifierJSON>().minify(v, vout),
101            #[cfg(feature = "toml")] Self::TOML => cfgmap.fetch::<toml::MinifierTOML>().minify(strip_bom(v), vout),
102            #[cfg(feature = "nbt")] Self::NBT => cfgmap.fetch::<nbt::MinifierNBT>().minify(v, vout),
103            #[cfg(feature = "ogg")] Self::OGG => cfgmap.fetch::<ogg::MinifierOGG>().minify(v, vout),
104            #[cfg(feature = "jar")] Self::JAR => cfgmap.fetch::<jar::MinifierJAR>().minify(v, vout),
105            Self::Hash => remove_line_comments("#", v, vout),
106            Self::Slash => remove_line_comments("//", v, vout),
107            Self::UnixLine => unixify_lines(v, vout)
108        }
109    }
110
111    /// Define a minimal size for file compression. Files with lower sizes will be stored as-is.
112    #[must_use]
113    pub const fn compress_min(&self) -> u16 {
114        match self {
115            #[cfg(feature = "png")] Self::PNG => 512,
116            Self::JSON => 64,
117            #[cfg(feature = "toml")] Self::TOML => 64,
118            #[cfg(feature = "nbt")] Self::NBT => 768,
119            _ => 24
120        }
121    }
122}
123
124type Result_ = anyhow::Result<()>;
125
126fn remove_line_comments(bs: &'static str, v: &[u8], vout: &mut Vec<u8>) -> Result_ {
127    let v = std::str::from_utf8(v)?;
128    for l in v.lines() {
129        let Some(ix) = l.as_bytes().iter().position(|&b| !b.is_ascii_whitespace()) else {
130            continue;
131        };
132        let l = &l[ix..];
133        let nix = l.find(bs);
134        let l = match nix {
135            Some(nix) if nix == ix => "",
136            Some(nix) => &l[..nix],
137            None => l
138        };
139        vout.extend_from_slice(l.trim_end().as_bytes());
140        vout.push(b'\n');
141    }
142    Ok(())
143}
144
145fn unixify_lines(v: &[u8], vout: &mut Vec<u8>) -> Result_ {
146    let v = std::str::from_utf8(v)?;
147    for l in v.lines() {
148        vout.extend_from_slice(l.trim_end().as_bytes());
149        vout.push(b'\n');
150    }
151    Ok(())
152}
153
154/// An error indicating that a file has mismatched pair of brackets
155#[derive(Debug)]
156pub struct BracketsError;
157impl std::error::Error for BracketsError {}
158impl std::fmt::Display for BracketsError {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.write_str("improper opening/closing brackets")
161    }
162}
163
164fn strip_string(s: &mut String) {
165    let Some(li) = s.bytes().position(|b| !b.is_ascii_whitespace()) else {
166        return;
167    };
168    *s = s.split_off(li);
169    let Some(ri) = s.bytes().rposition(|b| !b.is_ascii_whitespace()) else {
170        return;
171    };
172    s.truncate(ri + 1);
173}