Skip to main content

uu_dd/
parseargs.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5// spell-checker:ignore ctty, ctable, iseek, oseek, iconvflags, oconvflags parseargs outfile oconv
6
7#[cfg(test)]
8mod unit_tests;
9
10use super::{ConversionMode, IConvFlags, IFlags, Num, OConvFlags, OFlags, Settings, StatusLevel};
11use crate::conversion_tables::ConversionTable;
12use thiserror::Error;
13use uucore::display::Quotable;
14use uucore::error::UError;
15use uucore::parser::parse_size::{ParseSizeError, Parser as SizeParser};
16use uucore::show_warning;
17use uucore::translate;
18
19/// Parser Errors describe errors with parser input
20#[derive(Debug, PartialEq, Eq, Error)]
21pub enum ParseError {
22    #[error("{}", translate!("dd-error-unrecognized-operand", "operand" => .0.clone()))]
23    UnrecognizedOperand(String),
24    #[error("{}", translate!("dd-error-multiple-format-table"))]
25    MultipleFmtTable,
26    #[error("{}", translate!("dd-error-multiple-case"))]
27    MultipleUCaseLCase,
28    #[error("{}", translate!("dd-error-multiple-block"))]
29    MultipleBlockUnblock,
30    #[error("{}", translate!("dd-error-multiple-excl"))]
31    MultipleExclNoCreate,
32    #[error("{}", translate!("dd-error-invalid-flag", "flag" => .0.clone(), "cmd" => uucore::execution_phrase()))]
33    FlagNoMatch(String),
34    #[error("{}", translate!("dd-error-conv-flag-no-match", "flag" => .0.clone()))]
35    ConvFlagNoMatch(String),
36    #[error("{}", translate!("dd-error-multiplier-parse-failure", "input" => .0.clone()))]
37    MultiplierStringParseFailure(String),
38    #[error("{}", translate!("dd-error-multiplier-overflow", "input" => .0.clone()))]
39    MultiplierStringOverflow(String),
40    #[error("{}", translate!("dd-error-block-without-cbs"))]
41    BlockUnblockWithoutCBS,
42    #[error("{}", translate!("dd-error-status-not-recognized", "level" => .0.clone()))]
43    StatusLevelNotRecognized(String),
44    #[error("{}", translate!("dd-error-unimplemented", "feature" => .0.clone()))]
45    Unimplemented(String),
46    #[error("{}", translate!("dd-error-bs-out-of-range", "param" => .0.clone()))]
47    BsOutOfRange(String),
48    #[error("{}", translate!("dd-error-invalid-number", "input" => .0.clone()))]
49    InvalidNumber(String),
50    #[error("invalid number: ‘{0}’: {1}")]
51    InvalidNumberWithErrMsg(String, String),
52}
53
54/// Contains a temporary state during parsing of the arguments
55#[derive(Debug, PartialEq, Default)]
56pub struct Parser {
57    infile: Option<String>,
58    outfile: Option<String>,
59    /// The block size option specified on the command-line, if any.
60    bs: Option<usize>,
61    /// The input block size option specified on the command-line, if any.
62    ibs: Option<usize>,
63    /// The output block size option specified on the command-line, if any.
64    obs: Option<usize>,
65    cbs: Option<usize>,
66    skip: Num,
67    seek: Num,
68    count: Option<Num>,
69    conv: ConvFlags,
70    /// Whether a data-transforming `conv` option has been specified.
71    is_conv_specified: bool,
72    iflag: IFlags,
73    oflag: OFlags,
74    status: Option<StatusLevel>,
75}
76
77#[derive(Debug, Default, PartialEq, Eq)]
78pub struct ConvFlags {
79    ascii: bool,
80    ebcdic: bool,
81    ibm: bool,
82    ucase: bool,
83    lcase: bool,
84    block: bool,
85    unblock: bool,
86    swab: bool,
87    sync: bool,
88    noerror: bool,
89    sparse: bool,
90    excl: bool,
91    nocreat: bool,
92    notrunc: bool,
93    fdatasync: bool,
94    fsync: bool,
95}
96
97#[derive(Clone, Copy, PartialEq)]
98enum Conversion {
99    Ascii,
100    Ebcdic,
101    Ibm,
102}
103
104#[derive(Clone, Copy)]
105enum Case {
106    Lower,
107    Upper,
108}
109
110#[derive(Clone, Copy)]
111enum Block {
112    Block(usize),
113    Unblock(usize),
114}
115
116/// Return an Unimplemented error when the target is not Linux or Android
117macro_rules! linux_only {
118    ($s: expr, $val: expr) => {
119        if cfg!(any(target_os = "linux", target_os = "android")) {
120            $val
121        } else {
122            return Err(ParseError::Unimplemented($s.to_string()).into());
123        }
124    };
125}
126
127impl Parser {
128    pub(crate) fn new() -> Self {
129        Self::default()
130    }
131
132    pub(crate) fn parse(
133        self,
134        operands: impl IntoIterator<Item: AsRef<str>>,
135    ) -> Result<Settings, ParseError> {
136        self.read(operands)?.validate()
137    }
138
139    pub(crate) fn read(
140        mut self,
141        operands: impl IntoIterator<Item: AsRef<str>>,
142    ) -> Result<Self, ParseError> {
143        for operand in operands {
144            self.parse_operand(operand.as_ref())?;
145        }
146
147        Ok(self)
148    }
149
150    pub(crate) fn validate(self) -> Result<Settings, ParseError> {
151        let conv = self.conv;
152        let conversion = match (conv.ascii, conv.ebcdic, conv.ibm) {
153            (false, false, false) => None,
154            (true, false, false) => Some(Conversion::Ascii),
155            (false, true, false) => Some(Conversion::Ebcdic),
156            (false, false, true) => Some(Conversion::Ibm),
157            _ => return Err(ParseError::MultipleFmtTable),
158        };
159
160        let case = match (conv.ucase, conv.lcase) {
161            (false, false) => None,
162            (true, false) => Some(Case::Upper),
163            (false, true) => Some(Case::Lower),
164            (true, true) => return Err(ParseError::MultipleUCaseLCase),
165        };
166
167        let non_ascii = matches!(conversion, Some(Conversion::Ascii));
168        let conversion_table = get_ctable(conversion, case);
169
170        if conv.nocreat && conv.excl {
171            return Err(ParseError::MultipleExclNoCreate);
172        }
173
174        // The GNU docs state that
175        // - ascii implies unblock
176        // - ebcdic and ibm imply block
177        // This has a side effect in how it's implemented in GNU, because this errors:
178        //     conv=block,unblock
179        // but these don't:
180        //     conv=ascii,block,unblock
181        //     conv=block,ascii,unblock
182        //     conv=block,unblock,ascii
183        //     conv=block conv=unblock conv=ascii
184        let block = if let Some(cbs) = self.cbs {
185            match conversion {
186                Some(Conversion::Ascii) => Some(Block::Unblock(cbs)),
187                Some(_) => Some(Block::Block(cbs)),
188                None => match (conv.block, conv.unblock) {
189                    (false, false) => None,
190                    (true, false) => Some(Block::Block(cbs)),
191                    (false, true) => Some(Block::Unblock(cbs)),
192                    (true, true) => return Err(ParseError::MultipleBlockUnblock),
193                },
194            }
195        } else if conv.block || conv.unblock {
196            return Err(ParseError::BlockUnblockWithoutCBS);
197        } else {
198            None
199        };
200
201        let iconv = IConvFlags {
202            mode: conversion_mode(conversion_table, block, non_ascii, conv.sync),
203            swab: conv.swab,
204            sync: if conv.sync {
205                if block.is_some() {
206                    Some(b' ')
207                } else {
208                    Some(0u8)
209                }
210            } else {
211                None
212            },
213            noerror: conv.noerror,
214        };
215
216        let oconv = OConvFlags {
217            sparse: conv.sparse,
218            excl: conv.excl,
219            nocreat: conv.nocreat,
220            notrunc: conv.notrunc,
221            fdatasync: conv.fdatasync,
222            fsync: conv.fsync,
223        };
224
225        // Input and output block sizes.
226        //
227        // The `bs` option takes precedence. If either is not
228        // provided, `ibs` and `obs` are each 512 bytes by default.
229        let (ibs, obs) = match self.bs {
230            None => (self.ibs.unwrap_or(512), self.obs.unwrap_or(512)),
231            Some(bs) => (bs, bs),
232        };
233
234        // Whether to buffer partial output blocks until they are completed.
235        //
236        // From the GNU `dd` documentation for the `bs=BYTES` option:
237        //
238        // > [...] if no data-transforming 'conv' option is specified,
239        // > input is copied to the output as soon as it's read, even if
240        // > it is smaller than the block size.
241        //
242        let buffered = self.bs.is_none() || self.is_conv_specified;
243
244        let skip = self
245            .skip
246            .force_bytes_if(self.iflag.skip_bytes)
247            .to_bytes(ibs as u64);
248        // GNU coreutils has a limit of i64 (intmax_t)
249        if skip > i64::MAX as u64 {
250            return Err(ParseError::InvalidNumberWithErrMsg(
251                format!("{skip}"),
252                "Value too large for defined data type".to_string(),
253            ));
254        }
255
256        let seek = self
257            .seek
258            .force_bytes_if(self.oflag.seek_bytes)
259            .to_bytes(obs as u64);
260        // GNU coreutils has a limit of i64 (intmax_t)
261        if seek > i64::MAX as u64 {
262            return Err(ParseError::InvalidNumberWithErrMsg(
263                format!("{seek}"),
264                "Value too large for defined data type".to_string(),
265            ));
266        }
267
268        let count = self.count.map(|c| c.force_bytes_if(self.iflag.count_bytes));
269
270        Ok(Settings {
271            skip,
272            seek,
273            count,
274            iconv,
275            oconv,
276            ibs,
277            obs,
278            buffered,
279            infile: self.infile,
280            outfile: self.outfile,
281            iflags: self.iflag,
282            oflags: self.oflag,
283            status: self.status,
284        })
285    }
286
287    fn parse_operand(&mut self, operand: &str) -> Result<(), ParseError> {
288        match operand.split_once('=') {
289            None => return Err(ParseError::UnrecognizedOperand(operand.to_string())),
290            Some((k, v)) => match k {
291                "bs" => self.bs = Some(Self::parse_bytes(k, v)?),
292                "cbs" => self.cbs = Some(Self::parse_bytes(k, v)?),
293                "conv" => {
294                    self.is_conv_specified = true;
295                    self.parse_conv_flags(v)?;
296                }
297                "count" => self.count = Some(Self::parse_n(v)?),
298                "ibs" => self.ibs = Some(Self::parse_bytes(k, v)?),
299                "if" => self.infile = Some(v.to_string()),
300                "iflag" => self.parse_input_flags(v)?,
301                "obs" => self.obs = Some(Self::parse_bytes(k, v)?),
302                "of" => self.outfile = Some(v.to_string()),
303                "oflag" => self.parse_output_flags(v)?,
304                "seek" | "oseek" => self.seek = Self::parse_n(v)?,
305                "skip" | "iseek" => self.skip = Self::parse_n(v)?,
306                "status" => self.status = Some(Self::parse_status_level(v)?),
307                _ => return Err(ParseError::UnrecognizedOperand(operand.to_string())),
308            },
309        }
310        Ok(())
311    }
312
313    fn parse_n(val: &str) -> Result<Num, ParseError> {
314        let n = parse_bytes_with_opt_multiplier(val)?;
315        Ok(if val.contains('B') {
316            Num::Bytes(n)
317        } else {
318            Num::Blocks(n)
319        })
320    }
321
322    fn parse_bytes(arg: &str, val: &str) -> Result<usize, ParseError> {
323        parse_bytes_with_opt_multiplier(val)?
324            .try_into()
325            .map_err(|_| ParseError::BsOutOfRange(arg.to_string()))
326    }
327
328    fn parse_status_level(val: &str) -> Result<StatusLevel, ParseError> {
329        match val {
330            "none" => Ok(StatusLevel::None),
331            "noxfer" => Ok(StatusLevel::Noxfer),
332            "progress" => Ok(StatusLevel::Progress),
333            _ => Err(ParseError::StatusLevelNotRecognized(val.to_string())),
334        }
335    }
336
337    #[allow(clippy::cognitive_complexity)]
338    fn parse_input_flags(&mut self, val: &str) -> Result<(), ParseError> {
339        let i = &mut self.iflag;
340        for f in val.split(',') {
341            match f {
342                // Common flags
343                "cio" => return Err(ParseError::Unimplemented(f.to_string())),
344                "direct" => linux_only!(f, i.direct = true),
345                "directory" => linux_only!(f, i.directory = true),
346                "dsync" => linux_only!(f, i.dsync = true),
347                "sync" => linux_only!(f, i.sync = true),
348                "nocache" => linux_only!(f, i.nocache = true),
349                "nonblock" => linux_only!(f, i.nonblock = true),
350                "noatime" => linux_only!(f, i.noatime = true),
351                "noctty" => linux_only!(f, i.noctty = true),
352                "nofollow" => linux_only!(f, i.nofollow = true),
353                "nolinks" => return Err(ParseError::Unimplemented(f.to_string())),
354                "binary" => return Err(ParseError::Unimplemented(f.to_string())),
355                "text" => return Err(ParseError::Unimplemented(f.to_string())),
356
357                // Input-only flags
358                "fullblock" => i.fullblock = true,
359                "count_bytes" => i.count_bytes = true,
360                "skip_bytes" => i.skip_bytes = true,
361                // GNU silently ignores oflags given as iflag.
362                "append" | "seek_bytes" => {}
363                _ => return Err(ParseError::FlagNoMatch(f.to_string())),
364            }
365        }
366        Ok(())
367    }
368
369    #[allow(clippy::cognitive_complexity)]
370    fn parse_output_flags(&mut self, val: &str) -> Result<(), ParseError> {
371        let o = &mut self.oflag;
372        for f in val.split(',') {
373            match f {
374                // Common flags
375                "cio" => return Err(ParseError::Unimplemented(val.to_string())),
376                "direct" => linux_only!(f, o.direct = true),
377                "directory" => linux_only!(f, o.directory = true),
378                "dsync" => linux_only!(f, o.dsync = true),
379                "sync" => linux_only!(f, o.sync = true),
380                "nocache" => linux_only!(f, o.nocache = true),
381                "nonblock" => linux_only!(f, o.nonblock = true),
382                "noatime" => linux_only!(f, o.noatime = true),
383                "noctty" => linux_only!(f, o.noctty = true),
384                "nofollow" => linux_only!(f, o.nofollow = true),
385                "nolinks" => return Err(ParseError::Unimplemented(f.to_string())),
386                "binary" => return Err(ParseError::Unimplemented(f.to_string())),
387                "text" => return Err(ParseError::Unimplemented(f.to_string())),
388
389                // Output-only flags
390                "append" => o.append = true,
391                "seek_bytes" => o.seek_bytes = true,
392                // GNU silently ignores iflags given as oflag.
393                "fullblock" | "count_bytes" | "skip_bytes" => {}
394                _ => return Err(ParseError::FlagNoMatch(f.to_string())),
395            }
396        }
397        Ok(())
398    }
399
400    fn parse_conv_flags(&mut self, val: &str) -> Result<(), ParseError> {
401        let c = &mut self.conv;
402        for f in val.split(',') {
403            match f {
404                // Conversion
405                "ascii" => c.ascii = true,
406                "ebcdic" => c.ebcdic = true,
407                "ibm" => c.ibm = true,
408
409                // Case
410                "lcase" => c.lcase = true,
411                "ucase" => c.ucase = true,
412
413                // Block
414                "block" => c.block = true,
415                "unblock" => c.unblock = true,
416
417                // Other input
418                "swab" => c.swab = true,
419                "sync" => c.sync = true,
420                "noerror" => c.noerror = true,
421
422                // Output
423                "sparse" => c.sparse = true,
424                "excl" => c.excl = true,
425                "nocreat" => c.nocreat = true,
426                "notrunc" => c.notrunc = true,
427                "fdatasync" => c.fdatasync = true,
428                "fsync" => c.fsync = true,
429                _ => return Err(ParseError::ConvFlagNoMatch(f.to_string())),
430            }
431        }
432        Ok(())
433    }
434}
435
436impl UError for ParseError {
437    fn code(&self) -> i32 {
438        1
439    }
440}
441
442fn show_zero_multiplier_warning() {
443    show_warning!(
444        "{}",
445        translate!("dd-warning-zero-multiplier", "zero" => "0x".quote(), "alternative" => "00x".quote())
446    );
447}
448
449/// Parse bytes using [`str::parse`], then map error if needed.
450fn parse_bytes_only(s: &str, i: usize) -> Result<u64, ParseError> {
451    s[..i]
452        .parse()
453        .map_err(|_| ParseError::MultiplierStringParseFailure(s.to_string()))
454}
455
456/// Parse a number of bytes from the given string, assuming no `'x'` characters.
457///
458/// The `'x'` character means "multiply the number before the `'x'` by
459/// the number after the `'x'`". In order to compute the numbers
460/// before and after the `'x'`, use this function, which assumes there
461/// are no `'x'` characters in the string.
462///
463/// A suffix `'c'` means multiply by 1, `'w'` by 2, and `'b'` by
464/// 512. You can also use standard block size suffixes like `'k'` for
465/// 1024.
466///
467/// If the number would be too large, return [`u64::MAX`] instead.
468///
469/// # Errors
470///
471/// If a number cannot be parsed or if the multiplication would cause
472/// an overflow.
473///
474/// # Examples
475///
476/// ```rust,ignore
477/// assert_eq!(parse_bytes_no_x("123", "123").unwrap(), 123);
478/// assert_eq!(parse_bytes_no_x("2c", "2c").unwrap(), 2 * 1);
479/// assert_eq!(parse_bytes_no_x("3w", "3w").unwrap(), 3 * 2);
480/// assert_eq!(parse_bytes_no_x("2b", "2b").unwrap(), 2 * 512);
481/// assert_eq!(parse_bytes_no_x("2k", "2k").unwrap(), 2 * 1024);
482/// ```
483fn parse_bytes_no_x(full: &str, s: &str) -> Result<u64, ParseError> {
484    let parser = SizeParser {
485        capital_b_bytes: true,
486        no_empty_numeric: true,
487        ..Default::default()
488    };
489    let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) {
490        (None, None, None) => match parser.parse_u64(s) {
491            Ok(n) => (n, 1),
492            Err(ParseSizeError::SizeTooBig(_)) => (u64::MAX, 1),
493            Err(_) => return Err(ParseError::InvalidNumber(full.to_string())),
494        },
495        (Some(i), None, None) => (parse_bytes_only(s, i)?, 1),
496        (None, Some(i), None) => (parse_bytes_only(s, i)?, 2),
497        (None, None, Some(i)) => (parse_bytes_only(s, i)?, 512),
498        _ => return Err(ParseError::MultiplierStringParseFailure(full.to_string())),
499    };
500    num.checked_mul(multiplier)
501        .ok_or_else(|| ParseError::MultiplierStringOverflow(full.to_string()))
502}
503
504/// Parse byte and multiplier like 512, 5KiB, or 1G.
505/// Uses [`uucore::parser::parse_size`], and adds the 'w' and 'c' suffixes which are mentioned
506/// in dd's info page.
507pub fn parse_bytes_with_opt_multiplier(s: &str) -> Result<u64, ParseError> {
508    // TODO On my Linux system, there seems to be a maximum block size of 4096 bytes:
509    //
510    //     $ printf "%0.sa" {1..10000} | dd bs=4095 count=1 status=none | wc -c
511    //     4095
512    //     $ printf "%0.sa" {1..10000} | dd bs=4k count=1 status=none | wc -c
513    //     4096
514    //     $ printf "%0.sa" {1..10000} | dd bs=4097 count=1 status=none | wc -c
515    //     4096
516    //     $ printf "%0.sa" {1..10000} | dd bs=5k count=1 status=none | wc -c
517    //     4096
518    //
519
520    // Split on the 'x' characters. Each component will be parsed
521    // individually, then multiplied together.
522    let parts: Vec<&str> = s.split('x').collect();
523    if parts.len() == 1 {
524        parse_bytes_no_x(s, parts[0])
525    } else {
526        let mut total: u64 = 1;
527        for part in parts {
528            if part == "0" {
529                show_zero_multiplier_warning();
530            }
531            let num = parse_bytes_no_x(s, part)?;
532            total = total
533                .checked_mul(num)
534                .ok_or_else(|| ParseError::InvalidNumber(s.to_string()))?;
535        }
536        Ok(total)
537    }
538}
539
540fn get_ctable(
541    conversion: Option<Conversion>,
542    case: Option<Case>,
543) -> Option<&'static ConversionTable> {
544    #[allow(clippy::wildcard_imports)]
545    use crate::conversion_tables::*;
546
547    Some(match (conversion, case) {
548        (None, None) => return None,
549        (Some(conv), None) => match conv {
550            Conversion::Ascii => &EBCDIC_TO_ASCII,
551            Conversion::Ebcdic => &ASCII_TO_EBCDIC,
552            Conversion::Ibm => &ASCII_TO_IBM,
553        },
554        (None, Some(case)) => match case {
555            Case::Lower => &ASCII_UCASE_TO_LCASE,
556            Case::Upper => &ASCII_LCASE_TO_UCASE,
557        },
558        (Some(conv), Some(case)) => match (conv, case) {
559            (Conversion::Ascii, Case::Upper) => &EBCDIC_TO_ASCII_LCASE_TO_UCASE,
560            (Conversion::Ascii, Case::Lower) => &EBCDIC_TO_ASCII_UCASE_TO_LCASE,
561            (Conversion::Ebcdic, Case::Upper) => &ASCII_TO_EBCDIC_LCASE_TO_UCASE,
562            (Conversion::Ebcdic, Case::Lower) => &ASCII_TO_EBCDIC_UCASE_TO_LCASE,
563            (Conversion::Ibm, Case::Upper) => &ASCII_TO_IBM_UCASE_TO_LCASE,
564            (Conversion::Ibm, Case::Lower) => &ASCII_TO_IBM_LCASE_TO_UCASE,
565        },
566    })
567}
568
569/// Given the various command-line parameters, determine the conversion mode.
570///
571/// The `conv` command-line option can take many different values,
572/// each of which may combine with others. For example, `conv=ascii`,
573/// `conv=lcase`, `conv=sync`, and so on. The arguments to this
574/// function represent the settings of those various command-line
575/// parameters. This function translates those settings to a
576/// [`ConversionMode`].
577fn conversion_mode(
578    ctable: Option<&'static ConversionTable>,
579    block: Option<Block>,
580    is_ascii: bool,
581    is_sync: bool,
582) -> Option<ConversionMode> {
583    match (ctable, block) {
584        (Some(ct), None) => Some(ConversionMode::ConvertOnly(ct)),
585        (Some(ct), Some(Block::Block(cbs))) => {
586            if is_ascii {
587                Some(ConversionMode::ConvertThenBlock(ct, cbs, is_sync))
588            } else {
589                Some(ConversionMode::BlockThenConvert(ct, cbs, is_sync))
590            }
591        }
592        (Some(ct), Some(Block::Unblock(cbs))) => {
593            if is_ascii {
594                Some(ConversionMode::ConvertThenUnblock(ct, cbs))
595            } else {
596                Some(ConversionMode::UnblockThenConvert(ct, cbs))
597            }
598        }
599        (None, Some(Block::Block(cbs))) => Some(ConversionMode::BlockOnly(cbs, is_sync)),
600        (None, Some(Block::Unblock(cbs))) => Some(ConversionMode::UnblockOnly(cbs)),
601        (None, None) => None,
602    }
603}
604
605#[cfg(test)]
606mod tests {
607
608    use crate::Num;
609    use crate::parseargs::{Parser, parse_bytes_with_opt_multiplier};
610    use std::matches;
611    const BIG: &str = "9999999999999999999999999999999999999999999999999999999999999";
612
613    #[test]
614    fn test_parse_bytes_with_opt_multiplier_invalid() {
615        assert!(parse_bytes_with_opt_multiplier("123asdf").is_err());
616    }
617
618    #[test]
619    fn test_parse_bytes_with_opt_multiplier_without_x() {
620        assert_eq!(parse_bytes_with_opt_multiplier("123").unwrap(), 123);
621        assert_eq!(parse_bytes_with_opt_multiplier("123c").unwrap(), 123); // 123 * 1
622        assert_eq!(parse_bytes_with_opt_multiplier("123w").unwrap(), 123 * 2);
623        assert_eq!(parse_bytes_with_opt_multiplier("123b").unwrap(), 123 * 512);
624        assert_eq!(parse_bytes_with_opt_multiplier("123k").unwrap(), 123 * 1024);
625        assert_eq!(parse_bytes_with_opt_multiplier(BIG).unwrap(), u64::MAX);
626    }
627
628    #[test]
629    fn test_parse_bytes_with_opt_multiplier_with_x() {
630        assert_eq!(parse_bytes_with_opt_multiplier("123x3").unwrap(), 123 * 3);
631        assert_eq!(parse_bytes_with_opt_multiplier("1x2x3").unwrap(), 6); // 1 * 2 * 3
632        assert_eq!(
633            parse_bytes_with_opt_multiplier("1wx2cx3w").unwrap(),
634            2 * 2 * (3 * 2) // (1 * 2) * (2 * 1) * (3 * 2)
635        );
636    }
637    #[test]
638    fn test_parse_n() {
639        for arg in ["1x8x4", "1c", "123b", "123w"] {
640            assert!(matches!(Parser::parse_n(arg), Ok(Num::Blocks(_))));
641        }
642        for arg in ["1Bx8x4", "2Bx8", "2Bx8B", "2x8B"] {
643            assert!(matches!(Parser::parse_n(arg), Ok(Num::Bytes(_))));
644        }
645    }
646}