Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/codegen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ syn = { version = "2.0", features = ["full", "visit", "visit-mut", "extra-traits
proc-macro2 = "1.0.60"
devise = "0.4"
rkt_http = { workspace = true }
ubyte = "0.10.2"
unicode-xid = "0.2"
version_check = "0.9"
glob = "0.3"
Expand Down
5 changes: 5 additions & 0 deletions core/codegen/src/attribute/route/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,11 @@ fn incomplete_route(
data: method_attribute.data,
format: method_attribute.format,
rank: method_attribute.rank,
body_timeout: method_attribute.body_timeout,
body_min_rate: method_attribute.body_min_rate,
body_grace: method_attribute.body_grace,
body_policy: method_attribute.body_policy,
body_limit: method_attribute.body_limit,
};

codegen_route(Route::from(attribute, function)?)
Expand Down
130 changes: 129 additions & 1 deletion core/codegen/src/attribute/route/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ use devise::ext::{SpanDiagnosticExt, TypeExt};
use devise::{FromMeta, Result, SpanWrapped, Spanned};
use indexmap::{IndexMap, IndexSet};
use proc_macro2::Span;
use ubyte::ByteUnit;

use crate::attribute::param::{Dynamic, Guard, Parameter};
use crate::attribute::suppress::Lint;
use crate::http::ext::IntoOwned;
use crate::http::{parse_duration, Rate};
use crate::http::uri::{fmt, Origin};
use crate::http_codegen::{MediaType, Method};
use crate::name::Name;
use crate::proc_macro_ext::Diagnostics;
use crate::proc_macro_ext::{Diagnostics, StringLit};
use crate::syn_ext::FnArgExt;

/// This structure represents the parsed `route` attribute and associated items.
Expand Down Expand Up @@ -48,6 +50,12 @@ pub struct Attribute {
pub data: Option<SpanWrapped<Dynamic>>,
pub format: Option<MediaType>,
pub rank: Option<isize>,
pub body_timeout: Option<SpanWrapped<BodyTimeout>>,
pub body_min_rate: Option<SpanWrapped<BodyMinRate>>,
pub body_grace: Option<SpanWrapped<BodyGrace>>,
pub body_policy: Option<SpanWrapped<BodyPolicy>>,
#[allow(dead_code)] // Emitted as route metadata in a later step.
pub body_limit: Option<SpanWrapped<BodyLimit>>,
}

/// The parsed `#[method(..)]` (e.g, `get`, `put`, etc.) attribute.
Expand All @@ -58,6 +66,98 @@ pub struct MethodAttribute {
pub data: Option<SpanWrapped<Dynamic>>,
pub format: Option<MediaType>,
pub rank: Option<isize>,
pub body_timeout: Option<SpanWrapped<BodyTimeout>>,
pub body_min_rate: Option<SpanWrapped<BodyMinRate>>,
pub body_grace: Option<SpanWrapped<BodyGrace>>,
pub body_policy: Option<SpanWrapped<BodyPolicy>>,
pub body_limit: Option<SpanWrapped<BodyLimit>>,
}

/// The parsed value of `body_timeout`: a non-zero duration.
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] // Emitted as route metadata in a later step.
pub struct BodyTimeout(pub std::time::Duration);

/// The parsed value of `body_min_rate`: a non-zero rate.
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] // Emitted as route metadata in a later step.
pub struct BodyMinRate(pub Rate);

/// The parsed value of `body_grace`: a non-zero duration.
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] // Emitted as route metadata in a later step.
pub struct BodyGrace(pub std::time::Duration);

/// The parsed value of `body_policy`: only `"unlimited"` is accepted.
#[derive(Debug, Clone, Copy)]
pub struct BodyPolicy;

/// The parsed value of `body_limit`: a non-zero number of bytes.
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)] // Emitted as route metadata in a later step.
pub struct BodyLimit(pub ByteUnit);

fn parse_body_duration(meta: &devise::MetaItem) -> Result<std::time::Duration> {
let string = StringLit::from_meta(meta)?;
parse_duration(&string).map_err(|e| {
string.span()
.error(format!("invalid duration: {}", e))
.help("expected a duration such as \"30s\", \"250ms\", or \"1m30s\"")
})
}

impl FromMeta for BodyTimeout {
fn from_meta(meta: &devise::MetaItem) -> Result<Self> {
parse_body_duration(meta).map(BodyTimeout)
}
}

impl FromMeta for BodyGrace {
fn from_meta(meta: &devise::MetaItem) -> Result<Self> {
parse_body_duration(meta).map(BodyGrace)
}
}

