diff options
Diffstat (limited to 'third_party/rust/fluent-bundle/src/resolver')
6 files changed, 633 insertions, 0 deletions
diff --git a/third_party/rust/fluent-bundle/src/resolver/errors.rs b/third_party/rust/fluent-bundle/src/resolver/errors.rs new file mode 100644 index 0000000000..831d8474a5 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/errors.rs @@ -0,0 +1,96 @@ +use fluent_syntax::ast::InlineExpression; +use std::error::Error; + +#[derive(Debug, PartialEq, Clone)] +pub enum ReferenceKind { + Function { + id: String, + }, + Message { + id: String, + attribute: Option<String>, + }, + Term { + id: String, + attribute: Option<String>, + }, + Variable { + id: String, + }, +} + +impl<T> From<&InlineExpression<T>> for ReferenceKind +where + T: ToString, +{ + fn from(exp: &InlineExpression<T>) -> Self { + match exp { + InlineExpression::FunctionReference { id, .. } => Self::Function { + id: id.name.to_string(), + }, + InlineExpression::MessageReference { id, attribute } => Self::Message { + id: id.name.to_string(), + attribute: attribute.as_ref().map(|i| i.name.to_string()), + }, + InlineExpression::TermReference { id, attribute, .. } => Self::Term { + id: id.name.to_string(), + attribute: attribute.as_ref().map(|i| i.name.to_string()), + }, + InlineExpression::VariableReference { id, .. } => Self::Variable { + id: id.name.to_string(), + }, + _ => unreachable!(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ResolverError { + Reference(ReferenceKind), + NoValue(String), + MissingDefault, + Cyclic, + TooManyPlaceables, +} + +impl std::fmt::Display for ResolverError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Reference(exp) => match exp { + ReferenceKind::Function { id } => write!(f, "Unknown function: {}()", id), + ReferenceKind::Message { + id, + attribute: None, + } => write!(f, "Unknown message: {}", id), + ReferenceKind::Message { + id, + attribute: Some(attribute), + } => write!(f, "Unknown attribute: {}.{}", id, attribute), + ReferenceKind::Term { + id, + attribute: None, + } => write!(f, "Unknown term: -{}", id), + ReferenceKind::Term { + id, + attribute: Some(attribute), + } => write!(f, "Unknown attribute: -{}.{}", id, attribute), + ReferenceKind::Variable { id } => write!(f, "Unknown variable: ${}", id), + }, + Self::NoValue(id) => write!(f, "No value: {}", id), + Self::MissingDefault => f.write_str("No default"), + Self::Cyclic => f.write_str("Cyclical dependency detected"), + Self::TooManyPlaceables => f.write_str("Too many placeables"), + } + } +} + +impl<T> From<&InlineExpression<T>> for ResolverError +where + T: ToString, +{ + fn from(exp: &InlineExpression<T>) -> Self { + Self::Reference(exp.into()) + } +} + +impl Error for ResolverError {} diff --git a/third_party/rust/fluent-bundle/src/resolver/expression.rs b/third_party/rust/fluent-bundle/src/resolver/expression.rs new file mode 100644 index 0000000000..84de7248d3 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/expression.rs @@ -0,0 +1,65 @@ +use super::scope::Scope; +use super::WriteValue; + +use std::borrow::Borrow; +use std::fmt; + +use fluent_syntax::ast; + +use crate::memoizer::MemoizerKind; +use crate::resolver::{ResolveValue, ResolverError}; +use crate::resource::FluentResource; +use crate::types::FluentValue; + +impl<'p> WriteValue for ast::Expression<&'p str> { + fn write<'scope, 'errors, W, R, M: MemoizerKind>( + &'scope self, + w: &mut W, + scope: &mut Scope<'scope, 'errors, R, M>, + ) -> fmt::Result + where + W: fmt::Write, + R: Borrow<FluentResource>, + { + match self { + Self::InlineExpression(exp) => exp.write(w, scope), + Self::SelectExpression { selector, variants } => { + let selector = selector.resolve(scope); + match selector { + FluentValue::String(_) | FluentValue::Number(_) => { + for variant in variants { + let key = match variant.key { + ast::VariantKey::Identifier { name } => name.into(), + ast::VariantKey::NumberLiteral { value } => { + FluentValue::try_number(value) + } + }; + if key.matches(&selector, scope) { + return variant.value.write(w, scope); + } + } + } + _ => {} + } + + for variant in variants { + if variant.default { + return variant.value.write(w, scope); + } + } + scope.add_error(ResolverError::MissingDefault); + Ok(()) + } + } + } + + fn write_error<W>(&self, w: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match self { + Self::InlineExpression(exp) => exp.write_error(w), + Self::SelectExpression { .. } => unreachable!(), + } + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs b/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs new file mode 100644 index 0000000000..83cd622cc8 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/inline_expression.rs @@ -0,0 +1,179 @@ +use super::scope::Scope; +use super::{ResolveValue, ResolverError, WriteValue}; + +use std::borrow::Borrow; +use std::fmt; + +use fluent_syntax::ast; +use fluent_syntax::unicode::{unescape_unicode, unescape_unicode_to_string}; + +use crate::entry::GetEntry; +use crate::memoizer::MemoizerKind; +use crate::resource::FluentResource; +use crate::types::FluentValue; + +impl<'p> WriteValue for ast::InlineExpression<&'p str> { + fn write<'scope, 'errors, W, R, M: MemoizerKind>( + &'scope self, + w: &mut W, + scope: &mut Scope<'scope, 'errors, R, M>, + ) -> fmt::Result + where + W: fmt::Write, + R: Borrow<FluentResource>, + { + match self { + Self::StringLiteral { value } => unescape_unicode(w, value), + Self::MessageReference { id, attribute } => { + if let Some(msg) = scope.bundle.get_entry_message(id.name) { + if let Some(attr) = attribute { + msg.attributes + .iter() + .find_map(|a| { + if a.id.name == attr.name { + Some(scope.track(w, &a.value, self)) + } else { + None + } + }) + .unwrap_or_else(|| scope.write_ref_error(w, self)) + } else { + msg.value + .as_ref() + .map(|value| scope.track(w, value, self)) + .unwrap_or_else(|| { + scope.add_error(ResolverError::NoValue(id.name.to_string())); + w.write_char('{')?; + self.write_error(w)?; + w.write_char('}') + }) + } + } else { + scope.write_ref_error(w, self) + } + } + Self::NumberLiteral { value } => FluentValue::try_number(*value).write(w, scope), + Self::TermReference { + id, + attribute, + arguments, + } => { + let (_, resolved_named_args) = scope.get_arguments(arguments); + + scope.local_args = Some(resolved_named_args); + let result = scope + .bundle + .get_entry_term(id.name) + .and_then(|term| { + if let Some(attr) = attribute { + term.attributes.iter().find_map(|a| { + if a.id.name == attr.name { + Some(scope.track(w, &a.value, self)) + } else { + None + } + }) + } else { + Some(scope.track(w, &term.value, self)) + } + }) + .unwrap_or_else(|| scope.write_ref_error(w, self)); + scope.local_args = None; + result + } + Self::FunctionReference { id, arguments } => { + let (resolved_positional_args, resolved_named_args) = + scope.get_arguments(arguments); + + let func = scope.bundle.get_entry_function(id.name); + + if let Some(func) = func { + let result = func(resolved_positional_args.as_slice(), &resolved_named_args); + if let FluentValue::Error = result { + self.write_error(w) + } else { + w.write_str(&result.as_string(scope)) + } + } else { + scope.write_ref_error(w, self) + } + } + Self::VariableReference { id } => { + let args = scope.local_args.as_ref().or(scope.args); + + if let Some(arg) = args.and_then(|args| args.get(id.name)) { + arg.write(w, scope) + } else { + if scope.local_args.is_none() { + scope.add_error(self.into()); + } + w.write_char('{')?; + self.write_error(w)?; + w.write_char('}') + } + } + Self::Placeable { expression } => expression.write(w, scope), + } + } + + fn write_error<W>(&self, w: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match self { + Self::MessageReference { + id, + attribute: Some(attribute), + } => write!(w, "{}.{}", id.name, attribute.name), + Self::MessageReference { + id, + attribute: None, + } => w.write_str(id.name), + Self::TermReference { + id, + attribute: Some(attribute), + .. + } => write!(w, "-{}.{}", id.name, attribute.name), + Self::TermReference { + id, + attribute: None, + .. + } => write!(w, "-{}", id.name), + Self::FunctionReference { id, .. } => write!(w, "{}()", id.name), + Self::VariableReference { id } => write!(w, "${}", id.name), + _ => unreachable!(), + } + } +} + +impl<'p> ResolveValue for ast::InlineExpression<&'p str> { + fn resolve<'source, 'errors, R, M: MemoizerKind>( + &'source self, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> FluentValue<'source> + where + R: Borrow<FluentResource>, + { + match self { + Self::StringLiteral { value } => unescape_unicode_to_string(value).into(), + Self::NumberLiteral { value } => FluentValue::try_number(*value), + Self::VariableReference { id } => { + let args = scope.local_args.as_ref().or(scope.args); + + if let Some(arg) = args.and_then(|args| args.get(id.name)) { + arg.clone() + } else { + if scope.local_args.is_none() { + scope.add_error(self.into()); + } + FluentValue::Error + } + } + _ => { + let mut result = String::new(); + self.write(&mut result, scope).expect("Failed to write"); + result.into() + } + } + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/mod.rs b/third_party/rust/fluent-bundle/src/resolver/mod.rs new file mode 100644 index 0000000000..4413737bc1 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/mod.rs @@ -0,0 +1,48 @@ +//! The `ResolveValue` trait resolves Fluent AST nodes to [`FluentValues`]. +//! +//! This is an internal API used by [`FluentBundle`] to evaluate Messages, Attributes and other +//! AST nodes to [`FluentValues`] which can be then formatted to strings. +//! +//! [`FluentValues`]: ../types/enum.FluentValue.html +//! [`FluentBundle`]: ../bundle/struct.FluentBundle.html + +mod errors; +mod expression; +mod inline_expression; +mod pattern; +mod scope; + +pub use errors::ResolverError; +pub use scope::Scope; + +use std::borrow::Borrow; +use std::fmt; + +use crate::memoizer::MemoizerKind; +use crate::resource::FluentResource; +use crate::types::FluentValue; + +// Converts an AST node to a `FluentValue`. +pub(crate) trait ResolveValue { + fn resolve<'source, 'errors, R, M: MemoizerKind>( + &'source self, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> FluentValue<'source> + where + R: Borrow<FluentResource>; +} + +pub(crate) trait WriteValue { + fn write<'source, 'errors, W, R, M: MemoizerKind>( + &'source self, + w: &mut W, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> fmt::Result + where + W: fmt::Write, + R: Borrow<FluentResource>; + + fn write_error<W>(&self, _w: &mut W) -> fmt::Result + where + W: fmt::Write; +} diff --git a/third_party/rust/fluent-bundle/src/resolver/pattern.rs b/third_party/rust/fluent-bundle/src/resolver/pattern.rs new file mode 100644 index 0000000000..33d8e118a5 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/pattern.rs @@ -0,0 +1,105 @@ +use super::scope::Scope; +use super::{ResolverError, WriteValue}; + +use std::borrow::Borrow; +use std::fmt; + +use fluent_syntax::ast; + +use crate::memoizer::MemoizerKind; +use crate::resolver::ResolveValue; +use crate::resource::FluentResource; +use crate::types::FluentValue; + +const MAX_PLACEABLES: u8 = 100; + +impl<'p> WriteValue for ast::Pattern<&'p str> { + fn write<'scope, 'errors, W, R, M: MemoizerKind>( + &'scope self, + w: &mut W, + scope: &mut Scope<'scope, 'errors, R, M>, + ) -> fmt::Result + where + W: fmt::Write, + R: Borrow<FluentResource>, + { + let len = self.elements.len(); + + for elem in &self.elements { + if scope.dirty { + return Ok(()); + } + + match elem { + ast::PatternElement::TextElement { value } => { + if let Some(ref transform) = scope.bundle.transform { + w.write_str(&transform(value))?; + } else { + w.write_str(value)?; + } + } + ast::PatternElement::Placeable { ref expression } => { + scope.placeables += 1; + if scope.placeables > MAX_PLACEABLES { + scope.dirty = true; + scope.add_error(ResolverError::TooManyPlaceables); + return Ok(()); + } + + let needs_isolation = scope.bundle.use_isolating + && len > 1 + && !matches!(expression, ast::Expression::InlineExpression( + ast::InlineExpression::MessageReference { .. }, + ) + | ast::Expression::InlineExpression( + ast::InlineExpression::TermReference { .. }, + ) + | ast::Expression::InlineExpression( + ast::InlineExpression::StringLiteral { .. }, + )); + if needs_isolation { + w.write_char('\u{2068}')?; + } + scope.maybe_track(w, self, expression)?; + if needs_isolation { + w.write_char('\u{2069}')?; + } + } + } + } + Ok(()) + } + + fn write_error<W>(&self, _w: &mut W) -> fmt::Result + where + W: fmt::Write, + { + unreachable!() + } +} + +impl<'p> ResolveValue for ast::Pattern<&'p str> { + fn resolve<'source, 'errors, R, M: MemoizerKind>( + &'source self, + scope: &mut Scope<'source, 'errors, R, M>, + ) -> FluentValue<'source> + where + R: Borrow<FluentResource>, + { + let len = self.elements.len(); + + if len == 1 { + if let ast::PatternElement::TextElement { value } = self.elements[0] { + return scope + .bundle + .transform + .map_or_else(|| value.into(), |transform| transform(value).into()); + } + } + + let mut result = String::new(); + self.write(&mut result, scope) + .expect("Failed to write to a string."); + result.into() + } +} diff --git a/third_party/rust/fluent-bundle/src/resolver/scope.rs b/third_party/rust/fluent-bundle/src/resolver/scope.rs new file mode 100644 index 0000000000..6cbfb19cf2 --- /dev/null +++ b/third_party/rust/fluent-bundle/src/resolver/scope.rs @@ -0,0 +1,140 @@ +use crate::bundle::FluentBundleBase; +use crate::memoizer::MemoizerKind; +use crate::resolver::{ResolveValue, ResolverError, WriteValue}; +use crate::types::FluentValue; +use crate::{FluentArgs, FluentError, FluentResource}; +use fluent_syntax::ast; +use std::borrow::Borrow; +use std::fmt; + +/// State for a single `ResolveValue::to_value` call. +pub struct Scope<'scope, 'errors, R, M> { + /// The current `FluentBundleBase` instance. + pub bundle: &'scope FluentBundleBase<R, M>, + /// The current arguments passed by the developer. + pub(super) args: Option<&'scope FluentArgs<'scope>>, + /// Local args + pub(super) local_args: Option<FluentArgs<'scope>>, + /// The running count of resolved placeables. Used to detect the Billion + /// Laughs and Quadratic Blowup attacks. + pub(super) placeables: u8, + /// Tracks hashes to prevent infinite recursion. + travelled: smallvec::SmallVec<[&'scope ast::Pattern<&'scope str>; 2]>, + /// Track errors accumulated during resolving. + pub errors: Option<&'errors mut Vec<FluentError>>, + /// Makes the resolver bail. + pub dirty: bool, +} + +impl<'scope, 'errors, R, M: MemoizerKind> Scope<'scope, 'errors, R, M> { + pub fn new( + bundle: &'scope FluentBundleBase<R, M>, + args: Option<&'scope FluentArgs>, + errors: Option<&'errors mut Vec<FluentError>>, + ) -> Self { + Scope { + bundle, + args, + local_args: None, + placeables: 0, + travelled: Default::default(), + errors, + dirty: false, + } + } + + pub fn add_error(&mut self, error: ResolverError) { + if let Some(errors) = self.errors.as_mut() { + errors.push(error.into()); + } + } + + // This method allows us to lazily add Pattern on the stack, + // only if the Pattern::resolve has been called on an empty stack. + // + // This is the case when pattern is called from Bundle and it + // allows us to fast-path simple resolutions, and only use the stack + // for placeables. + pub fn maybe_track<W>( + &mut self, + w: &mut W, + pattern: &'scope ast::Pattern<&str>, + exp: &'scope ast::Expression<&str>, + ) -> fmt::Result + where + R: Borrow<FluentResource>, + W: fmt::Write, + { + if self.travelled.is_empty() { + self.travelled.push(pattern); + } + exp.write(w, self)?; + if self.dirty { + w.write_char('{')?; + exp.write_error(w)?; + w.write_char('}') + } else { + Ok(()) + } + } + + pub fn track<W>( + &mut self, + w: &mut W, + pattern: &'scope ast::Pattern<&str>, + exp: &ast::InlineExpression<&str>, + ) -> fmt::Result + where + R: Borrow<FluentResource>, + W: fmt::Write, + { + if self.travelled.contains(&pattern) { + self.add_error(ResolverError::Cyclic); + w.write_char('{')?; + exp.write_error(w)?; + w.write_char('}') + } else { + self.travelled.push(pattern); + let result = pattern.write(w, self); + self.travelled.pop(); + result + } + } + + pub fn write_ref_error<W>( + &mut self, + w: &mut W, + exp: &ast::InlineExpression<&str>, + ) -> fmt::Result + where + W: fmt::Write, + { + self.add_error(exp.into()); + w.write_char('{')?; + exp.write_error(w)?; + w.write_char('}') + } + + pub fn get_arguments( + &mut self, + arguments: &'scope Option<ast::CallArguments<&'scope str>>, + ) -> (Vec<FluentValue<'scope>>, FluentArgs<'scope>) + where + R: Borrow<FluentResource>, + { + let mut resolved_positional_args = Vec::new(); + let mut resolved_named_args = FluentArgs::new(); + + if let Some(ast::CallArguments { named, positional }) = arguments { + for expression in positional { + resolved_positional_args.push(expression.resolve(self)); + } + + for arg in named { + resolved_named_args.add(arg.name.name, arg.value.resolve(self)); + } + } + + (resolved_positional_args, resolved_named_args) + } +} |