Skip to content
Merged
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
65 changes: 63 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ log = "0.4"
env_logger = "0.11"
chrono = "0.4"
clap = { version = "4.5", features = ["derive", "cargo"] }
sha2 = "0.10.9"
directories = "6.0.0"
12 changes: 6 additions & 6 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,17 @@
export CLANG="${sunscreen-llvm}/bin/clang"
export RUST_LOG=info

# Set XDG_DATA_HOME to a temporary location for approval storage
# This allows the approval mechanism to work in the nix sandbox
export XDG_DATA_HOME=$TMPDIR/xdg-data

# Replace the mdbook-check-code path in book.toml
# to point to the built binary in this derivation.
sed -i "s|../../target/release/mdbook-check-code|${mdbook-check-code}/bin/mdbook-check-code|g" book.toml

# Approve the book.toml for security
${mdbook-check-code}/bin/mdbook-check-code allow

mdbook build

# After the build is successful, copy the final output to the expected $out path.
Expand Down
111 changes: 111 additions & 0 deletions src/approval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use anyhow::{Context, Result};
use directories::ProjectDirs;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

/// Compute SHA256 hash of path + "\n" + content (direnv style)
pub fn compute_hash(path: &Path, content: &str) -> String {
let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let input = format!("{}\n{}", canonical_path.display(), content);
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
format!("{:x}", hasher.finalize())
}

/// Get the approval directory path
fn get_approval_dir() -> Result<PathBuf> {
// Check for XDG_DATA_HOME environment variable first (respects XDG standard on all platforms)
if let Ok(xdg_data_home) = std::env::var("XDG_DATA_HOME") {
return Ok(PathBuf::from(xdg_data_home)
.join("mdbook-check-code")
.join("allow"));
}

// Fall back to platform-specific defaults via directories crate
let proj_dirs = ProjectDirs::from("", "", "mdbook-check-code")
.context("Failed to determine project directories")?;
Ok(proj_dirs.data_dir().join("allow"))
}

/// Check if a book.toml is approved
pub fn is_approved(book_toml_path: &Path) -> Result<bool> {
let content = fs::read_to_string(book_toml_path)
.with_context(|| format!("Failed to read {}", book_toml_path.display()))?;
let hash = compute_hash(book_toml_path, &content);
let approval_dir = get_approval_dir()?;
let approval_file = approval_dir.join(&hash);
Ok(approval_file.exists())
}

/// Approve a book.toml
pub fn approve(book_toml_path: &Path) -> Result<()> {
let content = fs::read_to_string(book_toml_path)
.with_context(|| format!("Failed to read {}", book_toml_path.display()))?;
let hash = compute_hash(book_toml_path, &content);
let approval_dir = get_approval_dir()?;

// Create approval directory if it doesn't exist
fs::create_dir_all(&approval_dir).with_context(|| {
format!(
"Failed to create approval directory: {}",
approval_dir.display()
)
})?;

// Write approval file with the path
let approval_file = approval_dir.join(&hash);
let canonical_path = book_toml_path
.canonicalize()
.unwrap_or_else(|_| book_toml_path.to_path_buf());
fs::write(&approval_file, canonical_path.display().to_string())
.with_context(|| format!("Failed to write approval file: {}", approval_file.display()))?;

Ok(())
}

/// Deny (remove approval) for a book.toml
pub fn deny(book_toml_path: &Path) -> Result<()> {
let content = fs::read_to_string(book_toml_path)
.with_context(|| format!("Failed to read {}", book_toml_path.display()))?;
let hash = compute_hash(book_toml_path, &content);
let approval_dir = get_approval_dir()?;
let approval_file = approval_dir.join(&hash);

if approval_file.exists() {
fs::remove_file(&approval_file).with_context(|| {
format!(
"Failed to remove approval file: {}",
approval_file.display()
)
})?;
}

Ok(())
}

/// List all approved books
pub fn list_approved() -> Result<Vec<String>> {
let approval_dir = get_approval_dir()?;

if !approval_dir.exists() {
return Ok(vec![]);
}

let mut approved = Vec::new();
for entry in fs::read_dir(&approval_dir).with_context(|| {
format!(
"Failed to read approval directory: {}",
approval_dir.display()
)
})? {
let entry = entry?;
if entry.path().is_file() {
if let Ok(path_content) = fs::read_to_string(entry.path()) {
approved.push(path_content);
}
}
}

Ok(approved)
}
Loading
Loading