mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
fix(date): cap strftime widths and account padding in memoryLimit
- Clamp numeric strftime pad widths to MAX_STRFTIME_PAD (1024) - Export estimateStrftimePaddingMemory for the date filter to charge memoryLimit - Replace unbounded pad() concatenation loop with ch.repeat + single concat - Add regression tests for clamping and memoryLimit on huge %width directives Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+2
-1
@@ -1,4 +1,4 @@
|
||||
import { toValue, stringify, isString, isNumber, LiquidDate, strftime, isNil } from '../util'
|
||||
import { toValue, stringify, isString, isNumber, LiquidDate, strftime, estimateStrftimePaddingMemory, isNil } from '../util'
|
||||
import { FilterImpl } from '../template'
|
||||
import { NormalizedFullOptions } from '../liquid-options'
|
||||
|
||||
@@ -9,6 +9,7 @@ export function date (this: FilterImpl, v: string | Date, format?: string, timez
|
||||
if (!date) return v
|
||||
format = toValue(format)
|
||||
format = isNil(format) ? this.context.opts.dateFormat : stringify(format)
|
||||
this.context.memoryLimit.use(estimateStrftimePaddingMemory(format))
|
||||
return strftime(date, format)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ describe('util/strftime', function () {
|
||||
expect(t(date, '%j')).toBe('061')
|
||||
})
|
||||
})
|
||||
it('should cap excessive numeric pad width for day-of-month', function () {
|
||||
expect(t(now, '%5d')).toBe('00004')
|
||||
expect(t(now, '%2000d').length).toBe(1024)
|
||||
})
|
||||
it('should format %q as date suffix', function () {
|
||||
const first = new TestDate('2016-03-01 03:05:03')
|
||||
const second = new TestDate('2016-03-02 03:05:03')
|
||||
@@ -87,6 +91,11 @@ describe('util/strftime', function () {
|
||||
expect(t(time, '%10N')).toBe('1290000000')
|
||||
expect(t(time, '%0N')).toBe('129000000')
|
||||
})
|
||||
it('should cap excessive numeric pad width for %N', function () {
|
||||
const time = new TestDate('2019-12-15 01:21:00.129')
|
||||
expect(t(time, '%2000N').length).toBe(1024)
|
||||
expect(t(time, '%50000N').length).toBe(1024)
|
||||
})
|
||||
it('should format %p as upper cased am/pm', function () {
|
||||
expect(t(now, '%p')).toBe('PM')
|
||||
expect(t(then, '%p')).toBe('AM')
|
||||
|
||||
+28
-2
@@ -1,6 +1,9 @@
|
||||
import { changeCase, padStart, padEnd } from './underscore'
|
||||
import { LiquidDate } from './liquid-date'
|
||||
|
||||
/** Upper bound for numeric strftime widths (%N, %15d, …) — avoids unbounded pad / CPU / memory. */
|
||||
export const MAX_STRFTIME_PAD = 1024
|
||||
|
||||
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
||||
interface FormatOptions {
|
||||
flags: object;
|
||||
@@ -93,7 +96,9 @@ const formatCodes = {
|
||||
m: (d: LiquidDate) => d.getMonth() + 1,
|
||||
M: (d: LiquidDate) => d.getMinutes(),
|
||||
N: (d: LiquidDate, opts: FormatOptions) => {
|
||||
const width = Number(opts.width) || 9
|
||||
let width = Number(opts.width) || 9
|
||||
if (!Number.isFinite(width) || width < 0) width = 9
|
||||
if (width > MAX_STRFTIME_PAD) width = MAX_STRFTIME_PAD
|
||||
const str = String(d.getMilliseconds()).slice(0, width)
|
||||
return padEnd(str, width, '0')
|
||||
},
|
||||
@@ -130,6 +135,25 @@ export function strftime (d: LiquidDate, formatStr: string) {
|
||||
return output + remaining
|
||||
}
|
||||
|
||||
/** Sum of clamped numeric widths in a strftime format string (for memoryLimit accounting). */
|
||||
export function estimateStrftimePaddingMemory (formatStr: string): number {
|
||||
if (!formatStr) return 0
|
||||
let sum = 0
|
||||
const re = /%([-_0^#:]+)?(\d+)?([EO])?(.)/g
|
||||
let m
|
||||
while ((m = re.exec(formatStr)) !== null) {
|
||||
const flagStr = m[1] || ''
|
||||
const width = m[2]
|
||||
const conversion = m[4]
|
||||
const flags: Record<string, boolean> = {}
|
||||
for (const flag of flagStr) flags[flag] = true
|
||||
if (flags['-']) continue
|
||||
if (width) sum += Math.min(Number(width), MAX_STRFTIME_PAD)
|
||||
else if (conversion === 'N') sum += Math.min(9, MAX_STRFTIME_PAD)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
function format (d: LiquidDate, match: RegExpExecArray) {
|
||||
const [input, flagStr = '', width, modifier, conversion] = match
|
||||
const convert = formatCodes[conversion]
|
||||
@@ -138,11 +162,13 @@ function format (d: LiquidDate, match: RegExpExecArray) {
|
||||
for (const flag of flagStr) flags[flag] = true
|
||||
let ret = String(convert(d, { flags, width, modifier }))
|
||||
let padChar = padSpaceChars.has(conversion) ? ' ' : '0'
|
||||
let padWidth = width || padWidths[conversion] || 0
|
||||
let padWidth = width ? Number(width) : (padWidths[conversion as keyof typeof padWidths] || 0)
|
||||
if (!Number.isFinite(padWidth) || padWidth < 0) padWidth = 0
|
||||
if (flags['^']) ret = ret.toUpperCase()
|
||||
else if (flags['#']) ret = changeCase(ret)
|
||||
if (flags['_']) padChar = ' '
|
||||
else if (flags['0']) padChar = '0'
|
||||
if (flags['-']) padWidth = 0
|
||||
else if (padWidth > MAX_STRFTIME_PAD) padWidth = MAX_STRFTIME_PAD
|
||||
return padStart(ret, padWidth, padChar)
|
||||
}
|
||||
|
||||
@@ -152,9 +152,11 @@ export function padEnd (str: any, length: number, ch = ' ') {
|
||||
|
||||
export function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) {
|
||||
str = String(str)
|
||||
let n = length - str.length
|
||||
while (n-- > 0) str = add(str, ch)
|
||||
return str
|
||||
const n = length - str.length
|
||||
if (n <= 0) return str
|
||||
const padChunk = ch.repeat(n)
|
||||
const probe = add('probe', ch)
|
||||
return probe[0] === ch ? padChunk + str : str + padChunk
|
||||
}
|
||||
|
||||
export function identify<T> (val: T): T {
|
||||
|
||||
@@ -204,6 +204,18 @@ describe('filters/date', function () {
|
||||
return test('{{ "1990-12-31T23:00:00Z" | date: "%Y-%m-%dT%H:%M:%S" }}', '1991-01-01T04:30:00', undefined, optsWithDateFormat)
|
||||
})
|
||||
})
|
||||
describe('strftime width / memoryLimit', () => {
|
||||
it('should charge memoryLimit for huge numeric strftime widths', () => {
|
||||
const liquid = new Liquid({ memoryLimit: 500 })
|
||||
expect(() => liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%5000000d' }))
|
||||
.toThrow('memory alloc limit exceeded')
|
||||
})
|
||||
it('should clamp numeric strftime pad width', () => {
|
||||
const liquid = new Liquid({ memoryLimit: 1e7 })
|
||||
const out = liquid.parseAndRenderSync('{{ d | date: f }}', { d: 'now', f: '%50000d' })
|
||||
expect(out.length).toBe(1024)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('filters/date_to_xmlschema', function () {
|
||||
const liquid = new Liquid()
|
||||
|
||||
Reference in New Issue
Block a user