Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/manifest/glob/normalize.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
//! Separator and escape normalisation for glob patterns.

/// Normalise forward slashes in a glob `pattern` to the platform separator.
///
/// Forward slashes are rewritten to [`std::path::MAIN_SEPARATOR`] so manifests
/// can use `/` portably. On Unix, backslash escapes are preserved for the
/// follow-up literal-escape pass rather than treated as separators here.
pub(crate) fn normalize_separators(pattern: &str) -> String {
let native = std::path::MAIN_SEPARATOR;
#[cfg(unix)]
Expand Down Expand Up @@ -75,6 +80,12 @@ fn push_normalized_backslash(
out.push('\\');
}

/// Rewrite selected backslash escapes into bracket character classes.
///
/// The `glob` crate treats `\` literally on Unix, so an escaped metacharacter
/// such as `\*` is converted to the single-element class `[*]` to match the
/// literal character. Only escapes for `*`, `?`, `[`, `]`, `{`, and `}` are
/// rewritten; other escapes are left for the `glob` crate to interpret.
#[cfg(unix)]
pub(super) fn force_literal_escapes(pattern: &str) -> String {
let mut out = String::with_capacity(pattern.len());
Expand Down
11 changes: 11 additions & 0 deletions src/manifest/glob/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ impl ValidationState {
}
}

/// Validate that brace groups in a glob `pattern` are balanced.
///
/// Braces inside a `[...]` character class are treated as literals, and on Unix
/// a backslash-escaped brace does not affect nesting depth. An unmatched
/// opening or closing brace yields a syntax error identifying the offending
/// character and position.
///
/// # Errors
///
/// Returns a [`minijinja::Error`](Error) with kind `SyntaxError` when an
/// opening brace is never closed or a closing brace has no matching opener.
pub(super) fn validate_brace_matching(pattern: &str) -> std::result::Result<(), Error> {
let mut state = ValidationState::new();

Expand Down
61 changes: 61 additions & 0 deletions src/runner/process/file_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ pub fn is_stdout_path(path: &Path) -> bool {
path.as_os_str() == "-"
}

/// Write `content` to a freshly created temporary `*.ninja` file.
///
/// The returned [`NamedTempFile`] keeps the file alive; it is deleted when the
/// guard is dropped, so callers must retain it for as long as the build file
/// is needed. The contents are flushed and `fsync`ed before returning so a
/// spawned `ninja` reads a complete file.
///
/// # Errors
///
/// Returns an error if the temporary file cannot be created, written, flushed,
/// or synced to disk.
pub fn create_temp_ninja_file(content: &NinjaContent) -> AnyResult<NamedTempFile> {
let mut tmp = Builder::new()
.prefix("netsuke.")
Expand All @@ -40,6 +51,16 @@ pub fn create_temp_ninja_file(content: &NinjaContent) -> AnyResult<NamedTempFile
Ok(tmp)
}

/// Write `content` to `path`, relative to the capability-scoped `dir`.
///
/// Any missing parent directories under `dir` are created first. The file is
/// flushed and `fsync`ed before returning. Using a [`cap_std`](cap_fs) handle
/// confines the write to the directory tree rooted at `dir`.
///
/// # Errors
///
/// Returns an error if a parent directory cannot be created, or if the file
/// cannot be created, written, flushed, or synced.
pub fn write_text_file_utf8(dir: &cap_fs::Dir, path: &Utf8Path, content: &str) -> AnyResult<()> {
if let Some(parent) = path.parent().filter(|p| !p.as_str().is_empty()) {
dir.create_dir_all(parent.as_str()).with_context(|| {
Expand Down Expand Up @@ -91,11 +112,31 @@ fn derive_dir_and_relative(path: &Utf8Path) -> AnyResult<(cap_fs::Dir, Utf8PathB
Ok((dir, relative))
}

/// Write generated Ninja `content` to `path`.
///
/// Thin wrapper over [`write_text_file`] that unwraps the [`NinjaContent`]
/// newtype.
///
/// # Errors
///
/// Returns an error if `path` is not valid UTF-8, if no existing ancestor
/// directory can be opened, or if the write fails.
pub fn write_ninja_file(path: &Path, content: &NinjaContent) -> AnyResult<()> {
write_text_file(path, content.as_str())?;
Ok(())
}

/// Write `content` to `path`, resolving it against a capability-scoped root.
///
/// The path is split into the deepest existing ancestor directory (opened as a
/// [`cap_std`](cap_fs) handle) and the remaining relative path, so the write is
/// confined to that directory tree. Relative paths resolve against the current
/// working directory.
///
/// # Errors
///
/// Returns an error if `path` is not valid UTF-8, if no existing ancestor
/// directory can be opened, or if [`write_text_file_utf8`] fails.
pub fn write_text_file(path: &Path, content: &str) -> AnyResult<()> {
let utf8_path = Utf8Path::from_path(path).ok_or_else(|| {
anyhow!(
Expand Down Expand Up @@ -130,10 +171,30 @@ fn flush_ignoring_broken_pipe(writer: &mut impl Write) -> io::Result<()> {
}
}

/// Write generated Ninja `content` to standard output.
///
/// Thin wrapper over [`write_text_stdout`] that unwraps the [`NinjaContent`]
/// newtype, used for the `-` stdout sentinel.
///
/// # Errors
///
/// Returns an error if writing to or flushing standard output fails for a
/// reason other than a broken pipe (a closed downstream reader is treated as
/// success).
pub fn write_ninja_stdout(content: &NinjaContent) -> AnyResult<()> {
write_text_stdout(content.as_str())
}

/// Write `content` to standard output, tolerating a closed downstream pipe.
///
/// A `BrokenPipe` error (for example when piping into `head`) is treated as
/// success so the runner exits cleanly rather than reporting spurious I/O
/// failure.
///
/// # Errors
///
/// Returns an error if writing to or flushing standard output fails for any
/// reason other than a broken pipe.
pub fn write_text_stdout(content: &str) -> AnyResult<()> {
let mut stdout = io::stdout().lock();
write_all_ignoring_broken_pipe(&mut stdout, content.as_bytes())
Expand Down
16 changes: 15 additions & 1 deletion src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,26 @@ impl ThemePreference {
///
/// Returns `Ok(ThemePreference)` on success, or `Err(Self::VALID_OPTIONS)`
/// on failure so callers can construct localised error messages using the
/// same canonical list of valid options.
/// same canonical list of valid options. Matching is case-insensitive and
/// ignores surrounding whitespace.
///
/// # Errors
///
/// Returns `Err(Self::VALID_OPTIONS)` if the input string does not match
/// any valid theme option (case-insensitive).
///
/// # Examples
///
/// ```
/// use netsuke::theme::ThemePreference;
///
/// assert_eq!(ThemePreference::parse_raw(" UNICODE "), Ok(ThemePreference::Unicode));
/// assert_eq!(ThemePreference::parse_raw("ascii"), Ok(ThemePreference::Ascii));
/// assert_eq!(
/// ThemePreference::parse_raw("solarized"),
/// Err(ThemePreference::VALID_OPTIONS),
/// );
/// ```
pub fn parse_raw(s: &str) -> Result<Self, &'static [&'static str]> {
let trimmed = s.trim().to_ascii_lowercase();
match trimmed.as_str() {
Expand Down
Loading