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}