diff options
Diffstat (limited to 'sq')
-rw-r--r-- | sq/Cargo.toml | 62 | ||||
-rw-r--r-- | sq/Makefile | 45 | ||||
-rw-r--r-- | sq/build.rs | 22 | ||||
-rw-r--r-- | sq/make-usage.sh | 54 | ||||
-rw-r--r-- | sq/src/commands/decrypt.rs | 354 | ||||
-rw-r--r-- | sq/src/commands/dump.rs | 946 | ||||
-rw-r--r-- | sq/src/commands/inspect.rs | 412 | ||||
-rw-r--r-- | sq/src/commands/key.rs | 248 | ||||
-rw-r--r-- | sq/src/commands/mod.rs | 515 | ||||
-rw-r--r-- | sq/src/commands/sign.rs | 347 | ||||
-rw-r--r-- | sq/src/sq-usage.rs | 768 | ||||
-rw-r--r-- | sq/src/sq.rs | 739 | ||||
-rw-r--r-- | sq/src/sq_cli.rs | 601 | ||||
-rw-r--r-- | sq/tests/sq-sign.rs | 813 |
14 files changed, 5926 insertions, 0 deletions
diff --git a/sq/Cargo.toml b/sq/Cargo.toml new file mode 100644 index 00000000..9023b02b --- /dev/null +++ b/sq/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "sequoia-sq" +description = "Command-line frontends for Sequoia" +version = "0.19.0" +authors = [ + "Justus Winter <justus@sequoia-pgp.org>", + "Kai Michaelis <kai@sequoia-pgp.org>", + "Neal H. Walfield <neal@sequoia-pgp.org>", +] +build = "build.rs" +documentation = "https://docs.sequoia-pgp.org/0.19.0/sq" +homepage = "https://sequoia-pgp.org/" +repository = "https://gitlab.com/sequoia-pgp/sequoia" +readme = "../README.md" +keywords = ["cryptography", "openpgp", "pgp", "encryption", "signing"] +categories = ["cryptography", "authentication", "command-line-utilities"] +license = "GPL-2.0-or-later" +edition = "2018" + +[badges] +gitlab = { repository = "sequoia-pgp/sequoia" } +maintenance = { status = "actively-developed" } + +[dependencies] +buffered-reader = { path = "../buffered-reader", version = "0.18", default-features = false } +sequoia-openpgp = { path = "../openpgp", version = "0.19", default-features = false } +sequoia-autocrypt = { path = "../autocrypt", version = "0.19", default-features = false } +sequoia-core = { path = "../core", version = "0.19", default-features = false } +sequoia-ipc = { path = "../ipc", version = "0.19", default-features = false } +sequoia-net = { path = "../net", version = "0.19", default-features = false } +sequoia-store = { path = "../store", version = "0.19", default-features = false } +anyhow = "1" +chrono = "0.4" +clap = "2.32.0" +itertools = "0.9" +prettytable-rs = "0.8.0" +tempfile = "3.0.4" +crossterm = "0.13" +tokio-core = "0.1" +rpassword = "5.0" + +[build-dependencies] +clap = "2.32.0" + +[dev-dependencies] +assert_cli = "0.6" + +[[bin]] +name = "sq" +path = "src/sq-usage.rs" + +[features] +default = [ + "buffered-reader/compression", + "sequoia-openpgp/default", + "sequoia-store/background-services" +] +crypto-nettle = ["sequoia-openpgp/crypto-nettle"] +crypto-cng = ["sequoia-openpgp/crypto-cng"] +compression = ["buffered-reader/compression", "sequoia-openpgp/compression"] +compression-deflate = ["buffered-reader/compression-deflate", "sequoia-openpgp/compression-deflate"] +compression-bzip2 = ["buffered-reader/compression-bzip2", "sequoia-openpgp/compression-bzip2"] diff --git a/sq/Makefile b/sq/Makefile new file mode 100644 index 00000000..8b7e11c1 --- /dev/null +++ b/sq/Makefile @@ -0,0 +1,45 @@ +# Configuration. +CARGO_TARGET_DIR ?= $(shell pwd)/../target +# We currently only support absolute paths. +CARGO_TARGET_DIR := $(abspath $(CARGO_TARGET_DIR)) +SQ ?= $(CARGO_TARGET_DIR)/debug/sq + +# Tools. +CARGO ?= cargo + +ifneq ($(filter Darwin BSD,$(shell uname -s)),"") + INSTALL ?= ginstall +else + INSTALL ?= install +endif + +all: src/sq-usage.rs + +# Installation. +.PHONY: build-release +build-release: + CARGO_TARGET_DIR=$(CARGO_TARGET_DIR) \ + $(CARGO) build $(CARGO_FLAGS) --release --package sequoia-sq + $(MAKE) -C../store build-release + +.PHONY: install +install: build-release + $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin + $(INSTALL) -t $(DESTDIR)$(PREFIX)/bin $(CARGO_TARGET_DIR)/release/sq + $(INSTALL) -d $(DESTDIR)$(PREFIX)/share/zsh/site-functions + $(INSTALL) -t $(DESTDIR)$(PREFIX)/share/zsh/site-functions \ + $(CARGO_TARGET_DIR)/_sq + $(INSTALL) -d $(DESTDIR)$(PREFIX)/share/bash-completion/completions + $(INSTALL) $(CARGO_TARGET_DIR)/sq.bash \ + $(DESTDIR)$(PREFIX)/share/bash-completion/completions/sq + $(INSTALL) -d $(DESTDIR)$(PREFIX)/share/fish/completions + $(INSTALL) -t $(DESTDIR)$(PREFIX)/share/fish/completions \ + $(CARGO_TARGET_DIR)/sq.fish + $(MAKE) -C../store install + +# Maintenance. +.PHONY: update-usage +update-usage: src/sq-usage.rs + +src/sq-usage.rs: make-usage.sh $(SQ) + sh make-usage.sh $(SQ) >$@~ && mv $@~ $@ diff --git a/sq/build.rs b/sq/build.rs new file mode 100644 index 00000000..b7d3147d --- /dev/null +++ b/sq/build.rs @@ -0,0 +1,22 @@ +use clap; + +use std::env; +use std::fs; +use clap::Shell; + +mod sq_cli { + include!("src/sq_cli.rs"); +} + +fn main() { + let outdir = match env::var_os("CARGO_TARGET_DIR") { + None => return, + Some(outdir) => outdir, + }; + fs::create_dir_all(&outdir).unwrap(); + let mut sq = sq_cli::build(); + for shell in &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell, + Shell::Elvish] { + sq.gen_completions("sq", *shell, &outdir); + } +} diff --git a/sq/make-usage.sh b/sq/make-usage.sh new file mode 100644 index 00000000..7bcbfcf2 --- /dev/null +++ b/sq/make-usage.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +tool=$1 + +quote() { + sed 's@^@//! @' | sed 's/ $//' +} + +begin_code() { + printf '```text\n' +} + +end_code() { + printf '```\n' +} + +dump_help() { # subcommand, indentation + if [ -z "$1" ] + then + printf "\n# Usage\n\n" + set "" "#" + else + printf "\n$2 Subcommand$1\n\n" + fi + + help="`$tool $1 --help`" + + begin_code + printf "$help\n" | tail -n +2 + end_code + + if echo $help | fgrep -q SUBCOMMANDS + then + printf "$help\n" | + sed -n '/^SUBCOMMANDS:/,$p' | + tail -n+2 | + grep '^ [^ ]' | + while read subcommand desc + do + if [ "$subcommand" = help ]; then + continue + fi + + dump_help "$1 $subcommand" "#$2" + done + fi +} + +( + printf "A command-line frontend for Sequoia.\n" + dump_help +) | quote + +printf '\ninclude!("'"$(basename $tool)"'.rs");\n' diff --git a/sq/src/commands/decrypt.rs b/sq/src/commands/decrypt.rs new file mode 100644 index 00000000..07adabe6 --- /dev/null +++ b/sq/src/commands/decrypt.rs @@ -0,0 +1,354 @@ +use crossterm::terminal; +use anyhow::Context as _; +use std::collections::HashMap; +use std::io; +use rpassword; + +use sequoia_openpgp as openpgp; +use sequoia_core::Context; +use crate::openpgp::types::SymmetricAlgorithm; +use crate::openpgp::fmt::hex; +use crate::openpgp::crypto::{self, SessionKey}; +use crate::openpgp::{Fingerprint, Cert, KeyID, Result}; +use crate::openpgp::packet; +use crate::openpgp::packet::prelude::*; +use crate::openpgp::parse::{ + Parse, + PacketParser, + PacketParserResult, +}; +use crate::openpgp::parse::stream::{ + VerificationHelper, DecryptionHelper, DecryptorBuilder, MessageStructure, +}; +use crate::openpgp::policy::Policy; +use sequoia_store as store; + +use super::{dump::PacketDumper, VHelper}; + +struct Helper<'a> { + vhelper: VHelper<'a>, + secret_keys: + HashMap<KeyID, Key<key::SecretParts, key::UnspecifiedRole>>, + key_identities: HashMap<KeyID, Fingerprint>, + key_hints: HashMap<KeyID, String>, + dump_session_key: bool, + dumper: Option<PacketDumper>, +} + +impl<'a> Helper<'a> { + fn new(ctx: &'a Context, policy: &'a dyn Policy, + mapping: &'a mut store::Mapping, + signatures: usize, certs: Vec<Cert>, secrets: Vec<Cert>, + dump_session_key: bool, dump: bool) + -> Self + { + let mut keys = HashMap::new(); + let mut identities: HashMap<KeyID, Fingerprint> = HashMap::new(); + let mut hints: HashMap<KeyID, String> = HashMap::new(); + for tsk in secrets { + let hint = match tsk.with_policy(policy, None) + .and_then(|valid_cert| valid_cert.primary_userid()).ok() + { + Some(uid) => format!("{} ({})", uid.userid(), + KeyID::from(tsk.fingerprint())), + None => format!("{}", KeyID::from(tsk.fingerprint())), + }; + + for ka in tsk.keys() + // XXX: Should use the message's creation time that we do not know. + .with_policy(policy, None) + .for_transport_encryption().for_storage_encryption() + .secret() + { + let id: KeyID = ka.key().fingerprint().into(); + keys.insert(id.clone(), ka.key().clone().into()); + identities.insert(id.clone(), tsk.fingerprint()); + hints.insert(id, hint.clone()); + } + } + + Helper { + vhelper: VHelper::new(ctx, mapping, signatures, certs), + secret_keys: keys, + key_identities: identities, + key_hints: hints, + dump_session_key: dump_session_key, + dumper: if dump { + let width = terminal::size().ok().map(|(cols, _)| cols as usize) + .unwrap_or(80); + Some(PacketDumper::new(width, false)) + } else { + None + }, + } + } + + /// Tries to decrypt the given PKESK packet with `keypair` and try + /// to decrypt the packet parser using `decrypt`. + fn try_decrypt<D>(&self, pkesk: &PKESK, + sym_algo: Option<SymmetricAlgorithm>, + keypair: &mut dyn crypto::Decryptor, + decrypt: &mut D) + -> Option<Option<Fingerprint>> + where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool + { + let keyid = keypair.public().fingerprint().into(); + match pkesk.decrypt(keypair, sym_algo) + .and_then(|(algo, sk)| { + if decrypt(algo, &sk) { Some(sk) } else { None } + }) + { + Some(sk) => { + if self.dump_session_key { + eprintln!("Session key: {}", hex::encode(&sk)); + } + Some(self.key_identities.get(&keyid).map(|fp| fp.clone())) + }, + None => None, + } + } +} + +impl<'a> VerificationHelper for Helper<'a> { + fn inspect(&mut self, pp: &PacketParser) -> Result<()> { + if let Some(dumper) = self.dumper.as_mut() { + dumper.packet(&mut io::stderr(), + pp.recursion_depth() as usize, + pp.header().clone(), pp.packet.clone(), + pp.map().map(|m| m.clone()), None)?; + } + Ok(()) + } + + fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> Result<Vec<Cert>> { + self.vhelper.get_certs(ids) + } + fn check(&mut self, structure: MessageStructure) -> Result<()> { + self.vhelper.check(structure) + } +} + +impl<'a> DecryptionHelper for Helper<'a> { + fn decrypt<D>(&mut self, pkesks: &[PKESK], skesks: &[SKESK], + sym_algo: Option<SymmetricAlgorithm>, + mut decrypt: D) -> openpgp::Result<Option<Fingerprint>> + where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool + { + // First, we try those keys that we can use without prompting + // for a password. + for pkesk in pkesks { + let keyid = pkesk.recipient(); + if let Some(key) = self.secret_keys.get(&keyid) { + if ! key.secret().is_encrypted() { + if let Some(fp) = key.clone().into_keypair().ok() + .and_then(|mut k| + self.try_decrypt(pkesk, sym_algo, &mut k, &mut decrypt)) + { + return Ok(fp); + } + } + } + } + + // Second, we try those keys that are encrypted. + for pkesk in pkesks { + // Don't ask the user to decrypt a key if we don't support + // the algorithm. + if ! pkesk.pk_algo().is_supported() { + continue; + } + + let keyid = pkesk.recipient(); + if let Some(key) = self.secret_keys.get_mut(&keyid) { + let mut keypair = loop { + if ! key.secret().is_encrypted() { + break key.clone().into_keypair().unwrap(); + } + + let p = rpassword::read_password_from_tty(Some( + &format!( + "Enter password to decrypt key {}: ", + self.key_hints.get(&keyid).unwrap())))?.into(); + + let algo = key.pk_algo(); + if let Some(()) = + key.secret_mut().decrypt_in_place(algo, &p).ok() { + break key.clone().into_keypair().unwrap() + } else { + eprintln!("Bad password."); + } + }; + + if let Some(fp) = + self.try_decrypt(pkesk, sym_algo, &mut keypair, + &mut decrypt) + { + return Ok(fp); + } + } + } + + // Third, we try to decrypt PKESK packets with wildcard + // recipients using those keys that we can use without + // prompting for a password. + for pkesk in pkesks.iter().filter(|p| p.recipient().is_wildcard()) { + for key in self.secret_keys.values() { + if ! key.secret().is_encrypted() { + if let Some(fp) = key.clone().into_keypair().ok() + .and_then(|mut k| + self.try_decrypt(pkesk, sym_algo, &mut k, &mut decrypt)) + { + return Ok(fp); + } + } + } + } + + // Fourth, we try to decrypt PKESK packets with wildcard + // recipients using those keys that are encrypted. + for pkesk in pkesks.iter().filter(|p| p.recipient().is_wildcard()) { + // Don't ask the user to decrypt a key if we don't support + // the algorithm. + if ! pkesk.pk_algo().is_supported() { + continue; + } + + // To appease the borrow checker, iterate over the + // hashmap, awkwardly. + for keyid in self.secret_keys.keys().cloned().collect::<Vec<_>>() + { + let mut keypair = loop { + let key = self.secret_keys.get_mut(&keyid).unwrap(); // Yuck + + if ! key.secret().is_encrypted() { + break key.clone().into_keypair().unwrap(); + } + + let p = rpassword::read_password_from_tty(Some( + &format!( + "Enter password to decrypt key {}: ", + self.key_hints.get(&keyid).unwrap())))?.into(); + + let algo = key.pk_algo(); + if let Some(()) = + key.secret_mut().decrypt_in_place(algo, &p).ok() { + break key.clone().into_keypair().unwrap() + } else { + eprintln!("Bad password."); + } + }; + + if let Some(fp) = + self.try_decrypt(pkesk, sym_algo, &mut keypair, + &mut decrypt) + { + return Ok(fp); + } + } + } + + if skesks.is_empty() { + return + Err(anyhow::anyhow!("No key to decrypt message")); + } + + // Finally, try to decrypt using the SKESKs. + loop { + let password = + rpassword::read_password_from_tty(Some( + "Enter password to decrypt message: "))?.into(); + + for skesk in skesks { + if let Some(sk) = skesk.decrypt(&password).ok() + .and_then(|(algo, sk)| { if decrypt(algo, &sk) { Some(sk) } else { None }}) + { + if self.dump_session_key { + eprintln!("Session key: {}", hex::encode(&sk)); + } + return Ok(None); + } + } + + eprintln!("Bad password."); + } + } +} + +pub fn decrypt(ctx: &Context, policy: &dyn Policy, mapping: &mut store::Mapping, + input: &mut dyn io::Read, output: &mut dyn io::Write, + signatures: usize, certs: Vec<Cert>, secrets: Vec<Cert>, + dump_session_key: bool, + dump: bool, hex: bool) + -> Result<()> { + let helper = Helper::new(ctx, policy, mapping, signatures, certs, secrets, + dump_session_key, dump || hex); + let mut decryptor = DecryptorBuilder::from_reader(input)? + .mapping(hex) + .with_policy(policy, None, helper) + .context("Decryption failed")?; + + io::copy(&mut decryptor, output).context("Decryption failed")?; + + let helper = decryptor.into_helper(); + if let Some(dumper) = helper.dumper.as_ref() { + dumper.flush(&mut io::stderr())?; + } + helper.vhelper.print_status(); + return Ok(()); +} + +pub fn decrypt_unwrap(ctx: &Context, policy: &dyn Policy, + mapping: &mut store::Mapping, + input: &mut dyn io::Read, output: &mut dyn io::Write, + secrets: Vec<Cert>, dump_session_key: bool) + -> Result<()> +{ + let mut helper = Helper::new(ctx, policy, mapping, 0, Vec::new(), secrets, + dump_session_key, false); + + let mut ppr = PacketParser::from_reader(input)?; + + let mut pkesks: Vec<packet::PKESK> = Vec::new(); + let mut skesks: Vec<packet::SKESK> = Vec::new(); + while let PacketParserResult::Some(mut pp) = ppr { + let sym_algo_hint = if let Packet::AED(ref aed) = pp.packet { + Some(aed.symmetric_algo()) + } else { + None + }; + + match pp.packet { + Packet::SEIP(_) | Packet::AED(_) => { + { + let decrypt = |algo, secret: &SessionKey| { + pp.decrypt(algo, secret).is_ok() + }; + helper.decrypt(&pkesks[..], &skesks[..], sym_algo_hint, + decrypt)?; + } + if pp.encrypted() { + return Err( + openpgp::Error::MissingSessionKey( + "No session key".into()).into()); + } + + io::copy(&mut pp, output)?; + return Ok(()); + }, + Packet::MDC(ref mdc) => if ! mdc.valid() { + return Err(openpgp::Error::ManipulatedMessage.into()); + }, + _ => (), + } + + let (p, ppr_tmp) = pp.recurse()?; + match p { + Packet::PKESK(pkesk) => pkesks.push(pkesk), + Packet::SKESK(skesk) => skesks.push(skesk), + _ => (), + } + ppr = ppr_tmp; + } + + Ok(()) +} diff --git a/sq/src/commands/dump.rs b/sq/src/commands/dump.rs new file mode 100644 index 00000000..6f35f56d --- /dev/null +++ b/sq/src/commands/dump.rs @@ -0,0 +1,946 @@ +use std::io::{self, Read}; + +use sequoia_openpgp as openpgp; +use self::openpgp::types::{Duration, Timestamp, SymmetricAlgorithm}; +use self::openpgp::fmt::hex; +use self::openpgp::crypto::mpi; +use self::openpgp::{Packet, Result}; +use self::openpgp::packet::prelude::*; +use self::openpgp::packet::header::CTB; +use self::openpgp::packet::{Header, header::BodyLength, Signature}; +use self::openpgp::packet::signature::subpacket::{Subpacket, SubpacketValue}; +use self::openpgp::crypto::{SessionKey, S2K}; +use self::openpgp::parse::{map::Map, Parse, PacketParserResult}; + +#[derive(Debug)] +pub enum Kind { + Message { + encrypted: bool, + }, + Keyring, + Cert, + Unknown, +} + +/// Converts sequoia_openpgp types for rendering. +pub trait Convert<T> { + /// Performs the conversion. + fn convert(self) -> T; +} + +impl Convert<chrono::Duration> for std::time::Duration { + fn convert(self) -> chrono::Duration { + chrono::Duration::seconds(self.as_secs() as i64) + } +} + +impl Convert<chrono::Duration> for Duration { + fn convert(self) -> chrono::Duration { + chrono::Duration::seconds(self.as_secs() as i64) + } +} + +impl Convert<chrono::DateTime<chrono::offset::Utc>> for std::time::SystemTime { + fn convert(self) -> chrono::DateTime<chrono::offset::Utc> { + chrono::DateTime::<chrono::offset::Utc>::from(self) + } +} + +impl Convert<chrono::DateTime<chrono::offset::Utc>> for Timestamp { + fn convert(self) -> chrono::DateTime<chrono::offset::Utc> { + std::time::SystemTime::from(self).convert() + } +} + +pub fn dump<W>(input: &mut dyn io::Read, output: &mut dyn io::Write, + mpis: bool, hex: bool, sk: Option<&SessionKey>, + width: W) + -> Result<Kind> + where W: Into<Option<usize>> +{ + let mut ppr + = self::openpgp::parse::PacketParserBuilder::from_reader(input)? + .map(hex).build()?; + let mut message_encrypted = false; + let width = width.into().unwrap_or(80); + let mut dumper = PacketDumper::new(width, mpis); + + while let PacketParserResult::Some(mut pp) = ppr { + let additional_fields = match pp.packet { + Packet::Literal(_) => { + let mut prefix = vec![0; 40]; + let n = pp.read(&mut prefix)?; + Some(vec![ + format!("Content: {:?}{}", + String::from_utf8_lossy(&prefix[..n]), + if n == prefix.len() { "..." } else { "" }), + ]) + }, + Packet::SEIP(_) if sk.is_none() => { + message_encrypted = true; + Some(vec!["No session key supplied".into()]) + } + Packet::SEIP(_) if sk.is_some() => { + message_encrypted = true; + let sk = sk.as_ref().unwrap(); + let mut decrypted_with = None; + for algo in 1..20 { + let algo = SymmetricAlgorithm::from(algo); + if let Ok(size) = algo.key_size() { + if size != sk.len() { continue; } + } else { + continue; + } + + if let Ok(_) = pp.decrypt(algo, sk) { + decrypted_with = Some(algo); + break; + } + } + let mut fields = Vec::new(); + fields.push(format!("Session key: {}", hex::encode(sk))); + if let Some(algo) = decrypted_with { + fields.push(format!("Symmetric algo: {}", algo)); + fields.push("Decryption successful".into()); + } else { + fields.push("Decryption failed".into()); + } + Some(fields) + }, + Packet::AED(_) if sk.is_none() => { + message_encrypted = true; + Some(vec!["No session key supplied".into()]) + } + Packet::AED(_) if sk.is_some() => { + message_encrypted = true; + let sk = sk.as_ref().unwrap(); + let algo = if let Packet::AED(ref aed) = pp.packet { + aed.symmetric_algo() + } else { + unreachable!() + }; + + let _ = pp.decrypt(algo, sk); + + let mut fields = Vec::new(); + fields.push(format!("Session key: {}", hex::encode(sk))); + if pp.encrypted() { + fields.push("Decryption failed".into()); + } else { + fields.push("Decryption successful".into()); + } + Some(fields) + }, + _ => None, + }; + + let header = pp.header().clone(); + let map = pp.take_map(); + + let recursion_depth = pp.recursion_depth(); + let packet = pp.packet.clone(); + + dumper.packet(output, recursion_depth as usize, + header, packet, map, additional_fields)?; + + let (_, ppr_) = match pp.recurse() { + Ok(v) => Ok(v), + Err(e) => { + let _ = dumper.flush(output); + Err(e) + }, + }?; + ppr = ppr_; + } + + dumper.flush(output)?; + + if let PacketParserResult::EOF(eof) = ppr { + if eof.is_message().is_ok() { + Ok(Kind::Message { + encrypted: message_encrypted, + }) + } else if eof.is_cert().is_ok() { + Ok(Kind::Cert) + } else if eof.is_keyring().is_ok() { + Ok(Kind::Keyring) + } else { + Ok(Kind::Unknown) + } + } else { + unreachable!() + } +} + +struct Node { + header: Header, + packet: Packet, + map: Option<Map>, + additional_fields: Option<Vec<String>>, + children: Vec<Node>, +} + +impl Node { + fn new(header: Header, packet: Packet, map: Option<Map>, + additional_fields: Option<Vec<String>>) -> Self { + Node { + header: header, + packet: packet, + map: map, + additional_fields: additional_fields, + children: Vec::new(), + } + } + + fn append(&mut self, depth: usize, node: Node) { + if depth == 0 { + self.children.push(node); + } else { + self.children.iter_mut().last().unwrap().append(depth - 1, node); + } + } |