F6WZWYNDIMFPVDTZC6SLW67IFJD5AKOU4SK5HMYHFCAKIJKZFQLAC fn main() {}fn not_found(msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string(format!("{msg}\n")).with_status_code(StatusCode(404))}fn internal_error() -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string("internal error\n").with_status_code(StatusCode(500))}#[cfg(test)]mod tests {use super::*;#[test]fn test_parse_query() {let mut params: HashMap<String, String> = HashMap::new();params.insert("param1".into(), "hello".into());params.insert("param2".into(), "world".into());assert_eq!(parse_query("param1=hello¶m2=world"), params);params.insert("param3".into(), "".into());assert_eq!(parse_query("param1=hello¶m2=world¶m3"), params);params.insert("param4".into(), "next param".into());assert_eq!(parse_query("param1=hello¶m2=world¶m3¶m4=next%20param"),params);}#[test]fn test_parse_url() {let mut params: HashMap<String, String> = HashMap::new();assert_eq!(parse_url("/.pijul"), (".pijul".into(), params.clone()));params.insert("a_param".into(), "a value".into());assert_eq!(parse_url("/.pijul?a_param=a%20value"), (".pijul".into(), params.clone()));}#[test]fn test_percent_decode() {assert_eq!(percent_decode("Hello%20World%21"), "Hello World!");assert_eq!(percent_decode("~%20is%20where%20the%20%3C3%20is"),"~ is where the <3 is");// ascii extended not allowed// assert_eq!(percent_decode("My%20pijul%20channel%20is%20named%20%22G%C3%B6ta+kanal%22"),"My pijul channel is named \"Göta kanal\"");}}let port = std::env::var("PIJUL_SRV_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080);let repo_base = std::env::var("PIJUL_SRV_REPO_DIR").map(PathBuf::from).unwrap_or_else(|_| PathBuf::from("/vcs/pijul"));let repo_path = if repo_base.join(".pijul").exists() {repo_base} else {repo_base.join("default")};if !repo_path.join(".pijul").exists() {eprintln!("Repository not found at {:?}", repo_path);std::process::exit(1);}let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();println!("{SERVER_NAME} listening on port {port}");println!("Serving repository from {:?}", repo_path);for request in server.incoming_requests() {if request.method() == &tiny_http::Method::Head {let _ = request.respond(Response::empty(200));continue;}let response = handle_request(request.url(), &repo_path);let _ = request.respond(response);}}.split('&').filter_map(|pair| {if pair.is_empty() {return None;}let (k, v) = pair.split_once('=').unwrap_or((pair, ""));Some((k.to_string(), percent_decode(v)))})if query.is_empty() {return serve_info(repo_path);}if let Some(change) = query.get("change") {return serve_change(repo_path, change);}if let Some(channel) = query.get("channel") {if query.contains_key("id") {return serve_channel_id(repo_path, channel);}return serve_channel(repo_path, channel);}if query.contains_key("state") {return serve_state(repo_path);}if query.contains_key("identities") {return serve_identities();}if query.contains_key("log") {return serve_info(repo_path);}if query.contains_key("tag") {return serve_tag(repo_path);}bad_request()}/* ---------------- protocol handlers ---------------- */fn serve_channel_id(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_channel(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_changelist(repo_path: &Path, channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join("changelists").join(channel);serve_file(&path, "text/plain")}fn serve_change(repo_path: &Path, hash: &str) -> Response<std::io::Cursor<Vec<u8>>> {if hash.len() < 3 || !hash.chars().all(|c| c.is_ascii_alphanumeric()) {return not_found("invalid change hash");}let (pfx, sfx) = hash.split_at(2);let path = repo_path.join(".pijul").join("changes").join(pfx).join(format!("{}.change", sfx));serve_file(&path, "application/octet-stream")}fn serve_state(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_tag(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_identities() -> Response<std::io::Cursor<Vec<u8>>> {ok_json(r#"{"id":[],"rev":0}"#)}fn serve_info(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let mut out = String::from("{");let path = repo_path.join("changelists");if let Ok(entries) = std::fs::read_dir(path) {let names: Vec<String> = entries.filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok())).map(|s| format!("\"{s}\"")).collect();if !names.is_empty() {out.push_str("\"channels\":[");out.push_str(&names.join(","));out.push_str("],");}}out.push_str("\"head\":null}");ok_json(&out)}/* ---------------- helpers ---------------- */fn serve_file(path: &Path, ty: &str) -> Response<std::io::Cursor<Vec<u8>>> {match std::fs::read(path) {Ok(data) => Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", ty).unwrap()).with_header(Header::from_bytes("Cache-Control", "public, max-age=31536000, immutable").unwrap(),),Err(_) => not_found("not found"),}}fn percent_decode(s: &str) -> String {let mut result = String::new();let mut chars = s.chars().peekable();while let Some(c) = chars.next() {if c == '%' {let hex: String = chars.by_ref().take(2).collect();result.push('%');result.push_str(&hex);} else if c == '+' {result.push(' ');} else {result.push(c);}}result}/* ---------------- responses ---------------- */fn ok_bin(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/octet-stream").unwrap())}fn ok_text(s: String) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_data(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "text/plain").unwrap())}fn ok_json(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/json").unwrap())}fn bad_request() -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string("bad request\n").with_status_code(StatusCode(400))if hex.len() == 2&& let Ok(byte) = u8::from_str_radix(&hex, 16){result.push(byte as char);continue;}if query.get("changelist").map(String::as_str) == Some("0")&& let Some(channel) = query.get("channel"){return serve_changelist(repo_path, channel);}.collect()}fn parse_url(url: &str) -> (String, HashMap<String, String>) {let (path, query_str) = url.split_once('?').unwrap_or((url, ""));let path = path.trim_start_matches('/');(path.into(), parse_query(query_str))}fn handle_request(url: &str, repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let (path, query) = parse_url(url);if !path.starts_with(".pijul") {return not_found("not found");}fn parse_query(query_str: &str) -> HashMap<String, String> {query_struse std::collections::HashMap;use std::path::{Path, PathBuf};use tiny_http::{Header, Response, StatusCode};const SERVER_NAME: &str = "pijul-srv-lite/0.1.0";
use std::collections::HashMap;fn percent_decode(s: &str) -> String {let mut result = String::new();let mut chars = s.chars().peekable();while let Some(c) = chars.next() {if c == '%' {let hex: String = chars.by_ref().take(2).collect();if hex.len() == 2&& let Ok(byte) = u8::from_str_radix(&hex, 16){result.push(byte as char);continue;}result.push('%');result.push_str(&hex);} else if c == '+' {result.push(' ');} else {result.push(c);}}result}fn parse_query(query_str: &str) -> HashMap<String, String> {query_str.split('&').filter_map(|pair| {if pair.is_empty() {return None;}let (k, v) = pair.split_once('=').unwrap_or((pair, ""));Some((k.to_string(), percent_decode(v)))}).collect()}pub fn parse_url(url: &str) -> (String, HashMap<String, String>) {let (path, query_str) = url.split_once('?').unwrap_or((url, ""));let path = path.trim_start_matches('/');(path.into(), parse_query(query_str))}#[cfg(test)]mod tests {use super::*;#[test]fn test_parse_query() {let mut params: HashMap<String, String> = HashMap::new();params.insert("param1".into(), "hello".into());params.insert("param2".into(), "world".into());assert_eq!(parse_query("param1=hello¶m2=world"), params);params.insert("param3".into(), "".into());assert_eq!(parse_query("param1=hello¶m2=world¶m3"), params);params.insert("param4".into(), "next param".into());assert_eq!(parse_query("param1=hello¶m2=world¶m3¶m4=next%20param"),params);}#[test]fn test_parse_url() {let mut params: HashMap<String, String> = HashMap::new();assert_eq!(parse_url("/.pijul"), (".pijul".into(), params.clone()));params.insert("a_param".into(), "a value".into());assert_eq!(parse_url("/.pijul?a_param=a%20value"), (".pijul".into(), params.clone()));}#[test]fn test_percent_decode() {assert_eq!(percent_decode("Hello%20World%21"), "Hello World!");assert_eq!(percent_decode("~%20is%20where%20the%20%3C3%20is"),"~ is where the <3 is");// ascii extended not allowed// assert_eq!(percent_decode("My%20pijul%20channel%20is%20named%20%22G%C3%B6ta+kanal%22"),"My pijul channel is named \"Göta kanal\"");}}
use std::collections::HashMap;use std::path::{Path, PathBuf};use pijul_srv_lite::parse_url;use tiny_http::{Header, Response, StatusCode};const SERVER_NAME: &str = "pijul-srv-lite/0.1.0";fn main() {let port = std::env::var("PIJUL_SRV_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080);let repo_base = std::env::var("PIJUL_SRV_REPO_DIR").map(PathBuf::from).unwrap_or_else(|_| PathBuf::from("/vcs/pijul"));let repo_path = if repo_base.join(".pijul").exists() {repo_base} else {repo_base.join("default")};if !repo_path.join(".pijul").exists() {eprintln!("Repository not found at {:?}", repo_path);std::process::exit(1);}let server = tiny_http::Server::http(format!("0.0.0.0:{port}")).unwrap();println!("{SERVER_NAME} listening on port {port}");println!("Serving repository from {:?}", repo_path);for request in server.incoming_requests() {if request.method() == &tiny_http::Method::Head {let _ = request.respond(Response::empty(200));continue;}let response = handle_request(request.url(), &repo_path);let _ = request.respond(response);}}fn handle_request(url: &str, repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let (path, query) = parse_url(url);if !path.starts_with(".pijul") {return not_found("not found");}if query.is_empty() {return serve_info(repo_path);}if let Some(change) = query.get("change") {return serve_change(repo_path, change);}if query.get("changelist").map(String::as_str) == Some("0")&& let Some(channel) = query.get("channel"){return serve_changelist(repo_path, channel);}if let Some(channel) = query.get("channel") {if query.contains_key("id") {return serve_channel_id(repo_path, channel);}return serve_channel(repo_path, channel);}if query.contains_key("state") {return serve_state(repo_path);}if query.contains_key("identities") {return serve_identities();}if query.contains_key("log") {return serve_info(repo_path);}if query.contains_key("tag") {return serve_tag(repo_path);}bad_request()}/* ---------------- protocol handlers ---------------- */fn serve_channel_id(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_channel(repo_path: &Path, _channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_changelist(repo_path: &Path, channel: &str) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join("changelists").join(channel);serve_file(&path, "text/plain")}fn serve_change(repo_path: &Path, hash: &str) -> Response<std::io::Cursor<Vec<u8>>> {if hash.len() < 3 || !hash.chars().all(|c| c.is_ascii_alphanumeric()) {return not_found("invalid change hash");}let (pfx, sfx) = hash.split_at(2);let path = repo_path.join(".pijul").join("changes").join(pfx).join(format!("{}.change", sfx));serve_file(&path, "application/octet-stream")}fn serve_state(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_tag(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let path = repo_path.join(".pijul").join("pristine").join("db");serve_file(&path, "application/octet-stream")}fn serve_identities() -> Response<std::io::Cursor<Vec<u8>>> {ok_json(r#"{"id":[],"rev":0}"#)}fn serve_info(repo_path: &Path) -> Response<std::io::Cursor<Vec<u8>>> {let mut out = String::from("{");let path = repo_path.join("changelists");if let Ok(entries) = std::fs::read_dir(path) {let names: Vec<String> = entries.filter_map(|e| e.ok().and_then(|e| e.file_name().into_string().ok())).map(|s| format!("\"{s}\"")).collect();if !names.is_empty() {out.push_str("\"channels\":[");out.push_str(&names.join(","));out.push_str("],");}}out.push_str("\"head\":null}");ok_json(&out)}/* ---------------- helpers ---------------- */fn serve_file(path: &Path, ty: &str) -> Response<std::io::Cursor<Vec<u8>>> {match std::fs::read(path) {Ok(data) => Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", ty).unwrap()).with_header(Header::from_bytes("Cache-Control", "public, max-age=31536000, immutable").unwrap(),),Err(_) => not_found("not found"),}}/* ---------------- responses ---------------- */fn ok_bin(data: Vec<u8>) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_data(data).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/octet-stream").unwrap())}fn ok_text(s: String) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_data(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "text/plain").unwrap())}fn ok_json(s: &str) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string(s).with_status_code(StatusCode(200)).with_header(Header::from_bytes("Content-Type", "application/json").unwrap())}fn bad_request() -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string("bad request\n").with_status_code(StatusCode(400))}fn not_found(msg: &str) -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string(format!("{msg}\n")).with_status_code(StatusCode(404))}fn internal_error() -> Response<std::io::Cursor<Vec<u8>>> {Response::from_string("internal error\n").with_status_code(StatusCode(500))}
use criterion::{criterion_group, criterion_main, Criterion};use std::hint::black_box;use pijul_srv_lite::parse_url;fn bench_parse_url(c: &mut Criterion) {let query_string = "/.pijul?channel=main&id=xyz&state=open&log=true";let mut group = c.benchmark_group("parse");group.bench_function("parse_url", |b| {b.iter(|| {let res = parse_url(black_box(query_string));black_box(res)})});group.finish();}criterion_group!(benches, bench_parse_url);criterion_main!(benches);
[dev-dependencies]criterion = "0.8.2"[[bench]]name = "parse_url"harness = false
[[package]]name = "aho-corasick"version = "1.1.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"dependencies = ["memchr",][[package]]name = "alloca"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"dependencies = ["cc",][[package]]name = "anes"version = "0.1.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
name = "autocfg"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"[[package]]name = "bumpalo"version = "3.20.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"[[package]]name = "cast"version = "0.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"[[package]]name = "cc"version = "1.2.56"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"dependencies = ["find-msvc-tools","shlex",][[package]]name = "cfg-if"version = "1.0.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"[[package]]
[[package]]name = "ciborium"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"dependencies = ["ciborium-io","ciborium-ll","serde",][[package]]name = "ciborium-io"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"[[package]]name = "ciborium-ll"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"dependencies = ["ciborium-io","half",][[package]]name = "clap"version = "4.5.60"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"dependencies = ["clap_builder",]
name = "clap_builder"version = "4.5.60"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"dependencies = ["anstyle","clap_lex",][[package]]name = "clap_lex"version = "1.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"[[package]]name = "criterion"version = "0.8.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"dependencies = ["alloca","anes","cast","ciborium","clap","criterion-plot","itertools","num-traits","oorandom","page_size","plotters","rayon","regex","serde","serde_json","tinytemplate","walkdir",][[package]]name = "criterion-plot"version = "0.8.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"dependencies = ["cast","itertools",][[package]]name = "crossbeam-deque"version = "0.8.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"dependencies = ["crossbeam-epoch","crossbeam-utils",][[package]]name = "crossbeam-epoch"version = "0.9.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"dependencies = ["crossbeam-utils",][[package]]name = "crossbeam-utils"version = "0.8.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"[[package]]name = "crunchy"version = "0.2.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"[[package]]name = "either"version = "1.15.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"[[package]]name = "find-msvc-tools"version = "0.1.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"[[package]]name = "half"version = "2.7.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"dependencies = ["cfg-if","crunchy","zerocopy",][[package]]
name = "itertools"version = "0.13.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"dependencies = ["either",][[package]]name = "itoa"version = "1.0.17"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"[[package]]name = "js-sys"version = "0.3.90"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6"dependencies = ["once_cell","wasm-bindgen",][[package]]name = "libc"version = "0.2.182"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"[[package]]
[[package]]name = "memchr"version = "2.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"[[package]]name = "num-traits"version = "0.2.19"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"dependencies = ["autocfg",][[package]]name = "once_cell"version = "1.21.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"[[package]]name = "oorandom"version = "11.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
name = "plotters"version = "0.3.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"dependencies = ["num-traits","plotters-backend","plotters-svg","wasm-bindgen","web-sys",][[package]]name = "plotters-backend"version = "0.3.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"[[package]]name = "plotters-svg"version = "0.3.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"dependencies = ["plotters-backend",][[package]]name = "proc-macro2"version = "1.0.106"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"dependencies = ["unicode-ident",][[package]]name = "quote"version = "1.0.44"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"dependencies = ["proc-macro2",][[package]]name = "rayon"version = "1.11.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"dependencies = ["either","rayon-core",][[package]]name = "rayon-core"version = "1.13.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"dependencies = ["crossbeam-deque","crossbeam-utils",][[package]]name = "regex"version = "1.12.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"dependencies = ["aho-corasick","memchr","regex-automata","regex-syntax",][[package]]name = "regex-automata"version = "0.4.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"dependencies = ["aho-corasick","memchr","regex-syntax",][[package]]name = "regex-syntax"version = "0.8.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"[[package]]name = "rustversion"version = "1.0.22"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"[[package]]name = "same-file"version = "1.0.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"dependencies = ["winapi-util",][[package]]name = "serde"version = "1.0.228"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"dependencies = ["serde_core","serde_derive",][[package]]name = "serde_core"version = "1.0.228"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"dependencies = ["serde_derive",][[package]]name = "serde_derive"version = "1.0.228"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"dependencies = ["proc-macro2","quote","syn",][[package]]name = "serde_json"version = "1.0.149"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"dependencies = ["itoa","memchr","serde","serde_core","zmij",][[package]]name = "shlex"version = "1.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"[[package]]name = "syn"version = "2.0.117"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"dependencies = ["proc-macro2","quote","unicode-ident",][[package]]
][[package]]name = "tinytemplate"version = "1.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"dependencies = ["serde","serde_json",][[package]]name = "unicode-ident"version = "1.0.24"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"[[package]]name = "walkdir"version = "2.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"dependencies = ["same-file","winapi-util",
[[package]]name = "wasm-bindgen"version = "0.2.113"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2"dependencies = ["cfg-if","once_cell","rustversion","wasm-bindgen-macro","wasm-bindgen-shared",][[package]]name = "wasm-bindgen-macro"version = "0.2.113"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950"dependencies = ["quote","wasm-bindgen-macro-support",][[package]]name = "wasm-bindgen-macro-support"version = "0.2.113"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60"dependencies = ["bumpalo","proc-macro2","quote","syn","wasm-bindgen-shared",][[package]]name = "wasm-bindgen-shared"version = "0.2.113"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5"dependencies = ["unicode-ident",][[package]]name = "web-sys"version = "0.3.90"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97"dependencies = ["js-sys","wasm-bindgen",][[package]]name = "winapi"version = "0.3.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"dependencies = ["winapi-i686-pc-windows-gnu","winapi-x86_64-pc-windows-gnu",][[package]]name = "winapi-i686-pc-windows-gnu"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"[[package]]name = "winapi-util"version = "0.1.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"dependencies = ["windows-sys",][[package]]name = "winapi-x86_64-pc-windows-gnu"version = "0.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"[[package]]name = "windows-link"version = "0.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"[[package]]name = "windows-sys"version = "0.61.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"dependencies = ["windows-link",][[package]]name = "zerocopy"version = "0.8.39"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"dependencies = ["zerocopy-derive",][[package]]name = "zerocopy-derive"version = "0.8.39"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"dependencies = ["proc-macro2","quote","syn",][[package]]name = "zmij"version = "1.0.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"