mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
* refactor: Update comments for clarity and add expectations.json files for performance metrics * feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks * feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks * refactor: Simplify code formatting for better readability in multiple files * refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration * feat: Update Java and Python patterns to include new security rules * refactor: Improve comment clarity and consistency across multiple Rust files * refactor: Simplify code formatting for improved readability in integration tests and module files * refactor: Improve comment formatting and enhance clarity in assertions across multiple files
361 lines
11 KiB
Rust
361 lines
11 KiB
Rust
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn main() {
|
|
render_docs_for_rustdoc();
|
|
|
|
// Only relevant when the serve feature is active
|
|
if std::env::var("CARGO_FEATURE_SERVE").is_err() {
|
|
return;
|
|
}
|
|
|
|
let dist_dir = Path::new("src/server/assets/dist");
|
|
let index_html = dist_dir.join("index.html");
|
|
|
|
// Re-run build.rs only when dist output is missing/changed
|
|
println!("cargo:rerun-if-changed=src/server/assets/dist/index.html");
|
|
|
|
if index_html.exists() {
|
|
// Dist already built, nothing to do
|
|
return;
|
|
}
|
|
|
|
// Dist missing, try to build frontend
|
|
let frontend_dir = Path::new("frontend");
|
|
if !frontend_dir.join("package.json").exists() {
|
|
emit_placeholder_and_warn(dist_dir);
|
|
return;
|
|
}
|
|
|
|
// Run npm install + build
|
|
println!("cargo:warning=Frontend dist not found, running npm install && npm run build...");
|
|
let npm_install = Command::new("npm")
|
|
.arg("install")
|
|
.current_dir(frontend_dir)
|
|
.status();
|
|
|
|
match npm_install {
|
|
Ok(s) if s.success() => {}
|
|
_ => {
|
|
emit_placeholder_and_warn(dist_dir);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let npm_build = Command::new("npm")
|
|
.arg("run")
|
|
.arg("build")
|
|
.current_dir(frontend_dir)
|
|
.status();
|
|
|
|
match npm_build {
|
|
Ok(s) if s.success() => {
|
|
println!("cargo:warning=Frontend built successfully.");
|
|
}
|
|
_ => {
|
|
emit_placeholder_and_warn(dist_dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rustdoc / docs.rs: render docs/*.md into $OUT_DIR with relative .md links
|
|
// rewritten to absolute github.com/elicpeter/nyx URLs so they resolve when the
|
|
// markdown is embedded in rustdoc via #![doc = include_str!(...)].
|
|
//
|
|
// Source of truth stays in docs/. Files that don't exist (published-crate
|
|
// builds where docs/ wasn't packaged) fall back to a one-line stub so rustdoc
|
|
// still compiles.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const GH_DOCS_BASE: &str = "https://github.com/elicpeter/nyx/blob/master/docs";
|
|
|
|
struct DocSpec {
|
|
/// Path under docs/, e.g. "how-it-works.md" or "detectors/taint.md".
|
|
src: &'static str,
|
|
/// Output filename in $OUT_DIR.
|
|
out: &'static str,
|
|
}
|
|
|
|
const DOC_SPECS: &[DocSpec] = &[
|
|
DocSpec {
|
|
src: "how-it-works.md",
|
|
out: "lib_intro.md",
|
|
},
|
|
DocSpec {
|
|
src: "detectors/taint.md",
|
|
out: "taint.md",
|
|
},
|
|
DocSpec {
|
|
src: "detectors/cfg.md",
|
|
out: "cfg_analysis.md",
|
|
},
|
|
DocSpec {
|
|
src: "detectors/state.md",
|
|
out: "state.md",
|
|
},
|
|
DocSpec {
|
|
src: "detectors/patterns.md",
|
|
out: "patterns.md",
|
|
},
|
|
DocSpec {
|
|
src: "auth.md",
|
|
out: "auth_analysis.md",
|
|
},
|
|
];
|
|
|
|
fn render_docs_for_rustdoc() {
|
|
let Ok(out_dir) = std::env::var("OUT_DIR") else {
|
|
return;
|
|
};
|
|
let out_dir = PathBuf::from(out_dir);
|
|
let docs_dir = Path::new("docs");
|
|
|
|
for spec in DOC_SPECS {
|
|
let src_path = docs_dir.join(spec.src);
|
|
println!("cargo:rerun-if-changed=docs/{}", spec.src);
|
|
let out_path = out_dir.join(spec.out);
|
|
let rendered = match std::fs::read_to_string(&src_path) {
|
|
Ok(raw) => rewrite_doc_links(&raw, spec.src),
|
|
Err(_) => format!(
|
|
"See [`{base}/{src}`]({base}/{src}).\n",
|
|
base = GH_DOCS_BASE,
|
|
src = spec.src,
|
|
),
|
|
};
|
|
if let Err(e) = std::fs::write(&out_path, rendered) {
|
|
println!(
|
|
"cargo:warning=failed to write rendered doc {}: {}",
|
|
out_path.display(),
|
|
e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Render markdown for embedding in rustdoc.
|
|
///
|
|
/// 1. Rewrites relative `.md` links to absolute github.com URLs:
|
|
/// - inline links: `](path.md)` and `](path.md#anchor)`
|
|
/// - reference defs: `[id]: path.md`
|
|
/// 2. Labels unmarked fenced code blocks as `text` so rustdoc does not try
|
|
/// to compile them as Rust (and choke on Unicode like `→`).
|
|
/// 3. Annotates `rust` fences with `,ignore` so rustdoc doesn't try to
|
|
/// compile or run prose-level snippets as doctests. GitHub still
|
|
/// highlights them as Rust because it keys off the first token.
|
|
///
|
|
/// Skips link rewriting inside code fences. Skips link rewriting for URLs
|
|
/// that are already absolute (have a scheme), pure anchors (`#section`),
|
|
/// or non-`.md` paths.
|
|
fn rewrite_doc_links(content: &str, source_rel: &str) -> String {
|
|
let source_dir = Path::new(source_rel)
|
|
.parent()
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
.unwrap_or_default();
|
|
|
|
let mut out = String::with_capacity(content.len() + 256);
|
|
let mut in_fence = false;
|
|
|
|
for line in content.split_inclusive('\n') {
|
|
let body = line.strip_suffix('\n').unwrap_or(line);
|
|
let trimmed = body.trim_start();
|
|
if trimmed.starts_with("```") {
|
|
let lang = trimmed.trim_start_matches('`').trim();
|
|
if in_fence {
|
|
in_fence = false;
|
|
out.push_str(line);
|
|
} else {
|
|
in_fence = true;
|
|
let indent_len = body.len() - trimmed.len();
|
|
if lang.is_empty() {
|
|
out.push_str(&body[..indent_len]);
|
|
out.push_str("```text");
|
|
if line.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
} else if is_rust_fence_needing_ignore(lang) {
|
|
out.push_str(&body[..indent_len]);
|
|
out.push_str("```rust,ignore");
|
|
if line.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
} else {
|
|
out.push_str(line);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if in_fence {
|
|
out.push_str(line);
|
|
} else {
|
|
rewrite_links_in_line(body, &source_dir, &mut out);
|
|
if line.ends_with('\n') {
|
|
out.push('\n');
|
|
}
|
|
}
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn rewrite_links_in_line(line: &str, source_dir: &str, out: &mut String) {
|
|
let bytes = line.as_bytes();
|
|
let mut i = 0;
|
|
while i < bytes.len() {
|
|
// Inline link: `](URL)`, markdown URLs do not contain a raw `)`.
|
|
if i + 1 < bytes.len() && bytes[i] == b']' && bytes[i + 1] == b'(' {
|
|
out.push_str("](");
|
|
i += 2;
|
|
let url_start = i;
|
|
while i < bytes.len() && bytes[i] != b')' {
|
|
i += 1;
|
|
}
|
|
let url = &line[url_start..i];
|
|
out.push_str(&maybe_rewrite_url(url, source_dir));
|
|
}
|
|
// Reference def: `]: URL`.
|
|
else if i + 2 < bytes.len()
|
|
&& bytes[i] == b']'
|
|
&& bytes[i + 1] == b':'
|
|
&& bytes[i + 2] == b' '
|
|
{
|
|
out.push_str("]: ");
|
|
i += 3;
|
|
let url_start = i;
|
|
while i < bytes.len() && bytes[i] != b' ' {
|
|
i += 1;
|
|
}
|
|
let url = &line[url_start..i];
|
|
out.push_str(&maybe_rewrite_url(url, source_dir));
|
|
} else {
|
|
// `]` (0x5D) is ASCII; UTF-8 continuation bytes are 0x80-0xBF
|
|
// and start bytes are 0xC0+, so byte-level scanning of `]` is
|
|
// safe. For non-ASCII bytes, copy the full codepoint at once.
|
|
let b = bytes[i];
|
|
if b < 0x80 {
|
|
out.push(b as char);
|
|
i += 1;
|
|
} else {
|
|
let len = utf8_seq_len(b);
|
|
let end = (i + len).min(bytes.len());
|
|
out.push_str(&line[i..end]);
|
|
i = end;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// True for `rust` / `rust,...` fences that don't already opt out of
|
|
/// doctest execution. We rewrite these to `rust,ignore` because the prose
|
|
/// snippets in docs/ are illustrative, not standalone-compilable.
|
|
fn is_rust_fence_needing_ignore(lang: &str) -> bool {
|
|
let mut parts = lang.split(',').map(|p| p.trim());
|
|
let Some(first) = parts.next() else {
|
|
return false;
|
|
};
|
|
if !first.eq_ignore_ascii_case("rust") {
|
|
return false;
|
|
}
|
|
for tag in parts {
|
|
let t = tag.to_ascii_lowercase();
|
|
if t == "ignore" || t == "no_run" || t == "compile_fail" || t == "should_panic" {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
fn utf8_seq_len(lead: u8) -> usize {
|
|
// lead < 0xC0 covers ASCII and unexpected continuation bytes; treat both as
|
|
// single-byte to make progress.
|
|
if lead < 0xC0 {
|
|
1
|
|
} else if lead < 0xE0 {
|
|
2
|
|
} else if lead < 0xF0 {
|
|
3
|
|
} else {
|
|
4
|
|
}
|
|
}
|
|
|
|
fn maybe_rewrite_url(url: &str, source_dir: &str) -> String {
|
|
if url.is_empty() {
|
|
return url.to_string();
|
|
}
|
|
// Already absolute (scheme://, mailto:, ssh://, etc.), leave alone.
|
|
if has_scheme(url) {
|
|
return url.to_string();
|
|
}
|
|
// Pure anchor, leave alone.
|
|
if url.starts_with('#') {
|
|
return url.to_string();
|
|
}
|
|
// Split off optional anchor.
|
|
let (path, anchor) = match url.find('#') {
|
|
Some(p) => (&url[..p], &url[p..]),
|
|
None => (url, ""),
|
|
};
|
|
// Only rewrite if the path looks like a markdown file.
|
|
if !path.ends_with(".md") {
|
|
return url.to_string();
|
|
}
|
|
// Resolve relative to source_dir.
|
|
let combined = if source_dir.is_empty() {
|
|
path.to_string()
|
|
} else {
|
|
format!("{}/{}", source_dir, path)
|
|
};
|
|
let normalised = normalise_path(&combined);
|
|
format!("{}/{}{}", GH_DOCS_BASE, normalised, anchor)
|
|
}
|
|
|
|
fn has_scheme(url: &str) -> bool {
|
|
// RFC 3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"
|
|
let mut chars = url.chars();
|
|
let first = match chars.next() {
|
|
Some(c) => c,
|
|
None => return false,
|
|
};
|
|
if !first.is_ascii_alphabetic() {
|
|
return false;
|
|
}
|
|
for c in chars {
|
|
if c == ':' {
|
|
return true;
|
|
}
|
|
if !(c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.')) {
|
|
return false;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
fn normalise_path(path: &str) -> String {
|
|
let mut stack: Vec<&str> = Vec::new();
|
|
for seg in path.split('/') {
|
|
match seg {
|
|
"" | "." => {}
|
|
".." => {
|
|
stack.pop();
|
|
}
|
|
other => stack.push(other),
|
|
}
|
|
}
|
|
stack.join("/")
|
|
}
|
|
|
|
fn emit_placeholder_and_warn(dist_dir: &Path) {
|
|
// Create minimal placeholder files so compilation succeeds
|
|
std::fs::create_dir_all(dist_dir).ok();
|
|
std::fs::write(
|
|
dist_dir.join("index.html"),
|
|
"<!DOCTYPE html><html><body><h1>Frontend not built</h1><p>Run: cd frontend && npm install && npm run build</p></body></html>",
|
|
)
|
|
.ok();
|
|
std::fs::write(dist_dir.join("app.js"), "// frontend not built\n").ok();
|
|
std::fs::write(dist_dir.join("style.css"), "/* frontend not built */\n").ok();
|
|
println!(
|
|
"cargo:warning=Node.js/npm not available — wrote placeholder frontend assets. Run 'cd frontend && npm install && npm run build' for the real UI."
|
|
);
|
|
}
|