Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.12.0 2025-08-29

- replaced dependency 'request' with built-in fetch() API
- replaced child_process.exec() with execFile()
- require Node >= 20

## 0.11.0 2025-08-11
Expand Down
21 changes: 0 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"set-value": "^4.1.0",
"shell-quote": "^1.8.3",
"source-map": "^0.7.4",
"tslib": "^2.6.3",
"winston": "^3.13.0",
Expand All @@ -39,7 +38,6 @@
"@types/js-yaml": "^3.12.1",
"@types/json-pointer": "^1.0.30",
"@types/node": "^20.0.0",
"@types/shell-quote": "^1.7.5",
"@types/yargs": "^13.0.0",
"eslint": "^8.57.0",
"jest": "^29.7.0",
Expand Down
75 changes: 30 additions & 45 deletions src/lib/validators/openApiDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@
// Licensed under the MIT License. See License.txt in the project root for license information.

import * as asyncFs from "@ts-common/fs"
import * as jsonParser from "@ts-common/json-parser"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated unused import

import { getFilePosition } from "@ts-common/source-map"
import * as child_process from "child_process"
import * as fs from "fs"
import JSON_Pointer from "json-pointer"
import * as jsonRefs from "json-refs"
import * as os from "os"
import * as path from "path"
import { quote } from "shell-quote"
import * as sourceMap from "source-map"
import * as util from "util"
import { log } from "../util/logging"
import { ResolveSwagger } from "../util/resolveSwagger"
import { pathToJsonPointer } from "../util/utils"
const _ = require("lodash")

const exec = util.promisify(child_process.exec)
const execFile = util.promisify(child_process.execFile)

export type Options = {
readonly consoleLogLevel?: unknown
Expand Down Expand Up @@ -83,28 +81,6 @@ const updateChangeProperties = (change: ChangeProperties, pf: ProcessedFile): Ch
}
}

/**
* Safely escapes shell arguments for cross-platform compatibility
* @param arg The argument to escape
* @returns The safely escaped argument
*/
function escapeShellArg(arg: string): string {
if (typeof arg !== "string") {
throw new Error("Argument must be a string")
}

if (process.platform === "win32") {
// For Windows cmd.exe, wrap in double quotes and escape internal quotes
// This handles paths with spaces and special characters safely
// Double quotes are escaped by doubling them in Windows
return `"${arg.replace(/"/g, '""')}"`
} else {
// On Unix-like systems, use shell-quote for proper escaping
// shell-quote handles all edge cases including spaces, special chars, etc.
return quote([arg])
}
}

/**
* @class
* Open API Diff class.
Expand Down Expand Up @@ -165,19 +141,19 @@ export class OpenApiDiff {
}

/**
* Gets path to the autorest application.
* Gets file and args to the autorest application.
*
* @returns {string} Path to the autorest app.js file.
* @returns {{ file: string; args: string[] }} File and args to the autorest app.js file.
*/
public autoRestPath(): string {
log.silly(`autoRestPath is being called`)
public autoRestFileArgs(): { file: string; args: string[] } {
log.silly(`autoRestFileArgs is being called`)

// When oad is installed globally
{
const result = path.join(__dirname, "..", "..", "..", "node_modules", "autorest", "dist", "app.js")
if (fs.existsSync(result)) {
log.silly(`Found autoRest:${result} `)
return `node ${escapeShellArg(result)}`
return { file: "node", args: [result] }
}
}

Expand All @@ -186,7 +162,7 @@ export class OpenApiDiff {
const result = path.join(__dirname, "..", "..", "..", "..", "..", "autorest", "dist", "app.js")
if (fs.existsSync(result)) {
log.silly(`Found autoRest:${result} `)
return `node ${escapeShellArg(result)}`
return { file: "node", args: [result] }
}
}

Expand All @@ -195,12 +171,12 @@ export class OpenApiDiff {
const result = path.resolve("node_modules/.bin/autorest")
if (fs.existsSync(result)) {
log.silly(`Found autoRest:${result} `)
return escapeShellArg(result)
return { file: result, args: [] }
}
}

// Assume that autorest is in the path
return "autorest"
return { file: "autorest", args: [] }
}

/**
Expand All @@ -211,7 +187,7 @@ export class OpenApiDiff {
public openApiDiffDllPath(): string {
log.silly(`openApiDiffDllPath is being called`)

return escapeShellArg(path.join(__dirname, "..", "..", "..", "dlls", "OpenApiDiff.dll"))
return path.join(__dirname, "..", "..", "..", "dlls", "OpenApiDiff.dll")
}

/**
Expand Down Expand Up @@ -250,16 +226,24 @@ export class OpenApiDiff {
const outputFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), "oad-"))
const outputFilePath = path.join(outputFolder, `${outputFileName}.json`)
const outputMapFilePath = path.join(outputFolder, `${outputFileName}.map`)
// Cross-platform shell argument escaping - behavior is validated in shellEscapingTest.ts
const autoRestCmd = tagName
? `${this.autoRestPath()} ${escapeShellArg(swaggerPath)} --v2 --tag=${escapeShellArg(tagName)} --output-artifact=swagger-document.json` +
` --output-artifact=swagger-document.map --output-file=${escapeShellArg(outputFileName)} --output-folder=${escapeShellArg(outputFolder)}`
: `${this.autoRestPath()} --v2 --input-file=${escapeShellArg(swaggerPath)} --output-artifact=swagger-document.json` +
` --output-artifact=swagger-document.map --output-file=${escapeShellArg(outputFileName)} --output-folder=${escapeShellArg(outputFolder)}`

log.debug(`Executing: "${autoRestCmd}"`)
const { file: autoRestFile, args: autoRestArgs } = this.autoRestFileArgs()

const swaggerArgs = tagName ? [swaggerPath, `--tag=${tagName}`] : [`--input-file=${swaggerPath}`]

const commonArgs = [
"--v2",
"--output-artifact=swagger-document.json",
"--output-artifact=swagger-document.map",
`--output-file=${outputFileName}`,
`--output-folder=${outputFolder}`
]

const args = [...autoRestArgs, ...swaggerArgs, ...commonArgs]

log.debug(`Executing: "${autoRestFile} ${args.join(" ")}"`)

const { stderr } = await exec(autoRestCmd, {
const { stderr } = await execFile(autoRestFile, args, {
encoding: "utf8",
maxBuffer: 1024 * 1024 * 64,
env: { ...process.env, NODE_OPTIONS: "--max-old-space-size=8192" }
Expand Down Expand Up @@ -319,10 +303,11 @@ export class OpenApiDiff {
throw new Error(`File "${newSwagger}" not found.`)
}

const cmd = `${this.dotNetPath()} ${this.openApiDiffDllPath()} -o ${oldSwagger} -n ${newSwagger}`
const file = this.dotNetPath()
const args = [this.openApiDiffDllPath(), "-o", oldSwagger, "-n", newSwagger]

log.debug(`Executing: "${cmd}"`)
const { stdout } = await exec(cmd, { encoding: "utf8", maxBuffer: 1024 * 1024 * 64 })
log.debug(`Executing: "${file} ${args.join(" ")}"`)
const { stdout } = await execFile(file, args, { encoding: "utf8", maxBuffer: 1024 * 1024 * 64 })
const resultJson = JSON.parse(stdout) as Messages

const updatedJson = resultJson.map(message => ({
Expand Down
116 changes: 0 additions & 116 deletions src/test/shellEscapingTest.ts

This file was deleted.

Loading