obj-db 1.1.2

Embedded document database. Stable file format, full ACID, single-file portability.
Documentation
//! M14 issue #118: assert every `[text](path.md)` cross-reference
//! in `docs/*.md` resolves to an existing file inside the same
//! `docs/` tree. A cheap shape check that catches typos, file
//! renames, and the kind of bit-rot a 2100-line spec accumulates.
//!
//! Anchor fragments (`#section-anchor`) are stripped before the
//! existence check — Markdown processors do not require anchors to
//! resolve to header text, and a heading rename would otherwise
//! turn into a doc-test failure with no actionable fix beyond
//! "rename the anchor too".
//!
//! Out-of-`docs/` links (anything that does not end in `.md`, or
//! whose path is absolute, or which is an `http(s)://` URL) are
//! ignored. The test is intentionally narrow — it protects the
//! intra-`docs/` link graph, nothing else.

// Test helpers panic on workspace-root lookup failure / dir-read
// failure — both indicate an unrunnable environment, not a docs
// failure, so the panic is the right shape and the lint about
// missing panic documentation is noise on test code.
#![allow(clippy::missing_panics_doc)]

use std::fs;
use std::path::{Path, PathBuf};

/// Walk up from `CARGO_MANIFEST_DIR` until we hit the workspace
/// root. The workspace root is the directory whose `Cargo.toml`
/// contains a `[workspace]` table.
fn workspace_root() -> PathBuf {
    let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let mut cur = manifest_dir;
    // Rule 2: bounded depth.
    for _ in 0..8 {
        if let Ok(text) = fs::read_to_string(cur.join("Cargo.toml")) {
            if text.contains("[workspace]") {
                return cur;
            }
        }
        if !cur.pop() {
            break;
        }
    }
    panic!("could not locate workspace root");
}

/// Yield every `(target_md_relative_path)` from every
/// `[text](target)` markdown link in `src`. Strips the `#anchor`
/// fragment. Skips links that are absolute, schemed, or whose
/// path does not end in `.md`.
fn extract_md_links(src: &str) -> Vec<String> {
    let bytes = src.as_bytes();
    let mut out: Vec<String> = Vec::new();
    let mut i = 0usize;
    // Rule 2: bounded by `bytes.len()` — every iteration advances
    // `i` by at least 1.
    while i < bytes.len() {
        // Find `](`. The `[` opener is not required to be checked
        // because every `](` in markdown only appears inside link
        // syntax (or inside fenced code that we don't traverse;
        // benign noise — the resulting "link" simply fails the
        // `.md` filter).
        if bytes[i] == b']' && i + 1 < bytes.len() && bytes[i + 1] == b'(' {
            let start = i + 2;
            let mut end = start;
            // Rule 2: bounded by `bytes.len()`.
            while end < bytes.len() && bytes[end] != b')' && bytes[end] != b'\n' {
                end += 1;
            }
            if end < bytes.len() && bytes[end] == b')' {
                let raw = &src[start..end];
                // Strip anchor.
                let path = raw.split('#').next().unwrap_or(raw).trim();
                if !path.is_empty()
                    && !path.starts_with("http://")
                    && !path.starts_with("https://")
                    && !path.starts_with("mailto:")
                    && !path.starts_with('/')
                    && std::path::Path::new(path)
                        .extension()
                        .is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
                {
                    out.push(path.to_owned());
                }
                i = end + 1;
                continue;
            }
        }
        i += 1;
    }
    out
}

/// Resolve `link` relative to `from_dir`, normalising
/// `..` / `.` segments. Returns the absolute candidate path.
fn resolve(from_dir: &Path, link: &str) -> PathBuf {
    let mut out: PathBuf = from_dir.to_path_buf();
    for segment in link.split('/') {
        match segment {
            "" | "." => {}
            ".." => {
                out.pop();
            }
            other => out.push(other),
        }
    }
    out
}

#[test]
fn every_intra_docs_md_link_resolves() {
    let root = workspace_root();
    let docs = root.join("docs");
    assert!(docs.is_dir(), "docs/ must exist at workspace root");

    let mut failures: Vec<String> = Vec::new();
    // Rule 2: ReadDir is iterable; bounded by the file count of the
    // docs directory (<10 files in v1.0 — tiny).
    let entries = fs::read_dir(&docs).expect("read_dir docs/");
    for entry in entries {
        let entry = entry.expect("dir entry");
        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) != Some("md") {
            continue;
        }
        let text =
            fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
        let links = extract_md_links(&text);
        // Rule 2: per-file links cap at ~hundreds; the worst
        // observed in `docs/format.md` is ~60.
        for link in links {
            let resolved = resolve(path.parent().expect("parent"), &link);
            if !resolved.exists() {
                failures.push(format!(
                    "{}: link `{}` resolves to missing file `{}`",
                    path.file_name().and_then(|s| s.to_str()).unwrap_or("?"),
                    link,
                    resolved.display(),
                ));
            }
        }
    }

    assert!(
        failures.is_empty(),
        "broken intra-docs links:\n{}",
        failures.join("\n")
    );
}