sark 0.2.2

Simple Asynchronous Rust webKit - Server
Documentation
use std::fs;
use std::path::PathBuf;

use sark::fs::ServeDir;
use sark_core::http::{Response, StatusCode};

struct Fixture {
    root: PathBuf,
}

impl Fixture {
    fn new(name: &str) -> Self {
        let root =
            std::env::temp_dir().join(format!("sark_serve_dir_{}_{}", std::process::id(), name));
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("static")).unwrap();
        Self { root }
    }

    fn write(&self, rel: &str, bytes: &[u8]) {
        let path = self.root.join(rel);
        fs::create_dir_all(path.parent().unwrap()).unwrap();
        fs::write(path, bytes).unwrap();
    }

    fn serve(&self) -> ServeDir {
        ServeDir::new(&self.root)
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

fn content_type(resp: &Response) -> String {
    resp.headers()
        .get("content-type")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("")
        .to_string()
}

fn wire(resp: &Response) -> String {
    String::from_utf8_lossy(resp.wire_headers()).to_ascii_lowercase()
}

#[test]
fn mime_table_exact_values() {
    let fx = Fixture::new("mime");
    fx.write("static/reset.css", b"body{}");
    fx.write("static/app.js", b"console.log(1)");
    fx.write("static/manifest.json", b"{}");
    fx.write("static/icon.svg", b"<svg/>");
    fx.write("static/blob.bin", b"\0\0");
    let serve = fx.serve();

    assert_eq!(
        content_type(&serve.serve(b"static/reset.css", b"")),
        "text/css"
    );
    assert_eq!(
        content_type(&serve.serve(b"static/app.js", b"")),
        "application/javascript"
    );
    assert_eq!(
        content_type(&serve.serve(b"static/manifest.json", b"")),
        "application/json"
    );
    assert_eq!(
        content_type(&serve.serve(b"static/icon.svg", b"")),
        "image/svg+xml"
    );
    assert_eq!(
        content_type(&serve.serve(b"static/blob.bin", b"")),
        "application/octet-stream"
    );
}

#[test]
fn identity_body_matches_disk_exactly() {
    let fx = Fixture::new("identity");
    let payload: Vec<u8> = (0..4096u32).map(|i| (i % 251) as u8).collect();
    fx.write("static/data.txt", &payload);
    let serve = fx.serve();

    let resp = serve.serve(b"static/data.txt", b"");
    assert_eq!(resp.status(), StatusCode::OK);
    assert_eq!(resp.body(), payload.as_slice());
    assert!(!wire(&resp).contains("content-encoding"));
}

#[test]
fn empty_file_serves_empty_body() {
    let fx = Fixture::new("empty");
    fx.write("static/empty.txt", b"");
    let resp = fx.serve().serve(b"static/empty.txt", b"");
    assert_eq!(resp.status(), StatusCode::OK);
    assert_eq!(resp.body().len(), 0);
}

#[test]
fn precompressed_br_preferred_then_gzip() {
    let fx = Fixture::new("precompress");
    fx.write("static/app.css", b"the original uncompressed bytes");
    fx.write("static/app.css.br", b"BR-BYTES");
    fx.write("static/app.css.gz", b"GZIP-BYTES-LONGER");
    let serve = fx.serve().precompressed_br().precompressed_gzip();

    let resp = serve.serve(b"static/app.css", b"br;q=1, gzip;q=0.8");
    assert_eq!(content_type(&resp), "text/css");
    assert_eq!(resp.body(), b"BR-BYTES");
    let w = wire(&resp);
    assert!(w.contains("content-encoding: br"));
    assert!(w.contains("vary: accept-encoding"));

    let resp = serve.serve(b"static/app.css", b"gzip");
    assert_eq!(content_type(&resp), "text/css");
    assert_eq!(resp.body(), b"GZIP-BYTES-LONGER");
    assert!(wire(&resp).contains("content-encoding: gzip"));
}

#[test]
fn higher_q_overrides_default_priority() {
    let fx = Fixture::new("qvalues");
    fx.write("static/app.css", b"original");
    fx.write("static/app.css.br", b"BR");
    fx.write("static/app.css.gz", b"GZIP");
    let serve = fx.serve().precompressed_br().precompressed_gzip();

    let resp = serve.serve(b"static/app.css", b"br;q=0.5, gzip;q=0.9");
    assert!(wire(&resp).contains("content-encoding: gzip"));

    let resp = serve.serve(b"static/app.css", b"br;q=0, gzip");
    assert!(wire(&resp).contains("content-encoding: gzip"));
}

#[test]
fn no_accept_encoding_serves_identity_even_with_siblings() {
    let fx = Fixture::new("identity_siblings");
    fx.write("static/app.css", b"original-bytes");
    fx.write("static/app.css.br", b"BR");
    let serve = fx.serve().precompressed_br().precompressed_gzip();

    let resp = serve.serve(b"static/app.css", b"");
    assert_eq!(resp.body(), b"original-bytes");
    assert!(!wire(&resp).contains("content-encoding"));
}

#[test]
fn option_off_ignores_siblings() {
    let fx = Fixture::new("option_off");
    fx.write("static/app.css", b"original-bytes");
    fx.write("static/app.css.br", b"BR");
    let serve = fx.serve();

    let resp = serve.serve(b"static/app.css", b"br, gzip");
    assert_eq!(resp.body(), b"original-bytes");
    assert!(!wire(&resp).contains("content-encoding"));
}

#[test]
fn missing_sibling_falls_back_to_identity() {
    let fx = Fixture::new("missing_sibling");
    fx.write("static/app.css", b"original-bytes");
    let serve = fx.serve().precompressed_br().precompressed_gzip();

    let resp = serve.serve(b"static/app.css", b"br, gzip");
    assert_eq!(resp.body(), b"original-bytes");
    assert!(!wire(&resp).contains("content-encoding"));
}

#[test]
fn missing_br_falls_back_to_gzip_sibling() {
    let fx = Fixture::new("br_to_gzip");
    fx.write("static/app.css", b"original-bytes");
    fx.write("static/app.css.gz", b"GZIP");
    let serve = fx.serve().precompressed_br().precompressed_gzip();

    let resp = serve.serve(b"static/app.css", b"br;q=1, gzip;q=0.8");
    assert_eq!(resp.body(), b"GZIP");
    assert!(wire(&resp).contains("content-encoding: gzip"));
}

#[test]
fn nonexistent_is_404() {
    let fx = Fixture::new("nonexistent");
    let resp = fx.serve().serve(b"static/nonexistent.txt", b"");
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[test]
fn traversal_attempts_are_rejected() {
    let fx = Fixture::new("traversal");
    fx.write("static/ok.txt", b"ok");
    let serve = fx.serve();

    for attack in [
        &b"../Cargo.toml"[..],
        b"static/../../etc/passwd",
        b"/etc/passwd",
        b"static\\..\\secret",
        b"static/..%2fsecret",
    ] {
        assert_eq!(
            serve.serve(attack, b"").status(),
            StatusCode::NOT_FOUND,
            "expected 404 for {:?}",
            String::from_utf8_lossy(attack)
        );
    }

    let mut nul = b"static/ok".to_vec();
    nul.push(0);
    nul.extend_from_slice(b".txt");
    assert_eq!(serve.serve(&nul, b"").status(), StatusCode::NOT_FOUND);

    assert_eq!(serve.serve(b"static/ok.txt", b"").status(), StatusCode::OK);
}