• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / storage / 30105224435

24 Jul 2026 03:25PM UTC coverage: 59.634% (-20.8%) from 80.45%
30105224435

Pull #1261

github

web-flow
Merge 56e9321c4 into 42bb7886c
Pull Request #1261: chore(deps): bump @opentelemetry/instrumentation-pg from 0.64.0 to 0.72.0

3705 of 7077 branches covered (52.35%)

Branch coverage included in aggregate %.

7681 of 12016 relevant lines covered (63.92%)

370.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.12
/src/http/plugins/xml.ts
1
import accepts from '@fastify/accepts'
2
import { ERRORS } from '@internal/errors'
3
import Builder from 'fast-xml-builder'
4
import { XMLParser } from 'fast-xml-parser'
5
import type { FastifyInstance, FastifyRequest } from 'fastify'
6
import fastifyPlugin from 'fastify-plugin'
7

8
type XmlParserOptions = {
9
  disableContentParser?: boolean
10
  parseAsArray?: string[]
11
  responseNamespace?: string
12
}
13

14
const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
29✔
15
const XML_NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*$/
29✔
16
const XML_ENTITY_REFERENCE = /&([^;&]*);|&/g
29✔
17
const XML_NUMERIC_ENTITY = /^#(?:x([\dA-F]+)|(\d+))$/i
29✔
18
const XML_ENTITIES: Record<string, string> = {
29✔
19
  amp: '&',
20
  apos: "'",
21
  gt: '>',
22
  lt: '<',
23
  quot: '"',
24
}
25
const XML_CHARACTER_RANGE = String.raw`\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}`
29✔
26
// biome-ignore lint/suspicious/noControlCharactersInRegex: XML 1.0 excludes these ranges.
27
const INVALID_XML_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/
29✔
28
const XML_ESCAPED_CHARACTER = new RegExp(`[&<>\\t\\n\\r]|[^${XML_CHARACTER_RANGE}]`, 'gu')
29✔
29
const HAS_XML_ESCAPED_CHARACTER = new RegExp(XML_ESCAPED_CHARACTER.source, 'u')
29✔
30
const XML_ESCAPES: Record<string, string> = {
29✔
31
  '&': '&amp;',
32
  '<': '&lt;',
33
  '>': '&gt;',
34
  '\t': '&#x9;',
35
  '\n': '&#xA;',
36
  '\r': '&#xD;',
37
}
38

39
const escapeXmlValue = (value: unknown, attribute: boolean) => {
29✔
40
  const string = typeof value === 'string' ? value : String(value)
1,133✔
41
  if (!HAS_XML_ESCAPED_CHARACTER.test(string)) return string
1,133✔
42

43
  return string.replace(XML_ESCAPED_CHARACTER, (character) =>
12✔
44
    !attribute && (character === '\t' || character === '\n')
2,201!
45
      ? character
46
      : (XML_ESCAPES[character] ?? '\uFFFD')
2,201!
47
  )
48
}
49
const serializeXmlText = (_tagName: string, value: unknown) =>
29✔
50
  escapeXmlValue(value instanceof Date ? value.toISOString() : value, false)
1,133!
51
const serializeXmlAttribute = (_name: string, value: unknown) => escapeXmlValue(value, true)
29✔
52

