cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

mod.rs (21803B)


      1 #![allow(dead_code)]
      2 
      3 use std::fs;
      4 use std::path::{Path, PathBuf};
      5 use std::process::{Command, Output};
      6 use std::sync::Mutex;
      7 
      8 use assert_cmd::prelude::*;
      9 use radroots_events::RadrootsNostrEvent;
     10 use radroots_events::ids::RadrootsListingAddress;
     11 use radroots_events::kinds::{KIND_FARM, KIND_LISTING};
     12 use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic};
     13 use radroots_local_events::{
     14     LocalEventRecord, LocalEventRecordInput, LocalEventsStore, LocalRecordFamily,
     15     LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime,
     16     canonical_relay_set_fingerprint,
     17 };
     18 use radroots_protected_store::RadrootsProtectedFileSecretVault;
     19 use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
     20 use radroots_secret_vault::RadrootsSecretVault;
     21 use radroots_sql_core::{SqlExecutor, SqliteExecutor};
     22 use serde_json::{Value, json};
     23 use tempfile::TempDir;
     24 
     25 #[cfg(unix)]
     26 use std::os::unix::fs::PermissionsExt;
     27 
     28 static COMMAND_LOCK: Mutex<()> = Mutex::new(());
     29 pub const ORDERABLE_LISTING_RELAY: &str = "ws://127.0.0.1:9";
     30 
     31 pub fn radroots() -> Command {
     32     Command::cargo_bin("radroots").expect("binary")
     33 }
     34 
     35 pub fn json_from_stdout(output: &Output) -> Value {
     36     serde_json::from_slice(&output.stdout).unwrap_or_else(|error| {
     37         panic!(
     38             "stdout was not json: {error}; stderr `{}`; stdout `{}`",
     39             String::from_utf8_lossy(&output.stderr),
     40             String::from_utf8_lossy(&output.stdout)
     41         )
     42     })
     43 }
     44 
     45 pub fn ndjson_from_stdout(output: &Output) -> Vec<Value> {
     46     let stdout = String::from_utf8_lossy(&output.stdout);
     47     let frames = stdout
     48         .lines()
     49         .filter(|line| !line.trim().is_empty())
     50         .map(|line| {
     51             serde_json::from_str::<Value>(line).unwrap_or_else(|error| {
     52                 panic!(
     53                     "stdout line was not json: {error}; stderr `{}`; line `{line}`; stdout `{stdout}`",
     54                     String::from_utf8_lossy(&output.stderr)
     55                 )
     56             })
     57         })
     58         .collect::<Vec<_>>();
     59     assert!(!frames.is_empty(), "stdout should contain ndjson frames");
     60     frames
     61 }
     62 
     63 pub struct RadrootsCliSandbox {
     64     root: TempDir,
     65 }
     66 
     67 impl RadrootsCliSandbox {
     68     pub fn new() -> Self {
     69         Self {
     70             root: TempDir::new().expect("tempdir"),
     71         }
     72     }
     73 
     74     pub fn root(&self) -> &Path {
     75         self.root.path()
     76     }
     77 
     78     pub fn command(&self) -> Command {
     79         let mut command = radroots();
     80         self.apply_base_env(&mut command);
     81         command
     82     }
     83 
     84     pub fn json_success(&self, args: &[&str]) -> Value {
     85         let _guard = COMMAND_LOCK.lock().expect("cli command lock");
     86         let output = self.command().args(args).output().expect("run command");
     87         assert!(
     88             output.status.success(),
     89             "`{args:?}` failed with stderr `{}` and stdout `{}`",
     90             String::from_utf8_lossy(&output.stderr),
     91             String::from_utf8_lossy(&output.stdout)
     92         );
     93         json_from_stdout(&output)
     94     }
     95 
     96     pub fn json_output(&self, args: &[&str]) -> (Output, Value) {
     97         let _guard = COMMAND_LOCK.lock().expect("cli command lock");
     98         let output = self.command().args(args).output().expect("run command");
     99         let value = json_from_stdout(&output);
    100         (output, value)
    101     }
    102 
    103     pub fn write_workspace_config(&self, raw: &str) -> PathBuf {
    104         let path = self.root.path().join("config.toml");
    105         fs::write(&path, raw).expect("write workspace config");
    106         path
    107     }
    108 
    109     pub fn write_app_config(&self, raw: &str) -> PathBuf {
    110         let path = self.root.path().join("config/apps/cli/config.toml");
    111         fs::create_dir_all(path.parent().expect("app config parent")).expect("app config dir");
    112         fs::write(&path, raw).expect("write app config");
    113         path
    114     }
    115 
    116     pub fn replica_db_path(&self) -> PathBuf {
    117         self.root
    118             .path()
    119             .join("data/apps/cli/replica/replica.sqlite")
    120     }
    121 
    122     pub fn local_events_db_path(&self) -> PathBuf {
    123         self.root
    124             .path()
    125             .join("data/shared/local_events/local_events.sqlite")
    126     }
    127 
    128     pub fn local_event_records(&self) -> Vec<LocalEventRecord> {
    129         let path = self.local_events_db_path();
    130         if !path.exists() {
    131             return Vec::new();
    132         }
    133         let executor = SqliteExecutor::open(path).expect("open local events db");
    134         let store = LocalEventsStore::new(executor);
    135         store.migrate_up().expect("migrate local events db");
    136         store
    137             .list_records_after_seq(0, 200)
    138             .expect("list local event records")
    139     }
    140 
    141     #[cfg(unix)]
    142     pub fn write_fake_myc(&self, name: &str, body: &str) -> PathBuf {
    143         let path = self.root.path().join("bin").join(name);
    144         fs::create_dir_all(path.parent().expect("fake myc parent")).expect("fake myc dir");
    145         fs::write(&path, format!("#!/bin/sh\nset -eu\n{body}\n")).expect("write fake myc");
    146         let mut permissions = fs::metadata(&path)
    147             .expect("fake myc metadata")
    148             .permissions();
    149         permissions.set_mode(0o755);
    150         fs::set_permissions(&path, permissions).expect("fake myc executable");
    151         path
    152     }
    153 
    154     fn apply_base_env(&self, command: &mut Command) {
    155         command.env("RADROOTS_CLI_PATHS_PROFILE", "repo_local");
    156         command.env("RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT", self.root.path());
    157         command.env("RADROOTS_CLI_ACCOUNT_SECRET_BACKEND", "encrypted_file");
    158         command.env("RADROOTS_CLI_ACCOUNT_SECRET_FALLBACK", "none");
    159     }
    160 }
    161 
    162 pub fn assert_no_removed_command_reference(value: &Value, args: &[&str]) {
    163     let raw = serde_json::to_string(value).expect("json value");
    164     for removed in [
    165         "radroots setup",
    166         "radroots status",
    167         "radroots doctor",
    168         "radroots sell",
    169         "radroots find",
    170         "radroots local",
    171         "radroots net",
    172         "radroots myc",
    173         "radroots rpc",
    174         "radroots account new",
    175         "radroots config show",
    176         "radroots runtime status get",
    177         "radroots runtime start",
    178         "radroots runtime stop",
    179         "radroots runtime restart",
    180         "radroots runtime log watch",
    181         "radroots runtime config get",
    182         "radroots runtime config show",
    183         "radroots runtime install",
    184         "radroots runtime uninstall",
    185         "radroots runtime config set",
    186         "radroots signer session",
    187         "myc status",
    188         "radroots job get",
    189         "radroots job list",
    190         "radroots job watch",
    191         "radroots job cancel",
    192         "radroots job retry",
    193         "radroots market search",
    194         "radroots market view",
    195         "radroots market update",
    196         "radroots order ls",
    197         "radroots order history",
    198         "radroots order watch",
    199         "radroots order new",
    200         "radroots order create",
    201         "radroots farm init",
    202         "radroots farm check",
    203         "radroots relay ls",
    204         "radroots product",
    205         "radroots message",
    206         "radroots approval",
    207         "radroots agent",
    208     ] {
    209         assert!(
    210             !raw.contains(removed),
    211             "`{args:?}` output should not contain removed command reference `{removed}`: {raw}"
    212         );
    213     }
    214 }
    215 
    216 pub fn assert_no_daemon_runtime_reference(value: &Value, args: &[&str]) {
    217     let raw = serde_json::to_string(value).expect("json value");
    218     for removed in ["radrootsd", "daemon", "bridge", "radroots job"] {
    219         assert!(
    220             !raw.contains(removed),
    221             "`{args:?}` output should not contain daemon runtime reference `{removed}`: {raw}"
    222         );
    223     }
    224 }
    225 
    226 pub fn assert_contains(value: &Value, needle: &str) {
    227     let value = value.as_str().expect("string value");
    228     assert!(
    229         value.contains(needle),
    230         "expected `{value}` to contain `{needle}`"
    231     );
    232 }
    233 
    234 pub fn assert_hex_len(value: &Value, expected_len: usize) {
    235     let value = value.as_str().expect("hex string");
    236     assert_eq!(value.len(), expected_len);
    237     assert!(value.chars().all(|ch| ch.is_ascii_hexdigit()));
    238 }
    239 
    240 pub fn seed_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) -> String {
    241     let store = sandbox.json_success(&["--format", "json", "store", "init"]);
    242     let db_path = store["result"]["path"]
    243         .as_str()
    244         .expect("replica db path from store init");
    245     let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr);
    246     let event_id = "2".repeat(64);
    247     let event = RadrootsNostrEvent {
    248         id: event_id.clone(),
    249         author: seller_pubkey.clone(),
    250         created_at: 1,
    251         kind: KIND_LISTING,
    252         tags: vec![
    253             vec!["d".to_owned(), listing_id],
    254             vec![
    255                 "a".to_owned(),
    256                 format!(
    257                     "{}:{}:{}",
    258                     KIND_FARM, seller_pubkey, "AAAAAAAAAAAAAAAAAAAAAA"
    259                 ),
    260             ],
    261             vec!["p".to_owned(), seller_pubkey],
    262             vec!["key".to_owned(), "pasture-eggs".to_owned()],
    263             vec!["title".to_owned(), "Market Eggs".to_owned()],
    264             vec!["category".to_owned(), "eggs".to_owned()],
    265             vec!["summary".to_owned(), "Pasture-raised eggs".to_owned()],
    266             vec!["process".to_owned(), "washed".to_owned()],
    267             vec!["lot".to_owned(), "lot-a".to_owned()],
    268             vec!["profile".to_owned(), "dozen".to_owned()],
    269             vec!["year".to_owned(), "2026".to_owned()],
    270             vec!["radroots:primary_bin".to_owned(), "bin-1".to_owned()],
    271             vec![
    272                 "radroots:bin".to_owned(),
    273                 "bin-1".to_owned(),
    274                 "12".to_owned(),
    275                 "each".to_owned(),
    276                 "12".to_owned(),
    277                 "each".to_owned(),
    278                 "dozen".to_owned(),
    279             ],
    280             vec![
    281                 "radroots:price".to_owned(),
    282                 "bin-1".to_owned(),
    283                 "6".to_owned(),
    284                 "USD".to_owned(),
    285                 "1".to_owned(),
    286                 "each".to_owned(),
    287                 "6".to_owned(),
    288                 "each".to_owned(),
    289             ],
    290             vec!["inventory".to_owned(), "5".to_owned()],
    291             vec!["status".to_owned(), "active".to_owned()],
    292         ],
    293         content: "# Market Eggs".to_owned(),
    294         sig: "f".repeat(128),
    295     };
    296     let executor = SqliteExecutor::open(Path::new(db_path)).expect("open replica db");
    297     assert_eq!(
    298         radroots_replica_ingest_event(&executor, &event).expect("ingest listing"),
    299         RadrootsReplicaIngestOutcome::Applied
    300     );
    301     seed_orderable_listing_signed_event(sandbox, &event, listing_addr);
    302     event_id
    303 }
    304 
    305 fn seed_orderable_listing_signed_event(
    306     sandbox: &RadrootsCliSandbox,
    307     event: &RadrootsNostrEvent,
    308     listing_addr: &str,
    309 ) {
    310     let database_path = sandbox.local_events_db_path();
    311     fs::create_dir_all(database_path.parent().expect("local events parent"))
    312         .expect("local events parent");
    313     let executor = SqliteExecutor::open(database_path).expect("open local events");
    314     let store = LocalEventsStore::new(executor);
    315     store.migrate_up().expect("migrate local events");
    316     let delivery = RelayDeliveryEvidence::acknowledged(
    317         [ORDERABLE_LISTING_RELAY],
    318         [ORDERABLE_LISTING_RELAY],
    319         [ORDERABLE_LISTING_RELAY],
    320         Vec::new(),
    321     )
    322     .expect("listing relay delivery evidence");
    323     store
    324         .append_record(&LocalEventRecordInput {
    325             record_id: format!("test:signed_listing:{}", event.id),
    326             family: LocalRecordFamily::SignedEvent,
    327             status: LocalRecordStatus::Published,
    328             source_runtime: SourceRuntime::Cli,
    329             created_at_ms: 1_779_000_001_000,
    330             inserted_at_ms: 1_779_000_001_000,
    331             owner_account_id: None,
    332             owner_pubkey: Some(event.author.clone()),
    333             farm_id: None,
    334             listing_addr: Some(listing_addr.to_owned()),
    335             local_work_json: None,
    336             event_id: Some(event.id.clone()),
    337             event_kind: Some(i64::from(event.kind)),
    338             event_pubkey: Some(event.author.clone()),
    339             event_created_at: Some(i64::try_from(event.created_at).expect("event created_at")),
    340             event_tags_json: Some(json!(event.tags)),
    341             event_content: Some(event.content.clone()),
    342             event_sig: Some(event.sig.clone()),
    343             raw_event_json: Some(json!(event)),
    344             outbox_status: PublishOutboxStatus::Acknowledged,
    345             relay_set_fingerprint: canonical_relay_set_fingerprint([ORDERABLE_LISTING_RELAY]),
    346             relay_delivery_json: Some(delivery.to_json_value().expect("delivery json")),
    347         })
    348         .expect("append listing signed event record");
    349 }
    350 
    351 pub fn remove_orderable_listing(sandbox: &RadrootsCliSandbox, listing_addr: &str) {
    352     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
    353     let params = serde_json::to_string(&vec![listing_addr]).expect("delete listing params");
    354     executor
    355         .exec(
    356             "DELETE FROM trade_product WHERE listing_addr = ?;",
    357             params.as_str(),
    358         )
    359         .expect("delete listing row");
    360 }
    361 
    362 pub fn update_orderable_listing_available_amount(
    363     sandbox: &RadrootsCliSandbox,
    364     listing_addr: &str,
    365     available_amount: i64,
    366 ) {
    367     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
    368     let params = serde_json::to_string(&serde_json::json!([available_amount, listing_addr]))
    369         .expect("update listing params");
    370     executor
    371         .exec(
    372             "UPDATE trade_product SET qty_avail = ? WHERE listing_addr = ?;",
    373             params.as_str(),
    374         )
    375         .expect("update listing available amount");
    376 }
    377 
    378 pub fn update_orderable_listing_primary_bin_id(
    379     sandbox: &RadrootsCliSandbox,
    380     listing_addr: &str,
    381     primary_bin_id: Option<&str>,
    382 ) {
    383     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
    384     let params = serde_json::to_string(&serde_json::json!([primary_bin_id, listing_addr]))
    385         .expect("update listing primary bin params");
    386     executor
    387         .exec(
    388             "UPDATE trade_product SET primary_bin_id = ? WHERE listing_addr = ?;",
    389             params.as_str(),
    390         )
    391         .expect("update listing primary bin");
    392 }
    393 
    394 pub fn duplicate_orderable_listing_row(sandbox: &RadrootsCliSandbox, listing_addr: &str) {
    395     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
    396     let params = serde_json::to_string(&json!([
    397         "33333333-3333-3333-3333-333333333333",
    398         listing_addr
    399     ]))
    400     .expect("duplicate listing params");
    401     executor
    402         .exec(
    403             "INSERT INTO trade_product (id, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id) SELECT ?, created_at, updated_at, key, category, title, summary, process, lot, profile, year, qty_amt, qty_unit, qty_label, qty_avail, price_amt, price_currency, price_qty_amt, price_qty_unit, notes, listing_addr, primary_bin_id, qty_amt_exact, price_amt_exact, price_qty_amt_exact, verified_primary_bin_id FROM trade_product WHERE listing_addr = ?;",
    404             params.as_str(),
    405         )
    406         .expect("duplicate listing row");
    407 }
    408 
    409 pub fn replace_latest_listing_event_id(
    410     sandbox: &RadrootsCliSandbox,
    411     listing_addr: &str,
    412     event_id: &str,
    413 ) {
    414     let (seller_pubkey, listing_id) = listing_addr_parts(listing_addr);
    415     let key = format!("{KIND_LISTING}:{seller_pubkey}:{listing_id}");
    416     let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica db");
    417     let params = serde_json::to_string(&vec![event_id, key.as_str()]).expect("update params");
    418     executor
    419         .exec(
    420             "UPDATE nostr_event_head SET last_event_id = ? WHERE key = ?;",
    421             params.as_str(),
    422         )
    423         .expect("update latest listing event id");
    424 }
    425 
    426 fn listing_addr_parts(listing_addr: &str) -> (String, String) {
    427     let parsed = RadrootsListingAddress::parse(listing_addr).expect("listing addr");
    428     let (_, rest) = parsed.as_str().split_once(':').expect("listing addr kind");
    429     let (seller_pubkey, listing_id) = rest.split_once(':').expect("listing addr parts");
    430     (seller_pubkey.to_owned(), listing_id.to_owned())
    431 }
    432 
    433 pub fn create_listing_draft(sandbox: &RadrootsCliSandbox, key: &str) -> PathBuf {
    434     let accounts = sandbox.json_success(&["--format", "json", "account", "list"]);
    435     if accounts["result"]["count"].as_u64().unwrap_or_default() == 0 {
    436         sandbox.json_success(&["--format", "json", "account", "create"]);
    437     }
    438     let listing_file = sandbox.root().join(format!("{key}.toml"));
    439     let listing_file_arg = listing_file.to_string_lossy();
    440     let value = sandbox.json_success(&[
    441         "--format",
    442         "json",
    443         "listing",
    444         "create",
    445         "--output",
    446         listing_file_arg.as_ref(),
    447         "--key",
    448         key,
    449         "--title",
    450         "Eggs",
    451         "--category",
    452         "eggs",
    453         "--summary",
    454         "Fresh eggs",
    455         "--bin-id",
    456         "bin-1",
    457         "--quantity-amount",
    458         "1",
    459         "--quantity-unit",
    460         "each",
    461         "--price-amount",
    462         "6",
    463         "--price-currency",
    464         "USD",
    465         "--price-per-amount",
    466         "1",
    467         "--price-per-unit",
    468         "each",
    469         "--available",
    470         "10",
    471     ]);
    472     assert_eq!(value["operation_id"], "listing.create");
    473     listing_file
    474 }
    475 
    476 pub fn identity_public(seed: u8) -> RadrootsIdentityPublic {
    477     identity_secret(seed).to_public()
    478 }
    479 
    480 pub fn identity_secret(seed: u8) -> RadrootsIdentity {
    481     let secret = [seed; 32];
    482     RadrootsIdentity::from_secret_key_bytes(&secret).expect("fixture identity")
    483 }
    484 
    485 pub fn store_test_session_secret(sandbox: &RadrootsCliSandbox, slot: &str, secret: &str) {
    486     let vault =
    487         RadrootsProtectedFileSecretVault::new(sandbox.root().join("secrets/shared/accounts"));
    488     vault
    489         .store_secret(slot, secret)
    490         .expect("store test session secret");
    491 }
    492 
    493 pub fn make_listing_publishable(path: &Path, farm_d_tag: &str) {
    494     let raw = fs::read_to_string(path).expect("listing draft");
    495     let mut seller_pubkey_present = false;
    496     let mut in_seller_actor = false;
    497     let patched = raw
    498         .lines()
    499         .map(|line| {
    500             let trimmed = line.trim_start();
    501             if trimmed.starts_with('[') {
    502                 in_seller_actor = trimmed == "[seller_actor]";
    503             }
    504             if in_seller_actor && trimmed.starts_with("pubkey =") {
    505                 seller_pubkey_present = !trimmed.ends_with("\"\"");
    506                 line.to_owned()
    507             } else if trimmed.starts_with("farm_d_tag =") {
    508                 format!("{}farm_d_tag = \"{}\"", line_indent(line), farm_d_tag)
    509             } else if trimmed.starts_with("method =") {
    510                 format!("{}method = \"pickup\"", line_indent(line))
    511             } else if trimmed.starts_with("primary =") {
    512                 format!("{}primary = \"farmstand\"", line_indent(line))
    513             } else {
    514                 line.to_owned()
    515             }
    516         })
    517         .collect::<Vec<_>>()
    518         .join("\n");
    519     assert!(seller_pubkey_present, "listing draft seller pubkey");
    520     fs::write(path, format!("{patched}\n")).expect("write listing draft");
    521 }
    522 
    523 pub fn make_listing_publishable_with_seller(path: &Path, farm_d_tag: &str, seller_pubkey: &str) {
    524     let raw = fs::read_to_string(path).expect("listing draft");
    525     let mut seller_pubkey_field_present = false;
    526     let mut in_seller_actor = false;
    527     let patched = raw
    528         .lines()
    529         .map(|line| {
    530             let trimmed = line.trim_start();
    531             if trimmed.starts_with('[') {
    532                 in_seller_actor = trimmed == "[seller_actor]";
    533             }
    534             if in_seller_actor && trimmed.starts_with("pubkey =") {
    535                 seller_pubkey_field_present = true;
    536                 format!("{}pubkey = \"{}\"", line_indent(line), seller_pubkey)
    537             } else if trimmed.starts_with("farm_d_tag =") {
    538                 format!("{}farm_d_tag = \"{}\"", line_indent(line), farm_d_tag)
    539             } else if trimmed.starts_with("method =") {
    540                 format!("{}method = \"pickup\"", line_indent(line))
    541             } else if trimmed.starts_with("primary =") {
    542                 format!("{}primary = \"farmstand\"", line_indent(line))
    543             } else {
    544                 line.to_owned()
    545             }
    546         })
    547         .collect::<Vec<_>>()
    548         .join("\n");
    549     assert!(
    550         seller_pubkey_field_present,
    551         "listing draft seller pubkey field"
    552     );
    553     fs::write(path, format!("{patched}\n")).expect("write listing draft");
    554 }
    555 
    556 pub fn shell_single_quoted(value: &str) -> String {
    557     value.replace('\'', "'\"'\"'")
    558 }
    559 
    560 pub fn toml_string(value: &str) -> String {
    561     value.replace('\\', "\\\\").replace('"', "\\\"")
    562 }
    563 
    564 pub fn write_public_identity_profile(
    565     sandbox: &RadrootsCliSandbox,
    566     name: &str,
    567     identity: &RadrootsIdentityPublic,
    568 ) -> PathBuf {
    569     let path = sandbox.root().join(format!("{name}.json"));
    570     fs::write(
    571         &path,
    572         serde_json::to_string_pretty(identity).expect("public identity json"),
    573     )
    574     .expect("write public identity");
    575     path
    576 }
    577 
    578 pub fn write_secret_identity_profile(
    579     sandbox: &RadrootsCliSandbox,
    580     name: &str,
    581     identity: &RadrootsIdentity,
    582 ) -> PathBuf {
    583     let path = sandbox.root().join(format!("{name}.json"));
    584     identity.save_json(&path).expect("write secret identity");
    585     path
    586 }
    587 
    588 fn line_indent(line: &str) -> &str {
    589     let trimmed = line.trim_start();
    590     &line[..line.len() - trimmed.len()]
    591 }