blob: d4ae000e2a36b5ac9a62532f6efd1a4e8577bc69 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
/// Extension methods for `std::path::Path` specific to Fuchsia
/// documentation checking.
pub trait DocPathExt {
/// Returns true if this path lies within developer-facing tools
/// (e.g., under 'skills').
fn is_ignored_doc(&self) -> bool;
/// Returns true if this path represents a doc navbar.
fn is_navbar_doc(&self) -> bool;
/// Returns true if the file is a macOS metadata file (starts with '._').
fn is_macos_hidden_doc(&self) -> bool;
/// Returns true if this path has hidden/private components (starts with
/// '_') relative to the documentation roots.
///
/// Strips `root_dir` and `reference_docs_root` prefixes before performing
/// the check, to prevent false-positive ignore matches if the checkout
/// directory itself contains an underscore.
fn is_hidden_doc(&self, root_dir: &Path, reference_docs_root: Option<&Path>) -> bool;
}
impl DocPathExt for Path {
fn is_ignored_doc(&self) -> bool {
self.components()
.any(|c| matches!(c, Component::Normal(name) if name == OsStr::new("skills")))
}
fn is_navbar_doc(&self) -> bool {
self.file_name() == Some(OsStr::new("navbar.md"))
}
fn is_macos_hidden_doc(&self) -> bool {
self.file_name().and_then(|name| name.to_str()).map_or(false, |s| s.starts_with("._"))
}
fn is_hidden_doc(&self, root_dir: &Path, reference_docs_root: Option<&Path>) -> bool {
let rel_p = self
.strip_prefix(root_dir)
.ok()
.or_else(|| reference_docs_root.and_then(|r| self.strip_prefix(r).ok()));
rel_p.map_or(false, |p| {
p.components().any(|c| {
matches!(c, Component::Normal(s) if s.to_str().unwrap_or_default().starts_with('_'))
})
})
}
}
/// Standard path normalization that resolves '.' and '..' components without accessing the filesystem.
/// Returns `Err` if the path escapes the root (i.e. starts with '..').
#[allow(dead_code)]
pub fn normalize_path(path: &Path) -> anyhow::Result<PathBuf> {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::Prefix(p) => normalized.push(p.as_os_str()),
Component::RootDir => normalized.push("/"),
Component::CurDir => {}
Component::ParentDir => {
if !normalized.pop() {
anyhow::bail!(
"Cannot normalize {}, references parent beyond root.",
path.display()
);
}
}
Component::Normal(p) => normalized.push(p),
}
}
Ok(normalized)
}
/// Normalizes the path and verifies it remains within the specified `root_dir`.
/// If the normalized path is outside `root_dir`, returns an error.
#[allow(dead_code)]
pub fn normalize_and_validate_path(path: &Path, root_dir: &Path) -> anyhow::Result<PathBuf> {
let normalized = normalize_path(&root_dir.join(path))?;
let normalized_root = normalize_path(root_dir).unwrap_or_else(|_| root_dir.to_path_buf());
if normalized.starts_with(&normalized_root) {
Ok(normalized)
} else {
anyhow::bail!("Included markdown file {:?} escapes workspace root.", path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[fuchsia::test]
fn test_is_ignored_doc() {
assert!(Path::new("docs/skills/SKILL.md").is_ignored_doc());
assert!(Path::new("vendor/google/skills/yaml/config.yaml").is_ignored_doc());
assert!(!Path::new("docs/contribute/governance.md").is_ignored_doc());
assert!(!Path::new("docs/_toc.yaml").is_ignored_doc());
}
#[fuchsia::test]
fn test_is_navbar_doc() {
assert!(Path::new("docs/navbar.md").is_navbar_doc());
assert!(Path::new("navbar.md").is_navbar_doc());
assert!(!Path::new("docs/README.md").is_navbar_doc());
}
#[fuchsia::test]
fn test_is_macos_hidden_doc() {
assert!(Path::new("docs/._README.md").is_macos_hidden_doc());
assert!(Path::new("._index.md").is_macos_hidden_doc());
assert!(!Path::new("docs/README.md").is_macos_hidden_doc());
}
#[fuchsia::test]
fn test_is_hidden_doc() {
let root_dir = PathBuf::from("/home/user/fuchsia");
let ref_dir = PathBuf::from("/home/user/reference_docs");
// Standard file (should not be hidden)
let p1 = PathBuf::from("/home/user/fuchsia/docs/getting-started.md");
assert!(!p1.is_hidden_doc(&root_dir, None));
// File starting with underscore (should be hidden)
let p2 = PathBuf::from("/home/user/fuchsia/docs/_index.md");
assert!(p2.is_hidden_doc(&root_dir, None));
// File inside a hidden folder (should be hidden)
let p3 = PathBuf::from("/home/user/fuchsia/docs/_common/header.md");
assert!(p3.is_hidden_doc(&root_dir, None));
// Standard file in reference docs (should not be hidden)
let p4 = PathBuf::from("/home/user/reference_docs/sdk/overview.md");
assert!(!p4.is_hidden_doc(&root_dir, Some(&ref_dir)));
// File inside a hidden folder in reference docs (should be hidden)
let p5 = PathBuf::from("/home/user/reference_docs/_internal/helper.md");
assert!(p5.is_hidden_doc(&root_dir, Some(&ref_dir)));
}
#[fuchsia::test]
fn test_is_hidden_doc_with_underscore_in_workspace_roots() {
// Scenario where workspace path contains underscore (e.g., /home/user/_workspace)
let root_dir = PathBuf::from("/home/user/_workspace/fuchsia");
let ref_dir = PathBuf::from("/home/user/_workspace/reference");
// Standard file should NOT be hidden, even though workspace path has underscore
let p1 = PathBuf::from("/home/user/_workspace/fuchsia/docs/getting-started.md");
assert!(!p1.is_hidden_doc(&root_dir, Some(&ref_dir)));
let p2 = PathBuf::from("/home/user/_workspace/reference/sdk/overview.md");
assert!(!p2.is_hidden_doc(&root_dir, Some(&ref_dir)));
// Hidden file inside workspace with underscore should still be detected correctly
let p3 = PathBuf::from("/home/user/_workspace/fuchsia/docs/_common/header.md");
assert!(p3.is_hidden_doc(&root_dir, Some(&ref_dir)));
let p4 = PathBuf::from("/home/user/_workspace/reference/_internal/helper.md");
assert!(p4.is_hidden_doc(&root_dir, Some(&ref_dir)));
}
#[fuchsia::test]
fn test_normalize_path() {
assert_eq!(normalize_path(Path::new("a/b/../c")).unwrap(), PathBuf::from("a/c"));
assert_eq!(normalize_path(Path::new("./a/b/.")).unwrap(), PathBuf::from("a/b"));
assert_eq!(normalize_path(Path::new("/a/b/c")).unwrap(), PathBuf::from("/a/b/c"));
assert!(normalize_path(Path::new("a/../../b")).is_err());
}
#[fuchsia::test]
fn test_normalize_and_validate_path() {
let root_dir = PathBuf::from("/home/user/fuchsia");
// Valid relative path
assert_eq!(
normalize_and_validate_path(Path::new("docs/getting-started.md"), &root_dir).unwrap(),
PathBuf::from("/home/user/fuchsia/docs/getting-started.md")
);
// Valid absolute path
assert_eq!(
normalize_and_validate_path(
Path::new("/home/user/fuchsia/docs/getting-started.md"),
&root_dir
)
.unwrap(),
PathBuf::from("/home/user/fuchsia/docs/getting-started.md")
);
// Escaping root path
assert!(normalize_and_validate_path(Path::new("../external/file.md"), &root_dir).is_err());
}
}