IVJOIJKBZ5UT5NFOXD7VRPJ7OOWDONFOVP5WDDUUOP4BWHK5XDRAC //!//! The pijul HTTP protocol uses query parameters on the `/.pijul` endpoint//! to specify operations. This module parses incoming request URLs directly//! into structured `Operation` values that handlers can match on.//!//! # Query Parameters (from pijul-remote/src/http.rs)//! - `change={hash}` - Download a change file//! - `tag={hash}` - Download a tag file//! - `changelist={from}&channel={name}` - List changes from position//! - `channel={name}&id` - Get channel RemoteId//! - `channel={name}` - Get channel state//! - `state={pos}&channel={name}` - Get state at position//! - `identities` - List identity certificates//! - `log` - Repository info (alias for no-query)
/// # Pijul Protocol Usage/// Used to decode channel names and other parameters that may contain/// special characters in the query string.fn percent_decode(s: &str) -> String {
/// # Priority Order/// Operations are matched in this order (first match wins):/// 1. empty query → `Info`/// 2. `change` parameter → `DownloadChange`/// 3. `tag` parameter → `DownloadTag`/// 4. `changelist` + `channel` → `Changelist`/// 5. `channel` + `id` flag → `ChannelId`/// 6. `state` (+ optional `channel`) → `State`/// 7. `channel` alone → `ChannelState`/// 8. `identities` flag → `Identities`/// 9. `log` flag → `Info`/// 10. Otherwise → `Invalid`#[derive(Debug, Clone, PartialEq, Eq)]pub enum Operation {/// Repository info/discovery (`GET /.pijul` or `?log`).Info,/// Download a change file (`?change={hash}`).DownloadChange { hash: String },/// Download a tag file (`?tag={hash}`).DownloadTag { hash: String },/// List changes from a position (`?changelist={from}&channel={name}`).Changelist { channel: String, from: u64 },/// Get channel RemoteId (`?channel={name}&id`).ChannelId { channel: String },/// Get channel state (`?channel={name}`).ChannelState { channel: String },/// Get state at position (`?state={pos}&channel={name}`).State { channel: String, pos: u64 },/// List identity certificates (`?identities`).Identities,/// Invalid or unrecognized query combination.Invalid,}impl From<&HashMap<String, Vec<String>>> for Operation {fn from(map: &HashMap<String, Vec<String>>) -> Self {if map.is_empty() {return Operation::Info;}if let Some(hash) = map.get("change").and_then(|v| v.first()) {return Operation::DownloadChange { hash: hash.clone() };}if let Some(hash) = map.get("tag").and_then(|v| v.first()) {return Operation::DownloadTag { hash: hash.clone() };}if let Some(from) = map.get("changelist").and_then(|v| v.first()) {if let Some(channel) = map.get("channel").and_then(|v| v.first()) {return Operation::Changelist {channel: channel.clone(),from: from.parse().unwrap_or(0),};}}if map.contains_key("id") {if let Some(channel) = map.get("channel").and_then(|v| v.first()) {return Operation::ChannelId {channel: channel.clone(),};}}if let Some(pos) = map.get("state").and_then(|v| v.first()) {let channel = map.get("channel").and_then(|v| v.first()).cloned().unwrap_or_else(|| "main".to_string());return Operation::State {channel,pos: pos.parse().unwrap_or(0),};}if let Some(channel) = map.get("channel").and_then(|v| v.first()) {return Operation::ChannelState {channel: channel.clone(),};}if map.contains_key("identities") {return Operation::Identities;}if map.contains_key("log") {return Operation::Info;}Operation::Invalid}}/// Decode URL percent-encoded string.////// Handles `%XX` hex escapes and `+` as space/// (application/x-www-form-urlencoded convention).pub fn percent_decode(s: &str) -> String {
/// Handles:/// - `key=value` pairs separated by `&`/// - Keys without values (e.g., `?id` -> `{"id": [""]}``)/// - Multiple values for the same key (e.g., `?path=a&path=b`)/// - Percent-decoded values////// # Pijul Protocol Usage/// The pijul protocol sometimes uses repeated parameters (e.g., `path` can/// appear multiple times for filtering). This parser preserves all values.fn parse_query(query_str: &str) -> HashMap<String, Vec<String>> {
/// Handles `key=value` pairs separated by `&`, bare keys without values,/// multiple values for the same key, and percent-decoded values.pub fn parse_query(query_str: &str) -> HashMap<String, Vec<String>> {
// ascii extended not allowedassert_ne!(percent_decode("My%20pijul%20channel%20is%20named%20%22G%C3%B6ta+kanal%22"),"My pijul channel is named \"Göta kanal\""
#[test]fn test_operation_from_empty() {assert_eq!(Operation::from(&parse_query("")), Operation::Info);}#[test]fn test_operation_from_change() {let (_, op) = parse_url("/.pijul?change=ABCDEF");assert_eq!(op,Operation::DownloadChange {hash: "ABCDEF".into()});}#[test]fn test_operation_from_tag() {let (_, op) = parse_url("/.pijul?tag=XYZABC");assert_eq!(op,Operation::DownloadTag {hash: "XYZABC".into()});}#[test]fn test_operation_from_changelist() {let (_, op) = parse_url("/.pijul?changelist=42&channel=main");assert_eq!(op,Operation::Changelist {channel: "main".into(),from: 42});}#[test]fn test_operation_from_channel_id() {let (_, op) = parse_url("/.pijul?channel=main&id");assert_eq!(op,Operation::ChannelId {channel: "main".into()});}#[test]fn test_operation_from_channel_state() {let (_, op) = parse_url("/.pijul?channel=feature");assert_eq!(op,Operation::ChannelState {channel: "feature".into()});}#[test]fn test_operation_from_state() {let (_, op) = parse_url("/.pijul?state=100&channel=dev");assert_eq!(op,Operation::State {channel: "dev".into(),pos: 100}
#[test]fn test_operation_from_state_default_channel() {let (_, op) = parse_url("/.pijul?state=50");assert_eq!(op,Operation::State {channel: "main".into(),pos: 50});}#[test]fn test_operation_from_identities() {let (_, op) = parse_url("/.pijul?identities");assert_eq!(op, Operation::Identities);}#[test]fn test_operation_from_log() {let (_, op) = parse_url("/.pijul?log");assert_eq!(op, Operation::Info);}#[test]fn test_operation_priority_change_over_tag() {let (_, op) = parse_url("/.pijul?change=ABC&tag=XYZ");assert_eq!(op, Operation::DownloadChange { hash: "ABC".into() });}#[test]fn test_operation_changelist_requires_channel() {let (_, op) = parse_url("/.pijul?changelist=0");assert_eq!(op, Operation::Invalid);}
//! pijul-srv-lite: A lightweight HTTP server for pijul repositories.//!//! This crate implements the server-side of the pijul HTTP remote protocol,//! allowing pijul clients to clone and pull from repositories over HTTP.//!//! The crate is organized into four modules://!//! - **url**: Parses HTTP request URLs and query parameters//! - **pristine**: Reads from pijul's sanakirja database//! - **handlers**: Implements protocol endpoints//! - **response**: Builds HTTP responses
if query.is_empty() {return serve_info(&reader);
match op {Operation::Info => serve_info(&reader),Operation::DownloadChange { hash } => serve_change(repo_path, &hash),Operation::DownloadTag { hash } => serve_tag(&repo_path, &hash),Operation::Changelist { channel, from } => serve_changelist(&reader, &channel, from),Operation::ChannelId { channel } => serve_channel_id(&reader, &channel),Operation::ChannelState { channel } => serve_channel(&reader, &channel),Operation::State { channel, pos } => serve_state(&reader, &channel, pos),Operation::Identities => serve_identities(),Operation::Invalid => bad_request(),
if let Some(change) = query.get("change")&& !change.is_empty(){return serve_change(repo_path, &change[0]);}if let Some(changelists) = query.get("changelist")&& let Some(channels) = query.get("channel"){match (changelists.as_slice(), channels.as_slice()) {([from], [channel]) => {if let Ok(from) = from.parse::<u64>() {return serve_changelist(&reader, channel, from);};}_ => (),}}if let Some(channel) = query.get("channel") {match channel.as_slice() {[channel] => match query.get("id") {Some(_) => return serve_channel_id(&reader, channel),None => return serve_channel(&reader, channel),},_ => (),}}if let Some(positions) = query.get("state")&& let Some(channels) = query.get("channel"){match (positions.as_slice(), channels.as_slice()) {([position], [channel]) => {if let Ok(position) = position.parse::<u64>() {return serve_state(&reader, channel, position);}}_ => (),}};if query.contains_key("identities") {return serve_identities();}if query.contains_key("log") {return serve_info(&reader);}if let Some(tags) = query.get("tag") {match tags.as_slice() {[tag] => return serve_tag(repo_path, tag),_ => (),}}bad_request()