Skip to content

Commit

Permalink
fix(oas): serialize binary and base64 data (#190)
Browse files Browse the repository at this point in the history
fixes #181
  • Loading branch information
derevnjuk authored Feb 28, 2023
1 parent 84b25bc commit 88621b7
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 25 deletions.
80 changes: 57 additions & 23 deletions packages/oas/src/converter/parts/postdata/BodyConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
implements SubConverter<PostData | null>
{
private readonly xmlSerializer = new XmlSerializer();
private readonly JPG_IMAGE = '/9j/7g=='; // 0xff, 0xd8, 0xff, 0xee
private readonly PNG_IMAGE = 'iVBORw0KGgo='; // 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0A, 0x1a, 0x0a
private readonly ICO_IMAGE = 'AAABAA=='; // 0x00, 0x00, 0x01, 0x00
private readonly GIF_IMAGE = 'R0lGODdh'; // 0x47, 0x49, 0x46, 0x38, 0x37, 0x61
private readonly JPG_IMAGE = '\xff\xd8\xff\xe0';
private readonly PNG_IMAGE = '\x89\x50\x4e\x47\x0d\x0A\x1a\x0a';
private readonly ICO_IMAGE = '\x00\x00\x01\x00';
private readonly GIF_IMAGE = '\x47\x49\x46\x38\x37\x61';
private readonly BOUNDARY = '956888039105887155673143';
private readonly BASE64_PATTERN =
/^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/;
private readonly BASE64_FORMATS: readonly string[] = ['byte', 'base64'];
private readonly BINARY_FORMATS: readonly string[] = [
'binary',
...this.BASE64_FORMATS
];

protected constructor(
protected readonly spec: T,
Expand Down Expand Up @@ -69,69 +72,100 @@ export abstract class BodyConverter<T extends OpenAPI.Document>
return this.encodeXml(value, schema);
case 'multipart/form-data':
case 'multipart/mixed':
return this.encodeMultipartFormData(value, fields);
return this.encodeMultipartFormData(value, fields, schema);
case 'image/x-icon':
case 'image/ico':
case 'image/vnd.microsoft.icon':
return this.ICO_IMAGE;
return this.encodeBinary(this.ICO_IMAGE, schema);
case 'image/jpg':
case 'image/jpeg':
return this.JPG_IMAGE;
return this.encodeBinary(this.JPG_IMAGE, schema);
case 'image/gif':
return this.GIF_IMAGE;
return this.encodeBinary(this.GIF_IMAGE, schema);
case 'image/png':
case 'image/*':
return this.PNG_IMAGE;
return this.encodeBinary(this.PNG_IMAGE, schema);
default:
return this.encodeOther(value);
}
}

private encodeBinary(
value: unknown,
schema?: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject
): string {
const encoded = this.encodeOther(value);

return this.BASE64_FORMATS.includes(schema?.format)
? btoa(encoded)
: encoded;
}

// TODO: move the logic that receives the content type from the encoding object
// to the {@link Oas3RequestBodyConverter} class.
private encodeMultipartFormData(
value: unknown,
fields?: Record<string, OpenAPIV3.EncodingObject>
fields?: Record<string, OpenAPIV3.EncodingObject>,
schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject
): string {
const EOL = '\r\n';

return Object.entries(value || {})
.map(([key, val]: [string, unknown]) => {
const propertySchema = this.getPropertySchema(key, schema);
const contentType =
fields?.[key]?.contentType ?? this.inferMultipartContentType(val);
const filenameRequired = this.filenameRequired(contentType);
const content = this.encodeOther(val);
fields?.[key]?.contentType ??
this.inferContentType(val, propertySchema);

const headers = [
`Content-Disposition: form-data; name="${key}"${
filenameRequired ? `; filename="${key}"` : ''
this.filenameRequired(contentType) ? `; filename="${key}"` : ''
}`,
...(contentType !== 'text/plain'
? [`Content-Type: ${contentType}`]
: []),
...(this.BASE64_PATTERN.test(content) &&
contentType === 'application/octet-stream'
? [`Content-Transfer-Encoding: base64`]
...(this.BASE64_FORMATS.includes(propertySchema?.format)
? ['Content-Transfer-Encoding: base64']
: [])
];
const body = `${headers.join(EOL)}${EOL}${EOL}${content}`;
const body = this.encodeOther(val);

return `--${this.BOUNDARY}${EOL}${body}`;
return `--${this.BOUNDARY}${EOL}${headers.join(
EOL
)}${EOL}${EOL}${body}`;
})
.join(EOL)
.concat(`${EOL}--${this.BOUNDARY}--`);
}

private getPropertySchema(
key: string,
schema?: OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject
): OpenAPIV3.SchemaObject | OpenAPIV2.SchemaObject | undefined {
if (schema?.type === 'object') {
return schema.properties?.[key];
}

if (schema?.type === 'array') {
return schema.items;
}

return undefined;
}

private filenameRequired(contentType: string): boolean {
return 'application/octet-stream' === contentType;
}

private inferMultipartContentType(value: unknown): string {
private inferContentType(
value: unknown,
schema?: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject
): string {
switch (typeof value) {
case 'object':
return 'application/json';
case 'string':
return this.BASE64_PATTERN.test(value)
return this.BINARY_FORMATS.includes(schema?.format)
? 'application/octet-stream'
: 'text/plain';
case 'number':
Expand Down
2 changes: 1 addition & 1 deletion packages/oas/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { OpenAPI, OpenAPIV2, OpenAPIV3 } from '@har-sdk/core';

export * from './Flattener';
export * from './isObject';
export * from './params';
export * from './operation';
export * from './params';

export const isOASV2 = (doc: OpenAPI.Document): doc is OpenAPIV2.Document =>
'swagger' in doc;
Expand Down
10 changes: 10 additions & 0 deletions packages/oas/tests/DefaultConverter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ describe('DefaultConverter', () => {
input: 'xml-models.swagger.yaml',
expected: 'xml-models.swagger.result.json',
message: 'should properly serialize models to XML (swagger)'
},
{
input: 'binary-body.swagger.yaml',
expected: 'binary-body.swagger.result.json',
message: 'should properly serialize binary types (swagger)'
},
{
input: 'binary-body.oas.yaml',
expected: 'binary-body.oas.result.json',
message: 'should properly serialize binary types (oas)'
}
].forEach(({ input: inputFile, expected: expectedFile, message }) => {
it(message, async () => {
Expand Down
78 changes: 78 additions & 0 deletions packages/oas/tests/fixtures/binary-body.oas.result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
[
{
"bodySize": 0,
"cookies": [],
"headers": [
{
"name": "content-type",
"value": "image/jpeg"
}
],
"headersSize": 0,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "image/jpeg",
"text": "\u00ff\u00d8\u00ff\u00e0"
},
"queryString": [],
"url": "https://petstore.swagger.io/binary"
},
{
"bodySize": 0,
"cookies": [],
"headers": [
{
"name": "content-type",
"value": "image/png"
}
],
"headersSize": 0,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "image/png",
"text": "iVBORw0KGgo="
},
"queryString": [],
"url": "https://petstore.swagger.io/byte"
},
{
"bodySize": 0,
"cookies": [],
"headers": [
{
"name": "content-type",
"value": "image/ico"
}
],
"headersSize": 0,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "image/ico",
"text": "AAABAA=="
},
"queryString": [],
"url": "https://petstore.swagger.io/base64"
},
{
"bodySize": 0,
"cookies": [],
"headers": [
{
"name": "content-type",
"value": "multipart/form-data"
}
],
"headersSize": 0,
"httpVersion": "HTTP/1.1",
"method": "POST",
"postData": {
"mimeType": "multipart/form-data; boundary=956888039105887155673143",
"text": "--956888039105887155673143\r\nContent-Disposition: form-data; name=\"base64\"\r\nContent-Type: image/vnd.microsoft.icon\r\nContent-Transfer-Encoding: base64\r\n\r\nAAABAA==\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"binary\"\r\nContent-Type: image/gif\r\n\r\nGIF87a\r\n--956888039105887155673143\r\nContent-Disposition: form-data; name=\"byte\"\r\nContent-Type: image/*\r\nContent-Transfer-Encoding: base64\r\n\r\niVBORw0KGgo=\r\n--956888039105887155673143--"
},
"queryString": [],
"url": "https://petstore.swagger.io/multipart"
}
]
79 changes: 79 additions & 0 deletions packages/oas/tests/fixtures/binary-body.oas.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
openapi: 3.0.0
info:
title: Binary Data API
version: 1.0.0
servers:
- url: https://petstore.swagger.io
paths:
/binary:
post:
summary: Upload plain binary data
requestBody:
content:
image/jpeg:
schema:
type: string
format: binary
required: true
responses:
'200':
description: OK
/byte:
post:
summary: Upload byte data
requestBody:
content:
image/png:
schema:
type: string
format: byte
required: true
responses:
'200':
description: OK
/base64:
post:
summary: Upload base64 data
requestBody:
content:
image/ico:
schema:
type: string
format: base64
required: true
responses:
'200':
description: OK
/multipart:
post:
summary: Upload multipart/form-data with binary data
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
base64:
type: string
format: base64
binary:
type: string
format: binary
byte:
type: string
format: byte
required:
- base64
- binary
- byte
encoding:
base64:
contentType: image/vnd.microsoft.icon
binary:
contentType: image/gif
byte:
contentType: image/*
required: true
responses:
'200':
description: OK
Loading

0 comments on commit 88621b7

Please sign in to comment.