Skip to content

Commit

Permalink
fix: reserve quotation marks when handling multiple values (#85)
Browse files Browse the repository at this point in the history
fixes #84
  • Loading branch information
cyjake authored Aug 9, 2024
1 parent 7034b4f commit 3dfb490
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 64 deletions.
53 changes: 33 additions & 20 deletions src/ssh-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface Directive {
after: string;
param: string;
separator: Separator;
value: string | string[];
value: string | { val: string, separator: string, quoted?: boolean }[];
quoted?: boolean;
}

Expand All @@ -34,7 +34,7 @@ export interface Section extends Directive {
}

export interface Match extends Section {
criteria: Record<string, string | string[]>
criteria: Record<string, string | { val: string, separator: string, quoted?: boolean }[]>
}

export interface Comment {
Expand Down Expand Up @@ -125,8 +125,9 @@ function match(criteria: Match['criteria'], context: ComputeContext): boolean {

for (const key in criteria) {
const criterion = criteria[key]
const values = Array.isArray(criterion) ? criterion.map(({ val }) => val) : criterion

if (!testCriterion(key, criterion)) {
if (!testCriterion(key, values)) {
return false
}
}
Expand Down Expand Up @@ -207,7 +208,7 @@ export default class SSHConfig extends Array<Line> {
const doPass = () => {
for (const line of this) {
if (line.type !== LineType.DIRECTIVE) continue
if (line.param === 'Host' && glob(line.value, context.params.Host)) {
if (line.param === 'Host' && glob(Array.isArray(line.value) ? line.value.map(({ val }) => val) : line.value, context.params.Host)) {
let canonicalizeHostName = false
let canonicalDomains: string[] = []
setProperty(line.param, line.value)
Expand All @@ -218,7 +219,7 @@ export default class SSHConfig extends Array<Line> {
canonicalizeHostName = true
}
if (/^CanonicalDomains$/i.test(subline.param) && Array.isArray(subline.value)) {
canonicalDomains = subline.value
canonicalDomains = subline.value.map(({ val }) => val)
}
}
}
Expand Down Expand Up @@ -333,7 +334,7 @@ export default class SSHConfig extends Array<Line> {
type: LineType.DIRECTIVE,
param,
separator: ' ',
value,
value: Array.isArray(value) ? value.map((val, i) => ({ val, separator: i === 0 ? '' : ' ' })) : value,
before: sectionLineFound ? indent : indent.replace(/ |\t/, ''),
after: '\n',
}
Expand Down Expand Up @@ -386,7 +387,7 @@ export default class SSHConfig extends Array<Line> {
type: LineType.DIRECTIVE,
param,
separator: ' ',
value,
value: Array.isArray(value) ? value.map((val, i) => ({ val, separator: i === 0 ? '' : ' ' })) : value,
before: '',
after: '\n',
}
Expand Down Expand Up @@ -530,8 +531,13 @@ export function parse(text: string): SSHConfig {
// Host * !local.dev
// Host "foo bar"
function values() {
const results: string[] = []
const results: { val: string, separator: string, quoted: boolean }[] = []
let val = ''
// whether current value is quoted or not
let valQuoted = false
// the separator preceding current value
let valSeparator = ' '
// whether current context is within quotations or not
let quoted = false
let escaped = false

Expand All @@ -548,11 +554,14 @@ export function parse(text: string): SSHConfig {
}
else if (quoted) {
val += chr
valQuoted = true
}
else if (/[ \t=]/.test(chr)) {
if (val) {
results.push(val)
results.push({ val, separator: valSeparator, quoted: valQuoted })
val = ''
valQuoted = false
valSeparator = chr
}
// otherwise ignore the space
}
Expand All @@ -567,10 +576,10 @@ export function parse(text: string): SSHConfig {
}

if (quoted || escaped) {
throw new Error(`Unexpected line break at ${results.concat(val).join(' ')}`)
throw new Error(`Unexpected line break at ${results.map(({ val }) => val).concat(val).join(' ')}`)
}
if (val) results.push(val)
return results.length > 1 ? results : results[0]
if (val) results.push({ val, separator: valSeparator, quoted: valQuoted })
return results.length > 1 ? results : results[0].val
}

function directive() {
Expand All @@ -592,12 +601,12 @@ export function parse(text: string): SSHConfig {
const criteria: Match['criteria'] = {}

if (typeof result.value === 'string') {
result.value = [result.value]
result.value = [{ val: result.value, separator: '', quoted: result.quoted }]
}

let i = 0
while (i < result.value.length) {
const keyword = result.value[i]
const { val: keyword } = result.value[i]

switch (keyword.toLowerCase()) {
case 'all':
Expand All @@ -610,7 +619,7 @@ export function parse(text: string): SSHConfig {
if (i + 1 >= result.value.length) {
throw new Error(`Missing value for match criteria ${keyword}`)
}
criteria[keyword] = result.value[i + 1]
criteria[keyword] = result.value[i + 1].val
i += 2
break
}
Expand Down Expand Up @@ -663,7 +672,11 @@ export function stringify(config: SSHConfig): string {

function formatValue(value: string | string[] | Record<string, any>, quoted: boolean) {
if (Array.isArray(value)) {
return value.map(chunk => formatValue(chunk, RE_SPACE.test(chunk))).join(' ')
let result = ''
for (const { val, separator, quoted } of value) {
result += (result ? separator : '') + formatValue(val, quoted || RE_SPACE.test(val))
}
return result
}
return quoted ? `"${value}"` : value
}
Expand All @@ -675,15 +688,15 @@ export function stringify(config: SSHConfig): string {
return `${line.param}${line.separator}${value}`
}

const format = line => {
const format = (line: Line) => {
str += line.before

if (line.type === LineType.COMMENT) {
str += line.content
}
else if (line.type === LineType.DIRECTIVE && MULTIPLE_VALUE_PROPS.includes(line.param)) {
[].concat(line.value).forEach(function (value, i, values) {
str += formatDirective({ ...line, value })
(Array.isArray(line.value) ? line.value : [line.value]).forEach((value, i, values) => {
str += formatDirective({ ...line, value: typeof value !== 'string' ? value.val : value })
if (i < values.length - 1) str += `\n${line.before}`
})
}
Expand All @@ -693,7 +706,7 @@ export function stringify(config: SSHConfig): string {

str += line.after

if (line.config) {
if ('config' in line) {
line.config.forEach(format)
}
}
Expand Down
9 changes: 9 additions & 0 deletions test/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const stripPattern = /^[ \t]*(?=[^\s]+)/mg

export function heredoc(text: string) {
const indentLen = text.match(stripPattern)!.reduce((min, line) => Math.min(min, line.length), Infinity)
const indent = new RegExp('^[ \\t]{' + indentLen + '}', 'mg')
return indentLen > 0
? text.replace(indent, '').trimStart().replace(/ +?$/, '')
: text
}
12 changes: 8 additions & 4 deletions test/unit/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ describe('parse', function() {
const config = parse('ProxyCommand ssh -W "%h:%p" firewall.example.org')
assert.equal(config[0].type, DIRECTIVE)
assert.equal(config[0].param, 'ProxyCommand')
assert.deepEqual(config[0].value, ['ssh', '-W', '%h:%p', 'firewall.example.org'])
assert.ok(Array.isArray(config[0].value))
assert.deepEqual(config[0].value.map(({ val }) => val), ['ssh', '-W', '%h:%p', 'firewall.example.org'])
})

// https://github.com/microsoft/vscode-remote-release/issues/5562
Expand All @@ -159,7 +160,8 @@ describe('parse', function() {
assert.ok('config' in config[0])
assert.equal(config[0].config[0].type, DIRECTIVE)
assert.equal(config[0].config[0].param, 'ProxyCommand')
assert.deepEqual(config[0].config[0].value, ['C:\\foo bar\\baz.exe', 'arg', 'arg', 'arg'])
assert.ok(Array.isArray(config[0].config[0].value))
assert.deepEqual(config[0].config[0].value.map(({ val }) => val), ['C:\\foo bar\\baz.exe', 'arg', 'arg', 'arg'])
})

it('.parse open ended values', function() {
Expand All @@ -180,7 +182,8 @@ describe('parse', function() {

assert.equal(config[0].type, DIRECTIVE)
assert.equal(config[0].param, 'Host')
assert.deepEqual(config[0].value, [
assert.ok(Array.isArray(config[0].value))
assert.deepEqual(config[0].value.map(({ val }) => val), [
'foo',
'!*.bar',
'baz ham',
Expand All @@ -192,7 +195,8 @@ describe('parse', function() {
const config = parse('Host me local wi*ldcard? thisVM "two words"')

assert.equal(config[0].type, DIRECTIVE)
assert.deepEqual(config[0].value, [
assert.ok(Array.isArray(config[0].value))
assert.deepEqual(config[0].value.map(({ val }) => val), [
'me',
'local',
'wi*ldcard?',
Expand Down
Loading

0 comments on commit 3dfb490

Please sign in to comment.