diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/atom.rs | 45 |
3 files changed, 43 insertions, 4 deletions
@@ -530,6 +530,7 @@ dependencies = [ "structopt 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "tracing 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", "tracing-subscriber 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -10,6 +10,7 @@ serde = "1.0" reqwest = { version = "0.10", features = [ "json" ] } structopt = "0.3" chrono = "0.4" +url = "2.1" futures = "0.3" smol = { version = "0.1", features = [ "tokio02" ] } diff --git a/src/atom.rs b/src/atom.rs index e87901d..f7e96ec 100644 --- a/src/atom.rs +++ b/src/atom.rs @@ -8,6 +8,7 @@ use atom_syndication::*; use anyhow::{ Result, Context }; use futures::{ Stream, StreamExt }; use chrono::{ Utc, TimeZone }; +use url::Url; use tracing::info; @@ -25,6 +26,24 @@ struct Issue { updated_at: i64 } +// Naive implementation of https://www.w3.org/TR/REC-xml/#syntax +fn entity_escape(from: &str) -> String { + let mut escaped = String::with_capacity(from.len()); + + for c in from.chars() { + match c { + '&' => escaped.push_str("&"), + '<' => escaped.push_str("<"), + '>' => escaped.push_str(">"), + '\'' => escaped.push_str("'"), + '"' => escaped.push_str("""), + any => escaped.push(any) + } + } + + escaped +} + async fn query_issues_for_label<'conn>(conn: &'conn mut Conn, repo_id: i64, label: &str) -> impl Stream<Item=sqlx::Result<Issue>> + 'conn { sqlx::query_as::<_, Issue>(r#" SELECT issues.number, state, title, body, user_login, html_url, updated_at FROM issues @@ -58,8 +77,8 @@ async fn issue_to_entry(conn: &mut Conn, repo_id: i64, issue: Issue) -> Result<E .await; Ok(EntryBuilder::default() - .title(issue.title) - .id(issue.html_url.clone()) + .title(entity_escape(&issue.title)) + .id(entity_escape(&issue.html_url)) .updated(Utc.timestamp(issue.updated_at, 0)) .authors(vec![ Person { @@ -75,7 +94,7 @@ async fn issue_to_entry(conn: &mut Conn, repo_id: i64, issue: Issue) -> Result<E .expect("Failed to build link")]) .content(ContentBuilder::default() .content_type(Some(String::from("html"))) - .value(issue.body) + .value(entity_escape(&issue.body)) .build() .expect("Failed to build content")) .build() @@ -102,8 +121,26 @@ pub async fn generate(mut conn: &mut Conn, (ref owner, ref name): (String, Strin for label in labels { info!("atom for {:?}", label); + let label_url = { + let mut url = Url::parse("https://github.com")?; + url.path_segments_mut() + .unwrap() + .push(owner).push(name) + .push("labels").push(&label); + url.into_string() + }; + let mut feed = FeedBuilder::default(); - feed.title(label.clone()); + feed.title(entity_escape(&label)); + feed.id(&label_url); + feed.updated(Utc::now()); + feed.links(vec![ + LinkBuilder::default() + .href(&label_url) + .rel("alternate") + .build() + .map_err(anyhow::Error::msg)? + ]); let issues: Vec<Issue> = query_issues_for_label(&mut conn, repo_id, &label).await .filter_map(|res| async { res.ok() }) |