@futpib/parser 1.0.7 → 1.0.8
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.
- package/build/arbitraryBash.d.ts +3 -0
- package/build/arbitraryBash.js +142 -0
- package/build/arbitraryJavaScript.js +4 -4
- package/build/arbitraryZipStream.d.ts +1 -1
- package/build/bashUnparser.d.ts +3 -0
- package/build/bashUnparser.js +157 -0
- package/build/bashUnparser.test.d.ts +1 -0
- package/build/bashUnparser.test.js +24 -0
- package/build/bsonParser.js +3 -3
- package/build/dalvikExecutableParser/stringSyntaxParser.js +20 -31
- package/build/dalvikExecutableParser.js +2 -5
- package/build/hasExecutable.js +1 -1
- package/build/jsonParser.js +2 -7
- package/build/regularExpression.d.ts +12 -3
- package/build/regularExpression.js +10 -1
- package/build/regularExpressionParser.js +39 -25
- package/build/regularExpressionParser.test.js +2 -2
- package/build/smaliParser.js +5 -9
- package/build/symbolicExpressionParser.js +8 -3
- package/package.json +9 -9
- package/readme.md +468 -7
- package/src/arbitraryBash.ts +237 -0
- package/src/arbitraryJavaScript.ts +4 -4
- package/src/bashUnparser.test.ts +37 -0
- package/src/bashUnparser.ts +211 -0
- package/src/bsonParser.ts +4 -7
- package/src/dalvikExecutableParser/stringSyntaxParser.ts +27 -74
- package/src/dalvikExecutableParser.ts +4 -10
- package/src/hasExecutable.ts +1 -1
- package/src/jsonParser.ts +2 -11
- package/src/regularExpression.ts +11 -1
- package/src/regularExpressionParser.test.ts +3 -3
- package/src/regularExpressionParser.ts +49 -30
- package/src/smaliParser.ts +11 -23
- package/src/symbolicExpressionParser.ts +9 -3
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as fc from 'fast-check';
|
|
2
|
+
import {
|
|
3
|
+
type BashWord,
|
|
4
|
+
type BashWordPart,
|
|
5
|
+
type BashWordPartLiteral,
|
|
6
|
+
type BashWordPartSingleQuoted,
|
|
7
|
+
type BashWordPartDoubleQuoted,
|
|
8
|
+
type BashWordPartVariable,
|
|
9
|
+
type BashWordPartVariableBraced,
|
|
10
|
+
type BashWordPartArithmeticExpansion,
|
|
11
|
+
type BashSimpleCommand,
|
|
12
|
+
type BashSubshell,
|
|
13
|
+
type BashBraceGroup,
|
|
14
|
+
type BashCommandUnit,
|
|
15
|
+
type BashPipeline,
|
|
16
|
+
type BashCommandList,
|
|
17
|
+
type BashRedirect,
|
|
18
|
+
type BashAssignment,
|
|
19
|
+
} from './bash.js';
|
|
20
|
+
|
|
21
|
+
const arbitraryBashIdentifier: fc.Arbitrary<string> = fc.stringMatching(/^[a-zA-Z_][a-zA-Z0-9_]*$/);
|
|
22
|
+
|
|
23
|
+
// Safe unquoted literal: no shell special chars, no leading {/} or #, no = (would be parsed as assignment)
|
|
24
|
+
const arbitraryBashWordPartLiteral: fc.Arbitrary<BashWordPartLiteral> = fc.record({
|
|
25
|
+
type: fc.constant('literal' as const),
|
|
26
|
+
value: fc.stringMatching(/^[a-zA-Z0-9][a-zA-Z0-9._@%,:^~-]*$/),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Single-quoted: no single quotes, no newlines inside (keep simple)
|
|
30
|
+
const arbitraryBashWordPartSingleQuoted: fc.Arbitrary<BashWordPartSingleQuoted> = fc.record({
|
|
31
|
+
type: fc.constant('singleQuoted' as const),
|
|
32
|
+
value: fc.stringMatching(/^[^'\n]*$/),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const arbitraryBashWordPartVariable: fc.Arbitrary<BashWordPartVariable> = fc.record({
|
|
36
|
+
type: fc.constant('variable' as const),
|
|
37
|
+
name: arbitraryBashIdentifier,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// variableBraced without operator/operand (always include the optional keys so deepEqual matches parser output)
|
|
41
|
+
const arbitraryBashWordPartVariableBraced: fc.Arbitrary<BashWordPartVariableBraced> = fc.record({
|
|
42
|
+
type: fc.constant('variableBraced' as const),
|
|
43
|
+
name: arbitraryBashIdentifier,
|
|
44
|
+
operator: fc.constant(undefined),
|
|
45
|
+
operand: fc.constant(undefined),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const arbitraryBashWordPartArithmeticExpansion: fc.Arbitrary<BashWordPartArithmeticExpansion> = fc.record({
|
|
49
|
+
type: fc.constant('arithmeticExpansion' as const),
|
|
50
|
+
expression: fc.stringMatching(/^[0-9+\- ]*$/),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
type RecursiveArbitraries = {
|
|
54
|
+
commandList: BashCommandList;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const recursiveArbitraries = fc.letrec<RecursiveArbitraries>(tie => {
|
|
58
|
+
const arbitraryCommandList = tie('commandList') as fc.Arbitrary<BashCommandList>;
|
|
59
|
+
|
|
60
|
+
// Double-quoted literal: no shell-special chars inside double quotes
|
|
61
|
+
const arbitraryDoubleQuotedLiteral: fc.Arbitrary<BashWordPartLiteral> = fc.record({
|
|
62
|
+
type: fc.constant('literal' as const),
|
|
63
|
+
value: fc.stringMatching(/^[^"\\$`\n]+$/),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const arbitraryBashWordPartDoubleQuoted: fc.Arbitrary<BashWordPartDoubleQuoted> = fc.record({
|
|
67
|
+
type: fc.constant('doubleQuoted' as const),
|
|
68
|
+
parts: fc.array(
|
|
69
|
+
fc.oneof(
|
|
70
|
+
{ weight: 3, arbitrary: arbitraryDoubleQuotedLiteral as fc.Arbitrary<BashWordPart> },
|
|
71
|
+
{ weight: 1, arbitrary: arbitraryBashWordPartVariable as fc.Arbitrary<BashWordPart> },
|
|
72
|
+
{ weight: 1, arbitrary: arbitraryBashWordPartVariableBraced as fc.Arbitrary<BashWordPart> },
|
|
73
|
+
),
|
|
74
|
+
{ minLength: 1, maxLength: 3 },
|
|
75
|
+
),
|
|
76
|
+
}).filter(dq =>
|
|
77
|
+
dq.parts.every((part, i) => {
|
|
78
|
+
const next = dq.parts[i + 1];
|
|
79
|
+
// Prevent adjacent literal parts (they merge when re-parsed)
|
|
80
|
+
if (part.type === 'literal' && next !== undefined && next.type === 'literal') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Prevent $var followed by literal starting with ident char (would be mis-parsed as one variable)
|
|
85
|
+
if (part.type === 'variable' && next !== undefined && next.type === 'literal') {
|
|
86
|
+
return next.value.length === 0 || !isIdentChar(next.value[0]!);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const arbitraryBashWordPartCommandSubstitution = fc.record({
|
|
94
|
+
type: fc.constant('commandSubstitution' as const),
|
|
95
|
+
command: arbitraryCommandList,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const arbitraryBashWordPart: fc.Arbitrary<BashWordPart> = fc.oneof(
|
|
99
|
+
{ weight: 4, arbitrary: arbitraryBashWordPartLiteral as fc.Arbitrary<BashWordPart> },
|
|
100
|
+
{ weight: 2, arbitrary: arbitraryBashWordPartSingleQuoted as fc.Arbitrary<BashWordPart> },
|
|
101
|
+
{ weight: 2, arbitrary: arbitraryBashWordPartDoubleQuoted as fc.Arbitrary<BashWordPart> },
|
|
102
|
+
{ weight: 2, arbitrary: arbitraryBashWordPartVariable as fc.Arbitrary<BashWordPart> },
|
|
103
|
+
{ weight: 1, arbitrary: arbitraryBashWordPartVariableBraced as fc.Arbitrary<BashWordPart> },
|
|
104
|
+
{ weight: 1, arbitrary: arbitraryBashWordPartArithmeticExpansion as fc.Arbitrary<BashWordPart> },
|
|
105
|
+
{ weight: 1, arbitrary: arbitraryBashWordPartCommandSubstitution as fc.Arbitrary<BashWordPart> },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
function isIdentChar(ch: string): boolean {
|
|
109
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '_';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const arbitraryWord: fc.Arbitrary<BashWord> = fc.record({
|
|
113
|
+
parts: fc.array(arbitraryBashWordPart, { minLength: 1, maxLength: 2 }),
|
|
114
|
+
}).filter(word =>
|
|
115
|
+
word.parts.every((part, i) => {
|
|
116
|
+
const next = word.parts[i + 1];
|
|
117
|
+
// Prevent adjacent literal parts (they merge when re-parsed)
|
|
118
|
+
if (part.type === 'literal' && next !== undefined && next.type === 'literal') {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Prevent $var followed by literal starting with ident char (would be mis-parsed as one variable)
|
|
123
|
+
if (part.type === 'variable' && next !== undefined && next.type === 'literal') {
|
|
124
|
+
return next.value.length === 0 || !isIdentChar(next.value[0]!);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Always include value key (even if undefined) to match createObjectParser behavior
|
|
132
|
+
const arbitraryBashAssignment: fc.Arbitrary<BashAssignment> = fc.record({
|
|
133
|
+
name: arbitraryBashIdentifier,
|
|
134
|
+
value: fc.option(arbitraryWord, { nil: undefined }),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Always include fd key (even if undefined) to match createObjectParser behavior
|
|
138
|
+
const arbitraryBashRedirect: fc.Arbitrary<BashRedirect> = fc.record({
|
|
139
|
+
fd: fc.constant(undefined),
|
|
140
|
+
operator: fc.oneof(
|
|
141
|
+
fc.constant('>' as const),
|
|
142
|
+
fc.constant('>>' as const),
|
|
143
|
+
fc.constant('<' as const),
|
|
144
|
+
),
|
|
145
|
+
target: arbitraryWord,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const arbitraryBashSimpleCommandWithName: fc.Arbitrary<BashSimpleCommand> = fc.record({
|
|
149
|
+
type: fc.constant('simple' as const),
|
|
150
|
+
name: arbitraryWord,
|
|
151
|
+
args: fc.array(arbitraryWord, { maxLength: 2 }),
|
|
152
|
+
redirects: fc.array(arbitraryBashRedirect, { maxLength: 1 }),
|
|
153
|
+
assignments: fc.array(arbitraryBashAssignment, { maxLength: 1 }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Commands with no name: only assignments and/or redirects (no args)
|
|
157
|
+
const arbitraryBashSimpleCommandNoName: fc.Arbitrary<BashSimpleCommand> = fc.record({
|
|
158
|
+
type: fc.constant('simple' as const),
|
|
159
|
+
name: fc.constant(undefined),
|
|
160
|
+
args: fc.constant([]),
|
|
161
|
+
redirects: fc.array(arbitraryBashRedirect, { maxLength: 1 }),
|
|
162
|
+
assignments: fc.array(arbitraryBashAssignment, { minLength: 1, maxLength: 2 }),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const arbitraryBashSimpleCommand: fc.Arbitrary<BashSimpleCommand> = fc.oneof(
|
|
166
|
+
{ weight: 4, arbitrary: arbitraryBashSimpleCommandWithName },
|
|
167
|
+
{ weight: 1, arbitrary: arbitraryBashSimpleCommandNoName },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const arbitraryBashSubshell: fc.Arbitrary<BashSubshell> = fc.record({
|
|
171
|
+
type: fc.constant('subshell' as const),
|
|
172
|
+
body: arbitraryCommandList,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Brace group bodies need trailing ';' on last entry (required by "{ cmd; }" syntax)
|
|
176
|
+
const arbitraryBraceGroupBody: fc.Arbitrary<BashCommandList> = arbitraryCommandList.map(list => {
|
|
177
|
+
const entries = list.entries.map((entry, i) => {
|
|
178
|
+
if (i === list.entries.length - 1 && entry.separator === undefined) {
|
|
179
|
+
return { pipeline: entry.pipeline, separator: ';' as const };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return entry;
|
|
183
|
+
});
|
|
184
|
+
return { ...list, entries };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const arbitraryBashBraceGroup: fc.Arbitrary<BashBraceGroup> = fc.record({
|
|
188
|
+
type: fc.constant('braceGroup' as const),
|
|
189
|
+
body: arbitraryBraceGroupBody,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const arbitraryBashCommandUnit: fc.Arbitrary<BashCommandUnit> = fc.oneof(
|
|
193
|
+
{ weight: 5, arbitrary: arbitraryBashSimpleCommand as fc.Arbitrary<BashCommandUnit> },
|
|
194
|
+
{ weight: 1, arbitrary: arbitraryBashSubshell as fc.Arbitrary<BashCommandUnit> },
|
|
195
|
+
{ weight: 1, arbitrary: arbitraryBashBraceGroup as fc.Arbitrary<BashCommandUnit> },
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const arbitraryBashPipeline: fc.Arbitrary<BashPipeline> = fc.record({
|
|
199
|
+
type: fc.constant('pipeline' as const),
|
|
200
|
+
negated: fc.boolean(),
|
|
201
|
+
commands: fc.array(arbitraryBashCommandUnit, { minLength: 1, maxLength: 2 }),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const commandListArbitrary: fc.Arbitrary<BashCommandList> = fc.record({
|
|
205
|
+
type: fc.constant('list' as const),
|
|
206
|
+
entries: fc.array(
|
|
207
|
+
fc.record({
|
|
208
|
+
pipeline: arbitraryBashPipeline,
|
|
209
|
+
separator: fc.option(
|
|
210
|
+
fc.oneof(
|
|
211
|
+
fc.constant('&&' as const),
|
|
212
|
+
fc.constant('||' as const),
|
|
213
|
+
fc.constant(';' as const),
|
|
214
|
+
),
|
|
215
|
+
{ nil: undefined },
|
|
216
|
+
),
|
|
217
|
+
}),
|
|
218
|
+
{ minLength: 1, maxLength: 2 },
|
|
219
|
+
),
|
|
220
|
+
}).map(list => {
|
|
221
|
+
const entries = list.entries.map((entry, i) => {
|
|
222
|
+
if (i < list.entries.length - 1 && entry.separator === undefined) {
|
|
223
|
+
return { pipeline: entry.pipeline, separator: ';' as const };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return entry;
|
|
227
|
+
});
|
|
228
|
+
return { ...list, entries };
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
commandList: commandListArbitrary,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
export const arbitraryBashCommandList: fc.Arbitrary<BashCommandList> =
|
|
237
|
+
recursiveArbitraries.commandList as fc.Arbitrary<BashCommandList>;
|
|
@@ -256,7 +256,7 @@ const arbitraryArrowFunctionExpression = fc.oneof(
|
|
|
256
256
|
fc.record({
|
|
257
257
|
type: fc.constant('ArrowFunctionExpression' as const),
|
|
258
258
|
id: fc.constant(null),
|
|
259
|
-
params: fc.
|
|
259
|
+
params: fc.uniqueArray(arbitraryIdentifier, { minLength: 0, maxLength: 3, selector: (id: Identifier) => id.name }),
|
|
260
260
|
body: arbitraryFunctionBodyBlockStatement,
|
|
261
261
|
expression: fc.constant(false),
|
|
262
262
|
generator: fc.constant(false),
|
|
@@ -265,7 +265,7 @@ const arbitraryArrowFunctionExpression = fc.oneof(
|
|
|
265
265
|
fc.record({
|
|
266
266
|
type: fc.constant('ArrowFunctionExpression' as const),
|
|
267
267
|
id: fc.constant(null),
|
|
268
|
-
params: fc.
|
|
268
|
+
params: fc.uniqueArray(arbitraryIdentifier, { minLength: 0, maxLength: 3, selector: (id: Identifier) => id.name }),
|
|
269
269
|
body: arbitraryLeafExpression,
|
|
270
270
|
expression: fc.constant(true),
|
|
271
271
|
generator: fc.constant(false),
|
|
@@ -276,7 +276,7 @@ const arbitraryArrowFunctionExpression = fc.oneof(
|
|
|
276
276
|
const arbitraryFunctionExpression = fc.record({
|
|
277
277
|
type: fc.constant('FunctionExpression' as const),
|
|
278
278
|
id: fc.constant(null),
|
|
279
|
-
params: fc.
|
|
279
|
+
params: fc.uniqueArray(arbitraryIdentifier, { minLength: 0, maxLength: 3, selector: (id: Identifier) => id.name }),
|
|
280
280
|
body: arbitraryFunctionBodyBlockStatement,
|
|
281
281
|
expression: fc.constant(false),
|
|
282
282
|
generator: fc.constant(false),
|
|
@@ -359,7 +359,7 @@ const arbitraryTryStatement: fc.Arbitrary<Statement> = fc.oneof(
|
|
|
359
359
|
const arbitraryFunctionDeclaration: fc.Arbitrary<Statement> = fc.record({
|
|
360
360
|
type: fc.constant('FunctionDeclaration' as const),
|
|
361
361
|
id: arbitraryIdentifier,
|
|
362
|
-
params: fc.
|
|
362
|
+
params: fc.uniqueArray(arbitraryIdentifier, { minLength: 0, maxLength: 3, selector: (id: Identifier) => id.name }),
|
|
363
363
|
body: arbitraryFunctionBodyBlockStatement,
|
|
364
364
|
expression: fc.constant(false),
|
|
365
365
|
generator: fc.constant(false),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { testProp } from '@fast-check/ava';
|
|
2
|
+
import { arbitraryBashCommandList } from './arbitraryBash.js';
|
|
3
|
+
import { bashScriptUnparser } from './bashUnparser.js';
|
|
4
|
+
import { bashScriptParser } from './bashParser.js';
|
|
5
|
+
import { runParser } from './parser.js';
|
|
6
|
+
import { runUnparser } from './unparser.js';
|
|
7
|
+
import { stringParserInputCompanion } from './parserInputCompanion.js';
|
|
8
|
+
import { stringUnparserOutputCompanion } from './unparserOutputCompanion.js';
|
|
9
|
+
|
|
10
|
+
const seed = process.env.SEED ? Number(process.env.SEED) : undefined;
|
|
11
|
+
|
|
12
|
+
async function collectString(asyncIterable: AsyncIterable<string>): Promise<string> {
|
|
13
|
+
let result = '';
|
|
14
|
+
for await (const chunk of asyncIterable) {
|
|
15
|
+
result += chunk;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
testProp(
|
|
22
|
+
'bash roundtrip',
|
|
23
|
+
[arbitraryBashCommandList],
|
|
24
|
+
async (t, command) => {
|
|
25
|
+
const source = await collectString(runUnparser(
|
|
26
|
+
bashScriptUnparser, command, stringUnparserOutputCompanion));
|
|
27
|
+
|
|
28
|
+
const reparsed = await runParser(
|
|
29
|
+
bashScriptParser, source, stringParserInputCompanion);
|
|
30
|
+
|
|
31
|
+
t.deepEqual(reparsed, command);
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
verbose: true,
|
|
35
|
+
seed,
|
|
36
|
+
},
|
|
37
|
+
);
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { type Unparser } from './unparser.js';
|
|
2
|
+
import {
|
|
3
|
+
type BashWord,
|
|
4
|
+
type BashWordPart,
|
|
5
|
+
type BashSimpleCommand,
|
|
6
|
+
type BashSubshell,
|
|
7
|
+
type BashBraceGroup,
|
|
8
|
+
type BashCommandUnit,
|
|
9
|
+
type BashPipeline,
|
|
10
|
+
type BashCommandList,
|
|
11
|
+
type BashRedirect,
|
|
12
|
+
type BashAssignment,
|
|
13
|
+
type BashCommand,
|
|
14
|
+
} from './bash.js';
|
|
15
|
+
|
|
16
|
+
function isIdentChar(ch: string): boolean {
|
|
17
|
+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '_';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nextPartStartsWithIdentChar(parts: BashWordPart[], index: number): boolean {
|
|
21
|
+
const next = parts[index + 1];
|
|
22
|
+
if (next === undefined) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (next.type === 'literal') {
|
|
27
|
+
return next.value.length > 0 && isIdentChar(next.value[0]!);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function unparseWord(word: BashWord): string {
|
|
34
|
+
return word.parts.map((part, i) => unparseWordPartInContext(part, word.parts, i)).join('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function unparseWordPartInContext(part: BashWordPart, parts: BashWordPart[], index: number): string {
|
|
38
|
+
return unparseWordPart(part);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unparseWordPart(part: BashWordPart): string {
|
|
42
|
+
switch (part.type) {
|
|
43
|
+
case 'literal':
|
|
44
|
+
return escapeLiteral(part.value);
|
|
45
|
+
|
|
46
|
+
case 'singleQuoted':
|
|
47
|
+
return "'" + part.value + "'";
|
|
48
|
+
|
|
49
|
+
case 'doubleQuoted':
|
|
50
|
+
return '"' + part.parts.map(p => unparseDoubleQuotedPart(p)).join('') + '"';
|
|
51
|
+
|
|
52
|
+
case 'variable':
|
|
53
|
+
return '$' + part.name;
|
|
54
|
+
|
|
55
|
+
case 'variableBraced': {
|
|
56
|
+
let result = '${' + part.name;
|
|
57
|
+
if (part.operator !== undefined) {
|
|
58
|
+
result += part.operator;
|
|
59
|
+
if (part.operand !== undefined) {
|
|
60
|
+
result += unparseWord(part.operand);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
result += '}';
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case 'commandSubstitution':
|
|
69
|
+
return '$( ' + unparseCommand(part.command) + ' )';
|
|
70
|
+
|
|
71
|
+
case 'backtickSubstitution':
|
|
72
|
+
return '`' + unparseCommand(part.command) + '`';
|
|
73
|
+
|
|
74
|
+
case 'arithmeticExpansion':
|
|
75
|
+
return '$((' + part.expression + '))';
|
|
76
|
+
|
|
77
|
+
case 'processSubstitution':
|
|
78
|
+
return part.direction + '(' + unparseCommand(part.command) + ')';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function unparseDoubleQuotedPart(part: BashWordPart): string {
|
|
83
|
+
switch (part.type) {
|
|
84
|
+
case 'literal': {
|
|
85
|
+
let result = '';
|
|
86
|
+
for (const ch of part.value) {
|
|
87
|
+
if (ch === '\\' || ch === '$' || ch === '`' || ch === '"') {
|
|
88
|
+
result += '\\' + ch;
|
|
89
|
+
} else {
|
|
90
|
+
result += ch;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
default:
|
|
98
|
+
return unparseWordPart(part);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function escapeLiteral(value: string): string {
|
|
103
|
+
let result = '';
|
|
104
|
+
for (const ch of value) {
|
|
105
|
+
if (' \t\n|&;<>()$`"\' \\'.includes(ch) || ch === '{' || ch === '}' || ch === '#') {
|
|
106
|
+
result += '\\' + ch;
|
|
107
|
+
} else {
|
|
108
|
+
result += ch;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function unparseRedirect(redirect: BashRedirect): string {
|
|
116
|
+
let result = '';
|
|
117
|
+
if (redirect.fd !== undefined) {
|
|
118
|
+
result += String(redirect.fd);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result += redirect.operator;
|
|
122
|
+
if ('type' in redirect.target && redirect.target.type === 'hereDoc') {
|
|
123
|
+
result += redirect.target.delimiter;
|
|
124
|
+
} else {
|
|
125
|
+
result += unparseWord(redirect.target as BashWord);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unparseAssignment(assignment: BashAssignment): string {
|
|
132
|
+
let result = assignment.name + '=';
|
|
133
|
+
if (assignment.value !== undefined) {
|
|
134
|
+
result += unparseWord(assignment.value);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function unparseSimpleCommand(cmd: BashSimpleCommand): string {
|
|
141
|
+
const parts: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (const assignment of cmd.assignments) {
|
|
144
|
+
parts.push(unparseAssignment(assignment));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (cmd.name !== undefined) {
|
|
148
|
+
parts.push(unparseWord(cmd.name));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const arg of cmd.args) {
|
|
152
|
+
parts.push(unparseWord(arg));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const wordParts = parts.join(' ');
|
|
156
|
+
const redirectParts = cmd.redirects.map(r => unparseRedirect(r)).join(' ');
|
|
157
|
+
|
|
158
|
+
if (redirectParts) {
|
|
159
|
+
return wordParts ? wordParts + ' ' + redirectParts : redirectParts;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return wordParts;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function unparseCommandUnit(unit: BashCommandUnit): string {
|
|
166
|
+
switch (unit.type) {
|
|
167
|
+
case 'simple':
|
|
168
|
+
return unparseSimpleCommand(unit);
|
|
169
|
+
|
|
170
|
+
case 'subshell':
|
|
171
|
+
return '(' + unparseCommand(unit.body) + ')';
|
|
172
|
+
|
|
173
|
+
case 'braceGroup':
|
|
174
|
+
return '{ ' + unparseCommand(unit.body) + ' }';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function unparsePipeline(pipeline: BashPipeline): string {
|
|
179
|
+
let result = '';
|
|
180
|
+
if (pipeline.negated) {
|
|
181
|
+
result += '! ';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
result += pipeline.commands.map(cmd => unparseCommandUnit(cmd)).join(' | ');
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function unparseCommand(command: BashCommand): string {
|
|
189
|
+
return unparseCommandList(command);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function unparseCommandList(list: BashCommandList): string {
|
|
193
|
+
let result = '';
|
|
194
|
+
for (let i = 0; i < list.entries.length; i++) {
|
|
195
|
+
const entry = list.entries[i]!;
|
|
196
|
+
if (i > 0) {
|
|
197
|
+
result += ' ';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
result += unparsePipeline(entry.pipeline);
|
|
201
|
+
if (entry.separator !== undefined) {
|
|
202
|
+
result += entry.separator;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const bashScriptUnparser: Unparser<BashCommand, string> = async function * (command) {
|
|
210
|
+
yield unparseCommand(command);
|
|
211
|
+
};
|
package/src/bsonParser.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { createTupleParser } from './tupleParser.js';
|
|
|
6
6
|
import { createSkipParser } from './skipParser.js';
|
|
7
7
|
import { createParserAccessorParser } from './parserAccessorParser.js';
|
|
8
8
|
import { createTerminatedArrayParser } from './terminatedArrayParser.js';
|
|
9
|
-
import { createElementParser } from './elementParser.js';
|
|
10
9
|
import { createExactElementParser } from './exactElementParser.js';
|
|
11
10
|
import { createUnionParser } from './unionParser.js';
|
|
12
11
|
import { parserCreatorCompose } from './parserCreatorCompose.js';
|
|
12
|
+
import { createPredicateElementParser } from './predicateElementParser.js';
|
|
13
13
|
|
|
14
14
|
const createFixedLengthBufferParser = (length: number): Parser<Buffer, Uint8Array> => promiseCompose(createFixedLengthSequenceParser<Uint8Array>(length), sequence => Buffer.from(sequence));
|
|
15
15
|
|
|
@@ -17,16 +17,13 @@ const buffer1Parser = createFixedLengthBufferParser(1);
|
|
|
17
17
|
const buffer4Parser = createFixedLengthBufferParser(4);
|
|
18
18
|
const buffer8Parser = createFixedLengthBufferParser(8);
|
|
19
19
|
|
|
20
|
-
const elementParser: Parser<number, Uint8Array> = createElementParser();
|
|
21
|
-
|
|
22
20
|
const nullByteParser: Parser<number, Uint8Array> = createExactElementParser(0);
|
|
23
21
|
|
|
22
|
+
const nonNullByteParser: Parser<number, Uint8Array> = createPredicateElementParser((byte: number) => byte !== 0);
|
|
23
|
+
|
|
24
24
|
const cstringParser: Parser<string, Uint8Array> = promiseCompose(
|
|
25
25
|
createTerminatedArrayParser(
|
|
26
|
-
|
|
27
|
-
() => elementParser,
|
|
28
|
-
(byte: number) => async parserContext => parserContext.invariant(byte, 'Expected non-null byte'),
|
|
29
|
-
)(),
|
|
26
|
+
nonNullByteParser,
|
|
30
27
|
nullByteParser,
|
|
31
28
|
),
|
|
32
29
|
([ sequence ]) => Buffer.from(sequence).toString('utf8'),
|
|
@@ -1,84 +1,37 @@
|
|
|
1
|
-
import invariant from 'invariant';
|
|
2
1
|
import { type Parser, setParserName } from '../parser.js';
|
|
3
|
-
import { type ParserContext } from '../parserContext.js';
|
|
4
2
|
import { promiseCompose } from '../promiseCompose.js';
|
|
5
3
|
import { createSeparatedArrayParser } from '../separatedArrayParser.js';
|
|
6
4
|
import { createExactSequenceParser } from '../exactSequenceParser.js';
|
|
7
5
|
import { createUnionParser } from '../unionParser.js';
|
|
8
6
|
import { createTupleParser } from '../tupleParser.js';
|
|
9
7
|
import { createArrayParser } from '../arrayParser.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|| (
|
|
38
|
-
character === '-'
|
|
39
|
-
)
|
|
40
|
-
|| (
|
|
41
|
-
character === '_'
|
|
42
|
-
)
|
|
43
|
-
|| (
|
|
44
|
-
character === '\u00A0'
|
|
45
|
-
)
|
|
46
|
-
|| (
|
|
47
|
-
character >= '\u00A1' && character <= '\u1FFF'
|
|
48
|
-
)
|
|
49
|
-
|| (
|
|
50
|
-
character >= '\u2000' && character <= '\u200A'
|
|
51
|
-
)
|
|
52
|
-
|| (
|
|
53
|
-
character >= '\u2010' && character <= '\u2027'
|
|
54
|
-
)
|
|
55
|
-
|| (
|
|
56
|
-
character === '\u202F'
|
|
57
|
-
)
|
|
58
|
-
|| (
|
|
59
|
-
character >= '\u2030' && character <= '\uD7FF'
|
|
60
|
-
)
|
|
61
|
-
|| (
|
|
62
|
-
character >= '\uE000' && character <= '\uFFEF'
|
|
63
|
-
)
|
|
64
|
-
|| (
|
|
65
|
-
character >= '\uD800' && character <= '\uDBFF'
|
|
66
|
-
)
|
|
67
|
-
) {
|
|
68
|
-
parserContext.skip(1);
|
|
69
|
-
|
|
70
|
-
characters.push(character);
|
|
71
|
-
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
parserContext.invariant(characters.length > 0, 'Expected at least one character');
|
|
76
|
-
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return characters.join('');
|
|
81
|
-
};
|
|
8
|
+
import { createNonEmptyArrayParser } from '../nonEmptyArrayParser.js';
|
|
9
|
+
import { createPredicateElementParser } from '../predicateElementParser.js';
|
|
10
|
+
|
|
11
|
+
function isSmaliSimpleNameChar(character: string): boolean {
|
|
12
|
+
return (
|
|
13
|
+
(character >= 'a' && character <= 'z')
|
|
14
|
+
|| (character >= 'A' && character <= 'Z')
|
|
15
|
+
|| (character >= '0' && character <= '9')
|
|
16
|
+
|| character === ' '
|
|
17
|
+
|| character === '$'
|
|
18
|
+
|| character === '-'
|
|
19
|
+
|| character === '_'
|
|
20
|
+
|| character === '\u00A0'
|
|
21
|
+
|| (character >= '\u00A1' && character <= '\u1FFF')
|
|
22
|
+
|| (character >= '\u2000' && character <= '\u200A')
|
|
23
|
+
|| (character >= '\u2010' && character <= '\u2027')
|
|
24
|
+
|| character === '\u202F'
|
|
25
|
+
|| (character >= '\u2030' && character <= '\uD7FF')
|
|
26
|
+
|| (character >= '\uE000' && character <= '\uFFEF')
|
|
27
|
+
|| (character >= '\uD800' && character <= '\uDBFF')
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const smaliSimpleNameParser: Parser<string, string> = promiseCompose(
|
|
32
|
+
createNonEmptyArrayParser(createPredicateElementParser(isSmaliSimpleNameChar)),
|
|
33
|
+
characters => characters.join(''),
|
|
34
|
+
);
|
|
82
35
|
|
|
83
36
|
setParserName(smaliSimpleNameParser, 'smaliSimpleNameParser');
|
|
84
37
|
|
|
@@ -13,6 +13,7 @@ import { createTupleParser } from './tupleParser.js';
|
|
|
13
13
|
import { createParserAccessorParser } from './parserAccessorParser.js';
|
|
14
14
|
import { createSkipToParser } from './skipToParser.js';
|
|
15
15
|
import { createLookaheadParser } from './lookaheadParser.js';
|
|
16
|
+
import { createPredicateElementParser } from './predicateElementParser.js';
|
|
16
17
|
import {
|
|
17
18
|
getIsoTypedNumberArray,
|
|
18
19
|
type IndexIntoFieldIds,
|
|
@@ -843,16 +844,9 @@ type DalvikExecutableTaggedEncodedValue =
|
|
|
843
844
|
| { type: 'boolean'; value: boolean };
|
|
844
845
|
|
|
845
846
|
const createByteWith5LeastSignificantBitsEqualParser = (leastSignificant5: number): Parser<number, Uint8Array> => {
|
|
846
|
-
const byteWith5LeastSignificantBitsEqualParser: Parser<number, Uint8Array> =
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
(byte & 0b0001_1111) === leastSignificant5,
|
|
850
|
-
'Expected byte with 5 least significant bits equal to %s, but got %s',
|
|
851
|
-
leastSignificant5.toString(2).padStart(8, '0'),
|
|
852
|
-
byte.toString(2).padStart(8, '0'),
|
|
853
|
-
);
|
|
854
|
-
return byte;
|
|
855
|
-
};
|
|
847
|
+
const byteWith5LeastSignificantBitsEqualParser: Parser<number, Uint8Array> = createPredicateElementParser(
|
|
848
|
+
(byte: number) => (byte & 0b0001_1111) === leastSignificant5,
|
|
849
|
+
);
|
|
856
850
|
|
|
857
851
|
setParserName(byteWith5LeastSignificantBitsEqualParser, `createByteWith5LeastSignificantBitsEqualParser(${leastSignificant5.toString(2).padStart(5, '0')})`);
|
|
858
852
|
|