impl FromMeta for BodyMinRate {
fn from_meta(meta: &devise::MetaItem) -> Result<Self> {
let string = StringLit::from_meta(meta)?;
string.parse::<Rate>().map(BodyMinRate).map_err(|e| {
string.span()
.error(format!("invalid rate: {}", e))
.help("expected a rate such as \"8KiB/s\" or \"1MiB/10s\"")
})
}
}

impl FromMeta for BodyPolicy {
fn from_meta(meta: &devise::MetaItem) -> Result<Self> {
let string = StringLit::from_meta(meta)?;
match &*string {
"unlimited" => Ok(BodyPolicy),
_ => Err(string.span()
.error("invalid body policy")
.help("the only supported body policy is \"unlimited\"")),
}
}
}

impl FromMeta for BodyLimit {
fn from_meta(meta: &devise::MetaItem) -> Result<Self> {
let string = StringLit::from_meta(meta)?;
let limit = string.parse::<ByteUnit>().map_err(|e| {
string.span()
.error(format!("invalid body limit: {}", e))
.help("expected a byte size such as \"2MiB\" or \"64KiB\"")
})?;

if limit.as_u64() == 0 {
return Err(string.span()
.error("body limit must be non-zero")
.help("the default body limit is finite and nonzero; use a non-zero size"));
}

Ok(BodyLimit(limit))
}
}

#[derive(Debug)]
Expand Down Expand Up @@ -168,6 +268,34 @@ impl Route {
}
}

// Validate the body policy options: at most one progress policy, and
// a grace period only alongside a minimum rate.
let progress = [
attr.body_timeout.as_ref().map(|o| ("body_timeout", o.full_span)),
attr.body_min_rate.as_ref().map(|o| ("body_min_rate", o.full_span)),
attr.body_policy.as_ref().map(|o| ("body_policy", o.full_span)),
];

let mut progress = progress.into_iter().flatten();
if let Some((first, first_span)) = progress.next() {
for (name, span) in progress {
diags.push(
span.error(format!("`{}` cannot be used with `{}`", name, first))
.span_note(first_span, "the progress policy was first set here")
.help("a route can have only one of `body_timeout`, `body_min_rate`, \
and `body_policy`"),
);
}
}

if let (Some(grace), None) = (&attr.body_grace, &attr.body_min_rate) {
diags.push(
grace.full_span
.error("`body_grace` requires `body_min_rate`")
.help("the grace period applies only to rate-based body policies"),
);
}

// Check the validity of function arguments.
let span = handler.sig.paren_token.span.join();
let mut arguments = Arguments {
Expand Down
85 changes: 85 additions & 0 deletions core/codegen/tests/route-body-policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Every valid combination of the body policy route options compiles, mounts,
// and leaves the route usable. The options are parsed and validated here;
// their runtime effect is tested in `rkt`'s `body-policy-*` tests.

#[macro_use] extern crate rkt;

use rkt::local::blocking::Client;
use rkt::http::Status;

#[post("/none", data = "<_d>")]
fn none(_d: Vec<u8>) -> &'static str { "none" }

#[post("/timeout", data = "<_d>", body_timeout = "30s")]
fn timeout(_d: Vec<u8>) -> &'static str { "timeout" }

#[post("/compound", data = "<_d>", body_timeout = "1m30s")]
fn compound(_d: Vec<u8>) -> &'static str { "compound" }

#[post("/rate", data = "<_d>", body_min_rate = "8KiB/s")]
fn rate(_d: Vec<u8>) -> &'static str { "rate" }

#[post("/rate_period", data = "<_d>", body_min_rate = "1MiB/10s")]
fn rate_period(_d: Vec<u8>) -> &'static str { "rate_period" }

#[post("/rate_grace", data = "<_d>", body_min_rate = "8KiB/s", body_grace = "30s")]
fn rate_grace(_d: Vec<u8>) -> &'static str { "rate_grace" }

#[post("/unlimited", data = "<_d>", body_policy = "unlimited")]
fn unlimited(_d: Vec<u8>) -> &'static str { "unlimited" }

#[post("/limit", data = "<_d>", body_limit = "2MiB")]
fn limit(_d: Vec<u8>) -> &'static str { "limit" }

#[post("/timeout_limit", data = "<_d>", body_timeout = "30s", body_limit = "2MiB")]
fn timeout_limit(_d: Vec<u8>) -> &'static str { "timeout_limit" }

#[post(
"/rate_grace_limit",
data = "<_d>",
body_min_rate = "8KiB/s",
body_grace = "30s",
body_limit = "5GiB"
)]
fn rate_grace_limit(_d: Vec<u8>) -> &'static str { "rate_grace_limit" }

