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