@loadmill/core 0.3.50 → 0.3.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/conf/extrema.d.ts +1 -1
  2. package/dist/conf/extrema.js +2 -2
  3. package/dist/conf/extrema.js.map +1 -1
  4. package/dist/conf/types.d.ts +12 -0
  5. package/dist/conf/validate.d.ts +2 -1
  6. package/dist/conf/validate.js +6 -1
  7. package/dist/conf/validate.js.map +1 -1
  8. package/dist/multipart-form-data/form-data-utils.js +2 -2
  9. package/dist/multipart-form-data/form-data-utils.js.map +1 -1
  10. package/dist/multipart-form-data/is-binary-file.d.ts +2 -0
  11. package/dist/multipart-form-data/is-binary-file.js +215 -0
  12. package/dist/multipart-form-data/is-binary-file.js.map +1 -0
  13. package/dist/multipart-form-data/multipart-text-to-post-form-data.d.ts +5 -1
  14. package/dist/multipart-form-data/multipart-text-to-post-form-data.js +96 -35
  15. package/dist/multipart-form-data/multipart-text-to-post-form-data.js.map +1 -1
  16. package/dist/parameters/extractions.d.ts +2 -1
  17. package/dist/parameters/extractions.js.map +1 -1
  18. package/dist/parameters/index.d.ts +1 -1
  19. package/dist/parameters/index.js +4 -0
  20. package/dist/parameters/index.js.map +1 -1
  21. package/dist/request/index.d.ts +1 -0
  22. package/dist/request/index.js.map +1 -1
  23. package/package.json +7 -2
  24. package/src/conf/extrema.ts +1 -0
  25. package/src/conf/types.ts +14 -0
  26. package/src/conf/validate.ts +12 -1
  27. package/src/multipart-form-data/form-data-utils.ts +4 -4
  28. package/src/multipart-form-data/is-binary-file.ts +206 -0
  29. package/src/multipart-form-data/multipart-text-to-post-form-data.ts +107 -47
  30. package/src/parameters/extractions.ts +10 -8
  31. package/src/parameters/index.ts +4 -1
  32. package/src/request/index.ts +1 -0
  33. package/test/multipart-form-data/form-data-utils.spec.ts +28 -7
@@ -1,26 +1,25 @@
1
+ import { extension } from 'mime-types';
2
+ import { isBase64 } from 'validator';
3
+
4
+ import { HarParam } from '../har';
1
5
  import { PostFormData, PostFormDataEntry } from '../request';
6
+ import { isBinaryFile } from './is-binary-file';
2
7
 
3
- export const multipartFormDataTextToPostFormData = (multipartFormDataText: string): PostFormData => {
4
- const result: PostFormData = [];
8
+ export const multipartFormDataTextToPostFormData = (multipartFormDataText: string = ''): PostFormData =>
9
+ getLines(multipartFormDataText)
10
+ .map(line => extractPostFormDataEntry(line));
5
11
 
6
- const lines = getLines(multipartFormDataText);
7
- for (const line of lines) {
8
- const postFormDataEntry: PostFormDataEntry = extractPostFormDataEntry(line);
9
- result.push(postFormDataEntry);
10
- }
11
- return result;
12
- };
13
-
14
- const getLines = (multipartFormDataText: string) => {
15
- const boundary = getBoundary(multipartFormDataText);
16
- const boundaryRegex = splitByBoundaryRegex(boundary);
17
- const linesSplittedByBoundary = multipartFormDataText.split(boundaryRegex);
18
- return removeEmptyLines(linesSplittedByBoundary);
19
- };
12
+ const getLines = (multipartFormDataText: string = ''): string[] =>
13
+ removeEmptyLines(
14
+ multipartFormDataText.split(
15
+ splitByBoundaryRegex(
16
+ getBoundary(multipartFormDataText)
17
+ )
18
+ )
19
+ );
20
20
 
21
- const getBoundary = (multipartFormDataText: string): string => {
22
- return multipartFormDataText.substring(0, multipartFormDataText.indexOf('\r\n'));
23
- };
21
+ const getBoundary = (multipartFormDataText: string): string =>
22
+ multipartFormDataText.substring(0, multipartFormDataText.indexOf('\r\n'));
24
23
 
