mount-fstab 0.1.1

Type-safe /etc/fstab parsing, editing, and validation library
Documentation
use mount_fstab::spec::Spec;
use mount_fstab::types::Fstab;
use std::path::PathBuf;

#[test]
fn parse_minimal_entry() {
    let input = "UUID=abc / ext4 defaults 0 1\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
    let e = &fstab.entries[0];
    assert_eq!(e.spec, Spec::Uuid("abc".into()));
    assert_eq!(e.file.as_path(), PathBuf::from("/"));
    assert_eq!(e.vfstype.as_str(), "ext4");
    assert!(e.options.has("defaults"));
    assert_eq!(e.freq, 0);
    assert_eq!(e.passno, 1);
}

#[test]
fn parse_entry_without_freq_passno() {
    let input = "proc /proc proc defaults\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
    assert_eq!(fstab.entries[0].freq, 0);
    assert_eq!(fstab.entries[0].passno, 0);
}

#[test]
fn parse_entry_without_options() {
    let input = "proc /proc proc\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert!(fstab.entries[0].options.is_empty());
}

#[test]
fn parse_entry_with_escaped_space() {
    let input = r"LABEL=My\040Drive /mnt/data ext4 defaults 0 0
";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert_eq!(e.spec, Spec::Label("My Drive".into()));
}

#[test]
fn parse_multiple_entries() {
    let input = "\
UUID=root / ext4 defaults 0 1
UUID=boot /boot ext4 defaults 0 2
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 2);
}

#[test]
fn parse_with_intro_comment() {
    let input = "\
# This is my fstab
# Generated by systemd

UUID=root / ext4 defaults 0 1
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert!(fstab.intro_comment.is_some());
    let intro = fstab.intro_comment.unwrap();
    assert!(intro.contains("This is my fstab"));
    assert!(intro.contains("Generated by systemd"));
}

#[test]
fn parse_with_per_entry_comment() {
    let input = "\
# Root filesystem
UUID=root / ext4 defaults 0 1
# Boot partition
UUID=boot /boot ext4 defaults 0 2
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 2);
    assert!(fstab.entries[0].comment.is_some());
    assert!(
        fstab.entries[0]
            .comment
            .as_ref()
            .unwrap()
            .contains("Root filesystem")
    );
    assert!(fstab.entries[1].comment.is_some());
    assert!(
        fstab.entries[1]
            .comment
            .as_ref()
            .unwrap()
            .contains("Boot partition")
    );
}

#[test]
fn parse_with_trailing_comment() {
    let input = "\
UUID=root / ext4 defaults 0 1

# End of fstab
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert!(fstab.trailing_comment.is_some());
    assert!(fstab.trailing_comment.unwrap().contains("End of fstab"));
}

#[test]
fn parse_blank_line_separates_intro() {
    let input = "\
# Intro comment

# This becomes per-entry comment for first entry
UUID=root / ext4 defaults 0 1
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert!(fstab.intro_comment.is_some());
    assert_eq!(fstab.entries.len(), 1);
    assert!(fstab.entries[0].comment.is_some());
}

#[test]
fn parse_empty_file() {
    let fstab = Fstab::parse_str("").unwrap();
    assert!(fstab.entries.is_empty());
}

#[test]
fn parse_comments_only() {
    let fstab = Fstab::parse_str("# Just a comment\n# And another\n").unwrap();
    assert!(fstab.entries.is_empty());
    assert!(fstab.intro_comment.is_some());
}

#[test]
fn parse_carriage_return_stripped() {
    let input = "UUID=root / ext4 defaults 0 1\r\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
}

#[test]
fn parse_nfs_entry() {
    let input = "server.example.com:/exports /mnt/nfs nfs defaults 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert_eq!(
        e.spec,
        Spec::NetworkMount {
            host: "server.example.com".into(),
            path: PathBuf::from("/exports"),
        }
    );
}

#[test]
fn parse_swap_entry() {
    let input = "/swap.img none swap sw 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert!(e.is_swap());
    assert!(e.file.is_swap());
}

#[test]
fn parse_tab_separated_fields() {
    let input = "UUID=abc\t/\text4\tdefaults\t0\t1\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
    assert_eq!(fstab.entries[0].vfstype.as_str(), "ext4");
}

