mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-26 13:45:15 -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 { FilterImpl } from '../template'
|
||||||
import { NormalizedFullOptions } from '../liquid-options'
|
import { NormalizedFullOptions } from '../liquid-options'
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ export function date (this: FilterImpl, v: string | Date, format?: string, timez
|
|||||||
if (!date) return v
|
if (!date) return v
|
||||||
format = toValue(format)
|
format = toValue(format)
|
||||||
format = isNil(format) ? this.context.opts.dateFormat : stringify(format)
|
format = isNil(format) ? this.context.opts.dateFormat : stringify(format)
|
||||||
|
this.context.memoryLimit.use(estimateStrftimePaddingMemory(format))
|
||||||
return strftime(date, format)
|
return strftime(date, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ describe('util/strftime', function () {
|
|||||||
expect(t(date, '%j')).toBe('061')
|
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 () {
|
it('should format %q as date suffix', function () {
|
||||||
const first = new TestDate('2016-03-01 03:05:03')
|
const first = new TestDate('2016-03-01 03:05:03')
|
||||||
const second = new TestDate('2016-03-02 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, '%10N')).toBe('1290000000')
|
||||||
expect(t(time, '%0N')).toBe('129000000')
|
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 () {
|
it('should format %p as upper cased am/pm', function () {
|
||||||
expect(t(now, '%p')).toBe('PM')
|
expect(t(now, '%p')).toBe('PM')
|
||||||
expect(t(then, '%p')).toBe('AM')
|
expect(t(then, '%p')).toBe('AM')
|
||||||
|
|||||||
+28
-2
@@ -1,6 +1,9 @@
|
|||||||
import { changeCase, padStart, padEnd } from './underscore'
|
import { changeCase, padStart, padEnd } from './underscore'
|
||||||
import { LiquidDate } from './liquid-date'
|
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])?(.)/
|
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
||||||
interface FormatOptions {
|
interface FormatOptions {
|
||||||
flags: object;
|
flags: object;
|
||||||
@@ -93,7 +96,9 @@ const formatCodes = {
|
|||||||
m: (d: LiquidDate) => d.getMonth() + 1,
|
m: (d: LiquidDate) => d.getMonth() + 1,
|
||||||
M: (d: LiquidDate) => d.getMinutes(),
|
M: (d: LiquidDate) => d.getMinutes(),
|
||||||
N: (d: LiquidDate, opts: FormatOptions) => {
|
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)
|
const str = String(d.getMilliseconds()).slice(0, width)
|
||||||
return padEnd(str, width, '0')
|
return padEnd(str, width, '0')
|
||||||
},
|
},
|
||||||
@@ -130,6 +135,25 @@ export function strftime (d: LiquidDate, formatStr: string) {
|
|||||||
return output + remaining
|
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) {
|
function format (d: LiquidDate, match: RegExpExecArray) {
|
||||||
const [input, flagStr = '', width, modifier, conversion] = match
|
const [input, flagStr = '', width, modifier, conversion] = match
|
||||||
const convert = formatCodes[conversion]
|
const convert = formatCodes[conversion]
|
||||||
@@ -138,11 +162,13 @@ function format (d: LiquidDate, match: RegExpExecArray) {
|
|||||||
for (const flag of flagStr) flags[flag] = true
|
for (const flag of flagStr) flags[flag] = true
|
||||||
let ret = String(convert(d, { flags, width, modifier }))
|
let ret = String(convert(d, { flags, width, modifier }))
|
||||||
let padChar = padSpaceChars.has(conversion) ? ' ' : '0'
|
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()
|
if (flags['^']) ret = ret.toUpperCase()
|
||||||
else if (flags['#']) ret = changeCase(ret)
|
else if (flags['#']) ret = changeCase(ret)
|
||||||
if (flags['_']) padChar = ' '
|
if (flags['_']) padChar = ' '
|
||||||
else if (flags['0']) padChar = '0'
|
else if (flags['0']) padChar = '0'
|
||||||
if (flags['-']) padWidth = 0
|
if (flags['-']) padWidth = 0
|
||||||
|
else if (padWidth > MAX_STRFTIME_PAD) padWidth = MAX_STRFTIME_PAD
|
||||||
return padStart(ret, padWidth, padChar)
|
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) {
|
export function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) {
|
||||||
str = String(str)
|
str = String(str)
|
||||||
let n = length - str.length
|
const n = length - str.length
|
||||||
while (n-- > 0) str = add(str, ch)
|
if (n <= 0) return str
|
||||||
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 {
|
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)
|
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 () {
|
describe('filters/date_to_xmlschema', function () {
|
||||||
const liquid = new Liquid()
|
const liquid = new Liquid()
|
||||||
|
|||||||
Reference in New Issue
Block a user