#[post("/unlimited_limit", data = "<_d>", body_policy = "unlimited", body_limit = "64MiB")]
fn unlimited_limit(_d: Vec<u8>) -> &'static str { "unlimited_limit" }

// Option order is irrelevant, and the options are accepted without `data`.
#[post("/reordered", body_limit = "1KiB", body_grace = "5s", body_min_rate = "1KiB/s", data = "<_d>")]
fn reordered(_d: Vec<u8>) -> &'static str { "reordered" }

#[put("/no_data", body_timeout = "5s")]
fn no_data() -> &'static str { "no_data" }

// The generic `route` attribute accepts the same options.
#[route("/generic", method = POST, data = "<_d>", body_timeout = "30s", body_limit = "1MiB")]
fn generic(_d: Vec<u8>) -> &'static str { "generic" }

#[test]
fn valid_body_policy_options_mount_and_route() {
let rocket = rkt::build().mount("/", routes![
none, timeout, compound, rate, rate_period, rate_grace, unlimited, limit,
timeout_limit, rate_grace_limit, unlimited_limit, reordered, no_data, generic,
]);

let client = Client::debug(rocket).unwrap();
let cases = [
("none", "none"), ("timeout", "timeout"), ("compound", "compound"),
("rate", "rate"), ("rate_period", "rate_period"), ("rate_grace", "rate_grace"),
("unlimited", "unlimited"), ("limit", "limit"), ("timeout_limit", "timeout_limit"),
("rate_grace_limit", "rate_grace_limit"), ("unlimited_limit", "unlimited_limit"),
("reordered", "reordered"), ("generic", "generic"),
];

for (path, expected) in cases {
let response = client.post(format!("/{path}")).body("hello").dispatch();
assert_eq!(response.status(), Status::Ok, "POST /{path}");
assert_eq!(response.into_string().unwrap(), expected, "POST /{path}");
}

let response = client.put("/no_data").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "no_data");
}
2 changes: 1 addition & 1 deletion core/codegen/tests/ui-fail-nightly/catch.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#[macro_use] extern crate rocket;
#[macro_use] extern crate rkt;

use rkt::Request;

Expand Down
91 changes: 91 additions & 0 deletions core/codegen/tests/ui-fail-nightly/route-body-policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#[macro_use] extern crate rkt;

// Only one progress policy per route.
#[post("/", data = "<_d>", body_timeout = "30s", body_min_rate = "8KiB/s")]
fn a0(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_timeout = "30s", body_policy = "unlimited")]
fn a1(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB/s", body_policy = "unlimited")]
fn a2(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_policy = "unlimited", body_timeout = "30s")]
fn a3(_d: Vec<u8>) {}

// A grace period needs a rate.
#[post("/", data = "<_d>", body_grace = "30s")]
fn b0(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_timeout = "30s", body_grace = "30s")]
fn b1(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_policy = "unlimited", body_grace = "30s")]
fn b2(_d: Vec<u8>) {}

// Unparsable durations, rates, and sizes.
#[post("/", data = "<_d>", body_timeout = "soon")]
fn c0(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_timeout = "30 seconds")]
fn c1(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB")]
fn c2(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "fast/s")]
fn c3(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB/s", body_grace = "later")]
fn c4(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_limit = "lots")]
fn c5(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_timeout = 30)]
fn c6(_d: Vec<u8>) {}

// Only `"unlimited"` is a valid policy.
#[post("/", data = "<_d>", body_policy = "limited")]
fn d0(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_policy = "")]
fn d1(_d: Vec<u8>) {}

// Zero values are rejected.
#[post("/", data = "<_d>", body_timeout = "0s")]
fn e0(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "0/s")]
fn e1(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB/0s")]
fn e2(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB/s", body_grace = "0s")]
fn e3(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_limit = "0B")]
fn e4(_d: Vec<u8>) {}

// Duplicate options.
#[post("/", data = "<_d>", body_timeout = "30s", body_timeout = "60s")]
fn f0(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB/s", body_min_rate = "1KiB/s")]
fn f1(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_min_rate = "8KiB/s", body_grace = "1s", body_grace = "2s")]
fn f2(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_policy = "unlimited", body_policy = "unlimited")]
fn f3(_d: Vec<u8>) {}

#[post("/", data = "<_d>", body_limit = "1MiB", body_limit = "2MiB")]
fn f4(_d: Vec<u8>) {}

// The same checks apply through the generic `route` attribute.
#[route("/", method = POST, data = "<_d>", body_timeout = "30s", body_policy = "unlimited")]
fn g0(_d: Vec<u8>) {}

fn main() { }
Loading
Loading