agate

Simple gemini server for static files
git clone https://github.com/mbrubeck/agate.git
Log | Files | Refs | README

commit 544f577b59c1a97336d110c87779dda3238b576e
parent 8f2cfe7a8f20370c748d0dd281f20ee0295f1172
Author: Johann150 <johann.galle@protonmail.com>
Date:   Wed, 10 Feb 2021 21:17:39 +0100

use YAML parser for sidecar files

The syntax so far is (compatible with) YAML.

Diffstat:
MCargo.lock | 16++++++++++++++++
MCargo.toml | 1+
Msrc/main.rs | 5+++--
Msrc/metadata.rs | 126++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
4 files changed, 94 insertions(+), 54 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "tokio", "tokio-rustls", "url", + "yaml-rust", ] [[package]] @@ -142,6 +143,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" [[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] name = "log" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -539,3 +546,12 @@ 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 = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml @@ -22,6 +22,7 @@ once_cell = "1.5" percent-encoding = "2.1" rustls = "0.19.0" url = "2.2" +yaml-rust = "0.4" [profile.release] lto = true diff --git a/src/main.rs b/src/main.rs @@ -30,11 +30,12 @@ fn main() -> Result { .init(); } Runtime::new()?.block_on(async { - let mimetypes = Arc::new(Mutex::new(FileOptions::new(PresetMeta::Parameters( + let default = PresetMeta::Parameters( ARGS.language .as_ref() .map_or(String::new(), |lang| format!(";lang={}", lang)), - )))); + ); + let mimetypes = Arc::new(Mutex::new(FileOptions::new(default))); let listener = TcpListener::bind(&ARGS.addrs[..]).await?; log::info!("Listening on {:?}...", ARGS.addrs); loop { diff --git a/src/metadata.rs b/src/metadata.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; -use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::time::SystemTime; +use yaml_rust::YamlLoader; static SIDECAR_FILENAME: &str = ".meta"; @@ -108,60 +108,82 @@ impl FileOptions { db.push(SIDECAR_FILENAME); let db = db.as_path(); - if let Ok(file) = std::fs::File::open(db) { - let r = BufReader::new(file); - r.lines() - // discard any I/O errors - .filter_map(|line| line.ok()) - // filter out comment lines - .filter(|line| !line.trim_start().starts_with('#')) - .for_each(|line| { - // split line at colon - let parts = line.splitn(2, ':').collect::<Vec<_>>(); - // only continue if line fits the format - if parts.len() == 2 { - // generate workspace-unique path - let mut path = db_dir.clone(); - path.push(parts[0].trim()); - // parse the line - let header = parts[1].trim(); + if let Ok(contents) = std::fs::read_to_string(db) { + let docs = match YamlLoader::load_from_str(&contents) { + Ok(docs) => docs, + Err(e) => { + log::error!("Invalid YAML document in {:?}: {}", db, e); + return; + } + }; + if let Some(files) = docs.get(0).and_then(|hash| hash.as_hash()) { + for (rel_path, header) in files { + // from YAML to Rust types + let rel_path = if let Some(rel_path) = rel_path.as_str() { + rel_path + } else { + log::error!( + "Expected string filename, but got {:?} in {:?}", + rel_path, + db + ); + continue; + }; + let header = if let Some(header) = header.as_str() { + header + } else { + log::error!("Expected string contents, but got {:?} in {:?}", header, db); + continue; + }; - let preset = if header.is_empty() || header.starts_with(';') { - PresetMeta::Parameters(header.to_string()) - } else if matches!(header.chars().next(), Some('1'..='6')) { - if header.len() < 3 - || !header.chars().nth(1).unwrap().is_ascii_digit() - || !header.chars().nth(2).unwrap().is_whitespace() - { - log::error!("Line for {:?} starts like a full header line, but it is incorrect; ignoring it.", path); - return; - } - let separator = header.chars().nth(2).unwrap(); - if separator != ' ' { - // the Gemini specification says that the third - // character has to be a space, so correct any - // other whitespace to it (e.g. tabs) - log::warn!("Full Header line for {:?} has an invalid character, treating {:?} as a space.", path, separator); - } - let status = header.chars() - .take(2) - .collect::<String>() - .parse::<u8>() - // unwrap since we alread checked it's a number - .unwrap(); - // not taking a slice here because the separator - // might be a whitespace wider than a byte - let meta = header.chars().skip(3).collect::<String>(); - PresetMeta::FullHeader(status, meta) - } else { - // must be a MIME type, but without status code - PresetMeta::FullMime(header.to_string()) - }; - self.file_meta.insert(path, preset); - } - }); + // generate workspace-unique path + let mut path = db_dir.clone(); + path.push(rel_path); + + // parse the preset + let preset = if header.is_empty() || header.starts_with(';') { + PresetMeta::Parameters(header.to_string()) + } else if matches!(header.chars().next(), Some('1'..='6')) { + if header.len() < 3 + || !header.chars().nth(1).unwrap().is_ascii_digit() + || !header.chars().nth(2).unwrap().is_whitespace() + { + log::error!("Line for {:?} starts like a full header line, but it is incorrect; ignoring it.", path); + return; + } + let separator = header.chars().nth(2).unwrap(); + if separator != ' ' { + // the Gemini specification says that the third + // character has to be a space, so correct any + // other whitespace to it (e.g. tabs) + log::warn!("Full Header line for {:?} has an invalid character, treating {:?} as a space.", path, separator); + } + let status = header + .chars() + .take(2) + .collect::<String>() + .parse::<u8>() + // unwrap since we alread checked it's a number + .unwrap(); + // not taking a slice here because the separator + // might be a whitespace wider than a byte + let meta = header.chars().skip(3).collect::<String>(); + PresetMeta::FullHeader(status, meta) + } else { + // must be a MIME type, but without status code + PresetMeta::FullMime(header.to_string()) + }; + + self.file_meta.insert(path, preset); + } + } else { + log::error!("no YAML document {:?}", db); + return; + }; self.databases_read .insert(db_dir.clone(), SystemTime::now()); + } else { + log::error!("could not read configuration file {:?}", db); } }