Skip to main content

base/
cmdtree.rs

1// cmdtree -- command trees for IOS-like command lines: unique prefix abbreviations ("sh ip int
2// br"), "?" help with descriptions, Tab completion, modes with prompts, the "no" form, typed
3// parameters and the position of an error for the IOS-like messages ("% Invalid input detected at
4// '^' marker.", "% Incomplete command.", "% Ambiguous command:")
5//
6//   Tree::new()                          an empty tree of commands of type C (an app's enum)
7//   mode(prompt, title, parent, no_help) a mode: its prompt after the host name ("(config)#"),
8//                                        the title of its "?" list, the mode its commands fall
9//                                        back to, the help of its "no" (None: no "no")
10//   add(mode, path, help, cmd, flags)    a command path; shared prefixes merge, the last word
11//                                        gets the help text, `cmd` makes the path complete
12//   also(mode, path, target)             the words after `target` may follow `path` too
13//                                        (options in any order)
14//   parse(mode, line, &dyn Names)        -> Result<Parsed<C>, Error>
15//   help(mode, line, &dyn Names)         -> Help: the answer to "?" typed after `line`
16//   complete(mode, line, &dyn Names)     -> Completion (Tab)
17//   Error::render / Help::render         the text an IOS-like console shows
18//   find_mode(prompt)                    a mode by its prompt (modes that parts of an app add
19//                                        need no fixed number)
20//
21// Paths are words separated by spaces.  A keyword stands for itself ("shutdown"; "access=mode"
22// stores it under the key "mode").  A parameter is TYPE or TYPE=key (a later value of a key
23// hides an earlier one, so "mode access=mode" reads as "access"):
24//
25//   <lo-hi>      a decimal number in the range          Val::Num
26//   A.B.C.D      an IPv4 address                         Val::Ipv4
27//   MASK         a contiguous dotted netmask              Val::Mask (its length)
28//   A.B.C.D/nn   an address with a prefix length          Val::Prefix
29//   X:X:X:X::X   an IPv6 address (every RFC 4291 form)    Val::Ipv6
30//   X:X:X:X::X/<0-128>   an IPv6 address with a prefix length   Val::Prefix6
31//   H.H.H        a MAC address (H.H.H or with colons)     Val::Mac
32//   WORD         one word                                 Val::Word
33//   LINE         the rest of the line, spaces included    Val::Line
34//   NAME, NAME1 .. NAME9   one of the caller's names (lists 0 .. 9, interfaces): a type and a
35//                number ("gi0/0/0", "Gi 0/0/0", "lo0"), the type abbreviated like a keyword,
36//                also as two words                       Val::Name (the full name)
37//
38// Matching is IOS-like: keywords ignore case and may be shortened to any prefix that no other
39// word of that place shares; a word that is a whole keyword wins; numbers, addresses and names
40// must be valid; WORD and LINE take what no keyword takes.  The error position is the first
41// character no candidate accepts (the "^" marker).  "no" is a keyword of every mode that has
42// it; it leads to the commands added with F_NO, and a no-form may end early where add_no says.
43// Help lists keywords and parameters in ASCII order (parameters and capitalised interface
44// types first) with "<cr>" when the command may end there.
45
46use alloc::format;
47use alloc::string::String;
48use alloc::vec::Vec;
49
50/// the index of a node of a tree
51pub type NodeId = u32;
52/// the index of a mode
53pub type ModeId = u16;
54
55/// a command may be negated ("no shutdown"); set on its first word
56pub const F_NO: u8 = 1;
57/// not listed by help and completion (aliases, commands of the configuration file only)
58pub const F_HIDDEN: u8 = 2;
59
60/// a typed parameter
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum Param {
64    Num(i64, i64),
65    Ipv4,
66    Mask,
67    Prefix,
68    Mac,
69    Word,
70    Line,
71    Name(u8),
72    Ipv6,
73    Prefix6,
74}
75
76impl Param {
77    /// what help shows for it ("<1-4094>", "A.B.C.D", "WORD")
78    pub fn label(&self) -> String {
79        match self {
80            Param::Num(a, b) => format!("<{}-{}>", a, b),
81            Param::Ipv4 | Param::Mask => String::from("A.B.C.D"),
82            Param::Prefix => String::from("A.B.C.D/nn"),
83            Param::Mac => String::from("H.H.H"),
84            Param::Word => String::from("WORD"),
85            Param::Line => String::from("LINE"),
86            Param::Name(_) => String::from("NAME"),
87            Param::Ipv6 => String::from("X:X:X:X::X"),
88            Param::Prefix6 => String::from("X:X:X:X::X/<0-128>"),
89        }
90    }
91    fn of(s: &str) -> Option<Param> {
92        Some(match s {
93            "A.B.C.D" => Param::Ipv4,
94            "MASK" => Param::Mask,
95            "A.B.C.D/nn" => Param::Prefix,
96            "H.H.H" => Param::Mac,
97            "WORD" => Param::Word,
98            "LINE" => Param::Line,
99            "NAME" => Param::Name(0),
100            "X:X:X:X::X" => Param::Ipv6,
101            "X:X:X:X::X/<0-128>" => Param::Prefix6,
102            _ => {
103                if let Some(d) = s.strip_prefix("NAME")
104                    && d.len() == 1
105                    && let Some(n) = d.chars().next().and_then(|c| c.to_digit(10))
106                {
107                    return Some(Param::Name(n as u8));
108                }
109                let r = s.strip_prefix('<')?.strip_suffix('>')?;
110                // "<-5-5>" is allowed: the dash after the first digit separates
111                let cut = r.char_indices().skip(1).find(|&(_, c)| c == '-').map(|(i, _)| i)?;
112                Param::Num(r[..cut].parse().ok()?, r[cut + 1..].parse().ok()?)
113            }
114        })
115    }
116}
117
118/// a value of a parsed command
119#[derive(Clone, Debug, PartialEq, Eq)]
120#[non_exhaustive]
121pub enum Val {
122    /// a keyword, written out in full
123    Key(String),
124    Num(i64),
125    Ipv4([u8; 4]),
126    Mask(u8),
127    Prefix([u8; 4], u8),
128    Mac([u8; 6]),
129    Word(String),
130    Line(String),
131    Name(String),
132    Ipv6([u8; 16]),
133    Prefix6([u8; 16], u8),
134}
135
136/// the names a NAME parameter takes (interfaces of a device); "*" in a name stands for a number
137/// ("Loopback*", "GigabitEthernet0/0/0.*")
138pub trait Names {
139    fn names(&self, list: u8) -> Vec<String>;
140    /// the help text of a type of names ("GigabitEthernet" -> "GigabitEthernet IEEE 802.3z")
141    fn type_help(&self, _list: u8, ty: &str) -> String {
142        format!("{} interface", ty)
143    }
144}
145
146/// no names at all
147pub struct NoNames;
148
149impl Names for NoNames {
150    fn names(&self, _list: u8) -> Vec<String> {
151        Vec::new()
152    }
153}
154
155#[derive(Clone, Debug)]
156struct Node<C> {
157    /// the keyword ("" for a parameter)
158    word: String,
159    param: Option<Param>,
160    key: String,
161    help: String,
162    next: Vec<NodeId>,
163    /// the words after this node may also follow it
164    also: Option<NodeId>,
165    cmd: Option<C>,
166    /// the command when the no-form ends here
167    no_cmd: Option<C>,
168    flags: u8,
169}
170
171/// a mode of the command line
172#[derive(Clone, Debug)]
173pub struct Mode {
174    /// what follows the host name in the prompt ("#", "(config-if)#")
175    pub prompt: String,
176    /// the first line of the "?" list ("Commands at the user level:")
177    pub title: String,
178    /// where a command that is not one of this mode goes (configuration submodes: the global
179    /// configuration); the caller decides whether it tries there
180    pub parent: Option<ModeId>,
181    root: NodeId,
182    /// the "no" keyword of this mode
183    no: Option<NodeId>,
184}
185
186/// a parsed command line
187#[derive(Clone, Debug, PartialEq, Eq)]
188pub struct Parsed<C> {
189    pub cmd: C,
190    /// the line started with "no"
191    pub no: bool,
192    /// the keywords and parameters in order, by key
193    pub vals: Vec<(String, Val)>,
194}
195
196impl<C> Parsed<C> {
197    /// the value of a key; a later one hides an earlier one ("name WORD=name" gives the word,
198    /// "ping X repeat 5 repeat 7" the 7)
199    pub fn get(&self, key: &str) -> Option<&Val> {
200        self.vals.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v)
201    }
202    /// the keyword or parameter is there
203    pub fn has(&self, key: &str) -> bool {
204        self.get(key).is_some()
205    }
206    pub fn num(&self, key: &str) -> Option<i64> {
207        match self.get(key)? {
208            Val::Num(n) => Some(*n),
209            _ => None,
210        }
211    }
212    pub fn ip(&self, key: &str) -> Option<[u8; 4]> {
213        match self.get(key)? {
214            Val::Ipv4(a) => Some(*a),
215            _ => None,
216        }
217    }
218    /// a netmask's length
219    pub fn mask(&self, key: &str) -> Option<u8> {
220        match self.get(key)? {
221            Val::Mask(m) => Some(*m),
222            _ => None,
223        }
224    }
225    pub fn prefix(&self, key: &str) -> Option<([u8; 4], u8)> {
226        match self.get(key)? {
227            Val::Prefix(a, l) => Some((*a, *l)),
228            _ => None,
229        }
230    }
231    pub fn mac(&self, key: &str) -> Option<[u8; 6]> {
232        match self.get(key)? {
233            Val::Mac(m) => Some(*m),
234            _ => None,
235        }
236    }
237    pub fn ip6(&self, key: &str) -> Option<[u8; 16]> {
238        match self.get(key)? {
239            Val::Ipv6(a) => Some(*a),
240            _ => None,
241        }
242    }
243    /// an IPv6 address with its prefix length
244    pub fn prefix6(&self, key: &str) -> Option<([u8; 16], u8)> {
245        match self.get(key)? {
246            Val::Prefix6(a, l) => Some((*a, *l)),
247            _ => None,
248        }
249    }
250    /// a word, line, name or keyword as text
251    pub fn text(&self, key: &str) -> Option<&str> {
252        match self.get(key)? {
253            Val::Key(s) | Val::Word(s) | Val::Line(s) | Val::Name(s) => Some(s),
254            _ => None,
255        }
256    }
257}
258
259/// why a line does not parse
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261#[non_exhaustive]
262pub enum Error {
263    /// nothing was typed
264    Empty,
265    /// no command goes on at this byte of the line (the "^" marker)
266    Invalid(usize),
267    /// the line ends before the command does
268    Incomplete,
269    /// the word at this byte fits several commands
270    Ambiguous(usize),
271}
272
273impl Error {
274    /// the console text; `col` is where the line starts on the screen (the prompt's
275    /// length), so the marker stands under the bad character
276    pub fn render(&self, col: usize, line: &str) -> String {
277        match self {
278            Error::Empty => String::new(),
279            Error::Invalid(at) => {
280                let chars = line.char_indices().take_while(|&(i, _)| i < *at).count();
281                let mut s: String = core::iter::repeat_n(' ', col + chars).collect();
282                s.push_str("^\n% Invalid input detected at '^' marker.\n\n");
283                s
284            }
285            Error::Incomplete => String::from("% Incomplete command.\n\n"),
286            Error::Ambiguous(_) => format!("% Ambiguous command:  \"{}\"\n", line.trim()),
287        }
288    }
289}
290
291/// the answer to "?"
292#[derive(Clone, Debug, PartialEq, Eq)]
293#[non_exhaustive]
294pub enum Help {
295    /// what may follow: (label, description) in display order; `cr` when the command may end
296    /// here; the mode's title for an empty line
297    List { title: Option<String>, items: Vec<(String, String)>, cr: bool },
298    /// the keywords a partly typed word may become
299    Words(Vec<String>),
300    /// nothing starts like the partly typed word
301    Unrecognized,
302    Error(Error),
303}
304
305impl Help {
306    /// the console text (`col`: the prompt's length, for an error marker)
307    pub fn render(&self, col: usize, line: &str) -> String {
308        match self {
309            Help::List { title, items, cr } => {
310                let mut s = String::new();
311                if let Some(t) = title {
312                    s.push_str(t);
313                    s.push('\n');
314                }
315                let w = items.iter().map(|(n, _)| n.chars().count()).max().unwrap_or(0) + 2;
316                for (n, d) in items {
317                    if d.is_empty() {
318                        s.push_str(&format!("  {}\n", n));
319                    } else {
320                        s.push_str(&format!("  {:<w$}{}\n", n, d, w = w));
321                    }
322                }
323                if *cr {
324                    s.push_str("  <cr>\n");
325                }
326                s.push('\n');
327                s
328            }
329            Help::Words(ws) => {
330                let mut s = String::new();
331                let mut col = 0;
332                for w in ws {
333                    if col > 0 && col + w.len() + 2 > 79 {
334                        s.push('\n');
335                        col = 0;
336                    }
337                    s.push_str(w);
338                    s.push_str("  ");
339                    col += w.len() + 2;
340                }
341                s.push_str("\n\n");
342                s
343            }
344            Help::Unrecognized => String::from("% Unrecognized command\n"),
345            Help::Error(e) => e.render(col, line),
346        }
347    }
348}
349
350/// what Tab does
351#[derive(Clone, Debug, Default, PartialEq, Eq)]
352pub struct Completion {
353    /// the line with the last word completed (None: nothing to complete, the console rings the bell)
354    pub line: Option<String>,
355    /// what the last word may become
356    pub choices: Vec<String>,
357}
358
359/// a word of a line and where it starts
360#[derive(Clone, Copy, Debug)]
361struct Tok<'a> {
362    s: &'a str,
363    at: usize,
364}
365
366fn tokens(line: &str) -> Vec<Tok<'_>> {
367    let mut v = Vec::new();
368    let b = line.as_bytes();
369    let mut i = 0;
370    while i < b.len() {
371        while i < b.len() && b[i].is_ascii_whitespace() {
372            i += 1;
373        }
374        let start = i;
375        while i < b.len() && !b[i].is_ascii_whitespace() {
376            i += 1;
377        }
378        if i > start {
379            v.push(Tok { s: &line[start..i], at: start });
380        }
381    }
382    v
383}
384
385/// a common prefix in characters, ignoring ASCII case
386fn common(a: &str, b: &str) -> usize {
387    a.bytes().zip(b.bytes()).take_while(|(x, y)| x.eq_ignore_ascii_case(y)).count()
388}
389
390fn starts_ci(word: &str, part: &str) -> bool {
391    word.len() >= part.len() && word.as_bytes()[..part.len()].eq_ignore_ascii_case(part.as_bytes())
392}
393
394/// "10.1.2.3" -> [10, 1, 2, 3] (four decimal parts of at most 255, no signs)
395pub fn parse_ipv4(s: &str) -> Option<[u8; 4]> {
396    let mut out = [0u8; 4];
397    let mut n = 0;
398    for part in s.split('.') {
399        if n == 4 || part.is_empty() || part.len() > 3 || !part.bytes().all(|c| c.is_ascii_digit()) {
400            return None;
401        }
402        let v: u32 = part.parse().ok()?;
403        if v > 255 {
404            return None;
405        }
406        out[n] = v as u8;
407        n += 1;
408    }
409    if n == 4 { Some(out) } else { None }
410}
411
412/// the length of a contiguous dotted netmask ("255.255.255.0" -> 24)
413pub fn parse_mask(s: &str) -> Option<u8> {
414    let a = parse_ipv4(s)?;
415    let v = u32::from_be_bytes(a);
416    let len = v.leading_ones();
417    if v.checked_shl(len).unwrap_or(0) == 0 { Some(len as u8) } else { None }
418}
419
420/// "aabb.ccdd.eeff", "aa:bb:cc:dd:ee:ff" or "aa-bb-cc-dd-ee-ff"
421pub fn parse_mac(s: &str) -> Option<[u8; 6]> {
422    let hex: String = match s.len() {
423        14 if s.as_bytes()[4] == b'.' && s.as_bytes()[9] == b'.' => s.split('.').collect(),
424        17 => {
425            let sep = s.as_bytes()[2];
426            if (sep != b':' && sep != b'-') || s.bytes().enumerate().any(|(i, c)| (i % 3 == 2) != (c == sep)) {
427                return None;
428            }
429            s.split(sep as char).collect()
430        }
431        _ => return None,
432    };
433    if hex.len() != 12 || !hex.bytes().all(|c| c.is_ascii_hexdigit()) {
434        return None;
435    }
436    let mut m = [0u8; 6];
437    for (i, o) in m.iter_mut().enumerate() {
438        *o = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).ok()?;
439    }
440    Some(m)
441}
442
443/// an IPv6 address in any RFC 4291 text form: eight groups of up to four hex digits, "::" once
444/// for a run of zero groups, the last 32 bits as dotted decimal ("::ffff:10.0.0.1"); hex digits
445/// in either case
446pub fn parse_ipv6(s: &str) -> Option<[u8; 16]> {
447    if s.is_empty() || s.len() > 45 {
448        return None;
449    }
450    // the groups before and after "::" (one side only without it)
451    let (head, tail, gap) = match s.find("::") {
452        Some(p) if s[p + 2..].contains("::") => return None,
453        Some(p) => (&s[..p], &s[p + 2..], true),
454        None => (s, "", false),
455    };
456    let groups = |part: &str, last: bool| -> Option<Vec<u16>> {
457        let mut v: Vec<u16> = Vec::new();
458        if part.is_empty() {
459            return Some(v);
460        }
461        let pieces: Vec<&str> = part.split(':').collect();
462        for (i, p) in pieces.iter().enumerate() {
463            if last && i + 1 == pieces.len() && p.contains('.') {
464                let a = parse_ipv4(p)?;
465                v.push(u16::from_be_bytes([a[0], a[1]]));
466                v.push(u16::from_be_bytes([a[2], a[3]]));
467            } else if p.is_empty() || p.len() > 4 || !p.bytes().all(|b| b.is_ascii_hexdigit()) {
468                return None;
469            } else {
470                v.push(u16::from_str_radix(p, 16).ok()?);
471            }
472        }
473        Some(v)
474    };
475    let front = groups(head, !gap)?;
476    let back = if gap { groups(tail, true)? } else { Vec::new() };
477    let n = front.len() + back.len();
478    if (gap && n > 7) || (!gap && n != 8) {
479        return None;
480    }
481    let mut seg = [0u16; 8];
482    seg[..front.len()].copy_from_slice(&front);
483    seg[8 - back.len()..].copy_from_slice(&back);
484    let mut out = [0u8; 16];
485    for (i, g) in seg.iter().enumerate() {
486        out[2 * i..2 * i + 2].copy_from_slice(&g.to_be_bytes());
487    }
488    Some(out)
489}
490
491/// the value of a typed parameter for a word (WORD, LINE and NAME are decided elsewhere)
492fn typed_value(p: Param, s: &str) -> Option<Val> {
493    match p {
494        Param::Num(lo, hi) => {
495            if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
496                return None;
497            }
498            let v: i64 = s.parse().ok()?;
499            if (lo..=hi).contains(&v) { Some(Val::Num(v)) } else { None }
500        }
501        Param::Ipv4 => parse_ipv4(s).map(Val::Ipv4),
502        Param::Mask => parse_mask(s).map(Val::Mask),
503        Param::Prefix => {
504            let (a, l) = s.split_once('/')?;
505            let a = parse_ipv4(a)?;
506            if l.is_empty() || l.len() > 2 || !l.bytes().all(|b| b.is_ascii_digit()) {
507                return None;
508            }
509            let l: u8 = l.parse().ok()?;
510            if l <= 32 { Some(Val::Prefix(a, l)) } else { None }
511        }
512        Param::Mac => parse_mac(s).map(Val::Mac),
513        Param::Ipv6 => parse_ipv6(s).map(Val::Ipv6),
514        Param::Prefix6 => {
515            let (a, l) = s.split_once('/')?;
516            let a = parse_ipv6(a)?;
517            if l.is_empty() || l.len() > 3 || !l.bytes().all(|b| b.is_ascii_digit()) {
518                return None;
519            }
520            let l: u8 = l.parse().ok()?;
521            if l <= 128 { Some(Val::Prefix6(a, l)) } else { None }
522        }
523        Param::Word | Param::Line | Param::Name(_) => None,
524    }
525}
526
527/// the type part of a name and its number: the number is the tail of digits, "/", "." and ":"
528/// (or "*") that starts with a digit, so types may hold digits themselves
529/// ("GigabitEthernet0/0/0.10" -> "GigabitEthernet", "Dot11Radio0" -> "Dot11Radio",
530/// "Loopback*" -> "Loopback")
531fn split_name(s: &str) -> (&str, &str) {
532    let b = s.as_bytes();
533    let mut at = b.len();
534    while at > 0 && (b[at - 1].is_ascii_digit() || b"/.:*".contains(&b[at - 1])) {
535        at -= 1;
536    }
537    while at < b.len() && !(b[at].is_ascii_digit() || b[at] == b'*') {
538        at += 1;
539    }
540    (&s[..at], &s[at..])
541}
542
543/// a name against a pattern where "*" is one or more digits (ASCII case ignored)
544fn name_fits(pattern: &str, name: &str) -> bool {
545    let (p, n) = (pattern.as_bytes(), name.as_bytes());
546    let (mut i, mut j) = (0, 0);
547    while i < p.len() {
548        if p[i] == b'*' {
549            let start = j;
550            while j < n.len() && n[j].is_ascii_digit() {
551                j += 1;
552            }
553            if j == start {
554                return false;
555            }
556            i += 1;
557        } else {
558            if j >= n.len() || !p[i].eq_ignore_ascii_case(&n[j]) {
559                return false;
560            }
561            i += 1;
562            j += 1;
563        }
564    }
565    j == n.len()
566}
567
568/// how a NAME parameter takes words
569enum NameFit {
570    /// the full name, words used
571    Name(String, usize),
572    /// the type is known, the number is missing (end of the line)
573    NeedNumber,
574    /// no type of the list starts like this (error at the word)
575    NoType,
576    /// several types start like this
577    AmbiguousType,
578    /// the type fits, the number does not (error at the number)
579    BadNumber(usize),
580}
581
582/// the distinct types of a list of names, in order
583fn name_types(names: &[String]) -> Vec<String> {
584    let mut v: Vec<String> = Vec::new();
585    for n in names {
586        let t = split_name(n).0;
587        if !t.is_empty() && !v.iter().any(|x| x.eq_ignore_ascii_case(t)) {
588            v.push(String::from(t));
589        }
590    }
591    v
592}
593
594/// the type a partial or whole type word stands for
595fn pick_type<'a>(types: &'a [String], t: &str) -> Result<&'a str, bool> {
596    if let Some(x) = types.iter().find(|x| x.eq_ignore_ascii_case(t)) {
597        return Ok(x);
598    }
599    let m: Vec<&String> = types.iter().filter(|x| starts_ci(x, t)).collect();
600    match m.len() {
601        1 => Ok(m[0]),
602        0 => Err(false),
603        _ => Err(true),
604    }
605}
606
607fn fit_name(names: &[String], toks: &[Tok], i: usize) -> NameFit {
608    let t = toks[i];
609    let (ty, num) = split_name(t.s);
610    if ty.is_empty() {
611        return NameFit::NoType;
612    }
613    let types = name_types(names);
614    let full_ty = match pick_type(&types, ty) {
615        Ok(x) => x,
616        Err(true) => return NameFit::AmbiguousType,
617        Err(false) => return NameFit::NoType,
618    };
619    let (num, used, num_at) = if num.is_empty() {
620        match toks.get(i + 1) {
621            Some(n) if n.s.starts_with(|c: char| c.is_ascii_digit()) => (n.s, 2, n.at),
622            Some(n) => return NameFit::BadNumber(n.at),
623            None => return NameFit::NeedNumber,
624        }
625    } else {
626        (num, 1, t.at + ty.len())
627    };
628    let want = format!("{}{}", full_ty, num);
629    if let Some(n) = names.iter().find(|n| !n.contains('*') && n.eq_ignore_ascii_case(&want)) {
630        return NameFit::Name(n.clone(), used);
631    }
632    if names.iter().any(|n| n.contains('*') && name_fits(n, &want)) {
633        return NameFit::Name(want, used);
634    }
635    NameFit::BadNumber(num_at)
636}
637
638/// one step of a walk: the node a word leads to
639enum Pick {
640    One { node: NodeId, val: Val, used: usize },
641    None(usize),
642    Ambiguous,
643    Incomplete,
644}
645
646/// a tree of commands in modes
647#[derive(Clone, Debug)]
648pub struct Tree<C> {
649    nodes: Vec<Node<C>>,
650    modes: Vec<Mode>,
651}
652
653impl<C: Copy> Default for Tree<C> {
654    fn default() -> Tree<C> {
655        Tree::new()
656    }
657}
658
659impl<C: Copy> Tree<C> {
660    pub fn new() -> Tree<C> {
661        Tree { nodes: Vec::new(), modes: Vec::new() }
662    }
663
664    fn new_node(&mut self, word: &str, param: Option<Param>, key: &str) -> NodeId {
665        self.nodes.push(Node { word: String::from(word), param, key: String::from(key), help: String::new(), next: Vec::new(), also: None, cmd: None, no_cmd: None, flags: 0 });
666        (self.nodes.len() - 1) as NodeId
667    }
668
669    /// a new mode; `no`: a "no" keyword negates its commands (`no_help` is its help text)
670    pub fn mode(&mut self, prompt: &str, title: &str, parent: Option<ModeId>, no_help: Option<&str>) -> ModeId {
671        let root = self.new_node("", None, "");
672        let no = no_help.map(|h| {
673            let n = self.new_node("no", None, "no");
674            self.nodes[n as usize].help = String::from(h);
675            n
676        });
677        self.modes.push(Mode { prompt: String::from(prompt), title: String::from(title), parent, root, no });
678        (self.modes.len() - 1) as ModeId
679    }
680    pub fn mode_info(&self, m: ModeId) -> &Mode {
681        &self.modes[m as usize]
682    }
683    /// the mode with this prompt suffix ("(config-ext-nacl)#"; the first one made)
684    pub fn find_mode(&self, prompt: &str) -> Option<ModeId> {
685        self.modes.iter().position(|m| m.prompt == prompt).map(|i| i as ModeId)
686    }
687    /// the prompt of a mode for a host name ("R1(config)#")
688    pub fn prompt(&self, m: ModeId, host: &str) -> String {
689        format!("{}{}", host, self.modes[m as usize].prompt)
690    }
691
692    /// the node a path word stands for under `at`, made when missing
693    fn step(&mut self, at: NodeId, w: &str) -> NodeId {
694        let (body, key) = match w.split_once('=') {
695            Some((b, k)) => (b, k),
696            None => (w, ""),
697        };
698        let (word, param) = match Param::of(body) {
699            Some(p) => ("", Some(p)),
700            None => (body, None),
701        };
702        let key = if key.is_empty() { body.to_ascii_lowercase() } else { String::from(key) };
703        let found = self.nodes[at as usize].next.iter().copied().find(|&n| {
704            let x = &self.nodes[n as usize];
705            x.param == param && x.word.eq_ignore_ascii_case(word) && x.key == key
706        });
707        match found {
708            Some(n) => n,
709            None => {
710                let n = self.new_node(word, param, &key);
711                self.nodes[at as usize].next.push(n);
712                n
713            }
714        }
715    }
716
717    /// the node at the end of a path (made when missing)
718    fn path(&mut self, mode: ModeId, path: &str) -> NodeId {
719        let mut at = self.modes[mode as usize].root;
720        for w in path.split_whitespace() {
721            at = self.step(at, w);
722        }
723        at
724    }
725
726    /// adds a command path: `help` describes its last word; `cmd` makes the command complete
727    /// there; `flags` (F_NO, F_HIDDEN) go to the last word
728    pub fn add(&mut self, mode: ModeId, path: &str, help: &str, cmd: Option<C>, flags: u8) -> NodeId {
729        let n = self.path(mode, path);
730        let node = &mut self.nodes[n as usize];
731        if !help.is_empty() {
732            node.help = String::from(help);
733        }
734        if cmd.is_some() {
735            node.cmd = cmd;
736        }
737        node.flags |= flags;
738        n
739    }
740    /// the no-form of a command may end at `path` ("no ip address" without an address)
741    pub fn add_no(&mut self, mode: ModeId, path: &str, cmd: C) {
742        let n = self.path(mode, path);
743        self.nodes[n as usize].no_cmd = Some(cmd);
744    }
745    /// after `path`, the words that follow `target` may follow as well (a list of options in
746    /// any order: "ping A repeat 5 size 100")
747    pub fn also(&mut self, mode: ModeId, path: &str, target: &str) {
748        let t = self.path(mode, target);
749        let n = self.path(mode, path);
750        if n != t {
751            self.nodes[n as usize].also = Some(t);
752        }
753    }
754
755    /// the nodes that may follow `at` (`no`: the no keyword of `mode` stands there)
756    fn children(&self, mode: ModeId, at: NodeId) -> Vec<NodeId> {
757        let m = &self.modes[mode as usize];
758        if Some(at) == m.no {
759            return self.nodes[m.root as usize].next.iter().copied().filter(|&n| self.nodes[n as usize].flags & F_NO != 0).collect();
760        }
761        let node = &self.nodes[at as usize];
762        let mut v = node.next.clone();
763        if at == m.root
764            && let Some(no) = m.no
765        {
766            v.push(no);
767        }
768        if let Some(a) = node.also {
769            for &n in &self.nodes[a as usize].next {
770                if !v.contains(&n) {
771                    v.push(n);
772                }
773            }
774        }
775        v
776    }
777
778    /// the node word `i` leads to from `cands`
779    fn pick(&self, cands: &[NodeId], toks: &[Tok], i: usize, line: &str, names: &dyn Names) -> Pick {
780        let t = toks[i];
781        let mut exact = None;
782        let mut fits: Vec<(NodeId, Val, usize)> = Vec::new();
783        let mut loose: Vec<(NodeId, Val)> = Vec::new();
784        let mut far = t.at;
785        let mut need_number = false;
786        let mut ambiguous_type = false;
787        for &c in cands {
788            let n = &self.nodes[c as usize];
789            match n.param {
790                None => {
791                    if n.word.eq_ignore_ascii_case(t.s) {
792                        exact = Some(c);
793                    } else if starts_ci(&n.word, t.s) {
794                        fits.push((c, Val::Key(n.word.clone()), 1));
795                    } else {
796                        far = far.max(t.at + common(&n.word, t.s));
797                    }
798                }
799                Some(p) => {
800                    let v = match p {
801                        Param::Num(..) | Param::Ipv4 | Param::Mask | Param::Prefix | Param::Mac | Param::Ipv6 | Param::Prefix6 => typed_value(p, t.s),
802                        Param::Word => {
803                            loose.push((c, Val::Word(String::from(t.s))));
804                            None
805                        }
806                        Param::Line => {
807                            loose.push((c, Val::Line(String::from(line[t.at..].trim_end()))));
808                            None
809                        }
810                        Param::Name(list) => match fit_name(&names.names(list), toks, i) {
811                            NameFit::Name(s, used) => {
812                                fits.push((c, Val::Name(s), used));
813                                None
814                            }
815                            NameFit::NeedNumber => {
816                                need_number = true;
817                                None
818                            }
819                            NameFit::NoType => None,
820                            NameFit::AmbiguousType => {
821                                ambiguous_type = true;
822                                None
823                            }
824                            NameFit::BadNumber(at) => {
825                                far = far.max(at);
826                                None
827                            }
828                        },
829                    };
830                    if let Some(v) = v {
831                        fits.push((c, v, 1));
832                    }
833                }
834            }
835        }
836        if let Some(e) = exact {
837            return Pick::One { node: e, val: Val::Key(self.nodes[e as usize].word.clone()), used: 1 };
838        }
839        if fits.len() == 1 && !ambiguous_type {
840            let (node, val, used) = fits.remove(0);
841            return Pick::One { node, val, used };
842        }
843        if fits.len() > 1 || ambiguous_type {
844            return Pick::Ambiguous;
845        }
846        if need_number {
847            return Pick::Incomplete;
848        }
849        if let Some((node, val)) = loose.into_iter().next() {
850            return Pick::One { node, val, used: 1 };
851        }
852        Pick::None(far)
853    }
854
855    /// walks the words of a line: (node, no-form, values, words used) or the error
856    fn walk(&self, mode: ModeId, line: &str, toks: &[Tok], names: &dyn Names) -> Result<(NodeId, bool, Vec<(String, Val)>), Error> {
857        let m = &self.modes[mode as usize];
858        let mut at = m.root;
859        let mut no = false;
860        let mut vals = Vec::new();
861        let mut i = 0;
862        while i < toks.len() {
863            let cands = self.children(mode, at);
864            match self.pick(&cands, toks, i, line, names) {
865                Pick::One { node, val, used } => {
866                    if Some(node) == m.no {
867                        no = true;
868                    } else {
869                        vals.push((self.nodes[node as usize].key.clone(), val));
870                    }
871                    at = node;
872                    if self.nodes[node as usize].param == Some(Param::Line) {
873                        return Ok((at, no, vals));
874                    }
875                    i += used;
876                }
877                Pick::None(far) => return Err(Error::Invalid(far)),
878                Pick::Ambiguous => return Err(Error::Ambiguous(toks[i].at)),
879                Pick::Incomplete => return Err(Error::Incomplete),
880            }
881        }
882        Ok((at, no, vals))
883    }
884
885    /// the command a line of `mode` is
886    pub fn parse(&self, mode: ModeId, line: &str, names: &dyn Names) -> Result<Parsed<C>, Error> {
887        let toks = tokens(line);
888        if toks.is_empty() {
889            return Err(Error::Empty);
890        }
891        let (at, no, vals) = self.walk(mode, line, &toks, names)?;
892        let n = &self.nodes[at as usize];
893        let cmd = if no { n.no_cmd.or(n.cmd) } else { n.cmd };
894        match cmd {
895            Some(cmd) => Ok(Parsed { cmd, no, vals }),
896            None => Err(Error::Incomplete),
897        }
898    }
899
900    /// the labels and help of the nodes that may follow `at`
901    fn listing(&self, mode: ModeId, at: NodeId, names: &dyn Names) -> Vec<(String, String)> {
902        let mut items: Vec<(String, String)> = Vec::new();
903        for c in self.children(mode, at) {
904            let n = &self.nodes[c as usize];
905            if n.flags & F_HIDDEN != 0 {
906                continue;
907            }
908            match n.param {
909                None => items.push((n.word.clone(), n.help.clone())),
910                Some(Param::Name(list)) => {
911                    for ty in name_types(&names.names(list)) {
912                        let h = names.type_help(list, &ty);
913                        if !items.iter().any(|(l, _)| *l == ty) {
914                            items.push((ty, h));
915                        }
916                    }
917                }
918                Some(p) => items.push((p.label(), n.help.clone())),
919            }
920        }
921        items.sort_by(|a, b| a.0.cmp(&b.0));
922        items.dedup_by(|a, b| a.0 == b.0);
923        items
924    }
925
926    fn complete_here(&self, at: NodeId, no: bool) -> bool {
927        let n = &self.nodes[at as usize];
928        n.cmd.is_some() || (no && n.no_cmd.is_some())
929    }
930
931    /// the answer to "?" typed right after `line` (a partly typed last word lists what it may
932    /// become; after a space, what may follow)
933    pub fn help(&self, mode: ModeId, line: &str, names: &dyn Names) -> Help {
934        let toks = tokens(line);
935        let partial = !line.is_empty() && !line.ends_with(|c: char| c.is_ascii_whitespace());
936        let done = if partial { &toks[..toks.len() - 1] } else { &toks[..] };
937        let (at, no, _) = match self.walk(mode, line, done, names) {
938            Ok(x) => x,
939            Err(e) => return Help::Error(e),
940        };
941        if self.nodes[at as usize].param == Some(Param::Line) && !done.is_empty() {
942            return Help::List { title: None, items: Vec::new(), cr: true };
943        }
944        if partial {
945            let part = toks[toks.len() - 1].s;
946            let mut ws: Vec<String> = Vec::new();
947            for c in self.children(mode, at) {
948                let n = &self.nodes[c as usize];
949                if n.flags & F_HIDDEN != 0 {
950                    continue;
951                }
952                match n.param {
953                    None if starts_ci(&n.word, part) => ws.push(n.word.clone()),
954                    None => {}
955                    Some(Param::Name(list)) => {
956                        let (ty, num) = split_name(part);
957                        if num.is_empty() {
958                            for t in name_types(&names.names(list)) {
959                                if starts_ci(&t, ty) {
960                                    ws.push(t);
961                                }
962                            }
963                        }
964                    }
965                    Some(p) => {
966                        if typed_value(p, part).is_some() {
967                            ws.push(p.label());
968                        }
969                    }
970                }
971            }
972            ws.sort();
973            ws.dedup();
974            return if ws.is_empty() { Help::Unrecognized } else { Help::Words(ws) };
975        }
976        let title = if toks.is_empty() { Some(self.modes[mode as usize].title.clone()) } else { None };
977        Help::List { title, items: self.listing(mode, at, names), cr: self.complete_here(at, no) }
978    }
979
980    /// Tab: the last, partly typed word written out when only one keyword or name type fits
981    pub fn complete(&self, mode: ModeId, line: &str, names: &dyn Names) -> Completion {
982        let toks = tokens(line);
983        if line.is_empty() || line.ends_with(|c: char| c.is_ascii_whitespace()) || toks.is_empty() {
984            return Completion::default();
985        }
986        let last = toks[toks.len() - 1];
987        let Ok((at, _, _)) = self.walk(mode, line, &toks[..toks.len() - 1], names) else {
988            return Completion::default();
989        };
990        let mut words: Vec<String> = Vec::new();
991        let mut types: Vec<String> = Vec::new();
992        for c in self.children(mode, at) {
993            let n = &self.nodes[c as usize];
994            if n.flags & F_HIDDEN != 0 && !n.word.eq_ignore_ascii_case(last.s) {
995                continue;
996            }
997            match n.param {
998                None if starts_ci(&n.word, last.s) => words.push(n.word.clone()),
999                Some(Param::Name(list)) => {
1000                    let (ty, num) = split_name(last.s);
1001                    if num.is_empty() && !ty.is_empty() {
1002                        for t in name_types(&names.names(list)) {
1003                            if starts_ci(&t, ty) && !types.contains(&t) {
1004                                types.push(t);
1005                            }
1006                        }
1007                    }
1008                }
1009                _ => {}
1010            }
1011        }
1012        let exact = words.iter().find(|w| w.eq_ignore_ascii_case(last.s)).cloned();
1013        let mut choices: Vec<String> = words.iter().chain(types.iter()).cloned().collect();
1014        choices.sort();
1015        let head = &line[..last.at];
1016        let out = match (exact, words.len() + types.len()) {
1017            (Some(w), _) => Some(format!("{}{} ", head, w)),
1018            (None, 1) if words.len() == 1 => Some(format!("{}{} ", head, words[0])),
1019            (None, 1) => Some(format!("{}{}", head, types[0])),
1020            _ => None,
1021        };
1022        Completion { line: out, choices }
1023    }
1024
1025    /// does the first word of a line name a command of `mode` (a keyword or the "no" of the
1026    /// mode, abbreviations included)?  An IOS-like EXEC answers unknown first words with a host
1027    /// name lookup instead of the marker.
1028    pub fn knows_first(&self, mode: ModeId, line: &str) -> bool {
1029        let Some(t) = tokens(line).first().copied() else {
1030            return false;
1031        };
1032        let m = &self.modes[mode as usize];
1033        self.children(mode, m.root).iter().any(|&c| {
1034            let n = &self.nodes[c as usize];
1035            n.param.is_some() || starts_ci(&n.word, t.s)
1036        })
1037    }
1038}