structured-field-values
Version:
Implementation of Structured Field Values from IETF httpbis RFC8941
554 lines (496 loc) • 12.1 kB
JavaScript
const ok = true
import { base64decode, base64encode } from "../index.js"
/// parser
export function parseItem(value) {
// trim leading/trailing space
// https://www.rfc-editor.org/rfc/rfc8941.html#section-4.2
const result = sf_item()(value.trim())
if (result.ok === false) {
throw new Error(`failed to parse`)
}
if (result.rest.length > 0) {
throw new Error(`failed to parse: trailing values ${result.rest}`)
}
return result.value
}
export function parseList(value) {
// return if empty
// https://www.rfc-editor.org/rfc/rfc8941.html#section-4.2.1
if (value === ``) return []
const result = sf_list()(value.trim())
if (result.ok === false) {
throw new Error(`failed to parse`)
}
if (result.rest.length > 0) {
throw new Error(`failed to parse: trailing values ${result.rest}`)
}
return result.value
}
export function parseDict(value) {
// return if empty
// https://www.rfc-editor.org/rfc/rfc8941.html#section-4.2.1
if (value === ``) return {}
const result = sf_dictionary()(value.trim())
if (result.ok === false) {
throw new Error(`failed to parse`)
}
if (result.rest.length > 0) {
throw new Error(`failed to parse: trailing values ${result.rest}`)
}
return result.value
}
/////////////////////////
// BFN utility
/////////////////////////
// a => token(/^a/)
export function token(reg) {
return (rest) => {
const result = reg.exec(rest)
if (result === null) {
return { ok: false, rest }
} else {
const value = result[0]
return { ok, value, rest: rest.substr(value.length) }
}
}
}
// (a / b) => alt([a(), b()])
export function alt(fns) {
return (rest) => {
for (let i = 0; i < fns.length; i++) {
const result = fns[i](rest)
if (result.ok) {
return result
}
}
return { ok: false, rest }
}
}
// (a b c) => list([a(), b(), c()])
export function list(fns) {
return (rest) => {
const value = []
const orig = rest
for (let i = 0; i < fns.length; i++) {
const result = fns[i](rest)
if (result.ok === false) {
return { ok: false, rest: orig }
}
value.push(result.value)
rest = result.rest
}
return { ok, value, rest }
}
}
// *(a b) => repeat(0, Infinity, list([a(), b()]))
export function repeat(min, max, fn) {
return (rest) => {
const value = []
const orig = rest
while (true) {
const result = fn(rest)
if (result.ok) {
value.push(result.value)
rest = result.rest
if (value.length === max) break
} else {
break
}
}
if (value.length < min) {
return { ok: false, rest: orig }
} else {
return { ok, value, rest }
}
}
}
// [ a b ] => optional(list[a(), b()])
export function optional(fn) {
return repeat(0, 1, fn)
}
/////////////////////////
// BNF in spec
/////////////////////////
// sf-integer
// = ["-"] 1*15DIGIT
export function sf_integer() {
return (rest) => {
const result = token(/^\-{0,1}\d{1,15}/)(rest)
if (result.ok) {
return { ok, value: parseInt(result.value), rest: result.rest }
}
return { ok: false, rest }
}
}
// sf-decimal
// = ["-"] 1*12DIGIT "." 1*3DIGIT
export function sf_decimal() {
return (rest) => {
const result = token(/^\-{0,1}\d{1,12}\.\d{1,3}/)(rest)
if (result.ok) {
return { ok, value: parseFloat(result.value), rest: result.rest }
}
return { ok: false, rest }
}
}
// sf-string
// = DQUOTE *chr DQUOTE
export function sf_string() {
return function (rest) {
const fn = list([
token(/^"/), // DQUOTE
repeat(0, 1024, char()), // *char
token(/^"/) // DQUOTE
])
const result = fn(rest)
if (result.ok) {
// ['"', ['a', 'b', 'c'], '"'] => "abc"
result.value = result.value[1].join(``)
return result
} else {
return result
}
}
}
// chr
// = unescaped
// / escaped
export function char() {
return alt([escaped(), unescaped()])
}
// unescaped
// = %x20-21 (x22 is ")
// / %x23-5B (x5c is \)
// / %x5D-7E
export function unescaped() {
return token(/^([\x20-\x21\x23-\x5B\x5D-\x7E])/)
}
// escaped
// = "\" ( DQUOTE / "\" )
export function escaped() {
return (rest) => {
const result = token(/^((\\\")|(\\\\))/)(rest)
if (result.ok) {
// unescape (\\" => ", \\\\ => \\)
result.value = result.value === `\\"` ? `"` : `\\`
}
return result
}
}
// sf-token
// = ( ALPHA / "*" ) *( tchar / ":" / "/" )
export function sf_token() {
return (rest) => {
const result = token(/^([a-zA-Z\*])([\!\#\$\%\&\'\*\+\-\.\^\_\`\|\~\w\:\/]){0,512}/)(rest)
if (result.ok) {
result.value = Symbol.for(result.value)
}
return result
}
}
// sf-binary
// = ":" *(base64) ":"
// base64
// = ALPHA
// / DIGIT
// / "+"
// / "/"
// / "="
export function sf_binary() {
return (rest) => {
const result = list([
token(/^:/),
token(/^([\w+/=]){0,16384}/), // base64
token(/^:/)
])(rest)
if (result.ok) {
// [":", "base64str", ":"] => Uint8Array
result.value = base64decode(result.value[1]) // remove ":"
}
return result
}
}
// sf-boolean
// = "?" boolean
// boolean
// = "0"
// / "1"
export function sf_boolean() {
return (rest) => {
const result = token(/^((\?0)|(\?1))/)(rest)
if (result.ok) {
// ?1 => true, ?0 => false
result.value = result.value === `?1`
}
return result
}
}
// sf-date
// = "@" ["-"] 1*15DIGIT
export function sf_date() {
return (rest) => {
const result = list([token(/^@/), token(/\-{0,1}\d{1,15}/)])(rest)
if (result.ok) {
return { ok, value: new Date(result.value[1] * 1000), rest: result.rest }
}
return { ok: false, rest }
}
}
// sf-list
// = list-member *( OWS "," OWS list-member )
export function sf_list() {
return (rest) => {
const result = list([
list_member(),
_repeat_list_member() // *( OWS "," OWS list-member )
])(rest)
if (result.ok) {
// [ [1,[]], [ [2,[]], [3,[]] .. ] ]
result.value = [result.value[0], ...result.value[1]]
}
return result
}
}
// [repeat of list member]
// = *( OWS "," OWS list-member )
export function _repeat_list_member() {
function fn() {
return (rest) => {
const result = list([
token(/^([ \t]*),([ \t]*)/), // OWS "," OWS
list_member()
])(rest)
if (result.ok) {
// [ ',', [a, []] => [a, []]
result.value = result.value[1]
}
return result
}
}
return repeat(0, 1024, fn())
}
// list-member
// = sf-item
// / inner-list
export function list_member() {
return alt([sf_item(), inner_list()])
}
// inner-list
// = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")" parameters
export function inner_list() {
return (rest) => {
const result = list([
token(/^\( */), // "(" *SP
_optional_inner_item(), // [ sf-item *( 1*SP sf-item ) *SP ]
token(/^\)/), // ")"
parameters()
])(rest)
if (result.ok) {
// [ "(", repeat, ")", param ] => [repeat, param]
const [_open, inner_list, _close, params] = result.value
result.value = { value: inner_list, params }
}
return result
}
}
// [ sf-item *( 1*SP sf-item ) *SP ]
export function _optional_inner_item() {
return (rest) => {
const result = optional(
list([
sf_item(),
_repeat_inner_item(), // *( 1*SP sf-item )
token(/^ */)
])
)(rest)
if (result.ok) {
if (result.value.length > 0) {
// [[sf_item, repeat, space]] => [sf_item, ...repeat]
result.value = [result.value[0][0], ...result.value[0][1]]
}
}
return result
}
}
export function _repeat_inner_item() {
function fn() {
return (rest) => {
const result = list([
token(/^ +/), // 1*SP
sf_item()
])(rest)
if (result.ok) {
// [token, sf_item] => sf_item
result.value = result.value[1]
}
return result
}
}
return repeat(0, 256, fn())
}
// sf-dictionary
// = dict-member *( OWS "," OWS dict-member )
export function sf_dictionary() {
return (rest) => {
const result = list([
dict_member(),
_repeat_dict_member() // *( OWS "," OWS dict-member )
])(rest)
if (result.ok) {
// [dict_member, repeated]
const [[key, [value]], rest] = result.value
result.value = Object.fromEntries([[key, value], ...rest])
}
return result
}
}
// repeat of dict member
export function _repeat_dict_member() {
function fn() {
return (rest) => {
const result = list([
token(/^([ \t]*),([ \t]*)/), // OWS "," OWS
dict_member()
])(rest)
if (result.ok) {
// [',', [ "a", [ [1,[]] ]]] => ["a", [1,[]]
const [_comma, [key, [param]]] = result.value
result.value = [key, param]
}
return result
}
}
return repeat(0, 1024, fn())
}
// dict-member
// = member-name [ "=" member-value ]
// = member-name [ ( "=" member-value ) / parameters ] TODO: https://github.com/httpwg/http-extensions/issues/1273
// member-name
// = key
export function dict_member() {
return list([
sf_key(), // key
optional(_optional_member_value())
])
}
export function _optional_member_value() {
return (rest) => {
const result = alt([
list([
token(/^=/), // "="
member_value()
]),
parameters()
])(rest)
if (result.ok) {
if (result.value[0] === `=`) {
// ['=', [member]] => [1, []]
result.value = result.value[1]
} else {
// value should be true if member is omitted
result.value = { value: true, params: result.value }
}
}
return result
}
}
// member-value
// = sf-item
// / inner-list
export function member_value() {
return alt([sf_item(), inner_list()])
}
// sf-item
// = bare-item parameters
export function sf_item() {
return (rest) => {
const result = list([bare_item(), parameters()])(rest)
if (result.ok) {
const [value, params] = result.value
result.value = { value, params }
}
return result
}
}
// bare-item
// = sf-integer
// / sf-decimal
// / sf-string
// / sf-token
// / sf-binary
// / sf-boolean
// / sf-date
export function bare_item() {
return alt([
sf_decimal(), // decimal first
sf_integer(),
sf_string(),
sf_token(),
sf_binary(),
sf_boolean(),
sf_date()
])
}
// parameters
// = *( ";" *SP parameter )
export function parameters() {
return (rest) => {
const result = repeat(0, 256, _inner_parameters())(rest)
if (result.ok) {
result.value = Object.fromEntries(result.value)
}
return result
}
}
export function _inner_parameters() {
return (rest) => {
const result = list([
token(/^; */), // ";"
parameter()
])(rest)
if (result.ok) {
// [';' , parameter] => parameter
result.value = result.value[1]
}
return result
}
}
// parameter
// = param-name [ "=" param-value ]
// param-name
// = key
// param-value
// = bare-item
export function parameter() {
return list([sf_key(), param_value()])
}
// [ "=" param-value ]
export function param_value() {
return (rest) => {
const result = optional(
list([
token(/^=/), // "="
bare_item()
])
)(rest)
if (result.ok) {
if (result.value.length === 0) {
// no parameter is true
// [] => true
result.value = true
} else {
// [['=', value]] => value
result.value = result.value[0][1]
}
}
return result
}
}
// key
// = ( lcalpha / "*" )
// *( lcalpha / DIGIT / "_" / "-" / "." / "*" )
// lcalpha
// = %x61-7A ; a-z
export function sf_key() {
return token(/^([a-z\*])([a-z0-9\_\-\.\*]){0,64}/)
}