Skip to main content

xidl_parser/rest_hir/semantics/
cors.rs

1use crate::hir;
2use serde::{Deserialize, Serialize};
3
4use super::annotations::{annotation_name, annotation_params};
5
6#[cfg(test)]
7mod tests;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub enum HttpCorsProfile {
11    Any,
12    Origins(Vec<String>),
13}
14
15pub fn effective_cors(
16    interface_annotations: &[hir::Annotation],
17    method_annotations: &[hir::Annotation],
18) -> Result<Option<HttpCorsProfile>, String> {
19    collect_cors(method_annotations)?.map_or_else(
20        || collect_cors(interface_annotations),
21        |profile| Ok(Some(profile)),
22    )
23}
24
25pub(crate) fn collect_cors(
26    annotations: &[hir::Annotation],
27) -> Result<Option<HttpCorsProfile>, String> {
28    let mut matches = annotations.iter().filter(|annotation| {
29        annotation_name(annotation)
30            .map(|name| name.eq_ignore_ascii_case("cors"))
31            .unwrap_or(false)
32    });
33    let Some(annotation) = matches.next() else {
34        return Ok(None);
35    };
36    if matches.next().is_some() {
37        return Err("duplicate @cors annotation".to_string());
38    }
39    parse_cors(annotation).map(Some)
40}
41
42fn parse_cors(annotation: &hir::Annotation) -> Result<HttpCorsProfile, String> {
43    let Some(params) = annotation_params(annotation) else {
44        return Ok(HttpCorsProfile::Any);
45    };
46    match params {
47        hir::AnnotationParams::ConstExpr(expr) => {
48            Ok(HttpCorsProfile::Origins(parse_const_expr_origins(expr)?))
49        }
50        hir::AnnotationParams::Positional(values) => Ok(HttpCorsProfile::Origins(
51            values
52                .iter()
53                .map(parse_string_const_expr)
54                .collect::<Result<Vec<_>, _>>()?,
55        )),
56        hir::AnnotationParams::Raw(_) | hir::AnnotationParams::Params(_) => {
57            Err(cors_syntax_error())
58        }
59    }
60}
61
62fn parse_const_expr_origins(expr: &hir::ConstExpr) -> Result<Vec<String>, String> {
63    Ok(vec![parse_string_const_expr(expr)?])
64}
65
66fn parse_string_const_expr(expr: &hir::ConstExpr) -> Result<String, String> {
67    match expr {
68        hir::ConstExpr::Literal(hir::Literal::StringLiteral(value)) => parse_origin_literal(value),
69        _ => Err(cors_syntax_error()),
70    }
71}
72
73fn parse_origin_literal(value: &str) -> Result<String, String> {
74    let Some(value) = trim_string_literal(value) else {
75        return Err(cors_syntax_error());
76    };
77    if value.is_empty() {
78        return Err("@cors origins must not be empty".to_string());
79    }
80    if !is_valid_origin(&value) {
81        return Err(format!("invalid @cors origin '{value}'"));
82    }
83    Ok(value)
84}
85
86fn cors_syntax_error() -> String {
87    "@cors only accepts comma-separated string literals".to_string()
88}
89
90fn is_valid_origin(value: &str) -> bool {
91    value == "*"
92        || (value.is_ascii()
93            && !value.bytes().any(|byte| byte.is_ascii_control())
94            && !value.is_empty())
95}
96
97fn trim_string_literal(value: &str) -> Option<String> {
98    let value = value.trim();
99    if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') {
100        Some(value[1..value.len() - 1].to_string())
101    } else {
102        None
103    }
104}