mount-fstab 0.1.1

Type-safe /etc/fstab parsing, editing, and validation library
Documentation
//! fstab string and file parsing.
//!
//! This module handles parsing of complete fstab files including:
//! - Data lines (6 whitespace-separated fields)
//! - Comment lines (starting with `#`)
//! - Blank line handling for comment attribution
//! - Carriage return stripping for cross-platform fstab files

use crate::error::{FstabError, ParseErrorKind};
use crate::escape::decode_escapes;
use crate::fstype::FsType;
use crate::options::Options;
use crate::spec::Spec;
use crate::types::{Entry, Fstab, MountPoint};
use std::path::Path;

/// Parse a complete fstab string into an [`Fstab`] structure.
///
/// Handles comments (lines starting with `#`), blank lines, and
/// data lines with 3-6 whitespace-separated fields.
fn parse_fstab(input: &str) -> Result<Fstab, FstabError> {
    let mut fstab = Fstab::new();
    let mut pending_comment = String::new();
    let mut saw_blank_after_intro = false;
    let mut has_entries = false;

    for (line_no, raw_line) in input.lines().enumerate() {
        let line_no = line_no + 1; // 1-indexed
        let line = raw_line.trim_end_matches('\r');
        let trimmed = line.trim_start_matches([' ', '\t']);
        let first_char = trimmed.chars().next();

        match first_char {
            Some(ch) if ch != '#' => {
                let entry = parse_data_line(trimmed, line_no)?;
                has_entries = true;
                let mut entry = entry;
                if !pending_comment.is_empty() {
                    entry.comment = Some(std::mem::take(&mut pending_comment));
                }
                fstab.entries.push(entry);
            }
            _ => {
                if first_char == Some('#') {
                    pending_comment.push_str(line);
                    pending_comment.push('\n');
                } else if first_char.is_none() {
                    if !has_entries {
                        if !saw_blank_after_intro && !pending_comment.is_empty() {
                            pending_comment.push('\n');
                            fstab.intro_comment = Some(std::mem::take(&mut pending_comment));
                            saw_blank_after_intro = true;
                        } else if saw_blank_after_intro {
                            pending_comment.push('\n');
                        }
                    } else {
                        pending_comment.push('\n');
                    }
                }
            }
        }
    }

    if !pending_comment.is_empty() {
        if !has_entries {
            fstab.intro_comment = Some(pending_comment);
        } else {
            fstab.trailing_comment = Some(pending_comment);
        }
    }

    Ok(fstab)
}

/// Parse a single data line (non-comment, non-blank) into an [`Entry`].
fn parse_data_line(line: &str, line_no: usize) -> Result<Entry, FstabError> {
    let fields: Vec<&str> = line.split([' ', '\t']).filter(|s| !s.is_empty()).collect();

    let mut iter = fields.into_iter();

    // Field 1: spec (required)
    let spec_raw = iter.next().ok_or(FstabError::Parse {
        line: line_no,
        kind: ParseErrorKind::MissingField("spec"),
    })?;
    let spec = Spec::parse_raw(spec_raw).map_err(|e| FstabError::Parse {
        line: line_no,
        kind: ParseErrorKind::InvalidSpec(e),
    })?;

    // Field 2: mount point (required)
    let file_raw = iter.next().ok_or(FstabError::Parse {
        line: line_no,
        kind: ParseErrorKind::MissingField("file"),
    })?;
    let file_decoded = decode_escapes(file_raw);
    let file = MountPoint::new(file_decoded).map_err(|e| FstabError::Parse {
        line: line_no,
        kind: ParseErrorKind::InvalidMountPoint(e),
    })?;

    // Field 3: fstype (required)
    let fstype_raw = iter.next().ok_or(FstabError::Parse {
        line: line_no,
        kind: ParseErrorKind::MissingField("vfstype"),
    })?;
    let fstype = FsType::parse(fstype_raw).map_err(|e| FstabError::Parse {
        line: line_no,
        kind: ParseErrorKind::InvalidFsType(e),
    })?;

    // Field 4: options (optional, default empty)
    let options_raw = iter.next();
    let options = match options_raw {
        Some(raw) => Options::parse(raw).map_err(|e| FstabError::Parse {
            line: line_no,
            kind: ParseErrorKind::InvalidOptions(e),
        })?,
        None => Options::new(),
    };

    // Field 5: freq (optional, default 0)
    let freq_raw = iter.next();
    let freq = match freq_raw {
        Some(raw) => raw.parse::<u32>().map_err(|_| FstabError::Parse {
            line: line_no,
            kind: ParseErrorKind::InvalidFreq(raw.to_owned()),
        })?,
        None => 0,
    };

    // Field 6: passno (optional, default 0)
    let passno_raw = iter.next();
    let passno = match passno_raw {
        Some(raw) => raw.parse::<u32>().map_err(|_| FstabError::Parse {
            line: line_no,
            kind: ParseErrorKind::InvalidPassNo(raw.to_owned()),
        })?,
        None => 0,
    };

    Ok(Entry {
        spec,
        file,
        vfstype: fstype,
        options,
        freq,
        passno,
        comment: None,
    })
}

impl Fstab {
    /// Parse a fstab string into an [`Fstab`] structure.
    ///
    /// # Examples
    ///
    /// ```
    /// # use mount_fstab::Fstab;
    /// let input = "UUID=root / ext4 defaults 0 1\nUUID=boot /boot ext4 defaults 0 2\n";
    /// let fstab = Fstab::parse_str(input).unwrap();
    /// assert_eq!(fstab.len(), 2);
    /// ```
    ///
    /// # Errors
    ///
    /// Returns [`FstabError`] on parse failures, with the line number and
    /// specific error kind (invalid spec, mount point, fstype, options, etc.).
    pub fn parse_str(input: &str) -> Result<Self, FstabError> {
        parse_fstab(input)
    }

    /// Parse a fstab file from the given path.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use mount_fstab::Fstab;
    /// let fstab = Fstab::parse_file("/etc/fstab").unwrap();
    /// ```
    ///
    /// # Errors
    ///
    /// Returns [`FstabError::Io`] if the file cannot be read, or
    /// returns parse errors from [`parse_str`](Self::parse_str).
    pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, FstabError> {
        let input = std::fs::read_to_string(path)?;
        Self::parse_str(&input)
    }
}