Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
"@smithy/util-body-length-browser": "^4.1.0",
"@smithy/util-middleware": "^4.1.1",
"@smithy/util-utf8": "^4.1.0",
"fast-xml-parser": "5.2.5",
"tslib": "^2.6.2"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { parseXML } from "@aws-sdk/xml-builder";
import { FromStringShapeDeserializer } from "@smithy/core/protocols";
import { NormalizedSchema } from "@smithy/core/schema";
import { getValueFromTextNode } from "@smithy/smithy-client";
import type { Schema, SerdeFunctions, ShapeDeserializer } from "@smithy/types";
import { toUtf8 } from "@smithy/util-utf8";
import { XMLParser } from "fast-xml-parser";

import { SerdeContextConfig } from "../ConfigurableSerdeContext";
import type { XmlSettings } from "./XmlCodec";
Expand Down Expand Up @@ -146,21 +146,9 @@ export class XmlShapeDeserializer extends SerdeContextConfig implements ShapeDes

protected parseXml(xml: string): any {
if (xml.length) {
const parser = new XMLParser({
attributeNamePrefix: "",
htmlEntities: true,
ignoreAttributes: false,
ignoreDeclaration: true,
parseTagValue: false,
trimValues: false,
tagValueProcessor: (_: any, val: any) => (val.trim() === "" && val.includes("\n") ? "" : undefined),
});
parser.addEntity("#xD", "\r");
parser.addEntity("#10", "\n");

let parsedObj;
try {
parsedObj = parser.parse(xml, true);
parsedObj = parseXML(xml);
} catch (e: any) {
if (e && typeof e === "object") {
Object.defineProperty(e, "$responseBodyText", {
Expand Down
16 changes: 2 additions & 14 deletions packages/core/src/submodules/protocols/xml/parseXmlBody.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parseXML } from "@aws-sdk/xml-builder";
import { getValueFromTextNode } from "@smithy/smithy-client";
import type { HttpResponse, SerdeContext } from "@smithy/types";
import { XMLParser } from "fast-xml-parser";

import { collectBodyString } from "../common";

Expand All @@ -10,21 +10,9 @@ import { collectBodyString } from "../common";
export const parseXmlBody = (streamBody: any, context: SerdeContext): any =>
collectBodyString(streamBody, context).then((encoded) => {
if (encoded.length) {
const parser = new XMLParser({
attributeNamePrefix: "",
htmlEntities: true,
ignoreAttributes: false,
ignoreDeclaration: true,
parseTagValue: false,
trimValues: false,
tagValueProcessor: (_: any, val: any) => (val.trim() === "" && val.includes("\n") ? "" : undefined),
});
parser.addEntity("#xD", "\r");
parser.addEntity("#10", "\n");

let parsedObj;
try {
parsedObj = parser.parse(encoded, true);
parsedObj = parseXML(encoded);
} catch (e: any) {
if (e && typeof e === "object") {
Object.defineProperty(e, "$responseBodyText", {
Expand Down
8 changes: 8 additions & 0 deletions packages/xml-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "XML builder for the AWS SDK",
"dependencies": {
"@smithy/types": "^4.5.0",
"fast-xml-parser": "5.2.5",
"tslib": "^2.6.2"
},
"scripts": {
Expand Down Expand Up @@ -38,6 +39,13 @@
"files": [
"dist-*/**"
],
"browser": {
"./dist-es/xml-parser": "./dist-es/xml-parser.browser"
},
"react-native": {
"./dist-es/xml-parser": "./dist-es/xml-parser",
"./dist-cjs/xml-parser": "./dist-cjs/xml-parser"
},
"homepage": "https://github.com/aws/aws-sdk-js-v3/tree/main/packages/xml-builder",
"repository": {
"type": "git",
Expand Down
5 changes: 5 additions & 0 deletions packages/xml-builder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export * from "./XmlNode";
* @internal
*/
export * from "./XmlText";

/**
* @internal
*/
export { parseXML } from "./xml-parser";
56 changes: 56 additions & 0 deletions packages/xml-builder/src/xml-parser.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const parser = new DOMParser();

export function parseXML(xmlString: string): any {
const xmlDocument = parser.parseFromString(xmlString, "application/xml");

// Recursive function to convert XML nodes to JS object
const xmlToObj = (node: Node): any => {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent?.trim()) {
return node.textContent;
}
}

if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
if (element.attributes.length === 0 && element.childNodes.length === 0) {
return "";
}

const obj: any = {};

for (const attr of Array.from(element.attributes)) {
obj[`${attr.name}`] = attr.value;
}

for (const child of Array.from(element.childNodes)) {
const childResult = xmlToObj(child);

if (childResult != null) {
const childName = child.nodeName;
if (childName === "#text") {
return childResult;
}

if (obj[childName]) {
if (Array.isArray(obj[childName])) {
obj[childName].push(childResult);
} else {
obj[childName] = [obj[childName], childResult];
}
} else {
obj[childName] = childResult;
}
}
}

return obj;
}

return null;
};

return {
[xmlDocument.documentElement.nodeName]: xmlToObj(xmlDocument.documentElement),
};
}
120 changes: 120 additions & 0 deletions packages/xml-builder/src/xml-parser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, expect, test as it } from "vitest";

import { parseXML } from "./xml-parser";
import { parseXML as parseXMLBrowser } from "./xml-parser.browser";

describe("xml parsing", () => {
for (const { name, parse } of [
{ name: "fast-xml-parser", parse: parseXML },
{ name: "DOMParser", parse: parseXMLBrowser },
]) {
describe(name, () => {
it("should parse a valid xml string without xml header", () => {
const xml = `<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<Credentials>
<AccessKeyId>STS_AR_ACCESS_KEY_ID</AccessKeyId>
<SecretAccessKey>STS_AR_SECRET_ACCESS_KEY</SecretAccessKey>
<SessionToken>STS_AR_SESSION_TOKEN_us-west-2</SessionToken>
<Expiration>3000-01-01T00:00:00.000Z</Expiration>
</Credentials>
</AssumeRoleResult>
<ResponseMetadata>
<RequestId>01234567-89ab-cdef-0123-456789abcdef</RequestId>
</ResponseMetadata>
</AssumeRoleResponse>`;
const object = parse(xml);
expect(object).toEqual({
AssumeRoleResponse: {
AssumeRoleResult: {
Credentials: {
AccessKeyId: "STS_AR_ACCESS_KEY_ID",
Expiration: "3000-01-01T00:00:00.000Z",
SecretAccessKey: "STS_AR_SECRET_ACCESS_KEY",
SessionToken: "STS_AR_SESSION_TOKEN_us-west-2",
},
},
ResponseMetadata: {
RequestId: "01234567-89ab-cdef-0123-456789abcdef",
},
xmlns: "https://sts.amazonaws.com/doc/2011-06-15/",
},
});
});

it("should parse ListBuckets response XML with xml header", () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult>
<Buckets>
<Bucket>
<BucketArn>string</BucketArn>
<BucketRegion>string</BucketRegion>
<CreationDate>timestamp</CreationDate>
<Name>string</Name>
</Bucket>
</Buckets>
<Owner>
<DisplayName>string</DisplayName>
<ID>string</ID>
</Owner>
<ContinuationToken>string</ContinuationToken>
<Prefix>string</Prefix>
</ListAllMyBucketsResult>`;
const object = parse(xml);
expect(object).toEqual({
ListAllMyBucketsResult: {
Buckets: {
Bucket: {
BucketArn: "string",
BucketRegion: "string",
CreationDate: "timestamp",
Name: "string",
},
},
ContinuationToken: "string",
Owner: {
DisplayName: "string",
ID: "string",
},
Prefix: "string",
},
});
});

it("should parse xml (custom)", () => {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<struct>
<empty></empty>
<text>abcdefg</text>
<duplicate>dup1</duplicate>
<duplicate>dup2</duplicate>
<duplicate>dup3</duplicate>
<spaced> s p a c e d </spaced>
<nested>
<empty></empty>
<text>abcdefg</text>
<duplicate>dup1</duplicate>
<duplicate>dup2</duplicate>
<duplicate>dup3</duplicate>
<spaced> s p a c e d </spaced>
</nested>
</struct>`;
const object = parse(xml);
expect(object).toEqual({
struct: {
empty: "",
text: "abcdefg",
duplicate: ["dup1", "dup2", "dup3"],
spaced: " s p a c e d ",
nested: {
empty: "",
text: "abcdefg",
duplicate: ["dup1", "dup2", "dup3"],
spaced: " s p a c e d ",
},
},
});
});
});
}
});
17 changes: 17 additions & 0 deletions packages/xml-builder/src/xml-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { XMLParser } from "fast-xml-parser";

const parser = new XMLParser({
attributeNamePrefix: "",
htmlEntities: true,
ignoreAttributes: false,
ignoreDeclaration: true,
parseTagValue: false,
trimValues: false,
tagValueProcessor: (_: any, val: any) => (val.trim() === "" && val.includes("\n") ? "" : undefined),
});
parser.addEntity("#xD", "\r");
parser.addEntity("#10", "\n");

export function parseXML(xmlString: string): any {
return parser.parse(xmlString, true);
}
2 changes: 1 addition & 1 deletion packages/xml-builder/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export default defineConfig({
test: {
exclude: ["**/*.{integ,e2e,browser}.spec.ts"],
include: ["**/*.spec.ts"],
environment: "node",
environment: "happy-dom",
},
});
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23534,7 +23534,6 @@ __metadata:
"@tsconfig/recommended": "npm:1.0.1"
concurrently: "npm:7.0.0"
downlevel-dts: "npm:0.10.1"
fast-xml-parser: "npm:5.2.5"
rimraf: "npm:3.0.2"
tslib: "npm:^2.6.2"
typescript: "npm:~5.8.3"
Expand Down Expand Up @@ -25005,6 +25004,7 @@ __metadata:
"@tsconfig/recommended": "npm:1.0.1"
concurrently: "npm:7.0.0"
downlevel-dts: "npm:0.10.1"
fast-xml-parser: "npm:5.2.5"
rimraf: "npm:3.0.2"
tslib: "npm:^2.6.2"
typescript: "npm:~5.8.3"
Expand Down
Loading