Skip to content

Commit

Permalink
Fix quoted and escaped whitespace for label and env directives
Browse files Browse the repository at this point in the history
  • Loading branch information
emosbaugh committed Sep 13, 2016
1 parent 8ae5531 commit df14ccd
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 39 deletions.
48 changes: 23 additions & 25 deletions lib/checks.js
@@ -1,3 +1,5 @@
var parser = require('./parser');

var commands = module.exports = {
expose_container_port_only: function(args) {
var ports = args.match(/\S+/g);
Expand All @@ -16,14 +18,14 @@ var commands = module.exports = {
},

label_format: function(args) {
var labels = args.match(/\S+/g);
// format LABEL <key>=<value> <key>=<value> <key>=<value> ...
// we do not support the old format LABEL <key> <value>
var result = [];
labels.forEach(function(label) {
if (label.split('=').length !== 2) {
result.push('label_invalid');
}
});

try {
parser.nameVal(args);
} catch(e) {
result.push('label_invalid');
}
return result;
},

Expand Down Expand Up @@ -132,26 +134,22 @@ var commands = module.exports = {
},

is_valid_env: function(args) {
// Dockerfile syntax is either a=b c=d (no internal spaces) or a b c d (for spaces)
var p = args.match(/\S+/g);

// if there is not a = in the first element, we shoud assume it's the second format above
if (!p[0].includes('=')) {
var words = parser.words(args);
if (words.length === 0) {
return [];
}
// format ENV <key> <value>
if (!words[0].includes('=')) {
return [];
} else {
var result = [];
var vars = args.match(/\S+/g);
vars.forEach(function(v) {
if (!v.includes('=')) {
result.push('invalid_format');
}
if (v.match(/\S+/g).length > 1) {
result.push('invalid_format');
}
});

return result;
}
// format ENV <key>=<value> ...
var result = [];
try {
parser.nameVal(args);
} catch(e) {
result.push('invalid_format');
}
return result;
},

is_valid_shell: function(args) {
Expand Down
113 changes: 113 additions & 0 deletions lib/parser.js
@@ -0,0 +1,113 @@
// ported from github.com/docker/docker
// https://github.com/docker/docker/blob/e95b6b51daed868094c7b66113381d5088e831b4/builder/dockerfile/parser/line_parsers.go

var parser = module.exports = {
nameVal: function(rest) {
var nameVal = {};

var words = parser.words(rest);
if (words.length === 0) {
return nameVal;
}

// Old style is currently not supported
// Old format (KEY name value)
// if (!words[0].includes('=')) {
// var strs = rest.split(/\s+/g);
// if (strs.length !== 2) {
// throw new Error("must have exactly two arguments");
// }
// nameVal[strs[0]] = strs[1];
// return nameVal;
// }

for (var i = 0; i < words.length; i++) {
var word = words[i];
if (!word.includes('=')) {
throw new Error("Syntax error - can't find = in " + word + ". Must be of the form: name=value");
}
var parts = word.split('=');
nameVal[parts.shift()] = parts.join('=');
}
return nameVal;
},

words: function(rest) {
var inSpaces = 0, inWord = 1, inQuote = 2;
var words = [];
var phase = inSpaces;
var word = '';
var quote;
var blankOK = false;
var ch;

for (var pos = 0, len = rest.length; pos <= len; pos++) {
if (pos !== len) {
ch = rest[pos];
}

if (phase === inSpaces) { // Looking for start of word
if (pos === len) { // end of input
break;
}
if (/\s/.test(ch)) { // skip spaces
continue;
}
phase = inWord; // found it, fall through
}
if ((phase === inWord || phase === inQuote) && (pos === len)) {
if (blankOK || word.length > 0) {
words.push(word);
}
break;
}
if (phase === inWord) {
if (/\s/.test(ch)) {
phase = inSpaces;
if (blankOK || word.length > 0) {
words.push(word);
}
word = '';
blankOK = false;
continue;
}
if (ch === '\'' || ch === '"') {
quote = ch;
blankOK = true;
phase = inQuote;
}
if (ch === '\\') {
if (pos+1 === len) {
continue; // just skip an escape token at end of line
}
// If we're not quoted and we see an escape token, then always just
// add the escape token plus the char to the word, even if the char
// is a quote.
word += ch;
pos += 1;
ch = rest[pos];
}
word += ch;
continue;
}
if (phase === inQuote) {
if (ch === quote) {
phase = inWord;
}
// The escape token is special except for ' quotes - can't escape anything for '
if (ch === '\\' && quote !== '\'') {
if (pos+1 === len) {
phase = inWord;
continue; // just skip the escape token at end
}
pos += 1
word += ch;
ch = rest[pos];
}
word += ch;
}
}

return words;
}
};
10 changes: 9 additions & 1 deletion test/checks.js
Expand Up @@ -21,10 +21,18 @@ describe("checks", function(){

describe("#label_format(args)", function(){
it("validates label command in key=value format", function(){
expect(checks.label_format("")).to.be.empty;
expect(checks.label_format("key=value")).to.be.empty;
expect(checks.label_format("key=value key=value")).to.be.empty;
expect(checks.label_format("namespace.key=value")).to.be.empty;
expect(checks.label_format("key=\"value value\"")).to.be.empty;
expect(checks.label_format("key=value value")).to.have.length(1);
expect(checks.label_format("key key=value")).to.have.length(1);
expect(checks.label_format("key=value\\ value")).to.be.empty;
expect(checks.label_format("key=")).to.be.empty;
expect(checks.label_format("key= ")).to.be.empty;
expect(checks.label_format("key")).to.have.length(1);
expect(checks.label_format("key value")).to.have.length(2);
expect(checks.label_format("key value")).to.have.length(1);
});
});

Expand Down
6 changes: 3 additions & 3 deletions test/cli_reporter.js
Expand Up @@ -98,7 +98,7 @@ describe('cli_reporter', () => {
expect(Object.keys(reporter.fileReports)).to.have.length(1);
let fileReport = reporter.fileReports[file];
expect(fileReport.uniqueIssues).to.equal(3);
expect(fileReport.contentArray).to.have.length(35);
expect(fileReport.contentArray).to.have.length(36);
expect(fileReport.itemsByLine).to.deep.equal({
'5': [ items[0] ],
'6': items.splice(1)
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('cli_reporter', () => {
expect(Object.keys(reporter.fileReports)).to.have.length(1);
let fileReport = reporter.fileReports[file];
expect(fileReport.uniqueIssues).to.equal(1);
expect(fileReport.contentArray).to.have.length(35);
expect(fileReport.contentArray).to.have.length(36);
expect(fileReport.itemsByLine).to.deep.equal({
'6': [ items[0] ]
});
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('cli_reporter', () => {
});
let file2Report = reporter.fileReports[file2];
expect(file2Report.uniqueIssues).to.equal(2);
expect(file2Report.contentArray).to.have.length(35);
expect(file2Report.contentArray).to.have.length(36);
expect(file2Report.itemsByLine).to.deep.equal({
'5': [ file2Items[0] ],
'6': [ file2Items[1] ]
Expand Down
5 changes: 3 additions & 2 deletions test/examples/Dockerfile.misc
Expand Up @@ -20,8 +20,9 @@ RUN apk add --no-cache python-dev build-base wget && apk del python-dev build-ba
EXPOSE 80a
EXPOSE 80:80
VOLUME /tmp
ENV testsomething=test valuye
LABEL test test
ENV testsomething=test value
LABEL test="value value" test='value value' test=value\ value
LABEL test value
COPY ./test /tmp
USER ubuntu ubuntu
WORKDIR in valid
Expand Down
13 changes: 5 additions & 8 deletions test/index.js
Expand Up @@ -159,22 +159,19 @@ describe("index", function(){
line: 23 },
{ title: 'Label Is Invalid',
rule: 'label_invalid',
line: 24 },
{ title: 'Label Is Invalid',
rule: 'label_invalid',
line: 24 },
line: 25 },
{ title: 'Extra Arguments',
rule: 'extra_args',
line: 26 },
line: 27 },
{ title: 'Invalid WORKDIR',
rule: 'invalid_workdir',
line: 27 },
line: 28 },
{ title: 'Invalid ADD Source',
rule: 'add_src_invalid',
line: 31 },
line: 32 },
{ title: 'Invalid ADD Destination',
rule: 'add_dest_invalid',
line: 31 }
line: 32 }
];

var result = dockerfilelint.run('./test/examples', fs.readFileSync('./test/examples/Dockerfile.misc', 'UTF-8'));
Expand Down
28 changes: 28 additions & 0 deletions test/parser.js
@@ -0,0 +1,28 @@
var expect = require('chai').expect
var parser = require('../lib/parser.js')

describe('parser', function() {
describe("#words(rest)", function() {
it("splits words", function(){
expect(parser.words("one two")).to.have.length(2)
.and.to.eql(['one', 'two']);
});

it("does not split quotes", function(){
expect(parser.words("one \"two three\" 'four five'")).to.have.length(3)
.and.to.eql(['one', "\"two three\"", "'four five'"]);
});

it("honor escape characters", function(){
expect(parser.words("one two\\ thre\\e \"fou\\r\"")).to.have.length(3)
.and.to.eql(['one', 'two\\ thre\\e', '"fou\\r"']);
});

it("ignore escape character at line end", function(){
expect(parser.words("one two\\")).to.have.length(2)
.and.to.eql(['one', 'two']);
expect(parser.words("one \"two\\")).to.have.length(2)
.and.to.eql(['one', '\"two']);
});
});
});

0 comments on commit df14ccd

Please sign in to comment.