use super::{
INVALID_EXPRESSION, InterpolationInput, InterpolationOperator, find_closing_brace, is_name_continue, is_name_start,
parse_braced_expression,
};
use crate::{
diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity},
source::SourceSpan,
syntax::ScalarPresentation,
};
use std::{error::Error, fmt, ops::Range, sync::Arc};
pub const INSPECTION_LIMIT: DiagnosticCode = DiagnosticCode::new("compose.interpolation.inspection-limit");
pub const INTERPOLATION_INSPECTION_MAX_BYTES: usize = 1_048_576;
pub const INTERPOLATION_INSPECTION_MAX_SEGMENTS: usize = 65_536;
pub const INTERPOLATION_INSPECTION_MAX_DEPTH: usize = 32;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InterpolationInspectionErrorKind {
InputLimit,
DepthLimit,
SegmentLimit,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InterpolationInspectionError {
kind: InterpolationInspectionErrorKind,
span: SourceSpan,
range: Option<Range<usize>>,
}
impl InterpolationInspectionError {
#[must_use]
pub const fn kind(&self) -> InterpolationInspectionErrorKind {
self.kind
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
#[must_use]
pub fn range(&self) -> Option<Range<usize>> {
self.range.clone()
}
#[must_use]
pub fn diagnostic(&self) -> Diagnostic {
Diagnostic::new(INSPECTION_LIMIT, Severity::Error, self.to_string()).with_label(DiagnosticLabel::primary(
self.span,
"interpolation inspection safety limit reached",
))
}
}
impl fmt::Display for InterpolationInspectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self.kind {
InterpolationInspectionErrorKind::InputLimit => "interpolation inspection input byte limit exceeded",
InterpolationInspectionErrorKind::DepthLimit => "interpolation inspection operand depth limit exceeded",
InterpolationInspectionErrorKind::SegmentLimit => "interpolation inspection total segment limit exceeded",
})
}
}
impl Error for InterpolationInspectionError {}
#[derive(Clone, Eq, PartialEq)]
struct Evidence {
root: Arc<str>,
range: Range<usize>,
span: SourceSpan,
sensitive: bool,
}
impl Evidence {
fn original(&self) -> &str {
&self.root[self.range.clone()]
}
}
impl fmt::Debug for Evidence {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("InterpolationEvidence")
.field("original", &if self.sensitive { "<redacted>" } else { self.original() })
.field("range", &self.range)
.field("span", &self.span)
.field("sensitive", &self.sensitive)
.finish_non_exhaustive()
}
}
macro_rules! evidence_accessors {
() => {
#[must_use]
pub fn original(&self) -> &str {
self.evidence.original()
}
#[must_use]
pub fn range(&self) -> Range<usize> {
self.evidence.range.clone()
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.evidence.span
}
#[must_use]
pub const fn is_sensitive(&self) -> bool {
self.evidence.sensitive
}
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InterpolationReferenceForm {
Unbraced,
Braced,
}
#[derive(Clone, Eq, PartialEq)]
pub struct InterpolationLiteral {
evidence: Evidence,
value: String,
}
impl InterpolationLiteral {
evidence_accessors!();
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
impl fmt::Debug for InterpolationLiteral {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("InterpolationLiteral")
.field("evidence", &self.evidence)
.field(
"value",
&if self.is_sensitive() {
"<redacted>"
} else {
self.value()
},
)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InterpolationReference {
evidence: Evidence,
name: Range<usize>,
operator: InterpolationOperator,
form: InterpolationReferenceForm,
operator_range: Option<Range<usize>>,
operand: Option<Range<usize>>,
operand_segments: Vec<InterpolationSegment>,
}
impl InterpolationReference {
evidence_accessors!();
#[must_use]
pub fn name(&self) -> &str {
&self.evidence.root[self.name.clone()]
}
#[must_use]
pub const fn operator(&self) -> InterpolationOperator {
self.operator
}
#[must_use]
pub const fn form(&self) -> InterpolationReferenceForm {
self.form
}
#[must_use]
pub fn operand(&self) -> Option<&str> {
self.operand.as_ref().map(|range| &self.evidence.root[range.clone()])
}
#[must_use]
pub fn operand_segments(&self) -> Option<&[InterpolationSegment]> {
self.operand.as_ref().map(|_| self.operand_segments.as_slice())
}
#[must_use]
pub fn name_range(&self) -> Range<usize> {
self.name.clone()
}
#[must_use]
pub fn operator_range(&self) -> Option<Range<usize>> {
self.operator_range.clone()
}
#[must_use]
pub fn operand_range(&self) -> Option<Range<usize>> {
self.operand.clone()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InterpolationIssueKind {
UnclosedBracedExpression,
InvalidVariableName,
UnsupportedOperator,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InvalidInterpolation {
evidence: Evidence,
kind: InterpolationIssueKind,
}
impl InvalidInterpolation {
evidence_accessors!();
#[must_use]
pub const fn kind(&self) -> InterpolationIssueKind {
self.kind
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum InterpolationSegment {
Literal(InterpolationLiteral),
Reference(InterpolationReference),
Invalid(InvalidInterpolation),
}
impl InterpolationSegment {
fn evidence(&self) -> &Evidence {
match self {
Self::Literal(value) => &value.evidence,
Self::Reference(value) => &value.evidence,
Self::Invalid(value) => &value.evidence,
}
}
#[must_use]
pub fn original(&self) -> &str {
self.evidence().original()
}
#[must_use]
pub fn range(&self) -> Range<usize> {
self.evidence().range.clone()
}
#[must_use]
pub fn span(&self) -> SourceSpan {
self.evidence().span
}
#[must_use]
pub fn is_sensitive(&self) -> bool {
self.evidence().sensitive
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct InterpolationInspection {
evidence: Evidence,
segments: Vec<InterpolationSegment>,
diagnostics: Vec<Diagnostic>,
}
impl InterpolationInspection {
evidence_accessors!();
#[must_use]
pub fn segments(&self) -> &[InterpolationSegment] {
&self.segments
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity() != Severity::Error)
}
}
impl fmt::Debug for InterpolationInspection {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("InterpolationInspection")
.field("evidence", &self.evidence)
.field("segments", &self.segments)
.field("diagnostics", &self.diagnostics)
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExcludedInterpolation {
evidence: Evidence,
presentation: ScalarPresentation,
}
impl ExcludedInterpolation {
evidence_accessors!();
#[must_use]
pub const fn presentation(&self) -> ScalarPresentation {
self.presentation
}
#[must_use]
pub fn contains_dollar(&self) -> bool {
self.original().contains('$')
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ScalarInterpolationInspection {
Inspected(InterpolationInspection),
Excluded(ExcludedInterpolation),
}
pub fn inspect_interpolation(
input: InterpolationInput<'_>,
) -> Result<InterpolationInspection, InterpolationInspectionError> {
check_input(input)?;
let evidence = Evidence {
root: Arc::from(input.value()),
range: 0..input.value().len(),
span: input.span(),
sensitive: input.is_sensitive(),
};
let mut inspector = Inspector {
evidence: evidence.clone(),
nodes: 0,
diagnostics: Vec::new(),
};
let segments = inspector.scan(0..input.value().len(), 0)?;
Ok(InterpolationInspection {
evidence,
segments,
diagnostics: inspector.diagnostics,
})
}
pub fn inspect_scalar_interpolation(
input: InterpolationInput<'_>,
presentation: ScalarPresentation,
) -> Result<ScalarInterpolationInspection, InterpolationInspectionError> {
match presentation {
ScalarPresentation::Plain | ScalarPresentation::DoubleQuoted => {
inspect_interpolation(input).map(ScalarInterpolationInspection::Inspected)
}
ScalarPresentation::SingleQuoted | ScalarPresentation::Literal | ScalarPresentation::Folded => {
check_input(input)?;
Ok(ScalarInterpolationInspection::Excluded(ExcludedInterpolation {
evidence: Evidence {
root: Arc::from(input.value()),
range: 0..input.value().len(),
span: input.span(),
sensitive: input.is_sensitive(),
},
presentation,
}))
}
}
}
fn check_input(input: InterpolationInput<'_>) -> Result<(), InterpolationInspectionError> {
if input.value().len() > INTERPOLATION_INSPECTION_MAX_BYTES {
return Err(InterpolationInspectionError {
kind: InterpolationInspectionErrorKind::InputLimit,
span: input.span(),
range: None,
});
}
Ok(())
}
struct Inspector {
evidence: Evidence,
nodes: usize,
diagnostics: Vec<Diagnostic>,
}
impl Inspector {
fn evidence(&self, range: Range<usize>) -> Evidence {
Evidence {
range,
..self.evidence.clone()
}
}
fn node(&mut self, range: Range<usize>) -> Result<(), InterpolationInspectionError> {
if self.nodes >= INTERPOLATION_INSPECTION_MAX_SEGMENTS {
return Err(InterpolationInspectionError {
kind: InterpolationInspectionErrorKind::SegmentLimit,
span: self.evidence.span,
range: Some(range),
});
}
self.nodes += 1;
Ok(())
}
fn literal(
&mut self,
segments: &mut Vec<InterpolationSegment>,
range: Range<usize>,
value: String,
) -> Result<(), InterpolationInspectionError> {
if !range.is_empty() {
self.node(range.clone())?;
segments.push(InterpolationSegment::Literal(InterpolationLiteral {
evidence: self.evidence(range),
value,
}));
}
Ok(())
}
fn invalid(
&mut self,
range: Range<usize>,
kind: InterpolationIssueKind,
) -> Result<InterpolationSegment, InterpolationInspectionError> {
self.node(range.clone())?;
self.diagnostics.push(
Diagnostic::new(
INVALID_EXPRESSION,
Severity::Error,
"interpolation expression is malformed or unsupported",
)
.with_label(DiagnosticLabel::primary(
self.evidence.span,
"invalid interpolation expression",
)),
);
Ok(InterpolationSegment::Invalid(InvalidInterpolation {
evidence: self.evidence(range),
kind,
}))
}
fn braced(
&mut self,
token: Range<usize>,
depth: usize,
) -> Result<InterpolationSegment, InterpolationInspectionError> {
let root = Arc::clone(&self.evidence.root);
let expression = &root[token.start + 2..token.end - 1];
let Some((name, operator, operand)) = parse_braced_expression(expression) else {
let kind = if expression.as_bytes().first().is_none_or(|byte| !is_name_start(*byte)) {
InterpolationIssueKind::InvalidVariableName
} else {
InterpolationIssueKind::UnsupportedOperator
};
return self.invalid(token, kind);
};
self.node(token.clone())?;
let name_start = token.start + 2;
let name_end = name_start + name.len();
let operand_start = token.end - 1 - operand.len();
let has_operand = operator != InterpolationOperator::Direct;
let operand_range = has_operand.then_some(operand_start..token.end - 1);
let operand_segments = if let Some(operand_range) = &operand_range {
self.scan(operand_range.clone(), depth + 1)?
} else {
Vec::new()
};
Ok(InterpolationSegment::Reference(InterpolationReference {
evidence: self.evidence(token),
name: name_start..name_end,
operator,
form: InterpolationReferenceForm::Braced,
operator_range: has_operand.then_some(name_end..operand_start),
operand: operand_range,
operand_segments,
}))
}
fn scan(
&mut self,
range: Range<usize>,
depth: usize,
) -> Result<Vec<InterpolationSegment>, InterpolationInspectionError> {
if depth > INTERPOLATION_INSPECTION_MAX_DEPTH {
return Err(InterpolationInspectionError {
kind: InterpolationInspectionErrorKind::DepthLimit,
span: self.evidence.span,
range: Some(range),
});
}
let root = Arc::clone(&self.evidence.root);
let text = &root[range.clone()];
let mut segments = Vec::new();
let mut cursor = 0;
let mut literal_start = 0;
let mut literal = String::new();
while let Some(relative) = text[cursor..].find('$') {
let dollar = cursor + relative;
literal.push_str(&text[cursor..dollar]);
let next = dollar + 1;
if next == text.len() {
literal.push('$');
cursor = next;
break;
}
if text.as_bytes()[next] == b'$' {
literal.push('$');
cursor = next + 1;
continue;
}
if text.as_bytes()[next] == b'{' {
self.literal(
&mut segments,
range.start + literal_start..range.start + dollar,
std::mem::take(&mut literal),
)?;
let Some(close) = find_closing_brace(text, next + 1) else {
segments.push(self.invalid(
range.start + dollar..range.end,
InterpolationIssueKind::UnclosedBracedExpression,
)?);
cursor = text.len();
literal_start = cursor;
break;
};
let token = range.start + dollar..range.start + close + 1;
segments.push(self.braced(token, depth)?);
cursor = close + 1;
literal_start = cursor;
continue;
}
if is_name_start(text.as_bytes()[next]) {
self.literal(
&mut segments,
range.start + literal_start..range.start + dollar,
std::mem::take(&mut literal),
)?;
let mut end = next + 1;
while end < text.len() && is_name_continue(text.as_bytes()[end]) {
end += 1;
}
let token = range.start + dollar..range.start + end;
self.node(token.clone())?;
segments.push(InterpolationSegment::Reference(InterpolationReference {
evidence: self.evidence(token),
name: range.start + next..range.start + end,
operator: InterpolationOperator::Direct,
form: InterpolationReferenceForm::Unbraced,
operator_range: None,
operand: None,
operand_segments: Vec::new(),
}));
cursor = end;
literal_start = cursor;
continue;
}
literal.push('$');
cursor = next;
}
literal.push_str(&text[cursor..]);
self.literal(&mut segments, range.start + literal_start..range.end, literal)?;
Ok(segments)
}
}