@@ -29,8 +29,10 @@ import { FluentType, FluentNone, FluentNumber, FluentDateTime }
2929 from "./types.js" ;
3030import * as builtins from "./builtins.js" ;
3131
32- // Prevent expansion of too long placeables.
33- const MAX_PLACEABLE_LENGTH = 2500 ;
32+ // The maximum number of placeables which can be expanded in a single call to
33+ // `formatPattern`. The limit protects against the Billion Laughs and Quadratic
34+ // Blowup attacks. See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx.
35+ const MAX_PLACEABLES = 100 ;
3436
3537// Unicode bidi isolation characters.
3638const FSI = "\u2068" ;
@@ -86,7 +88,7 @@ function getArguments(scope, args) {
8688 }
8789 }
8890
89- return [ positional , named ] ;
91+ return { positional, named} ;
9092}
9193
9294// Resolve an expression to a Fluent type.
@@ -115,15 +117,23 @@ function resolveExpression(scope, expr) {
115117
116118// Resolve a reference to a variable.
117119function VariableReference ( scope , { name} ) {
118- if ( ! scope . args || ! scope . args . hasOwnProperty ( name ) ) {
119- if ( scope . insideTermReference === false ) {
120- scope . reportError ( new ReferenceError ( `Unknown variable: $${ name } ` ) ) ;
120+ let arg ;
121+ if ( scope . params ) {
122+ // We're inside a TermReference. It's OK to reference undefined parameters.
123+ if ( scope . params . hasOwnProperty ( name ) ) {
124+ arg = scope . params [ name ] ;
125+ } else {
126+ return new FluentNone ( `$${ name } ` ) ;
121127 }
128+ } else if ( scope . args && scope . args . hasOwnProperty ( name ) ) {
129+ // We're in the top-level Pattern or inside a MessageReference. Missing
130+ // variables references produce ReferenceErrors.
131+ arg = scope . args [ name ] ;
132+ } else {
133+ scope . reportError ( new ReferenceError ( `Unknown variable: $${ name } ` ) ) ;
122134 return new FluentNone ( `$${ name } ` ) ;
123135 }
124136
125- const arg = scope . args [ name ] ;
126-
127137 // Return early if the argument already is an instance of FluentType.
128138 if ( arg instanceof FluentType ) {
129139 return arg ;
@@ -181,20 +191,23 @@ function TermReference(scope, {name, attr, args}) {
181191 return new FluentNone ( id ) ;
182192 }
183193
184- // Every TermReference has its own variables.
185- const [ , params ] = getArguments ( scope , args ) ;
186- const local = scope . cloneForTermReference ( params ) ;
187-
188194 if ( attr ) {
189195 const attribute = term . attributes [ attr ] ;
190196 if ( attribute ) {
191- return resolvePattern ( local , attribute ) ;
197+ // Every TermReference has its own variables.
198+ scope . params = getArguments ( scope , args ) . named ;
199+ const resolved = resolvePattern ( scope , attribute ) ;
200+ scope . params = null ;
201+ return resolved ;
192202 }
193203 scope . reportError ( new ReferenceError ( `Unknown attribute: ${ attr } ` ) ) ;
194204 return new FluentNone ( `${ id } .${ attr } ` ) ;
195205 }
196206
197- return resolvePattern ( local , term . value ) ;
207+ scope . params = getArguments ( scope , args ) . named ;
208+ const resolved = resolvePattern ( scope , term . value ) ;
209+ scope . params = null ;
210+ return resolved ;
198211}
199212
200213// Resolve a call to a Function with positional and key-value arguments.
@@ -213,7 +226,8 @@ function FunctionReference(scope, {name, args}) {
213226 }
214227
215228 try {
216- return func ( ...getArguments ( scope , args ) ) ;
229+ let resolved = getArguments ( scope , args ) ;
230+ return func ( resolved . positional , resolved . named ) ;
217231 } catch ( err ) {
218232 scope . reportError ( err ) ;
219233 return new FluentNone ( `${ name } ()` ) ;
@@ -259,25 +273,24 @@ export function resolveComplexPattern(scope, ptn) {
259273 continue ;
260274 }
261275
262- const part = resolveExpression ( scope , elem ) . toString ( scope ) ;
263-
264- if ( useIsolating ) {
265- result . push ( FSI ) ;
266- }
267-
268- if ( part . length > MAX_PLACEABLE_LENGTH ) {
276+ scope . placeables ++ ;
277+ if ( scope . placeables > MAX_PLACEABLES ) {
269278 scope . dirty . delete ( ptn ) ;
270279 // This is a fatal error which causes the resolver to instantly bail out
271280 // on this pattern. The length check protects against excessive memory
272281 // usage, and throwing protects against eating up the CPU when long
273282 // placeables are deeply nested.
274283 throw new RangeError (
275- " Too many characters in placeable " +
276- `( ${ part . length } , max allowed is ${ MAX_PLACEABLE_LENGTH } ) `
284+ ` Too many placeables expanded: ${ scope . placeables } , ` +
285+ `max allowed is ${ MAX_PLACEABLES } `
277286 ) ;
278287 }
279288
280- result . push ( part ) ;
289+ if ( useIsolating ) {
290+ result . push ( FSI ) ;
291+ }
292+
293+ result . push ( resolveExpression ( scope , elem ) . toString ( scope ) ) ;
281294
282295 if ( useIsolating ) {
283296 result . push ( PDI ) ;
0 commit comments