ctoolbox/cli/
base_conversion.rs

1use anyhow::Result;
2use futures::StreamExt;
3
4use crate::cli::{StringInput, ToolResult};
5use crate::formats::eite::encoding::base::{
6    BaseConversionPaddingMode, BaseStringFormatSettings, base_to_base_string,
7};
8
9#[derive(clap::Args, Debug)]
10#[allow(clippy::struct_excessive_bools)]
11pub struct BaseArgs {
12    /// Shortcut for -n -q --limit 255 --pad
13    #[arg(short, long, default_value_t = false)]
14    pub bytes: bool,
15
16    /// Invalid unless using --bytes option. Turns off padding.
17    #[arg(long, default_value_t = false)]
18    pub no_pad: bool,
19
20    /// Add prefix to each output number (e.g. 0x)
21    #[arg(long, default_value = "")]
22    pub prefix: String,
23
24    /// Separator inserted after numeric output values (except the last one).
25    ///
26    /// An empty separator is not quite equivalent to not having had a separator
27    /// at all during the conversion — it concatenates their string
28    /// representations, which can produce different numeric results depending
29    /// on leading zeros.
30    ///
31    /// Examples:
32    /// - Hex bytes [0x1A, 0x08] with a separator -> "1A 08".
33    /// - Concatenating without a separator -> "1A08", which is still two
34    ///   numbers (decimal 26 and 8), not a single number 0x1A08 = 6664 decimal.
35    ///   - Consider that normalizing the input numbers first by removing their
36    ///     leading zeroes would yield a different number, 0x1A8 (decimal 424).
37    #[arg(short, long, default_value = " ")]
38    pub separator: String,
39
40    /// Output numbers in base 11+ using lowercase letters, rather than the
41    /// default of uppercase. Does not change the case of input characters that
42    /// are not parts of numbers.
43    #[arg(short, long, default_value_t = true)]
44    pub lowercase: bool,
45
46    /// Whether to filter out bytes that aren't digits in the input base.
47    #[arg(short, long, default_value_t = true)]
48    pub filter_chars: bool,
49
50    /// Should filtered characters be totally ignored for parsing numbers? E.g.
51    /// `10_000` would get the _ filtered out and be treated as 10000.
52    #[arg(short, long, default_value_t = false)]
53    pub collapse_filtered: bool,
54
55    /// A list of filtered characters to collapse, leaving others as spaces.
56    #[arg(long, default_value = "[]")]
57    pub collapse_only: Vec<String>,
58
59    /// Whether to interpret existing prefixes (e.g. 0x) in the input. If set to
60    /// false, it may produce silly results in some cases, like when converting
61    /// hex with 0x prefixes to another base. If you also ask it to add
62    /// prefixes, you'll get three prefixes for each number! (Because it will
63    /// take 0 as a number, then pass through x, then take the actual number.)
64    #[arg(short, long, default_value_t = true)]
65    pub parse_prefixes: bool,
66
67    /// Limit width for each number. Input numbers will be split up if longer
68    /// than this value (0x0404 would be read as 0x04 04). The value of this
69    /// argument should be the maximum value that you need to represent, and
70    /// the width in bytes will be derived from that dependent on the base.
71    /// Set to 0 to disable limiting.
72    #[arg(short, long, default_value_t = 0)]
73    pub limit: u64,
74
75    /// Zero-pad the left of each number to the number of digits determined by
76    /// the limit argument. Requires a limit to be set.
77    #[arg(
78        short,
79        long,
80        default_value_t = false,
81        conflicts_with("pad_l"),
82        requires_if("true", "limit")
83    )]
84    pub pad: bool,
85
86    /// Zero-pad the left of each number to at least this many digits. Set to 0
87    /// or 1 to turn off.
88    #[arg(short, long, default_value_t = 1, conflicts_with("pad"))]
89    pub pad_l: u32,
90
91    /// Suppress warning messages
92    #[arg(short, long, default_value_t = false)]
93    pub quiet: bool,
94}
95
96#[derive(clap::Args, Debug)]
97pub struct BaseToBaseArgs {
98    /// Base of input numbers
99    #[arg(default_value_t = 10)]
100    pub from_base: u8,
101
102    /// Base of output numbers
103    #[arg(default_value_t = 10)]
104    pub to_base: u8,
105}
106
107// ---------------------------
108// Conversion Logic
109// ---------------------------
110
111#[allow(clippy::unnecessary_wraps)]
112pub fn run_base_convert(
113    from_base: &Option<u8>,
114    to_base: &Option<u8>,
115    string_input: &StringInput,
116    args: &BaseArgs,
117) -> Result<ToolResult> {
118    let mut format_settings = BaseStringFormatSettings {
119        prefix: args.prefix.clone(),
120        separator: args.separator.clone(),
121        lowercase: args.lowercase,
122        filter_chars: args.filter_chars,
123        collapse_filtered: args.collapse_filtered,
124        collapse_only: args.collapse_only.clone(),
125        parse_prefixes: args.parse_prefixes,
126        limit: args.limit,
127        pad: BaseConversionPaddingMode {
128            pad_l: args.pad_l,
129            pad_fit: args.pad,
130        },
131    };
132
133    if args.bytes {
134        format_settings.limit = u64::from(u8::MAX);
135        format_settings.pad = BaseConversionPaddingMode {
136            // I'm using 0 as the default here to indicate it's fully off, while
137            // the struct and the CLI argument default to 1 because logically,
138            // it makes sense to show each number as at least 1 byte wide. In
139            // practice, 0 and 1 have no effect.
140            pad_l: 0,
141            pad_fit: true,
142        };
143        if args.no_pad {
144            format_settings.pad = BaseConversionPaddingMode {
145                pad_l: 0,
146                pad_fit: false,
147            };
148        }
149    } else if args.no_pad {
150        return Ok(ToolResult::immediate_err(
151            "--no-pad is only valid with --bytes".as_bytes().to_vec(),
152            1,
153        ));
154    }
155
156    let quiet = args.quiet || args.bytes;
157
158    if (from_base.is_none() && !to_base.is_none())
159        || (!from_base.is_none() && to_base.is_none())
160    {
161        return Ok(ToolResult::immediate_err(
162            "Either both or neither base must be specified"
163                .as_bytes()
164                .to_vec(),
165            1,
166        ));
167    }
168
169    let converted = base_to_base_string(
170        string_input.input.as_str(),
171        from_base.unwrap_or(10),
172        to_base.unwrap_or(10),
173        &format_settings,
174    );
175
176    match converted {
177        Err(e) => Ok(ToolResult::immediate_err(
178            format!("{e:?}").as_bytes().to_vec(),
179            1,
180        )),
181        Ok((res, log)) => {
182            // Add any extra formatting to log if desired
183            let mut output_bytes = res.into_bytes();
184            output_bytes.push(b'\n');
185            let stderr_bytes = if quiet {
186                log.format_errors().into_bytes()
187            } else {
188                log.format_all().into_bytes()
189            };
190            Ok(ToolResult::Immediate {
191                stdout: output_bytes,
192                stderr: stderr_bytes,
193                exit_code: 0,
194            })
195        }
196    }
197}