From 26859df1d0ed1ab51bb852cc6eb185abbeaa27b2 Mon Sep 17 00:00:00 2001 From: Dan Allen Date: Fri, 2 Aug 2024 15:05:24 -0600 Subject: [PATCH] resolve #30 set windowsVerbatimArguments: false option on spawn instead of running command through shell on Windows --- CHANGELOG.adoc | 6 + .../lib/util/parse-command.js | 48 ++++ .../lib/util/run-command.js | 62 +---- packages/collector-extension/package.json | 3 + .../test/collector-extension-test.js | 73 +++--- .../.mvn/wrapper/maven-wrapper.properties | 18 ++ .../test-batch-script/docs/antora.yml | 8 + .../test/fixtures/test-batch-script/mvnw | 239 ++++++++++++++++++ .../test/fixtures/test-batch-script/mvnw.cmd | 145 +++++++++++ .../test/fixtures/test-batch-script/pom.xml | 41 +++ .../test/parse-command-test.js | 139 ++++++++++ 11 files changed, 691 insertions(+), 91 deletions(-) create mode 100644 packages/collector-extension/lib/util/parse-command.js create mode 100644 packages/collector-extension/test/fixtures/test-batch-script/.mvn/wrapper/maven-wrapper.properties create mode 100644 packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml create mode 100755 packages/collector-extension/test/fixtures/test-batch-script/mvnw create mode 100644 packages/collector-extension/test/fixtures/test-batch-script/mvnw.cmd create mode 100644 packages/collector-extension/test/fixtures/test-batch-script/pom.xml create mode 100644 packages/collector-extension/test/parse-command-test.js diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 23cdd24..625b921 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -19,6 +19,12 @@ For a detailed view of what's changed, refer to the {url-repo}/commits[commit hi === Changed * do not rewrite `node` argument at start of command (#29) +* set `windowsVerbatimArguments: false` option on spawn instead of running command through shell on Windows (#30) + +=== Fixed + +* preserve empty quoted arguments in command (#30) +* normalize non-quoted spaces in command (#30) == 1.0.0-alpha.7 (2024-07-24) diff --git a/packages/collector-extension/lib/util/parse-command.js b/packages/collector-extension/lib/util/parse-command.js new file mode 100644 index 0000000..3455d7b --- /dev/null +++ b/packages/collector-extension/lib/util/parse-command.js @@ -0,0 +1,48 @@ +'use strict' + +const QUOTE_RX = /["']/ +const SPACES_RX = / +/ + +const parseCommand = (cmd, { preserveQuotes } = {}) => { + if (!QUOTE_RX.test((cmd = cmd.trim()))) return cmd.split(SPACES_RX) + const chars = [...cmd] + const lastIdx = chars.length - 1 + return chars.reduce( + (accum, c, idx) => { + const { tokens, token, quotes } = accum + if (c === "'" || c === '"') { + const quote = quotes.get() + if (quote) { + if (c === quote) { + if (token[token.length - 1] === '\\') { + token[token.length - 1] = c + } else { + if (preserveQuotes) token.push(c) + tokens.push(token.length ? token.join('') : '') + token.length = quotes.clear() || 0 + } + } else { + token.push(c) + } + } else { + if (preserveQuotes) token.push(c) + quotes.set(undefined, c) + } + } else if (c === ' ') { + if (quotes.get()) { + token.push(c) + } else if (token.length) { + tokens.push(token.join('')) + token.length = 0 + } + } else { + token.push(c) + } + if (idx === lastIdx && token.length) tokens.push(token.join('')) + return accum + }, + { tokens: [], token: [], quotes: new Map() } + ).tokens +} + +module.exports = parseCommand diff --git a/packages/collector-extension/lib/util/run-command.js b/packages/collector-extension/lib/util/run-command.js index 2a0844f..9a825a9 100644 --- a/packages/collector-extension/lib/util/run-command.js +++ b/packages/collector-extension/lib/util/run-command.js @@ -4,11 +4,10 @@ const fs = require('node:fs') const { promises: fsp } = fs const ospath = require('node:path') const { PassThrough } = require('node:stream') +const parseCommand = require('./parse-command') const { spawn } = require('node:child_process') const IS_WIN = process.platform === 'win32' -const DBL_QUOTE_RX = /"/g -const QUOTE_RX = /["']/ // adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT class LazyReadable extends PassThrough { @@ -29,58 +28,9 @@ const fileExists = (p) => () => false ) -const parseCommand = (cmd) => { - if (!QUOTE_RX.test(cmd)) return cmd.split(' ') - const chars = [...cmd] - const lastIdx = chars.length - 1 - return chars.reduce( - (accum, c, idx) => { - const { tokens, token, quotes } = accum - if (c === "'" || c === '"') { - if (quotes.get()) { - if (quotes.get() === c) { - if (token[token.length - 1] === '\\') { - token.pop() - token.push(c) - } else { - if (token.length) tokens.push(token.join('')) - token.length = quotes.clear() || 0 - } - } else { - token.push(c) - } - } else { - quotes.set(undefined, c) - } - } else if (c === ' ') { - if (quotes.get()) { - token.push(c) - } else if (token.length) { - tokens.push(token.join('')) - token.length = 0 - } - } else { - token.push(c) - } - if (idx === lastIdx && token.length) tokens.push(token.join('')) - return accum - }, - { tokens: [], token: [], quotes: new Map() } - ).tokens -} - -const winShellEscape = (val) => { - if (val.charAt() === '-') return val - if (~val.indexOf('"')) { - return ~val.indexOf(' ') ? `"${val.replace(DBL_QUOTE_RX, '"""')}"` : val.replace(DBL_QUOTE_RX, '""') - } - if (~val.indexOf(' ')) return `"${val}"` - return val -} - -async function runCommand (cmd, argv = [], opts = {}) { - if (!cmd) throw new TypeError('Command not specified') - let cmdv = parseCommand(cmd) +async function runCommand (cmd = '', argv = [], opts = {}) { + const cmdv = parseCommand(String(cmd)) + if (!cmdv.length) throw new TypeError('Command not specified') const { input, output, quiet, implicitStdin, local, ...spawnOpts } = opts if (input) input instanceof Buffer ? implicitStdin || argv.push('-') : argv.push(input) if (IS_WIN) { @@ -92,9 +42,7 @@ async function runCommand (cmd, argv = [], opts = {}) { if ((await fileExists(absCmd0 + (ext = '.bat'))) || (await fileExists(absCmd0 + (ext = '.cmd')))) cmdv[0] += ext } } - cmdv = cmdv.map(winShellEscape) - argv = argv.map(winShellEscape) - Object.assign(spawnOpts, { shell: true, windowsHide: true }) + Object.assign(spawnOpts, { windowsHide: true, windowsVerbatimArguments: false }) } else if (local) { cmdv[0] = `./${cmdv[0]}` } diff --git a/packages/collector-extension/package.json b/packages/collector-extension/package.json index 62b2348..2c3c099 100644 --- a/packages/collector-extension/package.json +++ b/packages/collector-extension/package.json @@ -22,6 +22,9 @@ ".": "./lib/index.js", "./package.json": "./package.json" }, + "imports": { + "#parse-command": "./lib/util/parse-command.js" + }, "files": [ "lib" ], diff --git a/packages/collector-extension/test/collector-extension-test.js b/packages/collector-extension/test/collector-extension-test.js index 3373f5c..d1c1f8d 100644 --- a/packages/collector-extension/test/collector-extension-test.js +++ b/packages/collector-extension/test/collector-extension-test.js @@ -652,6 +652,21 @@ describe('collector extension', () => { }) }) + if (windows) { + it('should run batch script on Windows', async () => { + await runScenario({ + repoName: 'test-batch-script', + startPath: 'docs', + after: (contentAggregate) => { + expect(contentAggregate).to.have.lengthOf(1) + const bucket = contentAggregate[0] + expect(bucket.name).to.equal('test') + expect(bucket.version).to.equal('1.0') + }, + }) + }) + } + it('should run specified command from content root if dir option is .', async () => { const collectorConfig = { run: { command: '.gen-start-page', local: true, dir: '.' }, @@ -1719,22 +1734,19 @@ describe('collector extension', () => { expect(stderr).to.equal('there were some issues\n') }) - if (!windows) { - // FIXME reenable on Windows - it('should escape quoted string inside quoted string in command', async () => { - const collectorConfig = { run: { command: '$NODE -p \'"start\\nall done!"\'' } } - const stdout = await captureStdout(() => - runScenario({ - repoName: 'test-at-root', - collectorConfig, - before: (_, playbook) => { - delete playbook.runtime.quiet - }, - }) - ) - expect(stdout).to.equal('start\nall done!\n') - }) - } + it('should escape quoted string inside quoted string in command', async () => { + const collectorConfig = { run: { command: '$NODE -p \'"start\\nall done!"\'' } } + const stdout = await captureStdout(() => + runScenario({ + repoName: 'test-at-root', + collectorConfig, + before: (_, playbook) => { + delete playbook.runtime.quiet + }, + }) + ) + expect(stdout).to.equal('start\nall done!\n') + }) it('should not send output from command to stdout by default if quiet is set', async () => { const collectorConfig = { run: { command: '$NODE .gen-output.js' } } @@ -1744,10 +1756,9 @@ describe('collector extension', () => { it('should throw error if command is not found', async () => { const collectorConfig = { run: { command: 'no-such-command' } } - const expectedMessage = windows - ? 'Command failed' - : '(@antora/collector-extension): Command not found: no-such-command in ' + - `http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` + const expectedMessage = + '(@antora/collector-extension): Command not found: no-such-command in ' + + `http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` expect(await trapAsyncError(() => runScenario({ repoName: 'test-at-root', collectorConfig }))).to.throw( Error, expectedMessage @@ -1756,10 +1767,9 @@ describe('collector extension', () => { it('should throw error if command is not found for origin with start path', async () => { const collectorConfig = { run: { command: 'no-such-command' } } - const expectedMessage = windows - ? 'Command failed' - : '(@antora/collector-extension): Command not found: no-such-command in ' + - `http://localhost:${gitServerPort}/test-at-start-path/.git (branch: main | start path: docs)` + const expectedMessage = + '(@antora/collector-extension): Command not found: no-such-command in ' + + `http://localhost:${gitServerPort}/test-at-start-path/.git (branch: main | start path: docs)` expect( await trapAsyncError(() => runScenario({ repoName: 'test-at-start-path', collectorConfig, startPath: 'docs' })) ).to.throw(Error, expectedMessage) @@ -1767,10 +1777,9 @@ describe('collector extension', () => { it('should throw error if command is not found when using local worktree', async () => { const collectorConfig = { run: { command: 'no-such-command' } } - const expectedMessage = windows - ? 'Command failed' - : '(@antora/collector-extension): Command not found: no-such-command in ' + - `${ospath.join(REPOS_DIR, 'test-at-root')} (branch: main )` + const expectedMessage = + '(@antora/collector-extension): Command not found: no-such-command in ' + + `${ospath.join(REPOS_DIR, 'test-at-root')} (branch: main )` expect( await trapAsyncError(() => runScenario({ repoName: 'test-at-root', collectorConfig, local: true })) ).to.throw(Error, expectedMessage) @@ -1795,9 +1804,7 @@ describe('collector extension', () => { it('should log error if command fails when on_failure is log', async () => { const collectorConfig = { run: { command: 'no-such-command', on_failure: 'log' } } - const expectedMessage = windows - ? 'Command failed' - : `Command not found: no-such-command in http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` + const expectedMessage = `Command not found: no-such-command in http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` expect(await trapAsyncError(() => runScenario({ repoName: 'test-at-root', collectorConfig }))).to.not.throw() expect(messages).to.have.lengthOf(1) const message = messages[0] @@ -1809,9 +1816,7 @@ describe('collector extension', () => { it('should log at specified level if command fails when on_failure is log.warn', async () => { const collectorConfig = { run: { command: 'no-such-command', on_failure: 'log.warn' } } - const expectedMessage = windows - ? 'Command failed' - : `Command not found: no-such-command in http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` + const expectedMessage = `Command not found: no-such-command in http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` expect(await trapAsyncError(() => runScenario({ repoName: 'test-at-root', collectorConfig }))).to.not.throw() expect(messages).to.have.lengthOf(1) const message = messages[0] diff --git a/packages/collector-extension/test/fixtures/test-batch-script/.mvn/wrapper/maven-wrapper.properties b/packages/collector-extension/test/fixtures/test-batch-script/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..346d645 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-batch-script/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml b/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml new file mode 100644 index 0000000..e98bbb7 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml @@ -0,0 +1,8 @@ +name: '${project.artifactId}' +version: '${project.version}' +ext: + collector: + run: + command: mvnw process-resources + local: true + scan: target/docs diff --git a/packages/collector-extension/test/fixtures/test-batch-script/mvnw b/packages/collector-extension/test/fixtures/test-batch-script/mvnw new file mode 100755 index 0000000..633bbb7 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-batch-script/mvnw @@ -0,0 +1,239 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +(CYGWIN*|MINGW*) [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ] ; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$('set' +e; 'unset' -f command 2>/dev/null; 'command' -v java)" || : + JAVACCMD="$('set' +e; 'unset' -f command 2>/dev/null; 'command' -v javac)" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ] ; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + h=$(( ( h * 31 + $(LC_CTYPE=C printf %d "'$str") ) % 4294967296 )) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl="${value-}" ;; + distributionSha256Sum) distributionSha256Sum="${value-}" ;; + esac +done < "${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + + +case "${distributionUrl##*/}" in +(maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + (*AMD64:CYGWIN*|*AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + (:Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + (:Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + (:Linux*x86_64*) distributionPlatform=linux-amd64 ;; + (*) echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +(maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +(*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +(*?-bin.zip|*?maven-mvnd-?*-?*.zip) ;; +(*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget > /dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl > /dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat > "$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( new java.net.URL( args[0] ).openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum > /dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c > /dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c > /dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip > /dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" +fi +printf %s\\n "$distributionUrl" > "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/packages/collector-extension/test/fixtures/test-batch-script/mvnw.cmd b/packages/collector-extension/test/fixtures/test-batch-script/mvnw.cmd new file mode 100644 index 0000000..dd02e16 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-batch-script/mvnw.cmd @@ -0,0 +1,145 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/packages/collector-extension/test/fixtures/test-batch-script/pom.xml b/packages/collector-extension/test/fixtures/test-batch-script/pom.xml new file mode 100644 index 0000000..977be94 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-batch-script/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + com.example + test + 1.0 + pom + + UTF-8 + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + generate-docs + + copy-resources + + process-resources + + ${project.build.directory}/docs + + + docs + + antora.yml + + true + + + + + + + + + diff --git a/packages/collector-extension/test/parse-command-test.js b/packages/collector-extension/test/parse-command-test.js new file mode 100644 index 0000000..62fa4e1 --- /dev/null +++ b/packages/collector-extension/test/parse-command-test.js @@ -0,0 +1,139 @@ +/* eslint-env mocha */ +'use strict' + +const { expect } = require('@antora/collector-test-harness') +const parseCommand = require('#parse-command') + +describe('parseCommand', () => { + describe('without quoted argument', () => { + it('should parse single command', () => { + const command = 'ls' + const expected = ['ls'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should parse command with single argument', () => { + const command = 'antora antora-playbook.yml' + const expected = ['antora', 'antora-playbook.yml'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should parse command with multiple arguments', () => { + const command = 'npx antora antora-playbook.yml' + const expected = ['npx', 'antora', 'antora-playbook.yml'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should ignore extra spaces around arguments', () => { + const command = 'npx antora antora-playbook.yml ' + const expected = ['npx', 'antora', 'antora-playbook.yml'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should parse command with multiple arguments', () => { + const command = 'npx antora antora-playbook.yml' + const expected = ['npx', 'antora', 'antora-playbook.yml'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + }) + + describe('with quoted argument', () => { + it('should parse command with double-quoted argument', () => { + const command = 'echo "foo bar"' + const expected = ['echo', 'foo bar'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should preserve repeating spaces in double-quoted argument', () => { + const command = 'echo "left right"' + const expected = ['echo', 'left right'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should parse command with single-quoted argument', () => { + const command = "echo 'foo bar'" + const expected = ['echo', 'foo bar'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should preserve repeating spaces in single-quoted argument', () => { + const command = "echo 'left right'" + const expected = ['echo', 'left right'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should parse command with double-quoted argument that contains single quote', () => { + const command = 'echo "let\'s go!"' + const expected = ['echo', "let's go!"] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should parse command with single-quoted argument that contains double quotes', () => { + const command = 'echo \'"quoted"\'' + const expected = ['echo', '"quoted"'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should allow double-quoted string to include double quote by escaping it', () => { + const command = 'echo "use \\" to quote"' + const expected = ['echo', 'use " to quote'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should allow single-quoted string to include single quote by escaping it', () => { + const command = "echo 'use \\' to quote'" + const expected = ['echo', "use ' to quote"] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should preserve empty value enclosed in double quotes', () => { + const command = 'echo "" indented' + const expected = ['echo', '', 'indented'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should preserve empty value enclosed in single quotes', () => { + const command = "echo '' indented" + const expected = ['echo', '', 'indented'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + + it('should ignore extra spaces between arguments when one of the arguments is quoted', () => { + const command = 'npx antora "my playbook.yml" ' + const expected = ['npx', 'antora', 'my playbook.yml'] + const actual = parseCommand(command) + expect(actual).to.eql(expected) + }) + }) + + describe('preserveQuotes: true', () => { + it('should preserve quotes around double-quoted argument', () => { + const command = 'antora --title "My Site" antora-playbook.yml' + const expected = ['antora', '--title', '"My Site"', 'antora-playbook.yml'] + const actual = parseCommand(command, { preserveQuotes: true }) + expect(actual).to.eql(expected) + }) + + it('should preserve quotes around single-quoted argument', () => { + const command = "antora --title 'My Site' antora-playbook.yml" + const expected = ['antora', '--title', "'My Site'", 'antora-playbook.yml'] + const actual = parseCommand(command, { preserveQuotes: true }) + expect(actual).to.eql(expected) + }) + }) +}) -- GitLab