25
24
  const splitByBoundaryRegex = (boundary: string) => {
26
25
  const normalBoundary = boundary + '\r\n';
@@ -28,7 +27,7 @@ const splitByBoundaryRegex = (boundary: string) => {
28
27
  return new RegExp(`${normalBoundary}|${lastBoundary}`);
29
28
  };
30
29
 
31
- const removeEmptyLines = (lines: string[]): string[] => lines.filter(Boolean);
30
+ const removeEmptyLines = (lines: string[] = []): string[] => lines.filter(Boolean);
32
31
 
33
32
  const extractPostFormDataEntry = (line: string): PostFormDataEntry => {
34
33
  const indexOfFirst2Newlines = line.indexOf('\r\n\r\n');
@@ -38,52 +37,113 @@ const extractPostFormDataEntry = (line: string): PostFormDataEntry => {
38
37
 
39
38
  const postFormDataEntry: PostFormDataEntry = { name: fields.name, value };
40
39
  fields.fileName && (postFormDataEntry.fileName = fields.fileName);
40
+ fields.contentType && (postFormDataEntry.contentType = fields.contentType);
41
41
 
42
42
  return postFormDataEntry;
43
43
  };
44
44
 
45
- type Fields = Pick<PostFormDataEntry, 'name' | 'fileName'>;
45
+ type Fields = HarParam;
46
46
 
47
47
  const extractFields = (line: string, indexOfFirst2Newlines: number): Fields => {
48
48
  const rawFields = extractRawFields(line, indexOfFirst2Newlines);
49
49
  const res: Fields = { name: '' };
50
- const onlyRawNameFields = rawFields.split('\r\n')[0];
51
- const tokens = onlyRawNameFields.split(';').map(t => t.trim());
52
- for (const token of tokens) {
53
- const [k, v] = token.split('=');
54
- if (k === 'name') {
55
- res.name = removeDoubleQuotes(v);
56
- } else if (k === 'filename') {
57
- res.fileName = removeDoubleQuotes(v);
58
- }
59
- }
50
+ const fieldsLines = rawFields.split('\r\n');
51
+ addNameFields(fieldsLines[0], res);
52
+ fieldsLines.length > 1 && addContentType(fieldsLines[1], res);
60
53
  return res;
61
54
  };
62
55
 
63
- const extractRawFields = (line: string, indexOfFieldsValueSeperator: number): string => {
64
- return line.substring(0, indexOfFieldsValueSeperator);
56
+ const extractRawFields = (line: string, indexOfFieldsValueSeperator: number): string =>
57
+ line.substring(0, indexOfFieldsValueSeperator);
58
+
59
+ const removeDoubleQuotes = (v: string): string =>
60
+ v.replace(/"/g, '');
61
+
62
+ const calculateValue = ({ fileName, contentType }: Fields, line: string, indexOfFirst2Newlines: number): string | '' => {
63
+ const value = extractValue(line, indexOfFirst2Newlines);
64
+ return getBinaryOrTextValue({ value, fileName, contentType });
65
+ };
66
+
67
+ export const getBinaryOrTextValue = (fields: Omit<Fields, 'name'>): string => {
68
+ const { value = '', fileName } = fields;
69
+ if (fileName) {
70
+ const isBass64 = isBase64(value);
71
+ const toBinary = isToBinary(fields);
72
+ if (isBass64 || toBinary) {
73
+ return myAtob(value, isBass64, toBinary);
74
+ }
75
+ }
76
+ return value;
65
77
  };
66
78
 
67
- const removeDoubleQuotes = (v: string): string => {
68
- return v.replace(/"/g, '');
79
+ const myAtob = (target = '', fromBase64?: boolean, toBinary?: boolean) => {
80
+ const defaultEncoding = 'utf8';
81
+ const encodingFrom = fromBase64 ? 'base64' : defaultEncoding;
82
+ const encodingTo = toBinary ? 'binary' : defaultEncoding;
83
+ return Buffer.from(target, encodingFrom).toString(encodingTo);
69
84
  };
70
85
 
71
- function calculateValue(fields: Fields, line: string, indexOfFirst2Newlines: number): string | '' {
72
- return fields.fileName ? '' : extractValue(line, indexOfFirst2Newlines);
73
- }
86
+ const isToBinary = (fields: Omit<Fields, 'name'>): boolean => {
87
+ const { value = '', fileName, contentType } = fields;
88
+ if (isAlreadyBinary(value)) {
89
+ return false;
90
+ }
91
+ if (fileName) {
92
+ const ext = getExtension(fileName);
93
+ if (ext) {
94
+ return isBinaryFileExtension(ext);
95
+ }
96
+ }
97
+ if (contentType) {
98
+ return isBinaryContentType(contentType);
99
+ }
100
+ return false;
101
+ };
74
102
 
75
- const extractValue = (line: string, indexOfFieldsValueSeperator: number): string => {
76
- const value = line.substring(indexOfFieldsValueSeperator + 4);
77
- return removeEndingNewline(value);
103
+ const isAlreadyBinary = (target = '') => {
104
+ const bytes = Buffer.from(target);
105
+ const size = target.length;
106
+ return isBinaryFile(bytes, size);
78
107
  };
79
108
 
80
- const removeEndingNewline = (s: string): string => {
81
- if (s.endsWith('\r\n')) {
82
- s = removeLast2Chars(s);
109
+ const getExtension = (fileName: string): string | undefined =>
110
+ fileName.split('.').pop();
111
+
112
+ const isBinaryFileExtension = (ext: string): boolean =>
113
+ ['docx', 'pdf', 'doc', 'jpg', 'jpeg', 'png'].includes(ext);
114
+
115
+ const isBinaryContentType = (contentType: string): boolean =>
116
+ ['docx', 'pdf', 'bin'].includes(extension(contentType) || '');
117
+
118
+ const extractValue = (line: string, indexOfFieldsValueSeperator: number): string =>
119
+ removeEndingNewline(
120
+ line.substring(indexOfFieldsValueSeperator + 4)
121
+ );
122
+
123
+ const removeEndingNewline = (s: string): string =>
124
+ s.endsWith('\r\n') ? removeLast2Chars(s) : s;
125
+
126
+ const removeLast2Chars = (s: string): string =>
127
+ s.substring(0, s.length - 2);
128
+
129
+ const addNameFields = (nameFieldsLine: string, res: HarParam) => {
130
+ const tokens = extractTokens(nameFieldsLine);
131
+ for (const token of tokens) {
132
+ const [k, v] = token.split('=');
133
+ if (k === 'name') {
134
+ res.name = removeDoubleQuotes(v);
135
+ } else if (k === 'filename') {
136
+ res.fileName = removeDoubleQuotes(v);
137
+ }
83
138
  }
84
- return s;
85
139
  };
86
140
 
87
- const removeLast2Chars = (s: string) => {
88
- return s.substring(0, s.length - 2);
141
+ const extractTokens = (nameFieldsLine: string): string[] =>
142
+ nameFieldsLine.split(';').map(t => t.trim());
143
+
144
+ const addContentType = (contentTypeLine: string, res: HarParam) => {
145
+ const [k, v] = contentTypeLine.split(': ');
146
+ if (k === 'content-type') {
147
+ res.contentType = v;
148
+ }
89
149
  };
@@ -17,14 +17,7 @@ const { getParameterName } = parameterUtils;
17
17
  * @param ext The extraction object to take a part
18
18
  * @returns The spreaded parts of the given extraction object
19
19
  */
20
- export function getExtractionParts(ext: Extractions): {
21
- name: string,
22
- value: string | {
23
- query: string;
24
- attr?: string;
25
- }
26
- type?: string,
27
- } {
20
+ export function getExtractionParts(ext: Extractions): ExtractionParts {
28
21
  const name = getParameterName(ext);
29
22
  const extContent = ext[name];
30
23
  if (typeof extContent === 'string') {
@@ -35,6 +28,15 @@ export function getExtractionParts(ext: Extractions): {
35
28
  return { name, value, type };
36
29
  }
37
30
 
31
+ export type ExtractionParts = {
32
+ name: string;
33
+ value: string | {
34
+ query: string;
35
+ attr?: string;
36
+ };
37
+ type?: string;
38
+ };
39
+
38
40
  export function requestsExtractionsArr(requests, toIndex: number) {
39
41
  const extractionParamNames = [] as string[];
40
42
  for (let i = 0; i <= toIndex; i++) {
@@ -177,6 +177,9 @@ export const parameterUtils = {
177
177
  },
178
178
 
179
179
  findIndex(key, arr) {
180
+ if (!arr) {
181
+ return -1;
182
+ }
180
183
  return arr.findIndex((obj) => {
181
184
  return Object.keys(obj)[0] === key;
182
185
  });
@@ -244,7 +247,7 @@ export const parameterUtils = {
244
247
  return this.getUsedConfParams({ requests: [request] }, parameters);
245
248
  },
246
249
 
247
- getValueByKeyFromArr(key: string, parameters: Parameters[], returnArray: boolean = false) {
250
+ getValueByKeyFromArr(key: string, parameters: Parameters[] | undefined = [], returnArray: boolean = false) {
248
251
  let res;
249
252
  const param = getParameterByKey(key, parameters);
250
253
  if (param) {
@@ -398,6 +398,7 @@ export type PostFormDataEntry = {
398
398
  name: string;
399
399
  value: string;
400
400
  fileName?: string;
401
+ contentType?: string;
401
402
  };
402
403
 
403
404
  export type RequestRawFileData = {
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-len */
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  import { suite, describe, it } from 'mocha';
@@ -34,7 +35,6 @@ suite('form-data-utils - multipart form data', () => {
34
35
  describe('Form Class - manages and manipulates multipart form data', () => {
35
36
  it('to string', () => {
36
37
  const boundary = '------WebKitFormBoundary12345678ASDFGHJ';
37
- // eslint-disable-next-line max-len
38
38
  const expectedText = '--------WebKitFormBoundary12345678ASDFGHJ\r\nContent-Disposition: form-data; name="the_name_of_the_thing"; filename="some file name.ext"\r\ncontent-type: application/vnd.mspowerpoint-sniffing.x-rar-compressed\r\n\r\nthe_contents_of_the_file\r\n--------WebKitFormBoundary12345678ASDFGHJ\r\nContent-Disposition: form-data; name="entityId"\r\n\r\n123456789123456789123456789\r\n--------WebKitFormBoundary12345678ASDFGHJ\r\nContent-Disposition: form-data; name="shouldDoStuff"\r\n\r\ntrue\r\n--------WebKitFormBoundary12345678ASDFGHJ\r\nContent-Disposition: form-data; name="content-transfer-encoding"\r\n\r\nquoted-printable\r\n--------WebKitFormBoundary12345678ASDFGHJ--\r\n';
39
39
  const form = new Form(params, boundary);
40
40
  const result = form.toString();
@@ -49,7 +49,7 @@ suite('form-data-utils - multipart form data', () => {
49
49
  postFormData: [
50
50
  {
51
51
  name: 'the_name_of_the_thing',
52
- value: '',
52
+ value: 'the_contents_of_the_file',
53
53
  fileName: 'some file name.ext',
54
54
  },
55
55
  {
@@ -75,7 +75,7 @@ suite('form-data-utils - multipart form data', () => {
75
75
  postFormData: [
76
76
  {
77
77
  name: 'file',
78
- value: '',
78
+ value: 'ÿØÿà\u0000\u0010JFIF\u0000\u0001\u0001\u0000\u0000\u0001\u0000\u0001\u0000\u0000ÿÛ\u0000„\u0000\u0005\u0003\u0004\t\t\b\t\t\t\t\t\t\t\t\t\u0005u001c\u0017\t\b\u001a\t\t\u0006\u0018!\u0018\u001a\u001d\u001d\u001f\u001f\u001f\u0007\u000b"\u0018"\u001e\u0018\u001c\u001e\u001f\u001e\u0001\u0005\u0005\u0005\b\u0007\b\u000e\b\b\r\u0012\r\u000e\r\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012\u0012R#B?\u00005Ù',
79
79
  fileName: 'silk_sonic.jpeg',
80
80
  },
81
81
  ]
@@ -98,24 +98,45 @@ suite('form-data-utils - multipart form data', () => {
98
98
  });
99
99
 
100
100
  it('should return 2 name-values', () => {
101
- // eslint-disable-next-line max-len
102
101
  const text = '----10453916712\r\nContent-Disposition: form-data; name="arnon"\r\n\r\npeleg\r\n----10453916712\r\nContent-Disposition: form-data; name="rivi"\r\n\r\nshimi\r\n----10453916712--\r\n';
103
102
  const result = multipartFormDataTextToPostFormData(text);
104
103
  expect(result).to.have.deep.ordered.members([{ name: 'arnon', value: 'peleg' }, { name: 'rivi', value: 'shimi' }]);
105
104
  });
106
105
 
107
106
  it('should return name-value-filename', () => {
108
- // eslint-disable-next-line max-len
109
107
  const text = '-----------------------------10453916712809342873442003634\r\nContent-Disposition: form-data; name="file"; filename="silk_sonic.jpeg"\r\nContent-Type: image/jpeg\r\n\r\nXCXCXCXCXCXCXCXCXCXCXCXCXCXC\t\r\n\t \n\n\nXXC101010101010101101010101010\r\n-----------------------------10453916712809342873442003634--\r\n';
110
108
  const result = multipartFormDataTextToPostFormData(text);
111
109
  expect(result).to.have.deep.ordered.members([
112
110
  {
113
111
  name: 'file',
114
- value: '',
112
+ value: 'XCXCXCXCXCXCXCXCXCXCXCXCXCXC\t\r\n\t \n\n\nXXC101010101010101101010101010',
115
113
  fileName: 'silk_sonic.jpeg'
116
114
  }
117
115
  ]);
118
116
  });
119
- });
120
117
 
118
+ it('should return also the value of a simple file', () => {
119
+ const text = '------WebKitFormBoundaryntAaM2MokVD4YVOO\r\nContent-Disposition: form-data; name="file"; filename="Cars.csv"\r\nContent-Type: text/csv\r\n\r\nID,First_Name,Last_Name,Car_Make,Car_Model,Car_Color,Car_Model Year,Car_VIN\n1,Lenora,Hegdonne,Lincoln,Town Car,Blue,1994,WAUUL98E68A703943\n2,Jermaine,Bradane,Ford,GT,Pink,2005,1N6AA0EC1FN193806\r\n------WebKitFormBoundaryntAaM2MokVD4YVOO--\r\n';
120
+ const result = multipartFormDataTextToPostFormData(text);
121
+ expect(result).to.have.deep.ordered.members([
122
+ {
123
+ name: 'file',
124
+ value: 'ID,First_Name,Last_Name,Car_Make,Car_Model,Car_Color,Car_Model Year,Car_VIN\n1,Lenora,Hegdonne,Lincoln,Town Car,Blue,1994,WAUUL98E68A703943\n2,Jermaine,Bradane,Ford,GT,Pink,2005,1N6AA0EC1FN193806',
125
+ fileName: 'Cars.csv'
126
+ }
127
+ ]);
128
+ });
129
+ it('should detect content-type', () => {
130
+ const text = '----------------------------148649485734387205386089\r\nContent-Disposition: form-data; name="file"; filename="Bell data engineer.docx"\r\ncontent-type: application/vnd.openxmlformats-officedocument.wordprocessingml.document\r\n\r\nUEsDBBQABgAIAAAAIQCTkoLJhQEAACkHAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIo\r\n----------------------------148649485734387205386089--\r\n';
131
+ const result = multipartFormDataTextToPostFormData(text);
132
+ expect(result).to.have.deep.ordered.members([
133
+ {
134
+ name: 'file',
135
+ value: 'PK\u0003\u0004\u0014\u0000\u0006\u0000\b\u0000\u0000\u0000!\u0000“’‚É…\u0001\u0000\u0000)\u0007\u0000\u0000\u0013\u0000\b\u0002[Content_Types].xml ¢\u0004\u0002(',
136
+ fileName: 'Bell data engineer.docx',
137
+ contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
138
+ }
139
+ ]);
140
+ });
141
+ });
121
142
  });