Skip to main content

murk_cli/
export.rs

1//! Export and diff logic for vault secrets.
2
3use std::collections::{BTreeMap, HashMap};
4
5use zeroize::Zeroizing;
6
7use crate::types;
8
9/// Merge scoped overrides over shared values and filter by tag.
10/// Returns raw (unescaped) values suitable for env var injection.
11///
12/// Values are wrapped in `Zeroizing` so plaintext is cleared from memory
13/// when the returned map is dropped.
14pub fn resolve_secrets(
15    vault: &types::Vault,
16    murk: &types::Murk,
17    pubkey: &str,
18    tags: &[String],
19) -> BTreeMap<String, Zeroizing<String>> {
20    let mut values = murk
21        .values
22        .iter()
23        .map(|(k, v)| (k.clone(), v.clone()))
24        .collect::<HashMap<String, Zeroizing<String>>>();
25
26    // Apply scoped overrides.
27    for (key, scoped_map) in &murk.scoped {
28        if let Some(value) = scoped_map.get(pubkey) {
29            values.insert(key.clone(), value.clone());
30        }
31    }
32
33    // Filter by tag.
34    let allowed_keys: Option<std::collections::HashSet<&str>> = if tags.is_empty() {
35        None
36    } else {
37        Some(
38            vault
39                .schema
40                .iter()
41                .filter(|(_, e)| e.tags.iter().any(|t| tags.contains(t)))
42                .map(|(k, _)| k.as_str())
43                .collect(),
44        )
45    };
46
47    let mut result = BTreeMap::new();
48    for (k, v) in values {
49        if allowed_keys
50            .as_ref()
51            .is_some_and(|a| !a.contains(k.as_str()))
52        {
53            continue;
54        }
55        result.insert(k, v);
56    }
57    result
58}
59
60/// Build shell-escaped export key-value pairs for `eval $(murk export)`.
61/// Wraps values in single quotes with embedded quote escaping.
62pub fn export_secrets(
63    vault: &types::Vault,
64    murk: &types::Murk,
65    pubkey: &str,
66    tags: &[String],
67) -> BTreeMap<String, Zeroizing<String>> {
68    resolve_secrets(vault, murk, pubkey, tags)
69        .into_iter()
70        .map(|(k, v)| (k, Zeroizing::new(v.replace('\'', "'\\''"))))
71        .collect()
72}
73
74/// Decrypt all shared secret values from a vault.
75///
76/// Silently skips entries that fail to decrypt (the caller may not have been
77/// a recipient at the time the vault was written). Returns a map of key → plaintext value.
78pub fn decrypt_vault_values(
79    vault: &types::Vault,
80    identity: &crate::crypto::MurkIdentity,
81) -> HashMap<String, Zeroizing<String>> {
82    let pubkey = identity.pubkey_string().unwrap_or_default();
83    let mut values = HashMap::new();
84    for (key, entry) in &vault.secrets {
85        // Decrypt shared value.
86        if !entry.shared.is_empty()
87            && let Ok(value) = crate::decrypt_value(&entry.shared, identity).and_then(|pt| {
88                crate::plaintext_bytes_to_zeroizing_string(&pt)
89                    .map_err(|e| crate::error::MurkError::Secret(e.to_string()))
90            })
91        {
92            values.insert(key.clone(), value);
93        }
94        // Scoped override takes priority.
95        if let Some(encoded) = entry.scoped.get(&pubkey)
96            && let Ok(value) = crate::decrypt_value(encoded, identity).and_then(|pt| {
97                crate::plaintext_bytes_to_zeroizing_string(&pt)
98                    .map_err(|e| crate::error::MurkError::Secret(e.to_string()))
99            })
100        {
101            values.insert(key.clone(), value);
102        }
103    }
104    values
105}
106
107/// Parse a vault from its JSON string and decrypt all shared values.
108///
109/// Combines `vault::parse` with `decrypt_vault_values` for use cases
110/// where the vault contents come from a string (e.g., `git show`).
111pub fn parse_and_decrypt_values(
112    vault_contents: &str,
113    identity: &crate::crypto::MurkIdentity,
114) -> Result<HashMap<String, Zeroizing<String>>, String> {
115    let vault = crate::vault::parse(vault_contents).map_err(|e| e.to_string())?;
116    Ok(decrypt_vault_values(&vault, identity))
117}
118
119/// The kind of change in a diff entry.
120#[derive(Debug, PartialEq, Eq)]
121pub enum DiffKind {
122    Added,
123    Removed,
124    Changed,
125}
126
127/// A single entry in a secret diff.
128///
129/// `old_value` and `new_value` are held in `Zeroizing` so plaintext is cleared
130/// when the entry is dropped. Formatting/printing callers should take care not
131/// to retain their own unzeroed copies.
132#[derive(Debug)]
133pub struct DiffEntry {
134    pub key: String,
135    pub kind: DiffKind,
136    pub old_value: Option<Zeroizing<String>>,
137    pub new_value: Option<Zeroizing<String>>,
138}
139
140/// Compare two sets of secret values and return the differences.
141pub fn diff_secrets(
142    old: &HashMap<String, Zeroizing<String>>,
143    new: &HashMap<String, Zeroizing<String>>,
144) -> Vec<DiffEntry> {
145    let mut all_keys: Vec<&str> = old
146        .keys()
147        .chain(new.keys())
148        .map(String::as_str)
149        .collect::<std::collections::HashSet<_>>()
150        .into_iter()
151        .collect();
152    all_keys.sort_unstable();
153
154    let mut entries = Vec::new();
155    for key in all_keys {
156        match (old.get(key), new.get(key)) {
157            (None, Some(v)) => entries.push(DiffEntry {
158                key: key.into(),
159                kind: DiffKind::Added,
160                old_value: None,
161                new_value: Some(v.clone()),
162            }),
163            (Some(v), None) => entries.push(DiffEntry {
164                key: key.into(),
165                kind: DiffKind::Removed,
166                old_value: Some(v.clone()),
167                new_value: None,
168            }),
169            (Some(old_v), Some(new_v)) if **old_v != **new_v => entries.push(DiffEntry {
170                key: key.into(),
171                kind: DiffKind::Changed,
172                old_value: Some(old_v.clone()),
173                new_value: Some(new_v.clone()),
174            }),
175            _ => {}
176        }
177    }
178    entries
179}
180
181/// Format diff entries as display lines.
182/// Returns plain-text lines (no ANSI colors) suitable for testing.
183pub fn format_diff_lines(entries: &[DiffEntry], show_values: bool) -> Vec<String> {
184    entries
185        .iter()
186        .map(|entry| {
187            let symbol = match entry.kind {
188                DiffKind::Added => "+",
189                DiffKind::Removed => "-",
190                DiffKind::Changed => "~",
191            };
192            if show_values {
193                let old = entry.old_value.as_ref().map_or("", |v| v.as_str());
194                let new = entry.new_value.as_ref().map_or("", |v| v.as_str());
195                match entry.kind {
196                    DiffKind::Added => format!("{symbol} {} = {}", entry.key, new),
197                    DiffKind::Removed => format!("{symbol} {} = {}", entry.key, old),
198                    DiffKind::Changed => {
199                        format!("{symbol} {} {} → {}", entry.key, old, new)
200                    }
201                }
202            } else {
203                format!("{symbol} {}", entry.key)
204            }
205        })
206        .collect()
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::testutil::*;
213    use crate::types;
214
215    #[test]
216    fn export_secrets_basic() {
217        let mut vault = empty_vault();
218        vault.schema.insert(
219            "FOO".into(),
220            types::SchemaEntry {
221                description: String::new(),
222                example: None,
223                tags: vec![],
224                ..Default::default()
225            },
226        );
227
228        let mut murk = empty_murk();
229        murk.values.insert("FOO".into(), secret("bar"));
230
231        let exports = export_secrets(&vault, &murk, "age1pk", &[]);
232        assert_eq!(exports.len(), 1);
233        assert_eq!(exports["FOO"].as_str(), "bar");
234    }
235
236    #[test]
237    fn export_secrets_scoped_override() {
238        let mut vault = empty_vault();
239        vault.schema.insert(
240            "KEY".into(),
241            types::SchemaEntry {
242                description: String::new(),
243                example: None,
244                tags: vec![],
245                ..Default::default()
246            },
247        );
248
249        let mut murk = empty_murk();
250        murk.values.insert("KEY".into(), secret("shared"));
251        let mut scoped = HashMap::new();
252        scoped.insert("age1pk".into(), secret("override"));
253        murk.scoped.insert("KEY".into(), scoped);
254
255        let exports = export_secrets(&vault, &murk, "age1pk", &[]);
256        assert_eq!(exports["KEY"].as_str(), "override");
257    }
258
259    #[test]
260    fn export_secrets_tag_filter() {
261        let mut vault = empty_vault();
262        vault.schema.insert(
263            "A".into(),
264            types::SchemaEntry {
265                description: String::new(),
266                example: None,
267                tags: vec!["db".into()],
268                ..Default::default()
269            },
270        );
271        vault.schema.insert(
272            "B".into(),
273            types::SchemaEntry {
274                description: String::new(),
275                example: None,
276                tags: vec!["api".into()],
277                ..Default::default()
278            },
279        );
280
281        let mut murk = empty_murk();
282        murk.values.insert("A".into(), secret("val_a"));
283        murk.values.insert("B".into(), secret("val_b"));
284
285        let exports = export_secrets(&vault, &murk, "age1pk", &["db".into()]);
286        assert_eq!(exports.len(), 1);
287        assert_eq!(exports["A"].as_str(), "val_a");
288    }
289
290    #[test]
291    fn export_secrets_shell_escaping() {
292        let mut vault = empty_vault();
293        vault.schema.insert(
294            "KEY".into(),
295            types::SchemaEntry {
296                description: String::new(),
297                example: None,
298                tags: vec![],
299                ..Default::default()
300            },
301        );
302
303        let mut murk = empty_murk();
304        murk.values.insert("KEY".into(), secret("it's a test"));
305
306        let exports = export_secrets(&vault, &murk, "age1pk", &[]);
307        assert_eq!(exports["KEY"].as_str(), "it'\\''s a test");
308    }
309
310    #[test]
311    fn diff_secrets_no_changes() {
312        let old = HashMap::from([("K".into(), secret("V"))]);
313        let new = old.clone();
314        assert!(diff_secrets(&old, &new).is_empty());
315    }
316
317    #[test]
318    fn diff_secrets_added() {
319        let old = HashMap::new();
320        let new = HashMap::from([("KEY".into(), secret("val"))]);
321        let entries = diff_secrets(&old, &new);
322        assert_eq!(entries.len(), 1);
323        assert_eq!(entries[0].kind, DiffKind::Added);
324        assert_eq!(entries[0].key, "KEY");
325        assert_eq!(entries[0].new_value.as_deref(), Some(&String::from("val")));
326    }
327
328    #[test]
329    fn diff_secrets_removed() {
330        let old = HashMap::from([("KEY".into(), secret("val"))]);
331        let new = HashMap::new();
332        let entries = diff_secrets(&old, &new);
333        assert_eq!(entries.len(), 1);
334        assert_eq!(entries[0].kind, DiffKind::Removed);
335        assert_eq!(entries[0].old_value.as_deref(), Some(&String::from("val")));
336    }
337
338    #[test]
339    fn diff_secrets_changed() {
340        let old = HashMap::from([("KEY".into(), secret("old_val"))]);
341        let new = HashMap::from([("KEY".into(), secret("new_val"))]);
342        let entries = diff_secrets(&old, &new);
343        assert_eq!(entries.len(), 1);
344        assert_eq!(entries[0].kind, DiffKind::Changed);
345        assert_eq!(
346            entries[0].old_value.as_deref(),
347            Some(&String::from("old_val"))
348        );
349        assert_eq!(
350            entries[0].new_value.as_deref(),
351            Some(&String::from("new_val"))
352        );
353    }
354
355    #[test]
356    fn diff_secrets_mixed() {
357        let old = HashMap::from([
358            ("KEEP".into(), secret("same")),
359            ("REMOVE".into(), secret("gone")),
360            ("CHANGE".into(), secret("old")),
361        ]);
362        let new = HashMap::from([
363            ("KEEP".into(), secret("same")),
364            ("ADD".into(), secret("new")),
365            ("CHANGE".into(), secret("new")),
366        ]);
367        let entries = diff_secrets(&old, &new);
368        assert_eq!(entries.len(), 3);
369
370        let kinds: Vec<&DiffKind> = entries.iter().map(|e| &e.kind).collect();
371        assert!(kinds.contains(&&DiffKind::Added));
372        assert!(kinds.contains(&&DiffKind::Removed));
373        assert!(kinds.contains(&&DiffKind::Changed));
374    }
375
376    #[test]
377    fn diff_secrets_sorted_by_key() {
378        let old = HashMap::new();
379        let new = HashMap::from([
380            ("Z".into(), secret("z")),
381            ("A".into(), secret("a")),
382            ("M".into(), secret("m")),
383        ]);
384        let entries = diff_secrets(&old, &new);
385        let keys: Vec<&str> = entries.iter().map(|e| e.key.as_str()).collect();
386        assert_eq!(keys, vec!["A", "M", "Z"]);
387    }
388
389    // ── format_diff_lines tests ──
390
391    #[test]
392    fn format_diff_lines_without_values() {
393        let entries = vec![
394            DiffEntry {
395                key: "NEW_KEY".into(),
396                kind: DiffKind::Added,
397                old_value: None,
398                new_value: Some(secret("secret")),
399            },
400            DiffEntry {
401                key: "OLD_KEY".into(),
402                kind: DiffKind::Removed,
403                old_value: Some(secret("old")),
404                new_value: None,
405            },
406            DiffEntry {
407                key: "MOD_KEY".into(),
408                kind: DiffKind::Changed,
409                old_value: Some(secret("v1")),
410                new_value: Some(secret("v2")),
411            },
412        ];
413        let lines = format_diff_lines(&entries, false);
414        assert_eq!(lines, vec!["+ NEW_KEY", "- OLD_KEY", "~ MOD_KEY"]);
415    }
416
417    #[test]
418    fn format_diff_lines_with_values() {
419        let entries = vec![
420            DiffEntry {
421                key: "KEY".into(),
422                kind: DiffKind::Added,
423                old_value: None,
424                new_value: Some(secret("new_val")),
425            },
426            DiffEntry {
427                key: "KEY2".into(),
428                kind: DiffKind::Changed,
429                old_value: Some(secret("old")),
430                new_value: Some(secret("new")),
431            },
432        ];
433        let lines = format_diff_lines(&entries, true);
434        assert_eq!(lines[0], "+ KEY = new_val");
435        assert_eq!(lines[1], "~ KEY2 old → new");
436    }
437
438    #[test]
439    fn format_diff_lines_empty() {
440        let lines = format_diff_lines(&[], false);
441        assert!(lines.is_empty());
442    }
443
444    // ── resolve_secrets tests ──
445
446    #[test]
447    fn resolve_secrets_basic() {
448        let mut vault = empty_vault();
449        vault.schema.insert(
450            "FOO".into(),
451            types::SchemaEntry {
452                description: String::new(),
453                example: None,
454                tags: vec![],
455                ..Default::default()
456            },
457        );
458
459        let mut murk = empty_murk();
460        murk.values.insert("FOO".into(), secret("bar"));
461
462        let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
463        assert_eq!(resolved.len(), 1);
464        assert_eq!(resolved["FOO"].as_str(), "bar");
465    }
466
467    #[test]
468    fn resolve_secrets_no_escaping() {
469        let mut vault = empty_vault();
470        vault.schema.insert(
471            "KEY".into(),
472            types::SchemaEntry {
473                description: String::new(),
474                example: None,
475                tags: vec![],
476                ..Default::default()
477            },
478        );
479
480        let mut murk = empty_murk();
481        murk.values.insert("KEY".into(), secret("it's a test"));
482
483        let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
484        assert_eq!(resolved["KEY"].as_str(), "it's a test");
485    }
486
487    #[test]
488    fn resolve_secrets_scoped_override() {
489        let mut vault = empty_vault();
490        vault.schema.insert(
491            "KEY".into(),
492            types::SchemaEntry {
493                description: String::new(),
494                example: None,
495                tags: vec![],
496                ..Default::default()
497            },
498        );
499
500        let mut murk = empty_murk();
501        murk.values.insert("KEY".into(), secret("shared"));
502        let mut scoped = HashMap::new();
503        scoped.insert("age1pk".into(), secret("override"));
504        murk.scoped.insert("KEY".into(), scoped);
505
506        let resolved = resolve_secrets(&vault, &murk, "age1pk", &[]);
507        assert_eq!(resolved["KEY"].as_str(), "override");
508    }
509
510    #[test]
511    fn resolve_secrets_tag_filter() {
512        let mut vault = empty_vault();
513        vault.schema.insert(
514            "A".into(),
515            types::SchemaEntry {
516                description: String::new(),
517                example: None,
518                tags: vec!["db".into()],
519                ..Default::default()
520            },
521        );
522        vault.schema.insert(
523            "B".into(),
524            types::SchemaEntry {
525                description: String::new(),
526                example: None,
527                tags: vec!["api".into()],
528                ..Default::default()
529            },
530        );
531
532        let mut murk = empty_murk();
533        murk.values.insert("A".into(), secret("val_a"));
534        murk.values.insert("B".into(), secret("val_b"));
535
536        let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
537        assert_eq!(resolved.len(), 1);
538        assert_eq!(resolved["A"].as_str(), "val_a");
539    }
540
541    #[test]
542    fn resolve_secrets_tag_in_schema_but_no_secret() {
543        let mut vault = empty_vault();
544        // Schema says key "ORPHAN" exists with tag "db", but no secret value.
545        vault.schema.insert(
546            "ORPHAN".into(),
547            types::SchemaEntry {
548                description: "orphan key".into(),
549                example: None,
550                tags: vec!["db".into()],
551                ..Default::default()
552            },
553        );
554        vault.schema.insert(
555            "REAL".into(),
556            types::SchemaEntry {
557                description: "has a value".into(),
558                example: None,
559                tags: vec!["db".into()],
560                ..Default::default()
561            },
562        );
563
564        let mut murk = empty_murk();
565        // Only REAL has a value, ORPHAN does not.
566        murk.values.insert("REAL".into(), secret("real_val"));
567
568        let resolved = resolve_secrets(&vault, &murk, "age1pk", &["db".into()]);
569        // ORPHAN should not appear since it has no value.
570        assert_eq!(resolved.len(), 1);
571        assert_eq!(resolved["REAL"].as_str(), "real_val");
572        assert!(!resolved.contains_key("ORPHAN"));
573    }
574
575    #[test]
576    fn resolve_secrets_scoped_pubkey_not_in_recipients() {
577        let mut vault = empty_vault();
578        vault.recipients = vec!["age1alice".into()];
579        vault.schema.insert(
580            "KEY".into(),
581            types::SchemaEntry {
582                description: String::new(),
583                example: None,
584                tags: vec![],
585                ..Default::default()
586            },
587        );
588
589        let mut murk = empty_murk();
590        murk.values.insert("KEY".into(), secret("shared"));
591        // Scoped override for a pubkey NOT in vault.recipients.
592        let mut scoped = HashMap::new();
593        scoped.insert("age1outsider".into(), secret("outsider_val"));
594        murk.scoped.insert("KEY".into(), scoped);
595
596        // The outsider's override should still be applied (resolve doesn't gate on recipient list).
597        let resolved = resolve_secrets(&vault, &murk, "age1outsider", &[]);
598        assert_eq!(resolved["KEY"].as_str(), "outsider_val");
599
600        // Alice gets the shared value since she has no scoped override.
601        let resolved_alice = resolve_secrets(&vault, &murk, "age1alice", &[]);
602        assert_eq!(resolved_alice["KEY"].as_str(), "shared");
603    }
604
605    // ── New edge-case tests ──
606
607    #[test]
608    fn export_secrets_empty_vault() {
609        let vault = empty_vault();
610        let murk = empty_murk();
611        let exports = export_secrets(&vault, &murk, "age1pk", &[]);
612        assert!(exports.is_empty());
613    }
614
615    #[test]
616    fn decrypt_vault_values_basic() {
617        let (secret, pubkey) = generate_keypair();
618        let recipient = make_recipient(&pubkey);
619        let identity = make_identity(&secret);
620
621        let mut vault = empty_vault();
622        vault.recipients = vec![pubkey];
623        vault.secrets.insert(
624            "KEY1".into(),
625            types::SecretEntry {
626                shared: crate::encrypt_value(b"val1", std::slice::from_ref(&recipient)).unwrap(),
627                scoped: std::collections::BTreeMap::new(),
628            },
629        );
630        vault.secrets.insert(
631            "KEY2".into(),
632            types::SecretEntry {
633                shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
634                scoped: std::collections::BTreeMap::new(),
635            },
636        );
637
638        let values = crate::export::decrypt_vault_values(&vault, &identity);
639        assert_eq!(values.len(), 2);
640        assert_eq!(values["KEY1"].as_str(), "val1");
641        assert_eq!(values["KEY2"].as_str(), "val2");
642    }
643
644    #[test]
645    fn decrypt_vault_values_wrong_key_skips() {
646        let (_, pubkey) = generate_keypair();
647        let recipient = make_recipient(&pubkey);
648        let (wrong_secret, _) = generate_keypair();
649        let wrong_identity = make_identity(&wrong_secret);
650
651        let mut vault = empty_vault();
652        vault.recipients = vec![pubkey];
653        vault.secrets.insert(
654            "KEY1".into(),
655            types::SecretEntry {
656                shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
657                scoped: std::collections::BTreeMap::new(),
658            },
659        );
660
661        let values = crate::export::decrypt_vault_values(&vault, &wrong_identity);
662        assert!(values.is_empty());
663    }
664
665    #[test]
666    fn decrypt_vault_values_empty_vault() {
667        let (secret, _) = generate_keypair();
668        let identity = make_identity(&secret);
669        let vault = empty_vault();
670
671        let values = crate::export::decrypt_vault_values(&vault, &identity);
672        assert!(values.is_empty());
673    }
674
675    #[test]
676    fn diff_secrets_both_empty() {
677        let old = HashMap::new();
678        let new = HashMap::new();
679        assert!(diff_secrets(&old, &new).is_empty());
680    }
681
682    // ── parse_and_decrypt_values tests ──
683
684    #[test]
685    fn parse_and_decrypt_values_roundtrip() {
686        let (secret, pubkey) = generate_keypair();
687        let recipient = make_recipient(&pubkey);
688        let identity = make_identity(&secret);
689
690        let mut vault = empty_vault();
691        vault.recipients = vec![pubkey];
692        vault.secrets.insert(
693            "KEY1".into(),
694            types::SecretEntry {
695                shared: crate::encrypt_value(b"val1", std::slice::from_ref(&recipient)).unwrap(),
696                scoped: std::collections::BTreeMap::new(),
697            },
698        );
699        vault.secrets.insert(
700            "KEY2".into(),
701            types::SecretEntry {
702                shared: crate::encrypt_value(b"val2", &[recipient]).unwrap(),
703                scoped: std::collections::BTreeMap::new(),
704            },
705        );
706
707        let json = serde_json::to_string(&vault).unwrap();
708        let values = parse_and_decrypt_values(&json, &identity).unwrap();
709        assert_eq!(values.len(), 2);
710        assert_eq!(values["KEY1"].as_str(), "val1");
711        assert_eq!(values["KEY2"].as_str(), "val2");
712    }
713
714    #[test]
715    fn parse_and_decrypt_values_invalid_json() {
716        let (secret, _) = generate_keypair();
717        let identity = make_identity(&secret);
718
719        let result = parse_and_decrypt_values("not valid json", &identity);
720        assert!(result.is_err());
721    }
722
723    #[test]
724    fn parse_and_decrypt_values_wrong_key() {
725        let (_, pubkey) = generate_keypair();
726        let recipient = make_recipient(&pubkey);
727        let (wrong_secret, _) = generate_keypair();
728        let wrong_identity = make_identity(&wrong_secret);
729
730        let mut vault = empty_vault();
731        vault.recipients = vec![pubkey];
732        vault.secrets.insert(
733            "KEY1".into(),
734            types::SecretEntry {
735                shared: crate::encrypt_value(b"val1", &[recipient]).unwrap(),
736                scoped: std::collections::BTreeMap::new(),
737            },
738        );
739
740        let json = serde_json::to_string(&vault).unwrap();
741        let values = parse_and_decrypt_values(&json, &wrong_identity).unwrap();
742        assert!(values.is_empty());
743    }
744}