Skip to main content

sam_zfs_unlocker/
lib.rs

1mod zfs_finder;
2
3use crate::zfs_finder::pick_zfs_path_for_sudo;
4use std::collections::BTreeMap;
5use std::io::BufWriter;
6use std::io::Read;
7use std::io::Write;
8use std::path::PathBuf;
9use std::process::Command;
10
11#[derive(thiserror::Error, Debug)]
12pub enum ZfsError {
13    #[error("System error: {0}")]
14    SystemError(String),
15    #[error("Dataset {0} not found")]
16    DatasetNotFound(String),
17    #[error(
18        "Command returned unexpected state for key is available, other than 'available' and 'unavailable': {0}"
19    )]
20    UnexpectedStateForKey(String),
21    #[error("Command returned unexpected state for mount, other than 'yes' and 'no': {0}")]
22    UnexpectedStateForMount(String),
23    #[error("Command to check whether dataset {0} is mounted failed: {1}")]
24    IsMountedCheckCallFailed(String, String),
25    #[error("Command to list datasets mount points failed: {0}")]
26    ListDatasetsMountPointsCallFailed(String),
27    #[error("Command to list unmounted datasets failed: {0}")]
28    ListUnmountedDatasetsCallFailed(String),
29    #[error("Command to check whether key for dataset {0} is loaded failed: {1}")]
30    KeyLoadedCheckFailed(String, String),
31    #[error("Load key command for dataset {0} failed: {1}")]
32    LoadKeyCmdFailed(String, String),
33    #[error("Unload key command for dataset {0} failed: {1}")]
34    UnloadKeyCmdFailed(String, String),
35    #[error("Key must be loaded before mount for dataset {0}")]
36    KeyNotLoadedForMount(String),
37    #[error("Mount command for dataset {0} failed: {1}")]
38    MountCmdFailed(String, String),
39    #[error("Unmount command for dataset {0} failed: {1}")]
40    UnmountCmdFailed(String, String),
41    #[error("Dataset name is invalid: {0}")]
42    DatasetNameIsInvalid(String),
43    #[error("Failed to enumerate executable paths for {0}: {1}")]
44    WhichAllFailed(String, String),
45    #[error("Failed to run `sudo -n -l`: {0}")]
46    SudoListFailed(String),
47    #[error("No matching NOPASSWD sudoers rule for: {0}")]
48    SudoersNoMatchingRule(String),
49    #[error("No `zfs` executable found in PATH")]
50    ZfsNotFoundInPath,
51}
52
53#[derive(Debug, Clone, Eq, PartialEq)]
54pub struct DatasetMountedState {
55    pub dataset_name: String,
56    pub is_mounted: bool,
57    pub is_key_loaded: bool,
58}
59
60fn parse_key_available_state(state: impl AsRef<str>) -> Result<bool, ZfsError> {
61    match state.as_ref().trim() {
62        "available" => Ok(true),
63        "unavailable" => Ok(false),
64        _ => Err(ZfsError::UnexpectedStateForKey(state.as_ref().to_string())),
65    }
66}
67
68fn parse_dataset_mounted_state(state: impl AsRef<str>) -> Result<bool, ZfsError> {
69    match state.as_ref().trim() {
70        "yes" => Ok(true),
71        "no" => Ok(false),
72        _ => Err(ZfsError::UnexpectedStateForMount(
73            state.as_ref().to_string(),
74        )),
75    }
76}
77
78/// Note that the sanitization's purpose is not to perfectly mimic ZFS specs.
79/// The purpose is to prevent any kind of possible injection of commands.
80fn check_and_sanitize_zfs_dataset_name(zfs_dataset: impl AsRef<str>) -> Result<String, ZfsError> {
81    const ALLOWED_SYMBOLS: [char; 4] = ['-', '_', '.', ':'];
82
83    let dataset = zfs_dataset.as_ref().trim();
84
85    let check_func = |part: &str| {
86        part.chars()
87            .all(|c| c.is_ascii_alphanumeric() || ALLOWED_SYMBOLS.contains(&c))
88            && part.chars().all(|c| !c.is_whitespace())
89            && !part.is_empty()
90            && !part.starts_with(ALLOWED_SYMBOLS) // Can only begin with an alphanumeric
91    };
92
93    // Check the whole name, then the individual parts
94    check_func(dataset);
95
96    if !dataset.split('/').all(check_func) {
97        Err(ZfsError::DatasetNameIsInvalid(dataset.to_string()))
98    } else {
99        Ok(dataset.to_string())
100    }
101}
102
103/// Attempts to load-key for ZFS dataset
104/// Returns: Ok(()) if the key is successfully loaded OR already loaded
105/// Returns: Error if dataset not found or some other system error occurred.
106/// The command `zfs load-key <dataset-name>` should be authorized with visudo.
107pub fn zfs_load_key(
108    zfs_dataset: impl AsRef<str>,
109    passphrase: impl AsRef<str>,
110) -> Result<(), ZfsError> {
111    let passphrase = passphrase.as_ref();
112    let dataset = check_and_sanitize_zfs_dataset_name(zfs_dataset)?;
113
114    match zfs_is_key_loaded(&dataset)? {
115        Some(loaded) => {
116            if loaded {
117                return Ok(());
118            }
119        }
120        None => return Err(ZfsError::DatasetNotFound(dataset.to_string())),
121    }
122
123    let zfs_exe = pick_zfs_path_for_sudo()?;
124
125    // Create a command to run zfs load-key
126    let mut child = Command::new("sudo")
127        .arg("-n") // sudo isn't interactive
128        .arg(zfs_exe)
129        .arg("load-key")
130        .arg(&dataset)
131        .stdin(std::process::Stdio::piped())
132        .stdout(std::process::Stdio::piped())
133        .stderr(std::process::Stdio::piped())
134        .spawn()
135        .map_err(|e| ZfsError::LoadKeyCmdFailed(dataset.to_string(), e.to_string()))?;
136
137    // Get the stdin of the zfs command
138    if let Some(mut stdin) = child.stdin.take() {
139        // Write the key to stdin
140        let mut writer = BufWriter::new(&mut stdin);
141        writeln!(writer, "{}", passphrase).map_err(|e| ZfsError::SystemError(e.to_string()))?;
142        writer
143            .flush()
144            .map_err(|e| ZfsError::SystemError(e.to_string()))?;
145    }
146
147    // Capture the stdout handle of the child process
148    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
149    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
150
151    // Read stdout/stderr to a string
152    let mut stdout_string = String::new();
153    stdout
154        .read_to_string(&mut stdout_string)
155        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
156    let mut stderr_string = String::new();
157    stderr
158        .read_to_string(&mut stderr_string)
159        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
160
161    // Wait for the zfs command to complete
162    let status = child
163        .wait()
164        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
165
166    // Check if the command was successful
167    if status.success() {
168        Ok(())
169    } else {
170        Err(ZfsError::LoadKeyCmdFailed(
171            dataset.to_string(),
172            stderr_string,
173        ))
174    }
175}
176
177/// Attempts to load-key for ZFS dataset
178/// Returns: Ok(()) if the key is successfully unloaded OR already unloaded
179/// Returns: Error if dataset not found or some other system error occurred.
180/// The command `zfs unload-key <dataset-name>` should be authorized with visudo.
181pub fn zfs_unload_key(zfs_dataset: impl AsRef<str>) -> Result<(), ZfsError> {
182    let dataset = check_and_sanitize_zfs_dataset_name(zfs_dataset)?;
183
184    match zfs_is_key_loaded(&dataset)? {
185        Some(loaded) => match loaded {
186            true => (),
187            false => return Ok(()),
188        },
189        None => return Err(ZfsError::DatasetNotFound(dataset.to_string())),
190    }
191
192    let zfs_exe = pick_zfs_path_for_sudo()?;
193
194    // Create a command to run zfs load-key
195    let mut child = Command::new("sudo")
196        .arg("-n") // sudo isn't interactive
197        .arg(zfs_exe)
198        .arg("unload-key")
199        .arg(&dataset)
200        .stdin(std::process::Stdio::piped())
201        .stdout(std::process::Stdio::piped())
202        .stderr(std::process::Stdio::piped())
203        .spawn()
204        .map_err(|e| ZfsError::UnloadKeyCmdFailed(dataset.to_string(), e.to_string()))?;
205
206    // Capture the stdout handle of the child process
207    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
208    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
209
210    // Read stdout/stderr to a string
211    let mut stdout_string = String::new();
212    stdout
213        .read_to_string(&mut stdout_string)
214        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
215    let mut stderr_string = String::new();
216    stderr
217        .read_to_string(&mut stderr_string)
218        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
219
220    // Wait for the zfs command to complete
221    let status = child
222        .wait()
223        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
224
225    // Check if the command was successful
226    if status.success() {
227        Ok(())
228    } else {
229        Err(ZfsError::UnloadKeyCmdFailed(
230            dataset.to_string(),
231            stderr_string,
232        ))
233    }
234}
235
236/// Mounts a ZFS dataset
237/// Returns Ok(()) if successfully mounted or already mounted
238/// Returns Err otherwise
239/// The command `zfs mount <dataset-name>` should be authorized with visudo.
240pub fn zfs_mount_dataset(zfs_dataset: impl AsRef<str>) -> Result<(), ZfsError> {
241    let dataset = check_and_sanitize_zfs_dataset_name(zfs_dataset)?;
242
243    match zfs_is_key_loaded(&dataset)? {
244        Some(loaded) => match loaded {
245            true => (),
246            false => return Err(ZfsError::KeyNotLoadedForMount(dataset.to_string())),
247        },
248        None => return Err(ZfsError::DatasetNotFound(dataset.to_string())),
249    }
250
251    match zfs_is_dataset_mounted(&dataset)? {
252        Some(mounted) => {
253            if mounted {
254                return Ok(());
255            }
256        }
257        None => return Err(ZfsError::DatasetNotFound(dataset.to_string())),
258    }
259
260    let zfs_exe = pick_zfs_path_for_sudo()?;
261
262    // Create a command to run zfs load-key
263    let mut child = Command::new("sudo")
264        .arg("-n") // sudo isn't interactive
265        .arg(zfs_exe)
266        .arg("mount")
267        .arg(&dataset)
268        .stdin(std::process::Stdio::piped())
269        .stdout(std::process::Stdio::piped())
270        .stderr(std::process::Stdio::piped())
271        .spawn()
272        .map_err(|e| ZfsError::MountCmdFailed(dataset.to_string(), e.to_string()))?;
273
274    // Capture the stdout handle of the child process
275    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
276    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
277
278    // Read stdout/stderr to a string
279    let mut stdout_string = String::new();
280    stdout
281        .read_to_string(&mut stdout_string)
282        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
283    let mut stderr_string = String::new();
284    stderr
285        .read_to_string(&mut stderr_string)
286        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
287
288    // Wait for the zfs command to complete
289    let status = child
290        .wait()
291        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
292
293    // Check if the command was successful
294    if status.success() {
295        Ok(())
296    } else {
297        Err(ZfsError::MountCmdFailed(dataset.to_string(), stderr_string))
298    }
299}
300
301/// Unmounts a ZFS dataset
302/// Returns: Ok(()) on success or if is already mounted
303/// Returns: Err otherwise.
304/// The command `zfs unmount <dataset-name>` should be authorized with visudo.
305pub fn zfs_unmount_dataset(zfs_dataset: impl AsRef<str>) -> Result<(), ZfsError> {
306    let dataset = check_and_sanitize_zfs_dataset_name(zfs_dataset)?;
307
308    match zfs_is_dataset_mounted(&dataset)? {
309        Some(mounted) => match mounted {
310            true => (),
311            false => return Ok(()),
312        },
313        None => return Err(ZfsError::DatasetNotFound(dataset.to_string())),
314    }
315
316    let zfs_exe = pick_zfs_path_for_sudo()?;
317
318    // Create a command to run zfs load-key
319    let mut child = Command::new("sudo")
320        .arg("-n") // sudo isn't interactive
321        .arg(zfs_exe)
322        .arg("umount")
323        .arg(&dataset)
324        .stdin(std::process::Stdio::piped())
325        .stdout(std::process::Stdio::piped())
326        .stderr(std::process::Stdio::piped())
327        .spawn()
328        .map_err(|e| ZfsError::UnmountCmdFailed(dataset.to_string(), e.to_string()))?;
329
330    // Capture the stdout handle of the child process
331    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
332    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
333
334    // Read stdout/stderr to a string
335    let mut stdout_string = String::new();
336    stdout
337        .read_to_string(&mut stdout_string)
338        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
339    let mut stderr_string = String::new();
340    stderr
341        .read_to_string(&mut stderr_string)
342        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
343
344    // Wait for the zfs command to complete
345    let status = child
346        .wait()
347        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
348
349    // Check if the command was successful
350    if status.success() {
351        Ok(())
352    } else {
353        Err(ZfsError::UnmountCmdFailed(
354            dataset.to_string(),
355            stderr_string,
356        ))
357    }
358}
359
360/// Checks whether key is loaded
361/// Returns: Some(true): Key is available/loaded and/or doesn't need it
362/// Returns: Some(false): Key is not loaded
363/// Returns: None: The dataset is not found
364/// Otherwise, an error is returned
365pub fn zfs_is_key_loaded(zfs_dataset: impl AsRef<str>) -> Result<Option<bool>, ZfsError> {
366    let dataset = check_and_sanitize_zfs_dataset_name(zfs_dataset)?;
367
368    let zfs_exe = pick_zfs_path_for_sudo()?;
369
370    // Create a command to run zfs load-key
371    let mut child = Command::new(zfs_exe)
372        .arg("get")
373        .arg("keystatus")
374        .arg("-H") // No table header
375        .arg("-o")
376        .arg("name,value") // Only show two columns, dataset name and whether key is available
377        .stdin(std::process::Stdio::piped())
378        .stdout(std::process::Stdio::piped())
379        .stderr(std::process::Stdio::piped())
380        .spawn()
381        .map_err(|e| ZfsError::KeyLoadedCheckFailed(dataset.to_string(), e.to_string()))?;
382
383    // Capture the stdout handle of the child process
384    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
385    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
386
387    // Read stdout/stderr to a string
388    let mut stdout_string = String::new();
389    stdout
390        .read_to_string(&mut stdout_string)
391        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
392    let mut stderr_string = String::new();
393    stderr
394        .read_to_string(&mut stderr_string)
395        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
396
397    // Wait for the zfs command to complete
398    let status = child
399        .wait()
400        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
401
402    // Check if the command was successful
403    if status.success() {
404        let lines = stdout_string.lines();
405        let datasets_results = lines
406            .into_iter()
407            .map(|l| l.split_whitespace().collect::<Vec<_>>())
408            .filter(|v| v.len() >= 2)
409            .map(|v| (v[0], v[1]))
410            .collect::<BTreeMap<&str, &str>>();
411        match datasets_results.get(&*dataset) {
412            Some(is_key_available) => parse_key_available_state(is_key_available).map(Some),
413            None => Ok(None),
414        }
415    } else {
416        Err(ZfsError::KeyLoadedCheckFailed(
417            dataset.to_string(),
418            stderr_string,
419        ))
420    }
421}
422
423/// Checks whether a dataset is mounted
424/// Returns: Some(true): The dataset is mounted
425/// Returns: Some(false): The dataset is not mounted
426/// Returns: None: The dataset is not found
427/// Otherwise, an error is returned
428pub fn zfs_is_dataset_mounted(zfs_dataset: impl AsRef<str>) -> Result<Option<bool>, ZfsError> {
429    let dataset = check_and_sanitize_zfs_dataset_name(zfs_dataset)?;
430
431    let zfs_exe = pick_zfs_path_for_sudo()?;
432
433    // Create a command to run zfs load-key
434    let mut child = Command::new(zfs_exe)
435        .arg("list")
436        .arg("-H") // No table header
437        .arg("-o")
438        .arg("name,mounted") // Only show two columns, dataset name and whether dataset is mounted
439        .stdin(std::process::Stdio::piped())
440        .stdout(std::process::Stdio::piped())
441        .stderr(std::process::Stdio::piped())
442        .spawn()
443        .map_err(|e| ZfsError::IsMountedCheckCallFailed(dataset.to_string(), e.to_string()))?;
444
445    // Capture the stdout handle of the child process
446    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
447    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
448
449    // Read stdout/stderr to a string
450    let mut stdout_string = String::new();
451    stdout
452        .read_to_string(&mut stdout_string)
453        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
454    let mut stderr_string = String::new();
455    stderr
456        .read_to_string(&mut stderr_string)
457        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
458
459    // Wait for the zfs command to complete
460    let status = child
461        .wait()
462        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
463
464    // Check if the command was successful
465    if status.success() {
466        let lines = stdout_string.lines();
467        let datasets_results = lines
468            .into_iter()
469            .map(|l| l.split_whitespace().collect::<Vec<_>>())
470            .filter(|v| v.len() >= 2)
471            .map(|v| (v[0], v[1]))
472            .collect::<BTreeMap<&str, &str>>();
473        match datasets_results.get(&*dataset) {
474            Some(is_dataset_mounted) => match *is_dataset_mounted {
475                "yes" => Ok(Some(true)),
476                "no" => Ok(Some(false)),
477                _ => Err(ZfsError::UnexpectedStateForMount(
478                    is_dataset_mounted.to_string(),
479                )),
480            },
481            None => Ok(None),
482        }
483    } else {
484        Err(ZfsError::IsMountedCheckCallFailed(
485            dataset.to_string(),
486            stderr_string,
487        ))
488    }
489}
490
491pub fn zfs_list_datasets_mountpoints() -> Result<BTreeMap<String, PathBuf>, ZfsError> {
492    let zfs_exe = pick_zfs_path_for_sudo()?;
493
494    // Create a command to run zfs load-key
495    let mut child = Command::new(zfs_exe)
496        .arg("list")
497        .arg("-H") // No table header
498        .arg("-o")
499        .arg("name,mountpoint") // Only show two columns, dataset name and mountpoint
500        .stdin(std::process::Stdio::piped())
501        .stdout(std::process::Stdio::piped())
502        .stderr(std::process::Stdio::piped())
503        .spawn()
504        .map_err(|e| ZfsError::ListDatasetsMountPointsCallFailed(e.to_string()))?;
505
506    // Capture the stdout handle of the child process
507    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
508    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
509
510    // Read stdout/stderr to a string
511    let mut stdout_string = String::new();
512    stdout
513        .read_to_string(&mut stdout_string)
514        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
515    let mut stderr_string = String::new();
516    stderr
517        .read_to_string(&mut stderr_string)
518        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
519
520    // Wait for the zfs command to complete
521    let status = child
522        .wait()
523        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
524
525    // Check if the command was successful
526    if status.success() {
527        let lines = stdout_string.lines();
528        let datasets_results = lines
529            .into_iter()
530            .map(|l| l.split_whitespace().collect::<Vec<_>>())
531            .filter(|v| v.len() >= 2)
532            .map(|v| (v[0].to_string(), PathBuf::from(v[1])))
533            .collect::<BTreeMap<String, PathBuf>>();
534        Ok(datasets_results)
535    } else {
536        Err(ZfsError::ListDatasetsMountPointsCallFailed(stderr_string))
537    }
538}
539
540pub fn zfs_list_encrypted_datasets() -> Result<BTreeMap<String, DatasetMountedState>, ZfsError> {
541    let zfs_exe = pick_zfs_path_for_sudo()?;
542
543    // Create a command to run zfs load-key
544    let mut child = Command::new(zfs_exe)
545        .arg("list")
546        .arg("-H") // No table header
547        .arg("-o")
548        .arg("name,mounted,keystatus") // Only show two columns, dataset name and mountpoint
549        .stdin(std::process::Stdio::piped())
550        .stdout(std::process::Stdio::piped())
551        .stderr(std::process::Stdio::piped())
552        .spawn()
553        .map_err(|e| ZfsError::ListDatasetsMountPointsCallFailed(e.to_string()))?;
554
555    // Capture the stdout handle of the child process
556    let mut stdout = child.stdout.take().expect("Failed to capture stdout");
557    let mut stderr = child.stderr.take().expect("Failed to capture stderr");
558
559    // Read stdout/stderr to a string
560    let mut stdout_string = String::new();
561    stdout
562        .read_to_string(&mut stdout_string)
563        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
564    let mut stderr_string = String::new();
565    stderr
566        .read_to_string(&mut stderr_string)
567        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
568
569    // Wait for the zfs command to complete
570    let status = child
571        .wait()
572        .map_err(|e| ZfsError::SystemError(e.to_string()))?;
573
574    // Check if the command was successful
575    if status.success() {
576        let lines = stdout_string.lines();
577        let datasets_results = lines
578            .into_iter()
579            .map(|l| l.split_whitespace().collect::<Vec<_>>())
580            .filter(|v| v.len() >= 3)
581            .filter(|v| v[2].trim() != "-") // Filter unencrypted datasets
582            .map(|v| {
583                let dataset_name = v[0].to_string();
584                let is_mounted = parse_dataset_mounted_state(v[1])?;
585                let is_key_loaded = parse_key_available_state(v[2])?;
586                Ok((
587                    dataset_name.clone(),
588                    DatasetMountedState {
589                        dataset_name,
590                        is_mounted,
591                        is_key_loaded,
592                    },
593                ))
594            })
595            .collect::<Result<BTreeMap<String, DatasetMountedState>, _>>()?;
596        Ok(datasets_results)
597    } else {
598        Err(ZfsError::ListUnmountedDatasetsCallFailed(stderr_string))
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    #[test]
607    fn basic() {
608        // Feel free to update these entries to your machine's entries to test
609        let hostname = "ZFSAutomountTester";
610        let ds_name = "SamRandomPool/EncryptedDataset1";
611        let passphrase = "abcdefghijklmnop";
612        let mount_point = "/SamRandomPoolEncryptedDS1";
613
614        if hostname::get().unwrap().to_string_lossy().to_lowercase() == hostname.to_lowercase() {
615            // Try with a non-existent database
616            assert_eq!(zfs_is_key_loaded("some_random_stuff").unwrap(), None);
617
618            // Unmount, before messing with the key
619            zfs_unmount_dataset(ds_name).unwrap();
620
621            // Ensure the key is unloaded and db is unmounted, load it, then unload it
622            zfs_unload_key(ds_name).unwrap();
623            assert_eq!(zfs_is_key_loaded(ds_name).unwrap(), Some(false));
624            assert_eq!(
625                zfs_list_encrypted_datasets()
626                    .unwrap()
627                    .get(ds_name)
628                    .unwrap()
629                    .is_key_loaded,
630                false
631            );
632            zfs_load_key(ds_name, passphrase).unwrap();
633            assert_eq!(zfs_is_key_loaded(ds_name).unwrap(), Some(true));
634            assert_eq!(
635                zfs_list_encrypted_datasets()
636                    .unwrap()
637                    .get(ds_name)
638                    .unwrap()
639                    .is_key_loaded,
640                true
641            );
642            zfs_unload_key(ds_name).unwrap();
643            assert_eq!(
644                zfs_list_encrypted_datasets()
645                    .unwrap()
646                    .get(ds_name)
647                    .unwrap()
648                    .is_key_loaded,
649                false
650            );
651            assert_eq!(zfs_is_key_loaded(ds_name).unwrap(), Some(false));
652
653            zfs_load_key(ds_name, passphrase).unwrap();
654            assert_eq!(
655                zfs_list_encrypted_datasets()
656                    .unwrap()
657                    .get(ds_name)
658                    .unwrap()
659                    .is_key_loaded,
660                true
661            );
662            assert_eq!(zfs_is_key_loaded(ds_name).unwrap(), Some(true));
663
664            zfs_unmount_dataset(ds_name).unwrap();
665            assert_eq!(zfs_is_dataset_mounted(ds_name).unwrap(), Some(false));
666            assert_eq!(
667                zfs_list_encrypted_datasets()
668                    .unwrap()
669                    .get(ds_name)
670                    .unwrap()
671                    .is_mounted,
672                false
673            );
674            zfs_mount_dataset(ds_name).unwrap();
675            assert_eq!(zfs_is_dataset_mounted(ds_name).unwrap(), Some(true));
676            assert_eq!(
677                zfs_list_encrypted_datasets()
678                    .unwrap()
679                    .get(ds_name)
680                    .unwrap()
681                    .is_mounted,
682                true
683            );
684            zfs_unmount_dataset(ds_name).unwrap();
685            assert_eq!(zfs_is_dataset_mounted(ds_name).unwrap(), Some(false));
686            assert_eq!(
687                zfs_list_encrypted_datasets()
688                    .unwrap()
689                    .get(ds_name)
690                    .unwrap()
691                    .is_mounted,
692                false
693            );
694
695            zfs_unload_key(ds_name).unwrap();
696            assert_eq!(zfs_is_key_loaded(ds_name).unwrap(), Some(false));
697
698            let mount_points = zfs_list_datasets_mountpoints().unwrap();
699            assert_eq!(
700                mount_points.get(ds_name).unwrap().to_string_lossy(),
701                mount_point,
702            );
703        } else {
704            let err = "WARNING: No tests were run. Update the tests to test on your machine.";
705            println!("{}", err);
706            eprintln!("{}", err);
707        }
708    }
709
710    #[test]
711    fn test_valid_zfs_dataset_names() {
712        let f = check_and_sanitize_zfs_dataset_name;
713
714        f("pool/dataset1").unwrap();
715        f("pool/dataset_2").unwrap();
716        f("pool.dataset/dataset-3").unwrap();
717        f("pool.dataset/dataset:3").unwrap();
718        f("pool:1/dataset.with.multiple.levels").unwrap();
719        f(" pool:1/dataset.with.multiple.levels").unwrap();
720        f(" pool:1/dataset.with.multiple.levels  ").unwrap();
721    }
722
723    #[test]
724    fn test_invalid_zfs_dataset_names() {
725        let f = check_and_sanitize_zfs_dataset_name;
726
727        f("").unwrap_err();
728        f("_R").unwrap_err();
729        f("-R").unwrap_err();
730        f(":R").unwrap_err();
731        f(".R").unwrap_err();
732        f(" _R").unwrap_err();
733        f(" -R").unwrap_err();
734        f(" :R").unwrap_err();
735        f(" .R").unwrap_err();
736        f("pool/_R").unwrap_err();
737        f("pool/-R").unwrap_err();
738        f("pool/:R").unwrap_err();
739        f("pool/.R").unwrap_err();
740        f("pool/ _R").unwrap_err();
741        f("pool/ -R").unwrap_err();
742        f("pool/ :R").unwrap_err();
743        f("pool/ .R").unwrap_err();
744        f("pool/dataset name").unwrap_err();
745        f("pool/dataset!").unwrap_err();
746        f("pool/dataset@name").unwrap_err();
747        f("pool//dataset").unwrap_err();
748        f("pool/ dataset").unwrap_err();
749    }
750
751    #[test]
752    fn key_loaded_state() {
753        assert_eq!(parse_key_available_state("available").unwrap(), true);
754        assert_eq!(parse_key_available_state("unavailable").unwrap(), false);
755        assert_eq!(parse_key_available_state(" available").unwrap(), true);
756        assert_eq!(parse_key_available_state(" unavailable").unwrap(), false);
757        assert_eq!(parse_key_available_state("available ").unwrap(), true);
758        assert_eq!(parse_key_available_state("unavailable ").unwrap(), false);
759        assert_eq!(parse_key_available_state(" available ").unwrap(), true);
760        assert_eq!(parse_key_available_state(" unavailable ").unwrap(), false);
761
762        parse_key_available_state("yes").unwrap_err();
763        parse_key_available_state("no").unwrap_err();
764        parse_key_available_state(" ").unwrap_err();
765        parse_key_available_state(".").unwrap_err();
766        parse_key_available_state("2222").unwrap_err();
767    }
768
769    #[test]
770    fn is_mounted_state() {
771        assert_eq!(parse_dataset_mounted_state("yes").unwrap(), true);
772        assert_eq!(parse_dataset_mounted_state("no").unwrap(), false);
773        assert_eq!(parse_dataset_mounted_state(" yes").unwrap(), true);
774        assert_eq!(parse_dataset_mounted_state(" no").unwrap(), false);
775        assert_eq!(parse_dataset_mounted_state("yes ").unwrap(), true);
776        assert_eq!(parse_dataset_mounted_state("no ").unwrap(), false);
777        assert_eq!(parse_dataset_mounted_state(" yes ").unwrap(), true);
778        assert_eq!(parse_dataset_mounted_state(" no ").unwrap(), false);
779
780        parse_dataset_mounted_state("available").unwrap_err();
781        parse_dataset_mounted_state("unavailable").unwrap_err();
782        parse_dataset_mounted_state(" ").unwrap_err();
783        parse_dataset_mounted_state(".").unwrap_err();
784        parse_dataset_mounted_state("2222").unwrap_err();
785    }
786}