mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-09 18:38:33 +00:00
fix: removed unsafe shell when executing process
Ref: PNX-3671 Co-authored-by: per1234 <accounts@perglass.com> Co-authored-by: Akos Kitta <a.kitta@arduino.cc> Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
@@ -43,19 +43,13 @@ class SilentArduinoDaemonImpl extends ArduinoDaemonImpl {
|
||||
}
|
||||
|
||||
private async initCliConfig(): Promise<string> {
|
||||
const cliPath = await this.getExecPath();
|
||||
const cliPath = this.getExecPath();
|
||||
const destDir = track.mkdirSync();
|
||||
await spawnCommand(`"${cliPath}"`, [
|
||||
'config',
|
||||
'init',
|
||||
'--dest-dir',
|
||||
destDir,
|
||||
]);
|
||||
await spawnCommand(cliPath, ['config', 'init', '--dest-dir', destDir]);
|
||||
const content = fs.readFileSync(path.join(destDir, CLI_CONFIG), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const cliConfig = safeLoad(content) as any;
|
||||
// cliConfig.daemon.port = String(this.port);
|
||||
const cliConfig = safeLoad(content);
|
||||
const modifiedContent = safeDump(cliConfig);
|
||||
fs.writeFileSync(path.join(destDir, CLI_CONFIG), modifiedContent, {
|
||||
encoding: 'utf8',
|
||||
|
||||
162
arduino-ide-extension/src/test/node/clang-formatter.test.ts
Normal file
162
arduino-ide-extension/src/test/node/clang-formatter.test.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
Disposable,
|
||||
DisposableCollection,
|
||||
} from '@theia/core/lib/common/disposable';
|
||||
import { FileUri } from '@theia/core/lib/node/file-uri';
|
||||
import { expect } from 'chai';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import temp from 'temp';
|
||||
import {
|
||||
clangFormatFilename,
|
||||
ClangFormatter,
|
||||
} from '../../node/clang-formatter';
|
||||
import { spawnCommand } from '../../node/exec-util';
|
||||
import { createBaseContainer, startDaemon } from './test-bindings';
|
||||
|
||||
const unformattedContent = `void setup ( ) { pinMode(LED_BUILTIN, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite( LED_BUILTIN , HIGH );
|
||||
delay( 1000 ) ;
|
||||
digitalWrite( LED_BUILTIN , LOW);
|
||||
delay ( 1000 ) ;
|
||||
}
|
||||
`;
|
||||
const formattedContent = `void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(LED_BUILTIN, HIGH);
|
||||
delay(1000);
|
||||
digitalWrite(LED_BUILTIN, LOW);
|
||||
delay(1000);
|
||||
}
|
||||
`;
|
||||
|
||||
type ClangStyleValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| ClangStyleValue[]
|
||||
| { [key: string]: ClangStyleValue };
|
||||
type ClangConfiguration = Record<string, ClangStyleValue>;
|
||||
|
||||
export interface ClangStyle {
|
||||
readonly key: string;
|
||||
readonly value: ClangStyleValue;
|
||||
}
|
||||
|
||||
const singleClangStyles: ClangStyle[] = [
|
||||
{
|
||||
key: 'SpacesBeforeTrailingComments',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
key: 'SortIncludes',
|
||||
value: 'Never',
|
||||
},
|
||||
{
|
||||
key: 'AlignTrailingComments',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: 'IfMacros',
|
||||
value: ['KJ_IF_MAYBE'],
|
||||
},
|
||||
{
|
||||
key: 'SpacesInLineCommentPrefix',
|
||||
value: {
|
||||
Minimum: 0,
|
||||
Maximum: -1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function expectNoChanges(
|
||||
formatter: ClangFormatter,
|
||||
styleArg: string
|
||||
): Promise<void> {
|
||||
const minimalContent = `
|
||||
void setup() {}
|
||||
void loop() {}
|
||||
`.trim();
|
||||
const execPath = formatter['execPath']();
|
||||
const actual = await spawnCommand(
|
||||
execPath,
|
||||
['-style', styleArg],
|
||||
console.error,
|
||||
minimalContent
|
||||
);
|
||||
expect(actual).to.be.equal(minimalContent);
|
||||
}
|
||||
|
||||
describe('clang-formatter', () => {
|
||||
let tracked: typeof temp;
|
||||
let formatter: ClangFormatter;
|
||||
let toDispose: DisposableCollection;
|
||||
|
||||
before(async () => {
|
||||
tracked = temp.track();
|
||||
toDispose = new DisposableCollection(
|
||||
Disposable.create(() => tracked.cleanupSync())
|
||||
);
|
||||
const container = await createBaseContainer({
|
||||
additionalBindings: (bind) =>
|
||||
bind(ClangFormatter).toSelf().inSingletonScope(),
|
||||
});
|
||||
await startDaemon(container, toDispose);
|
||||
formatter = container.get<ClangFormatter>(ClangFormatter);
|
||||
});
|
||||
|
||||
after(() => toDispose.dispose());
|
||||
|
||||
singleClangStyles
|
||||
.map((style) => ({
|
||||
...style,
|
||||
styleArg: JSON.stringify({ [style.key]: style.value }),
|
||||
}))
|
||||
.map(({ value, styleArg }) =>
|
||||
it(`should execute the formatter with a single ${
|
||||
Array.isArray(value) ? 'array' : typeof value
|
||||
} type style configuration value: ${styleArg}`, async () => {
|
||||
await expectNoChanges(formatter, styleArg);
|
||||
})
|
||||
);
|
||||
|
||||
it('should execute the formatter with a multiple clang formatter styles', async () => {
|
||||
const styleArg = JSON.stringify(
|
||||
singleClangStyles.reduce((config, curr) => {
|
||||
config[curr.key] = curr.value;
|
||||
return config;
|
||||
}, {} as ClangConfiguration)
|
||||
);
|
||||
await expectNoChanges(formatter, styleArg);
|
||||
});
|
||||
|
||||
it('should format with the default styles', async () => {
|
||||
const actual = await formatter.format({
|
||||
content: unformattedContent,
|
||||
formatterConfigFolderUris: [],
|
||||
});
|
||||
expect(actual).to.be.equal(formattedContent);
|
||||
});
|
||||
|
||||
it('should format with custom formatter configuration file', async () => {
|
||||
const tempPath = tracked.mkdirSync();
|
||||
await fs.writeFile(
|
||||
path.join(tempPath, clangFormatFilename),
|
||||
'SpaceInEmptyParentheses: true',
|
||||
{
|
||||
encoding: 'utf8',
|
||||
}
|
||||
);
|
||||
const actual = await formatter.format({
|
||||
content: 'void foo() {}',
|
||||
formatterConfigFolderUris: [FileUri.create(tempPath).toString()],
|
||||
});
|
||||
expect(actual).to.be.equal('void foo( ) {}');
|
||||
});
|
||||
});
|
||||
@@ -1,33 +1,164 @@
|
||||
import * as os from 'node:os';
|
||||
import { expect, use } from 'chai';
|
||||
import { getExecPath } from '../../node/exec-util';
|
||||
import { assert, expect } from 'chai';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
ArduinoBinaryName,
|
||||
BinaryName,
|
||||
ClangBinaryName,
|
||||
getExecPath,
|
||||
spawnCommand,
|
||||
} from '../../node/exec-util';
|
||||
import temp from 'temp';
|
||||
|
||||
use(require('chai-string'));
|
||||
describe('exec-utils', () => {
|
||||
describe('spawnCommand', () => {
|
||||
let tracked: typeof temp;
|
||||
|
||||
describe('getExecPath', () => {
|
||||
it('should resolve arduino-cli', async () => {
|
||||
const actual = await getExecPath('arduino-cli', onError, 'version');
|
||||
const expected =
|
||||
os.platform() === 'win32' ? '\\arduino-cli.exe' : '/arduino-cli';
|
||||
expect(actual).to.endsWith(expected);
|
||||
before(() => {
|
||||
tracked = temp.track();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (tracked) {
|
||||
tracked.cleanupSync();
|
||||
}
|
||||
});
|
||||
|
||||
it("should execute the command without 'shell:true' even if the path contains spaces but is not escaped", async () => {
|
||||
const segment = 'with some spaces';
|
||||
const cliPath = getExecPath('arduino-cli');
|
||||
const filename = path.basename(cliPath);
|
||||
const tempPath = tracked.mkdirSync();
|
||||
const tempPathWitSpaces = path.join(tempPath, segment);
|
||||
fs.mkdirSync(tempPathWitSpaces, { recursive: true });
|
||||
const cliCopyPath = path.join(tempPathWitSpaces, filename);
|
||||
fs.copyFileSync(cliPath, cliCopyPath);
|
||||
expect(fs.accessSync(cliCopyPath, fs.constants.X_OK)).to.be.undefined;
|
||||
expect(cliCopyPath.includes(segment)).to.be.true;
|
||||
const stdout = await spawnCommand(cliCopyPath, ['version']);
|
||||
expect(stdout.includes(filename)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve arduino-language-server', async () => {
|
||||
const actual = await getExecPath('arduino-language-server');
|
||||
const expected =
|
||||
os.platform() === 'win32'
|
||||
? '\\arduino-language-server.exe'
|
||||
: '/arduino-language-server';
|
||||
expect(actual).to.endsWith(expected);
|
||||
});
|
||||
describe('getExecPath', () => {
|
||||
type AssertOutput = (stdout: string) => void;
|
||||
|
||||
it('should resolve clangd', async () => {
|
||||
const actual = await getExecPath('clangd', onError, '--version');
|
||||
const expected = os.platform() === 'win32' ? '\\clangd.exe' : '/clangd';
|
||||
expect(actual).to.endsWith(expected);
|
||||
});
|
||||
interface GetExecPathTestSuite {
|
||||
readonly name: BinaryName;
|
||||
readonly flags?: string[];
|
||||
readonly assertOutput: AssertOutput;
|
||||
/**
|
||||
* The Arduino LS repository is not as shiny as the CLI or the firmware uploader.
|
||||
* It does not support `version` flag either, so non-zero exit is expected.
|
||||
*/
|
||||
readonly expectNonZeroExit?: boolean;
|
||||
}
|
||||
|
||||
function onError(error: Error): void {
|
||||
console.error(error);
|
||||
}
|
||||
const binaryNameToVersionMapping: Record<BinaryName, string> = {
|
||||
'arduino-cli': 'cli',
|
||||
'arduino-language-server': 'languageServer',
|
||||
'arduino-fwuploader': 'fwuploader',
|
||||
clangd: 'clangd',
|
||||
'clang-format': 'clangd',
|
||||
};
|
||||
|
||||
function readVersionFromPackageJson(name: BinaryName): string {
|
||||
const raw = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', '..', 'package.json'),
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
const json = JSON.parse(raw);
|
||||
expect(json.arduino).to.be.not.undefined;
|
||||
const mappedName = binaryNameToVersionMapping[name];
|
||||
expect(mappedName).to.be.not.undefined;
|
||||
const version = json.arduino[mappedName].version;
|
||||
expect(version).to.be.not.undefined;
|
||||
return version;
|
||||
}
|
||||
|
||||
function createTaskAssert(name: ArduinoBinaryName): AssertOutput {
|
||||
const version = readVersionFromPackageJson(name);
|
||||
if (typeof version === 'string') {
|
||||
return (stdout: string) => {
|
||||
expect(stdout.includes(name)).to.be.true;
|
||||
expect(stdout.includes(version)).to.be.true;
|
||||
expect(stdout.includes('git-snapshot')).to.be.false;
|
||||
};
|
||||
}
|
||||
return (stdout: string) => {
|
||||
expect(stdout.includes(name)).to.be.true;
|
||||
expect(stdout.includes('git-snapshot')).to.be.true;
|
||||
};
|
||||
}
|
||||
|
||||
function createClangdAssert(name: ClangBinaryName): AssertOutput {
|
||||
const version = readVersionFromPackageJson(name);
|
||||
return (stdout: string) => {
|
||||
expect(stdout.includes(name)).to.be.true;
|
||||
expect(stdout.includes(`version ${version}`)).to.be.true;
|
||||
};
|
||||
}
|
||||
|
||||
const suites: GetExecPathTestSuite[] = [
|
||||
{
|
||||
name: 'arduino-cli',
|
||||
flags: ['version'],
|
||||
assertOutput: createTaskAssert('arduino-cli'),
|
||||
},
|
||||
{
|
||||
name: 'arduino-fwuploader',
|
||||
flags: ['version'],
|
||||
assertOutput: createTaskAssert('arduino-fwuploader'),
|
||||
},
|
||||
{
|
||||
name: 'arduino-language-server',
|
||||
assertOutput: (stderr: string) => {
|
||||
expect(stderr.includes('Path to ArduinoCLI config file must be set.'))
|
||||
.to.be.true;
|
||||
},
|
||||
expectNonZeroExit: true,
|
||||
},
|
||||
{
|
||||
name: 'clangd',
|
||||
flags: ['--version'],
|
||||
assertOutput: createClangdAssert('clangd'),
|
||||
},
|
||||
{
|
||||
name: 'clang-format',
|
||||
flags: ['--version'],
|
||||
assertOutput: createClangdAssert('clang-format'),
|
||||
},
|
||||
];
|
||||
|
||||
// This is not a functional test but it ensures all executables provided by IDE2 are tested.
|
||||
it('should cover all provided executables', () => {
|
||||
expect(suites.length).to.be.equal(
|
||||
Object.keys(binaryNameToVersionMapping).length
|
||||
);
|
||||
});
|
||||
|
||||
suites.map((suite) =>
|
||||
it(`should resolve '${suite.name}'`, async () => {
|
||||
const execPath = getExecPath(suite.name);
|
||||
expect(execPath).to.be.not.undefined;
|
||||
expect(execPath).to.be.not.empty;
|
||||
expect(fs.accessSync(execPath, fs.constants.X_OK)).to.be.undefined;
|
||||
if (suite.expectNonZeroExit) {
|
||||
try {
|
||||
await spawnCommand(execPath, suite.flags ?? []);
|
||||
assert.fail('Expected a non-zero exit code');
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceOf(Error);
|
||||
const stderr = (<Error>err).message;
|
||||
expect(stderr).to.be.not.undefined;
|
||||
expect(stderr).to.be.not.empty;
|
||||
suite.assertOutput(stderr);
|
||||
}
|
||||
} else {
|
||||
const stdout = await spawnCommand(execPath, suite.flags ?? []);
|
||||
suite.assertOutput(stdout);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user