53
// Escapes a string for use inside a double-quoted XML attribute value.
54
export function escapeXmlAttribute(value: string): string {
55
  return value
3,190✔
56
    .replace(/&/g, '&amp;')
57
    .replace(/"/g, '&quot;')
58
    .replace(/</g, '&lt;')
59
    .replace(/>/g, '&gt;')
60
}
61

62
/**
63
 * Inserts a precomputed ` xmlns="..."` attribute into the root tag of an
64
 * already-built XML document. The scan is quote-aware and stops at the end of
65
 * the opening root tag, so its cost is independent from the response body size.
66
 */
67
export function insertRootNamespace(xmlPayload: string, namespaceAttribute: string): string {
68
  const declarationEnd = xmlPayload.indexOf('?>')
164✔
69
  const rootStart = declarationEnd === -1 ? 0 : declarationEnd + 2
164!
70

71
  let quote = 0
164✔
72
  for (let index = rootStart; index < xmlPayload.length; index++) {
164✔
73
    const character = xmlPayload.charCodeAt(index)
3,212✔
74
    if (quote !== 0) {
3,212!
75
      if (character === quote) {
×
76
        quote = 0
×
77
      }
78
      continue
×
79
    }
80

81
    if (character === 34 || character === 39) {
3,212!
82
      quote = character
×
83
      continue
×
84
    }
85

86
    if (character === 32 && xmlPayload.startsWith('xmlns=', index + 1)) {
3,212!
87
      return xmlPayload
×
88
    }
89

90
    if (character === 62) {
3,212✔
91
      const tagEnd = xmlPayload.charCodeAt(index - 1) === 47 ? index - 1 : index
164!
92
      return xmlPayload.slice(0, tagEnd) + namespaceAttribute + xmlPayload.slice(tagEnd)
164✔
93
    }
94
  }
95

96
  return xmlPayload
×
97
}
98

99
function isValidXmlCodePoint(codePoint: number) {
100
  return (
2✔
101
    codePoint === 0x9 ||
10!
102
    codePoint === 0xa ||
103
    codePoint === 0xd ||
104
    (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
105
    (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
106
    (codePoint >= 0x10000 && codePoint <= 0x10ffff)
107
  )
108
}
109

110
const xmlEntityDecoder = {
29✔
111
  setExternalEntities() {},
112
  addInputEntities() {
113
    throw new Error('DOCTYPE is not supported')
×
114
  },
115
  reset() {},
116
  setXmlVersion() {},
117
  decode(text: string) {
118
    if (text.indexOf('&') === -1) return text
96✔
119

120
    return text.replace(XML_ENTITY_REFERENCE, (_reference, entity: string | undefined) => {
11✔
121
      if (entity === undefined) throw new Error('Unescaped ampersand')
20!
122

123
      if (Object.hasOwn(XML_ENTITIES, entity)) {
20✔
124
        return XML_ENTITIES[entity]
18✔
125
      }
126

127
      const match = XML_NUMERIC_ENTITY.exec(entity)
2✔
128
      const codePoint = match
2!
129
        ? Number.parseInt(match[1] ?? match[2], match[1] === undefined ? 10 : 16)
5✔
130
        : Number.NaN
131

132
      if (!isValidXmlCodePoint(codePoint)) {
20!
133
        const message = entity.startsWith('#')
×
134
          ? 'Invalid numeric character reference'
135
          : 'Undeclared XML entity'
136
        throw new Error(message)
×
137
      }
138

139
      return String.fromCodePoint(codePoint)
2✔
140
    })
141
  },
142
}
143

144
const xmlBuilder = new Builder({
29✔
145
  format: false,
146
  ignoreAttributes: false,
147
  attributeNamePrefix: '',
148
  attributesGroupName: '$',
149
  textNodeName: '_',
150
  suppressEmptyNode: true,
151
  suppressBooleanAttributes: false,
152
  processEntities: false,
153
  tagValueProcessor: serializeXmlText,
154
  attributeValueProcessor: serializeXmlAttribute,
155
})
156

157
const acceptedTypes = ['application/xml', 'text/xml', 'text/html']
29✔
158
const buildXml = (payload: unknown) => XML_DECLARATION + xmlBuilder.build(payload)
164✔
159

160
function acceptsXmlResponse(req: FastifyRequest): boolean {
161
  const accept = req.headers.accept
164✔
162
  if (accept === undefined || accept === '*/*' || acceptedTypes.includes(accept)) {
164!
163
    return true
164✔
164
  }
165

166
  const normalizedAccept = accept.trim().toLowerCase()
×
167
  if (
×
168
    normalizedAccept !== accept &&
×
169
    (normalizedAccept === '*/*' || acceptedTypes.includes(normalizedAccept))
170
  ) {
171
    return true
×
172
  }
173

174
  return req.accepts().types(acceptedTypes) !== false
×
175
}
176

177
function isXmlDocumentPayload(payload: unknown): payload is Record<string, unknown> {
178
  if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) {
185!
179
    return false
×
180
  }
181

182
  const rootKeys = Object.keys(payload)
185✔
183
  if (rootKeys.length !== 1 || !XML_NAME.test(rootKeys[0])) {
185!
184
    return false
×
185
  }
186

187
  const rootValue = (payload as Record<string, unknown>)[rootKeys[0]]
185✔
188
  return rootValue !== undefined && !Array.isArray(rootValue)
185✔
189
}
190

191
export const xmlParser = fastifyPlugin(
29✔
192
  async function (fastify: FastifyInstance, opts: XmlParserOptions) {
193
    fastify.register(accepts)
3,190✔
194

195
    if (!opts.disableContentParser) {
3,190✔
196
      const arrayPaths = new Set(opts.parseAsArray?.filter(Boolean))
2,900✔
197
      const arrayTags = new Set(
2,900✔
198
        [...arrayPaths].map((path) => path.slice(path.lastIndexOf('.') + 1))
580✔
199
      )
200
      const parser = new XMLParser({
2,900✔
201
        ignoreDeclaration: true,
202
        ignoreAttributes: false,
203
        attributeNamePrefix: '',
204
        attributesGroupName: '$',
205
        textNodeName: '_',
206
        parseTagValue: false,
207
        trimValues: false,
208
        ignorePiTags: true,
209
        jPath: false,
210
        entityDecoder: xmlEntityDecoder,
211
        tagValueProcessor: (_tagName, value, _jPath, _hasAttributes, isLeafNode) =>
212
          !isLeafNode && value.trim() === '' ? '' : undefined,
48!
213
        isArray: (tagName, path) => arrayTags.has(tagName) && arrayPaths.has(String(path)),
107✔
214
      })
215

216
      fastify.addContentTypeParser(
2,900✔
217
        ['text/xml', 'application/xml'],
218
        { parseAs: 'string' },
219
        (_request, body, done) => {
220
          if (!body) {
121✔
221
            done(null, null)
99✔
222
            return
99✔
223
          }
224

225
          try {
22✔
226
            const raw = body.toString()
22✔
227
            const xml = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw
22!
228
            if (INVALID_XML_CHARACTER.test(xml) || !xml.isWellFormed()) {
121!
229
              throw new Error('Invalid XML character')
×
230
            }
231
            const payload = parser.parse(xml, true)
22✔
232
            if (!isXmlDocumentPayload(payload)) {
22!
233
              throw new Error('XML payload must contain a single root element')
×
234
            }
235
            done(null, payload)
21✔
236
          } catch (error) {
237
            const message = error instanceof Error ? error.message : String(error)
1!
238
            done(
1✔
239
              Object.assign(ERRORS.InvalidRequest(`Invalid XML payload: ${message}`), {
240
                statusCode: 400,
241
              })
242
            )
243
          }
244
        }
245
      )
246
    }
247

248
    const namespaceAttribute = opts.responseNamespace
3,190!
249
      ? ` xmlns="${escapeXmlAttribute(opts.responseNamespace)}"`
250
      : undefined
251
    const serializeXml = namespaceAttribute
3,190!
252
      ? (payload: unknown) => insertRootNamespace(buildXml(payload), namespaceAttribute)
164✔
253
      : buildXml
254

255
    fastify.addHook('preSerialization', (req, res, payload, done) => {
3,190✔
256
      if (acceptsXmlResponse(req)) {
164!
257
        if (!isXmlDocumentPayload(payload)) {
164!
258
          done(
×
259
            ERRORS.InternalError(
260
              undefined,
261
              'XML response payload must be an object with a single root element'
262
            )
263
          )
264
          return
×
265
        }
266

267
        res.header('content-type', 'application/xml; charset=utf-8')
164✔
268
        res.serializer(serializeXml)
164✔
269
      }
270

271
      done(null, payload)
164✔
272
    })
273
  },
274
  { name: 'xml-parser' }
275
)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc