Skip to content

Commit c62036f

Browse files
authored
fix: align implementation with most recent specs (#9)
Upstream: arduino/arduino-cli#2460 Upstream: arduino/arduino-cli#2509 Closes: #8 Signed-off-by: dankeboy36 <[email protected]>
1 parent 6664828 commit c62036f

File tree

4 files changed

+81
-15
lines changed

4 files changed

+81
-15
lines changed

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@
88
"typescript.tsc.autoDetect": "off",
99
"typescript.tsdk": "./node_modules/typescript/lib",
1010
"editor.codeActionsOnSave": {
11-
"source.fixAll.eslint": true
11+
"source.fixAll.eslint": "explicit"
1212
}
1313
}

README.md

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,9 @@
22

33
Arduino FQBN (fully qualified board name)
44

5-
```
6-
VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]
7-
```
8-
9-
> ℹ️ [What's the FQBN string?](https://arduino.github.io/arduino-cli/latest/FAQ/#whats-the-fqbn-string)
5+
> ℹ️ [What's the FQBN string?](https://arduino.github.io/arduino-cli/dev/FAQ/#whats-the-fqbn-string)
106
11-
> ℹ️ Check the `{build.fqbn}` entry in the Arduino [Platform specification](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties) for more details.
7+
> FQBN stands for Fully Qualified Board Name. It has the following format: `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]`, with each `MENU_ID=OPTION_ID` being an optional key-value pair configuration. Each field accepts letters (`A-Z` or `a-z`), numbers (`0-9`), underscores (`_`), dashes(`-`) and dots(`.`). The special character `=` is accepted in the configuration value. For a deeper understanding of how FQBN works, you should understand the [Arduino platform specification](https://arduino.github.io/arduino-cli/dev/platform-specification/).
128
139
## Install
1410

src/__tests__/index.spec.ts

+51-3
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,35 @@ describe('fqbn', () => {
1111
assert.ok(valid('a:b:c:o1=v1'));
1212
});
1313

14+
it('should be OK with config option value is empty', () => {
15+
assert.ok(valid('a:b:c:o1='));
16+
});
17+
1418
it('should be OK with multiple config options', () => {
1519
assert.ok(valid('a:b:c:o1=v1,o2=v2'));
1620
});
1721

22+
it("should be OK with multiple equal ('=') signs in the value part", () => {
23+
assert.ok(valid('a:b:c:o1=v1='));
24+
});
25+
26+
it('should be OK with empty vendor', () => {
27+
assert.ok(valid(':avr:uno'));
28+
});
29+
30+
it('should be OK with empty arch', () => {
31+
assert.ok(valid('arduino::uno'));
32+
});
33+
34+
it('should be OK with empty vendor and arch', () => {
35+
assert.ok(valid('::uno'));
36+
});
37+
1838
it('should fail when invalid', () => {
1939
assert.strictEqual(valid('invalid'), undefined);
2040
});
2141

22-
it('should fail when has trailing comma', () => {
42+
it('should fail when config key value is empty', () => {
2343
assert.strictEqual(valid('a:b:c:'), undefined);
2444
});
2545

@@ -32,13 +52,21 @@ describe('fqbn', () => {
3252
});
3353

3454
it('should fail when invalid config options syntax', () => {
35-
assert.strictEqual(valid('a:b:c:o1=v1='), undefined);
55+
assert.strictEqual(valid('a:b:c:o1=v1*'), undefined);
3656
});
3757

3858
it('should fail when contains duplicate config options', () => {
3959
assert.strictEqual(valid('a:b:c:o1=v1,o1=v2'), undefined);
4060
});
4161

62+
it('should fail when has trailing comma (no config options)', () => {
63+
assert.strictEqual(valid('a:b:c,'), undefined);
64+
});
65+
66+
it('should fail when has trailing comma (with config options)', () => {
67+
assert.strictEqual(valid('a:b:c:o1=v1,'), undefined);
68+
});
69+
4270
it('should fail when config options has trailing comma', () => {
4371
assert.strictEqual(valid('a:b:c:o1=v1,o2=v2,'), undefined);
4472
});
@@ -47,6 +75,10 @@ describe('fqbn', () => {
4775
assert.strictEqual(valid('a:b:c:o1=v1,,o2=v2'), undefined);
4876
});
4977

78+
it('should fail when config option key is empty', () => {
79+
assert.strictEqual(valid('a:b:c:=v1'), undefined);
80+
});
81+
5082
it('should rethrow unhandled errors', () => {
5183
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5284
const invalid: any = undefined;
@@ -64,6 +96,22 @@ describe('fqbn', () => {
6496
assert.strictEqual(fqbn.options, undefined);
6597
});
6698

99+
[
100+
'ardui_no:av_r:un_o',
101+
'arduin.o:av.r:un.o',
102+
'arduin-o:av-r:un-o',
103+
'arduin-o:av-r:un-o:a=b=c=d',
104+
].map((fqbn) =>
105+
it(`should create: ${fqbn}`, () =>
106+
assert.doesNotThrow(() => new FQBN(fqbn)))
107+
);
108+
109+
['arduin-o:av-r:un=o', 'arduin?o:av-r:uno', 'arduino:av*r:uno'].map(
110+
(fqbn) =>
111+
it(`should not create: ${fqbn}`, () =>
112+
assert.throws(() => new FQBN(fqbn)))
113+
);
114+
67115
it('should create with a config option', () => {
68116
const fqbn = new FQBN('a:b:c:o1=v1');
69117
assert.strictEqual(fqbn.vendor, 'a');
@@ -88,7 +136,7 @@ describe('fqbn', () => {
88136
});
89137

90138
it('should error when invalid config options syntax', () => {
91-
assert.throws(() => new FQBN('a:b:c:o1='), /ConfigOptionError: .*/);
139+
assert.throws(() => new FQBN('a:b:c:=v1'), /ConfigOptionError: .*/);
92140
});
93141

94142
it('should error when has duplicate config options', () => {

src/index.ts

+27-5
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,42 @@ export class FQBN {
2626
readonly options?: Readonly<ConfigOptions>;
2727

2828
constructor(fqbn: string) {
29-
const [vendor, arch, boardId, rest] = fqbn.split(':');
30-
if (!vendor || !arch || !boardId) {
29+
const fqbnSegments = fqbn.split(':');
30+
if (fqbnSegments.length < 3 || fqbnSegments.length > 4) {
31+
throw new InvalidFQBNError(fqbn);
32+
}
33+
for (let i = 0; i < 3; i++) {
34+
if (!/^[a-zA-Z0-9_.-]*$/.test(fqbnSegments[i])) {
35+
throw new InvalidFQBNError(fqbn);
36+
}
37+
}
38+
const [vendor, arch, boardId, rest] = fqbnSegments;
39+
if (!boardId) {
3140
throw new InvalidFQBNError(fqbn);
3241
}
3342

3443
const options: Record<string, string> = {};
3544
if (typeof rest === 'string') {
3645
const tuples = rest.split(',');
3746
for (const tuple of tuples) {
38-
const [key, value, unexpected] = tuple.split('=');
39-
if (!key || !value || typeof unexpected === 'string') {
47+
const configSegments = tuple.split('=', 2);
48+
if (configSegments.length !== 2) {
49+
throw new ConfigOptionError(
50+
fqbn,
51+
`Invalid config option: '${tuple}'`
52+
);
53+
}
54+
const [key, value] = configSegments;
55+
if (!/^[a-zA-Z0-9_.-]+$/.test(key)) {
56+
throw new ConfigOptionError(
57+
fqbn,
58+
`Invalid config option key: '${key}' (${tuple})`
59+
);
60+
}
61+
if (!/^[a-zA-Z0-9=_.-]*$/.test(value)) {
4062
throw new ConfigOptionError(
4163
fqbn,
42-
`Invalid config option syntax: '${tuple}'`
64+
`Invalid config option value: '${value}' (${tuple})`
4365
);
4466
}
4567
const existingValue = options[key];

0 commit comments

Comments
 (0)