///|
/// Static restrictions for an application that consumes URI templates.
/// This checks template structure only; it cannot validate a final URI host.
pub(all) struct TemplatePolicy {
allow_reserved : Bool
allow_fragment : Bool
max_level : Int
max_references : Int
forbid_repeated_variables : Bool
}
///|
/// Policy matching the syntactic capabilities of this library.
pub fn permissive_policy() -> TemplatePolicy {
{
allow_reserved: true,
allow_fragment: true,
max_level: 4,
max_references: 4096,
forbid_repeated_variables: false,
}
}
///|
pub(all) enum PolicyError {
InvalidFeatureLevel
InvalidReferenceLimit
}
///|
pub fn PolicyError::code(self : PolicyError) -> String {
match self {
InvalidFeatureLevel => "invalid_feature_level"
InvalidReferenceLimit => "invalid_reference_limit"
}
}
///|
fn TemplatePolicy::validate(self : TemplatePolicy) -> Result[Unit, PolicyError] {
if self.max_level < 1 || self.max_level > 4 {
return Err(InvalidFeatureLevel)
}
if self.max_references < 0 {
return Err(InvalidReferenceLimit)
}
Ok(())
}
///|
pub(all) enum PolicyFindingKind {
ReservedExpansion
FragmentExpansion
FeatureLevelExceeded(Int)
ReferenceLimitExceeded
RepeatedVariable(String)
}
///|
pub fn PolicyFindingKind::code(self : PolicyFindingKind) -> String {
match self {
ReservedExpansion => "reserved_expansion"
FragmentExpansion => "fragment_expansion"
FeatureLevelExceeded(_) => "feature_level_exceeded"
ReferenceLimitExceeded => "reference_limit_exceeded"
RepeatedVariable(_) => "repeated_variable"
}
}
///|
/// UTF-16 source range of a static policy finding.
pub(all) struct PolicyFinding {
kind : PolicyFindingKind
start : Int
end : Int
}
///|
/// Check a compiled template without bindings. All findings are returned in
/// source order; a single expression may produce several distinct findings.
pub fn Template::check_policy(
self : Template,
policy : TemplatePolicy,
) -> Result[Array[PolicyFinding], PolicyError] {
match policy.validate() {
Ok(_) => ()
Err(error) => return Err(error)
}
let analysis = self.analyze()
let findings : Array[PolicyFinding] = []
let seen : Map[String, Bool] = Map([])
let uses = analysis.uses()
for expression in analysis.expressions() {
if expression.level > policy.max_level {
findings.push({
kind: FeatureLevelExceeded(expression.level),
start: expression.start,
end: expression.end,
})
}
if expression.permits_reserved && !policy.allow_reserved {
findings.push({
kind: ReservedExpansion,
start: expression.start,
end: expression.end,
})
}
if expression.operator == "#" && !policy.allow_fragment {
findings.push({
kind: FragmentExpansion,
start: expression.start,
end: expression.end,
})
}
}
if uses.length() > policy.max_references {
findings.push({
kind: ReferenceLimitExceeded,
start: 0,
end: self.source.length(),
})
}
if policy.forbid_repeated_variables {
for use_ in uses {
if seen.contains(use_.name) {
findings.push({
kind: RepeatedVariable(use_.name),
start: use_.start,
end: use_.end,
})
} else {
seen[use_.name] = true
}
}
}
findings.sort_by(fn(a, b) { a.start.compare(b.start) })
Ok(findings)
}