diff --git a/codegen/src/parser/mod.rs b/codegen/src/parser/mod.rs index c9cfb19d..cb152019 100644 --- a/codegen/src/parser/mod.rs +++ b/codegen/src/parser/mod.rs @@ -3,6 +3,7 @@ mod route; mod error; mod param; mod function; +mod uri; pub use self::keyvalue::KVSpanned; pub use self::route::RouteParams; diff --git a/codegen/src/parser/param.rs b/codegen/src/parser/param.rs index 6bc9dea2..4d0d3449 100644 --- a/codegen/src/parser/param.rs +++ b/codegen/src/parser/param.rs @@ -2,24 +2,24 @@ use syntax::ast::Ident; use syntax::ext::base::ExtCtxt; use syntax::codemap::{Span, Spanned, BytePos}; -use utils::{span, SpanExt, is_valid_ident}; +use utils::span; #[derive(Debug)] pub enum Param { Single(Spanned), - Many(Spanned) + Many(Spanned), } impl Param { pub fn inner(&self) -> &Spanned { match *self { - Param::Single(ref ident) | Param::Many(ref ident) => ident + Param::Single(ref ident) | Param::Many(ref ident) => ident, } } pub fn ident(&self) -> &Ident { match *self { - Param::Single(ref ident) | Param::Many(ref ident) => &ident.node + Param::Single(ref ident) | Param::Many(ref ident) => &ident.node, } } } @@ -32,11 +32,7 @@ pub struct ParamIter<'s, 'a, 'c: 'a> { impl<'s, 'a, 'c: 'a> ParamIter<'s, 'a, 'c> { pub fn new(c: &'a ExtCtxt<'c>, s: &'s str, p: Span) -> ParamIter<'s, 'a, 'c> { - ParamIter { - ctxt: c, - span: p, - string: s, - } + ParamIter { ctxt: c, span: p, string: s } } } @@ -45,7 +41,7 @@ impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> { fn next(&mut self) -> Option { let err = |ecx: &ExtCtxt, sp: Span, msg: &str| { - ecx.span_err(sp, msg); + ecx.span_err(sp, msg); None }; @@ -53,14 +49,14 @@ impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> { let (start, end) = match self.string.find('<') { Some(i) => match self.string.find('>') { Some(j) => (i, j), - None => return err(self.ctxt, self.span, "malformed parameter list") + None => return err(self.ctxt, self.span, "malformed parameters") }, _ => return None, }; // Ensure we found a valid parameter. if end <= start { - return err(self.ctxt, self.span, "malformed parameter list"); + return err(self.ctxt, self.span, "malformed parameters"); } // Calculate the parameter's ident. @@ -79,27 +75,11 @@ impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> { self.string = &self.string[(end + 1)..]; self.span.lo = self.span.lo + BytePos((end + 1) as u32); - // Check for nonemptiness, that the characters are correct, and return. - if param.is_empty() { - err(self.ctxt, param_span, "parameter names cannot be empty") - } else if !is_valid_ident(param) { - err(self.ctxt, param_span, "parameter names must be valid identifiers") - } else if param.starts_with('_') { - err(self.ctxt, param_span, "parameters cannot be ignored") - } else if is_many && !self.string.is_empty() { - let sp = self.span.shorten_to(self.string.len()); - self.ctxt.struct_span_err(sp, "text after a trailing '..' param") - .span_note(param_span, "trailing param is here") - .emit(); - None + let spanned_ident = span(Ident::from_str(param), param_span); + if is_many { + Some(Param::Many(spanned_ident)) } else { - let spanned_ident = span(Ident::from_str(param), param_span); - if is_many { - Some(Param::Many(spanned_ident)) - } else { - Some(Param::Single(spanned_ident)) - } + Some(Param::Single(spanned_ident)) } - } } diff --git a/codegen/src/parser/route.rs b/codegen/src/parser/route.rs index a7fc1785..fc8bc00a 100644 --- a/codegen/src/parser/route.rs +++ b/codegen/src/parser/route.rs @@ -8,6 +8,7 @@ use syntax::codemap::{Span, Spanned, dummy_spanned}; use utils::{span, MetaItemExt, SpanExt, is_valid_ident}; use super::{Function, ParamIter}; use super::keyvalue::KVSpanned; +use super::uri::validate_uri; use rocket::http::{Method, ContentType}; use rocket::http::uri::URI; @@ -151,7 +152,7 @@ pub fn kv_from_nested(item: &NestedMetaItem) -> Option> { }) } -fn param_string_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option> { +pub fn param_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option> { let string = s.node; if string.starts_with('<') && string.ends_with('>') { let param = &string[1..(string.len() - 1)]; @@ -187,27 +188,20 @@ fn parse_method(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { dummy_spanned(Method::Get) } -fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) - -> (Spanned>, Option>) { - let from_string = |string: &str, sp: Span| { - let query_param = string.find('?') - .map(|i| span(&string[(i + 1)..], sp.trim_left(i + 1))) - .and_then(|spanned_q_param| param_string_to_ident(ecx, spanned_q_param)); - - (span(URI::from(string.to_string()), sp), query_param) - }; - +fn parse_path(ecx: &ExtCtxt, + meta_item: &NestedMetaItem) + -> (Spanned>, Option>) { let sp = meta_item.span(); if let Some((name, lit)) = meta_item.name_value() { if name != &"path" { ecx.span_err(sp, "the first key, if any, must be 'path'"); } else if let LitKind::Str(ref s, _) = lit.node { - return from_string(&s.as_str(), lit.span); + return validate_uri(ecx, &s.as_str(), lit.span); } else { ecx.span_err(lit.span, "`path` value must be a string") } } else if let Some(s) = meta_item.str_lit() { - return from_string(&s.as_str(), sp); + return validate_uri(ecx, &s.as_str(), sp); } else { ecx.struct_span_err(sp, r#"expected `path = string` or a path string"#) .help(r#"you can specify the path directly as a string, \ @@ -229,7 +223,7 @@ fn parse_data(ecx: &ExtCtxt, kv: &KVSpanned) -> Ident { let mut ident = Ident::from_str("unknown"); if let LitKind::Str(ref s, _) = *kv.value() { ident = Ident::from_str(&s.as_str()); - if let Some(id) = param_string_to_ident(ecx, span(&s.as_str(), kv.value.span)) { + if let Some(id) = param_to_ident(ecx, span(&s.as_str(), kv.value.span)) { return id.node; } } diff --git a/codegen/src/parser/uri.rs b/codegen/src/parser/uri.rs new file mode 100644 index 00000000..693fcabe --- /dev/null +++ b/codegen/src/parser/uri.rs @@ -0,0 +1,114 @@ +use syntax::ast::*; +use syntax::codemap::{Span, Spanned, dummy_spanned}; +use syntax::ext::base::ExtCtxt; + +use rocket::http::uri::URI; +use super::route::param_to_ident; +use utils::{span, SpanExt, is_valid_ident}; + +// We somewhat arbitrarily enforce absolute paths. This is mostly because we +// want the initial "/" to represent the mount point. Empty segments are +// stripped out at runtime. So, to avoid any confusion, we issue an error at +// compile-time for empty segments. At the moment, this disallows trailing +// slashes as well, since then the last segment is empty. +fn valid_path(ecx: &ExtCtxt, uri: &URI, sp: Span) -> bool { + let cleaned = uri.to_string(); + if !uri.as_str().starts_with('/') { + ecx.struct_span_err(sp, "route paths must be absolute") + .note(&format!("expected {:?}, found {:?}", cleaned, uri.as_str())) + .emit() + } else if cleaned != uri.as_str() { + ecx.struct_span_err(sp, "paths cannot contain empty segments") + .note(&format!("expected {:?}, found {:?}", cleaned, uri.as_str())) + .emit() + } else { + return true; + } + + false +} + +fn valid_segments(ecx: &ExtCtxt, uri: &URI, sp: Span) -> bool { + let mut validated = true; + let mut segments_span = None; + for segment in uri.segments() { + // We add one to the index to account for the '/'. + let index = segment.as_ptr() as usize - uri.path().as_ptr() as usize; + let span = sp.trim_left(index + 1).shorten_to(segment.len()); + + // If we're iterating after a '..' param, that's a hard error. + if let Some(span) = segments_span { + let rem_sp = sp.trim_left(index).trim_right(1); + ecx.struct_span_err(rem_sp, "text after a trailing '..' param") + .help("a segments param must be the final text in a path") + .span_note(span, "trailing param is here") + .emit(); + return false; + } + + // Check if this is a dynamic param. If so, check it's well-formedness. + if segment.starts_with("<") && segment.ends_with(">") { + let mut param = &segment[1..(segment.len() - 1)]; + if segment.ends_with("..>") { + segments_span = Some(span); + param = ¶m[..(param.len() - 2)]; + } + + if param.is_empty() { + ecx.span_err(span, "parameters cannot be empty"); + } else if !is_valid_ident(param) { + ecx.struct_span_err(span, "parameter names must be valid identifiers") + .note(&format!("{:?} is not a valid identifier", param)) + .emit(); + } else if param.starts_with('_') { + ecx.struct_span_err(span, "parameters cannot be ignored") + .note(&format!("{:?} is being ignored", param)) + .emit(); + } else { + continue + } + + validated = false; + } else if segment.starts_with("<") { + if segment[1..].contains("<") || segment.contains(">") { + ecx.struct_span_err(span, "malformed parameter") + .help("parameters must be of the form ''") + .emit(); + } else { + ecx.struct_span_err(span, "parameter is missing a closing bracket") + .help(&format!("perhaps you meant '{}>'?", segment)) + .emit(); + } + + validated = false; + } else if URI::percent_encode(segment) != segment { + if segment.contains("<") || segment.contains(">") { + ecx.struct_span_err(span, "malformed parameter") + .help("parameters must be of the form ''") + .emit(); + } else { + ecx.span_err(span, "segment contains invalid characters"); + } + + validated = false; + } + } + + validated +} + +pub fn validate_uri(ecx: &ExtCtxt, + string: &str, + sp: Span) + -> (Spanned>, Option>) { + let uri = URI::from(string.to_string()); + let query_param = string.find('?') + .map(|i| span(&string[(i + 1)..], sp.trim_left(i + 1))) + .and_then(|spanned_q_param| param_to_ident(ecx, spanned_q_param)); + + if valid_segments(ecx, &uri, sp) && valid_path(ecx, &uri, sp) { + (span(uri, sp), query_param) + } else { + (dummy_spanned(URI::new("")), query_param) + } +} diff --git a/codegen/src/utils/span_ext.rs b/codegen/src/utils/span_ext.rs index f1b34596..f6ea4631 100644 --- a/codegen/src/utils/span_ext.rs +++ b/codegen/src/utils/span_ext.rs @@ -8,6 +8,9 @@ pub trait SpanExt { /// Trim the span on the left by `length`. fn trim_left(self, length: usize) -> Span; + + /// Trim the span on the right by `length`. + fn trim_right(self, length: usize) -> Span; } impl SpanExt for Span { @@ -16,6 +19,11 @@ impl SpanExt for Span { self } + fn trim_right(mut self, length: usize) -> Span { + self.hi = self.hi - BytePos(length as u32); + self + } + fn shorten_to(mut self, to_length: usize) -> Span { self.hi = self.lo + BytePos(to_length as u32); self diff --git a/codegen/tests/compile-fail/absolute-mount-paths.rs b/codegen/tests/compile-fail/absolute-mount-paths.rs new file mode 100644 index 00000000..a484b27b --- /dev/null +++ b/codegen/tests/compile-fail/absolute-mount-paths.rs @@ -0,0 +1,13 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +#[get("a")] //~ ERROR absolute +fn get() -> &'static str { "hi" } + +#[get("")] //~ ERROR absolute +fn get1(name: &str) -> &'static str { "hi" } + +#[get("a/b/c")] //~ ERROR absolute +fn get2(name: &str) -> &'static str { "hi" } + +fn main() { } diff --git a/codegen/tests/compile-fail/bad-attribute-param.rs b/codegen/tests/compile-fail/bad-attribute-param.rs index b2a4b3e1..2bb7a0c1 100644 --- a/codegen/tests/compile-fail/bad-attribute-param.rs +++ b/codegen/tests/compile-fail/bad-attribute-param.rs @@ -3,7 +3,7 @@ extern crate rocket; -#[get(path = "hello", 123)] //~ ERROR expected +#[get(path = "/hello", 123)] //~ ERROR expected fn get() -> &'static str { "hi" } fn main() { diff --git a/codegen/tests/compile-fail/bad-value-types-in-attribute.rs b/codegen/tests/compile-fail/bad-value-types-in-attribute.rs index d3ae9464..b6ca9ec9 100644 --- a/codegen/tests/compile-fail/bad-value-types-in-attribute.rs +++ b/codegen/tests/compile-fail/bad-value-types-in-attribute.rs @@ -9,10 +9,10 @@ fn get0() -> &'static str { "hi" } #[get(path = 1)] //~ ERROR must be a string fn get1() -> &'static str { "hi" } -#[get(path = "h", rank = "2")] //~ ERROR must be an int +#[get(path = "/", rank = "2")] //~ ERROR must be an int fn get2() -> &'static str { "hi" } -#[get(path = "h", format = 100)] //~ ERROR must be a "content/type" +#[get(path = "/", format = 100)] //~ ERROR must be a "content/type" fn get3() -> &'static str { "hi" } fn main() { diff --git a/codegen/tests/compile-fail/empty-segments.rs b/codegen/tests/compile-fail/empty-segments.rs new file mode 100644 index 00000000..13b1a690 --- /dev/null +++ b/codegen/tests/compile-fail/empty-segments.rs @@ -0,0 +1,28 @@ +#![feature(plugin)] +#![plugin(rocket_codegen)] + +#[get("/a/b/c//d")] //~ ERROR paths cannot contain empty segments +fn get() -> &'static str { "hi" } + +#[get("//")] //~ ERROR paths cannot contain empty segments +fn get1(name: &str) -> &'static str { "hi" } + +#[get("/a/")] //~ ERROR paths cannot contain empty segments +fn get2(name: &str) -> &'static str { "hi" } + +#[get("////")] //~ ERROR paths cannot contain empty segments +fn get3() -> &'static str { "hi" } + +#[get("/a///")] //~ ERROR paths cannot contain empty segments +fn get4() -> &'static str { "hi" } + +#[get("/a/b//")] //~ ERROR paths cannot contain empty segments +fn get5() -> &'static str { "hi" } + +#[get("/a/b/c/")] //~ ERROR paths cannot contain empty segments +fn get6() -> &'static str { "hi" } + +#[get("/a/b/c/d//e/")] //~ ERROR paths cannot contain empty segments +fn get7() -> &'static str { "hi" } + +fn main() { } diff --git a/codegen/tests/compile-fail/malformed-param-list.rs b/codegen/tests/compile-fail/malformed-param-list.rs index 08160126..cc51808b 100644 --- a/codegen/tests/compile-fail/malformed-param-list.rs +++ b/codegen/tests/compile-fail/malformed-param-list.rs @@ -7,7 +7,7 @@ fn get() -> &'static str { "hi" } #[get("/<")] //~ ERROR malformed fn get1(name: &str) -> &'static str { "hi" } -#[get("/<<<<")] //~ ERROR identifiers +#[get("/<<<<")] //~ ERROR malformed fn get2(name: &str) -> &'static str { "hi" } #[get("/")] //~ ERROR identifiers @@ -19,7 +19,13 @@ fn get4() -> &'static str { "hi" } #[get("/<1>")] //~ ERROR identifiers fn get5() -> &'static str { "hi" } -#[get("/<>name><")] //~ ERROR cannot be empty +#[get("/<>name><")] //~ ERROR malformed fn get6() -> &'static str { "hi" } +#[get("/:")] //~ ERROR identifiers +fn get7() -> &'static str { "hi" } + +#[get("/<>")] //~ ERROR empty +fn get8() -> &'static str { "hi" } + fn main() { } diff --git a/codegen/tests/compile-fail/route-bad-method.rs b/codegen/tests/compile-fail/route-bad-method.rs index 00b815c4..f8b955f8 100644 --- a/codegen/tests/compile-fail/route-bad-method.rs +++ b/codegen/tests/compile-fail/route-bad-method.rs @@ -3,7 +3,7 @@ extern crate rocket; -#[route(FIX, "hello")] //~ ERROR is not a valid HTTP method +#[route(FIX, "/hello")] //~ ERROR is not a valid HTTP method //~^ ERROR valid HTTP method fn get() -> &'static str { "hi" } diff --git a/codegen/tests/compile-fail/route-invalid-method.rs b/codegen/tests/compile-fail/route-invalid-method.rs index 8d3b2201..e15fa8c6 100644 --- a/codegen/tests/compile-fail/route-invalid-method.rs +++ b/codegen/tests/compile-fail/route-invalid-method.rs @@ -3,7 +3,7 @@ extern crate rocket; -#[route(CONNECT, "hello")] //~ ERROR valid HTTP method +#[route(CONNECT, "/hello")] //~ ERROR valid HTTP method fn get() -> &'static str { "hi" } fn main() { diff --git a/codegen/tests/compile-fail/unknown-attribute-param.rs b/codegen/tests/compile-fail/unknown-attribute-param.rs index 8b1f1932..5106fad8 100644 --- a/codegen/tests/compile-fail/unknown-attribute-param.rs +++ b/codegen/tests/compile-fail/unknown-attribute-param.rs @@ -3,7 +3,7 @@ extern crate rocket; -#[get(path = "hello", unknown = 123)] //~ ERROR 'unknown' is not a known param +#[get(path = "/hello", unknown = 123)] //~ ERROR 'unknown' is not a known param fn get() -> &'static str { "hi" } fn main() { diff --git a/codegen/tests/run-pass/empty-fn.rs b/codegen/tests/run-pass/empty-fn.rs index 66ccad22..2b3b6add 100644 --- a/codegen/tests/run-pass/empty-fn.rs +++ b/codegen/tests/run-pass/empty-fn.rs @@ -3,7 +3,7 @@ extern crate rocket; -#[get("")] +#[get("/")] fn get() -> &'static str { "hi" } #[get("/")]