Compile-time enforce paths as absolute, non-empty, valid segments.

This commit is contained in:
Sergio Benitez 2017-02-06 04:03:30 -08:00
parent 20a548b11b
commit 8eef42a256
14 changed files with 199 additions and 55 deletions

View File

@ -3,6 +3,7 @@ mod route;
mod error; mod error;
mod param; mod param;
mod function; mod function;
mod uri;
pub use self::keyvalue::KVSpanned; pub use self::keyvalue::KVSpanned;
pub use self::route::RouteParams; pub use self::route::RouteParams;

View File

@ -2,24 +2,24 @@ use syntax::ast::Ident;
use syntax::ext::base::ExtCtxt; use syntax::ext::base::ExtCtxt;
use syntax::codemap::{Span, Spanned, BytePos}; use syntax::codemap::{Span, Spanned, BytePos};
use utils::{span, SpanExt, is_valid_ident}; use utils::span;
#[derive(Debug)] #[derive(Debug)]
pub enum Param { pub enum Param {
Single(Spanned<Ident>), Single(Spanned<Ident>),
Many(Spanned<Ident>) Many(Spanned<Ident>),
} }
impl Param { impl Param {
pub fn inner(&self) -> &Spanned<Ident> { pub fn inner(&self) -> &Spanned<Ident> {
match *self { 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 { pub fn ident(&self) -> &Ident {
match *self { 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> { 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> { pub fn new(c: &'a ExtCtxt<'c>, s: &'s str, p: Span) -> ParamIter<'s, 'a, 'c> {
ParamIter { ParamIter { ctxt: c, span: p, string: s }
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<Param> { fn next(&mut self) -> Option<Param> {
let err = |ecx: &ExtCtxt, sp: Span, msg: &str| { let err = |ecx: &ExtCtxt, sp: Span, msg: &str| {
ecx.span_err(sp, msg); ecx.span_err(sp, msg);
None None
}; };
@ -53,14 +49,14 @@ impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> {
let (start, end) = match self.string.find('<') { let (start, end) = match self.string.find('<') {
Some(i) => match self.string.find('>') { Some(i) => match self.string.find('>') {
Some(j) => (i, j), 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, _ => return None,
}; };
// Ensure we found a valid parameter. // Ensure we found a valid parameter.
if end <= start { 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. // 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.string = &self.string[(end + 1)..];
self.span.lo = self.span.lo + BytePos((end + 1) as u32); self.span.lo = self.span.lo + BytePos((end + 1) as u32);
// Check for nonemptiness, that the characters are correct, and return. let spanned_ident = span(Ident::from_str(param), param_span);
if param.is_empty() { if is_many {
err(self.ctxt, param_span, "parameter names cannot be empty") Some(Param::Many(spanned_ident))
} 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
} else { } else {
let spanned_ident = span(Ident::from_str(param), param_span); Some(Param::Single(spanned_ident))
if is_many {
Some(Param::Many(spanned_ident))
} else {
Some(Param::Single(spanned_ident))
}
} }
} }
} }

View File

@ -8,6 +8,7 @@ use syntax::codemap::{Span, Spanned, dummy_spanned};
use utils::{span, MetaItemExt, SpanExt, is_valid_ident}; use utils::{span, MetaItemExt, SpanExt, is_valid_ident};
use super::{Function, ParamIter}; use super::{Function, ParamIter};
use super::keyvalue::KVSpanned; use super::keyvalue::KVSpanned;
use super::uri::validate_uri;
use rocket::http::{Method, ContentType}; use rocket::http::{Method, ContentType};
use rocket::http::uri::URI; use rocket::http::uri::URI;
@ -151,7 +152,7 @@ pub fn kv_from_nested(item: &NestedMetaItem) -> Option<KVSpanned<LitKind>> {
}) })
} }
fn param_string_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option<Spanned<Ident>> { pub fn param_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option<Spanned<Ident>> {
let string = s.node; let string = s.node;
if string.starts_with('<') && string.ends_with('>') { if string.starts_with('<') && string.ends_with('>') {
let param = &string[1..(string.len() - 1)]; let param = &string[1..(string.len() - 1)];
@ -187,27 +188,20 @@ fn parse_method(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned<Method> {
dummy_spanned(Method::Get) dummy_spanned(Method::Get)
} }
fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) fn parse_path(ecx: &ExtCtxt,
-> (Spanned<URI<'static>>, Option<Spanned<Ident>>) { meta_item: &NestedMetaItem)
let from_string = |string: &str, sp: Span| { -> (Spanned<URI<'static>>, Option<Spanned<Ident>>) {
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)
};
let sp = meta_item.span(); let sp = meta_item.span();
if let Some((name, lit)) = meta_item.name_value() { if let Some((name, lit)) = meta_item.name_value() {
if name != &"path" { if name != &"path" {
ecx.span_err(sp, "the first key, if any, must be 'path'"); ecx.span_err(sp, "the first key, if any, must be 'path'");
} else if let LitKind::Str(ref s, _) = lit.node { } 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 { } else {
ecx.span_err(lit.span, "`path` value must be a string") ecx.span_err(lit.span, "`path` value must be a string")
} }
} else if let Some(s) = meta_item.str_lit() { } 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 { } else {
ecx.struct_span_err(sp, r#"expected `path = string` or a path string"#) ecx.struct_span_err(sp, r#"expected `path = string` or a path string"#)
.help(r#"you can specify the path directly as a string, \ .help(r#"you can specify the path directly as a string, \
@ -229,7 +223,7 @@ fn parse_data(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> Ident {
let mut ident = Ident::from_str("unknown"); let mut ident = Ident::from_str("unknown");
if let LitKind::Str(ref s, _) = *kv.value() { if let LitKind::Str(ref s, _) = *kv.value() {
ident = Ident::from_str(&s.as_str()); 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; return id.node;
} }
} }

114
codegen/src/parser/uri.rs Normal file
View File

@ -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 = &param[..(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 '<param>'")
.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 '<param>'")
.emit();
} else {
ecx.span_err(span, "segment contains invalid characters");
}
validated = false;
}
}
validated
}
pub fn validate_uri(ecx: &ExtCtxt,
string: &str,
sp: Span)
-> (Spanned<URI<'static>>, Option<Spanned<Ident>>) {
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)
}
}

View File

@ -8,6 +8,9 @@ pub trait SpanExt {
/// Trim the span on the left by `length`. /// Trim the span on the left by `length`.
fn trim_left(self, length: usize) -> Span; 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 { impl SpanExt for Span {
@ -16,6 +19,11 @@ impl SpanExt for Span {
self 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 { fn shorten_to(mut self, to_length: usize) -> Span {
self.hi = self.lo + BytePos(to_length as u32); self.hi = self.lo + BytePos(to_length as u32);
self self

View File

@ -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() { }

View File

@ -3,7 +3,7 @@
extern crate rocket; extern crate rocket;
#[get(path = "hello", 123)] //~ ERROR expected #[get(path = "/hello", 123)] //~ ERROR expected
fn get() -> &'static str { "hi" } fn get() -> &'static str { "hi" }
fn main() { fn main() {

View File

@ -9,10 +9,10 @@ fn get0() -> &'static str { "hi" }
#[get(path = 1)] //~ ERROR must be a string #[get(path = 1)] //~ ERROR must be a string
fn get1() -> &'static str { "hi" } 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" } 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 get3() -> &'static str { "hi" }
fn main() { fn main() {

View File

@ -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() { }

View File

@ -7,7 +7,7 @@ fn get() -> &'static str { "hi" }
#[get("/<name><")] //~ ERROR malformed #[get("/<name><")] //~ ERROR malformed
fn get1(name: &str) -> &'static str { "hi" } fn get1(name: &str) -> &'static str { "hi" }
#[get("/<<<<name><")] //~ ERROR identifiers #[get("/<<<<name><")] //~ ERROR malformed
fn get2(name: &str) -> &'static str { "hi" } fn get2(name: &str) -> &'static str { "hi" }
#[get("/<!>")] //~ ERROR identifiers #[get("/<!>")] //~ ERROR identifiers
@ -19,7 +19,13 @@ fn get4() -> &'static str { "hi" }
#[get("/<1>")] //~ ERROR identifiers #[get("/<1>")] //~ ERROR identifiers
fn get5() -> &'static str { "hi" } fn get5() -> &'static str { "hi" }
#[get("/<>name><")] //~ ERROR cannot be empty #[get("/<>name><")] //~ ERROR malformed
fn get6() -> &'static str { "hi" } fn get6() -> &'static str { "hi" }
#[get("/<name>:<id>")] //~ ERROR identifiers
fn get7() -> &'static str { "hi" }
#[get("/<>")] //~ ERROR empty
fn get8() -> &'static str { "hi" }
fn main() { } fn main() { }

View File

@ -3,7 +3,7 @@
extern crate rocket; 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 //~^ ERROR valid HTTP method
fn get() -> &'static str { "hi" } fn get() -> &'static str { "hi" }

View File

@ -3,7 +3,7 @@
extern crate rocket; extern crate rocket;
#[route(CONNECT, "hello")] //~ ERROR valid HTTP method #[route(CONNECT, "/hello")] //~ ERROR valid HTTP method
fn get() -> &'static str { "hi" } fn get() -> &'static str { "hi" }
fn main() { fn main() {

View File

@ -3,7 +3,7 @@
extern crate rocket; 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 get() -> &'static str { "hi" }
fn main() { fn main() {

View File

@ -3,7 +3,7 @@
extern crate rocket; extern crate rocket;
#[get("")] #[get("/")]
fn get() -> &'static str { "hi" } fn get() -> &'static str { "hi" }
#[get("/")] #[get("/")]