#[test]
fn parse_mixed_tabs_and_spaces() {
    let input = "UUID=abc  \t/\t  ext4 \t defaults\t0\t1\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
}

#[test]
fn parse_leading_whitespace_on_data_line() {
    let input = "   UUID=abc / ext4 defaults 0 1\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
}

#[test]
fn parse_three_field_entry() {
    let input = "proc /proc proc\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert!(e.options.is_empty());
    assert_eq!(e.freq, 0);
    assert_eq!(e.passno, 0);
}

#[test]
fn parse_five_field_entry() {
    let input = "UUID=abc / ext4 defaults 1\n"; // has freq but no passno
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries[0].freq, 1);
    assert_eq!(fstab.entries[0].passno, 0);
}

#[test]
fn parse_comment_with_leading_whitespace() {
    let input = "   # Indented comment\nUUID=abc / ext4 defaults 0 1\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert!(fstab.entries[0].comment.is_some());
    assert!(
        fstab.entries[0]
            .comment
            .as_ref()
            .unwrap()
            .contains("Indented comment")
    );
}

#[test]
fn parse_blank_lines_between_entries_preserved_as_spacing() {
    let input = "UUID=a /mnt1 ext4 defaults 0 0\n\n\nUUID=b /mnt2 ext4 defaults 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 2);
}

#[test]
fn parse_multiple_comment_lines_before_entry() {
    let input =
        "# First comment line\n# Second comment line\n# Third\nUUID=abc / ext4 defaults 0 1\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let comment = fstab.entries[0].comment.as_ref().unwrap();
    assert!(comment.contains("First comment line"));
    assert!(comment.contains("Second comment line"));
    assert!(comment.contains("Third"));
}

#[test]
fn parse_file_without_trailing_newline() {
    let input = "UUID=abc / ext4 defaults 0 1"; // no \n at end
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 1);
}

#[test]
fn parse_double_escaped_backslash() {
    // In fstab: \\\\ → parse_raw decodes to \\ (single decode, no double-decode bug)
    let input = r"LABEL=test\\\\label /mnt ext4 defaults 0 0
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries[0].spec, Spec::Label(r"test\\label".into()));
}

#[test]
fn parse_swap_with_priority_option() {
    let input = "/swap.img none swap sw,pri=10 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert!(e.is_swap());
    assert_eq!(e.options.get("pri"), Some("10"));
}

#[test]
fn parse_tmpfs_with_size_option() {
    let input = "tmpfs /tmp tmpfs size=10G,mode=1777 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert_eq!(e.options.get("size"), Some("10G"));
    assert_eq!(e.options.get("mode"), Some("1777"));
}

#[test]
fn parse_bind_mount() {
    let input = "/mnt/data /srv/data none bind 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    let e = &fstab.entries[0];
    assert!(e.is_bind_mount());
}

#[test]
fn parse_nfs_with_multiple_options() {
    let input = "server:/path /mnt nfs rw,hard,intr,timeo=100,retrans=3 0 0\n";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries[0].options.get("timeo"), Some("100"));
    assert_eq!(fstab.entries[0].options.get("retrans"), Some("3"));
}

#[test]
fn parse_cifs_with_credentials() {
    let input = r"//server/share /mnt cifs credentials=/etc/samba/creds,uid=1000,gid=1000 0 0
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(
        fstab.entries[0].options.get("credentials"),
        Some("/etc/samba/creds")
    );
}

#[test]
fn parse_entries_with_different_delimiters() {
    // Each entry in the same file with different whitespace styles
    let input = "\
UUID=a / ext4 defaults 0 1
UUID=b\t/boot\text4\tdefaults\t0\t2
UUID=c  /data  xfs  defaults  0  0
";
    let fstab = Fstab::parse_str(input).unwrap();
    assert_eq!(fstab.entries.len(), 3);
}

#[test]
fn parse_error_reports_line_number() {
    let input = "good /mnt ext4 defaults 0 0\nbadline\n";
    let err = Fstab::parse_str(input).unwrap_err();
    let msg = err.to_string();
    assert!(
        msg.contains("line 2"),
        "expected line 2 in error, got: {msg}"
    );
}