diff options
Diffstat (limited to 'libexec/nuageinit/yaml.lua')
-rw-r--r-- | libexec/nuageinit/yaml.lua | 586 |
1 files changed, 586 insertions, 0 deletions
diff --git a/libexec/nuageinit/yaml.lua b/libexec/nuageinit/yaml.lua new file mode 100644 index 000000000000..8ab17b15eed7 --- /dev/null +++ b/libexec/nuageinit/yaml.lua @@ -0,0 +1,586 @@ +-- SPDX-License-Identifier: MIT +-- +-- Copyright (c) 2017 Dominic Letz dominicletz@exosite.com + +local table_print_value +table_print_value = function(value, indent, done) + indent = indent or 0 + done = done or {} + if type(value) == "table" and not done [value] then + done [value] = true + + local list = {} + for key in pairs (value) do + list[#list + 1] = key + end + table.sort(list, function(a, b) return tostring(a) < tostring(b) end) + local last = list[#list] + + local rep = "{\n" + local comma + for _, key in ipairs (list) do + if key == last then + comma = '' + else + comma = ',' + end + local keyRep + if type(key) == "number" then + keyRep = key + else + keyRep = string.format("%q", tostring(key)) + end + rep = rep .. string.format( + "%s[%s] = %s%s\n", + string.rep(" ", indent + 2), + keyRep, + table_print_value(value[key], indent + 2, done), + comma + ) + end + + rep = rep .. string.rep(" ", indent) -- indent it + rep = rep .. "}" + + done[value] = false + return rep + elseif type(value) == "string" then + return string.format("%q", value) + else + return tostring(value) + end +end + +local table_print = function(tt) + print('return '..table_print_value(tt)) +end + +local table_clone = function(t) + local clone = {} + for k,v in pairs(t) do + clone[k] = v + end + return clone +end + +local string_trim = function(s, what) + what = what or " " + return s:gsub("^[" .. what .. "]*(.-)["..what.."]*$", "%1") +end + +local push = function(stack, item) + stack[#stack + 1] = item +end + +local pop = function(stack) + local item = stack[#stack] + stack[#stack] = nil + return item +end + +local context = function (str) + if type(str) ~= "string" then + return "" + end + + str = str:sub(0,25):gsub("\n","\\n"):gsub("\"","\\\""); + return ", near \"" .. str .. "\"" +end + +local Parser = {} +function Parser.new (self, tokens) + self.tokens = tokens + self.parse_stack = {} + self.refs = {} + self.current = 0 + return self +end + +local exports = {version = "1.2"} + +local word = function(w) return "^("..w..")([%s$%c])" end + +local tokens = { + {"comment", "^#[^\n]*"}, + {"indent", "^\n( *)"}, + {"space", "^ +"}, + {"true", word("enabled"), const = true, value = true}, + {"true", word("true"), const = true, value = true}, + {"true", word("yes"), const = true, value = true}, + {"true", word("on"), const = true, value = true}, + {"false", word("disabled"), const = true, value = false}, + {"false", word("false"), const = true, value = false}, + {"false", word("no"), const = true, value = false}, + {"false", word("off"), const = true, value = false}, + {"null", word("null"), const = true, value = nil}, + {"null", word("Null"), const = true, value = nil}, + {"null", word("NULL"), const = true, value = nil}, + {"null", word("~"), const = true, value = nil}, + {"id", "^\"([^\"]-)\" *(:[%s%c])"}, + {"id", "^'([^']-)' *(:[%s%c])"}, + {"string", "^\"([^\"]-)\"", force_text = true}, + {"string", "^'([^']-)'", force_text = true}, + {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)"}, + {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)"}, + {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)"}, + {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)"}, + {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)"}, + {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)"}, + {"doc", "^%-%-%-[^%c]*"}, + {",", "^,"}, + {"string", "^%b{} *[^,%c]+", noinline = true}, + {"{", "^{"}, + {"}", "^}"}, + {"string", "^%b[] *[^,%c]+", noinline = true}, + {"[", "^%["}, + {"]", "^%]"}, + {"-", "^%-", noinline = true}, + {":", "^:"}, + {"pipe", "^(|)(%d*[+%-]?)", sep = "\n"}, + {"pipe", "^(>)(%d*[+%-]?)", sep = " "}, + {"id", "^([%w][%w %-_]*)(:[%s%c])"}, + {"string", "^[^%c]+", noinline = true}, + {"string", "^[^,%]}%c ]+"} +}; +exports.tokenize = function (str) + local token + local row = 0 + local ignore + local indents = 0 + local lastIndents + local stack = {} + local indentAmount = 0 + local inline = false + str = str:gsub("\r\n","\010") + + while #str > 0 do + for i in ipairs(tokens) do + local captures = {} + if not inline or tokens[i].noinline == nil then + captures = {str:match(tokens[i][2])} + end + + if #captures > 0 then + captures.input = str:sub(0, 25) + token = table_clone(tokens[i]) + token[2] = captures + local str2 = str:gsub(tokens[i][2], "", 1) + token.raw = str:sub(1, #str - #str2) + str = str2 + + if token[1] == "{" or token[1] == "[" then + inline = true + elseif token.const then + -- Since word pattern contains last char we're re-adding it + str = token[2][2] .. str + token.raw = token.raw:sub(1, #token.raw - #token[2][2]) + elseif token[1] == "id" then + -- Since id pattern contains last semi-colon we're re-adding it + str = token[2][2] .. str + token.raw = token.raw:sub(1, #token.raw - #token[2][2]) + -- Trim + token[2][1] = string_trim(token[2][1]) + elseif token[1] == "string" then + -- Finding numbers + local snip = token[2][1] + if not token.force_text then + if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then + token[1] = "number" + end + end + + elseif token[1] == "comment" then + ignore = true; + elseif token[1] == "indent" then + row = row + 1 + inline = false + lastIndents = indents + if indentAmount == 0 then + indentAmount = #token[2][1] + end + + if indentAmount ~= 0 then + indents = (#token[2][1] / indentAmount); + else + indents = 0 + end + + if indents == lastIndents then + ignore = true; + elseif indents > lastIndents + 2 then + error("SyntaxError: invalid indentation, got " .. tostring(indents) + .. " instead of " .. tostring(lastIndents) .. context(token[2].input)) + elseif indents > lastIndents + 1 then + push(stack, token) + elseif indents < lastIndents then + local input = token[2].input + token = {"dedent", {"", input = ""}} + token.input = input + while lastIndents > indents + 1 do + lastIndents = lastIndents - 1 + push(stack, token) + end + end + end -- if token[1] == XXX + token.row = row + break + end -- if #captures > 0 + end + + if not ignore then + if token then + push(stack, token) + token = nil + else + error("SyntaxError " .. context(str)) + end + end + + ignore = false; + end + + return stack +end + +Parser.peek = function (self, offset) + offset = offset or 1 + return self.tokens[offset + self.current] +end + +Parser.advance = function (self) + self.current = self.current + 1 + return self.tokens[self.current] +end + +Parser.advanceValue = function (self) + return self:advance()[2][1] +end + +Parser.accept = function (self, type) + if self:peekType(type) then + return self:advance() + end +end + +Parser.expect = function (self, type, msg) + return self:accept(type) or + error(msg .. context(self:peek()[1].input)) +end + +Parser.expectDedent = function (self, msg) + return self:accept("dedent") or (self:peek() == nil) or + error(msg .. context(self:peek()[2].input)) +end + +Parser.peekType = function (self, val, offset) + return self:peek(offset) and self:peek(offset)[1] == val +end + +Parser.ignore = function (self, items) + local advanced + repeat + advanced = false + for _,v in pairs(items) do + if self:peekType(v) then + self:advance() + advanced = true + end + end + until advanced == false +end + +Parser.ignoreSpace = function (self) + self:ignore{"space"} +end + +Parser.ignoreWhitespace = function (self) + self:ignore{"space", "indent", "dedent"} +end + +Parser.parse = function (self) + + local ref = nil + if self:peekType("string") and not self:peek().force_text then + local char = self:peek()[2][1]:sub(1,1) + if char == "&" then + ref = self:peek()[2][1]:sub(2) + self:advanceValue() + self:ignoreSpace() + elseif char == "*" then + ref = self:peek()[2][1]:sub(2) + return self.refs[ref] + end + end + + local result + local c = { + indent = self:accept("indent") and 1 or 0, + token = self:peek() + } + push(self.parse_stack, c) + + if c.token[1] == "doc" then + result = self:parseDoc() + elseif c.token[1] == "-" then + result = self:parseList() + elseif c.token[1] == "{" then + result = self:parseInlineHash() + elseif c.token[1] == "[" then + result = self:parseInlineList() + elseif c.token[1] == "id" then + result = self:parseHash() + elseif c.token[1] == "string" then + result = self:parseString("\n") + elseif c.token[1] == "timestamp" then + result = self:parseTimestamp() + elseif c.token[1] == "number" then + result = tonumber(self:advanceValue()) + elseif c.token[1] == "pipe" then + result = self:parsePipe() + elseif c.token.const == true then + self:advanceValue(); + result = c.token.value + else + error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input)) + end + + pop(self.parse_stack) + while c.indent > 0 do + c.indent = c.indent - 1 + local term = "term "..c.token[1]..": '"..c.token[2][1].."'" + self:expectDedent("last ".. term .." is not properly dedented") + end + + if ref then + self.refs[ref] = result + end + return result +end + +Parser.parseDoc = function (self) + self:accept("doc") + return self:parse() +end + +Parser.inline = function (self) + local current = self:peek(0) + if not current then + return {}, 0 + end + + local inline = {} + local i = 0 + + while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do + inline[self:peek(i)[1]] = true + i = i - 1 + end + return inline, -i +end + +Parser.isInline = function (self) + local _, i = self:inline() + return i > 0 +end + +Parser.parent = function(self, level) + level = level or 1 + return self.parse_stack[#self.parse_stack - level] +end + +Parser.parentType = function(self, type, level) + return self:parent(level) and self:parent(level).token[1] == type +end + +Parser.parseString = function (self) + if self:isInline() then + local result = self:advanceValue() + + --[[ + - a: this looks + flowing: but is + no: string + --]] + local types = self:inline() + if types["id"] and types["-"] then + if not self:peekType("indent") or not self:peekType("indent", 2) then + return result + end + end + + --[[ + a: 1 + b: this is + a flowing string + example + c: 3 + --]] + if self:peekType("indent") then + self:expect("indent", "text block needs to start with indent") + local addtl = self:accept("indent") + + result = result .. "\n" .. self:parseTextBlock("\n") + + self:expectDedent("text block ending dedent missing") + if addtl then + self:expectDedent("text block ending dedent missing") + end + end + return result + else + --[[ + a: 1 + b: + this is also + a flowing string + example + c: 3 + --]] + return self:parseTextBlock("\n") + end +end + +Parser.parsePipe = function (self) + local pipe = self:expect("pipe") + self:expect("indent", "text block needs to start with indent") + local result = self:parseTextBlock(pipe.sep) + self:expectDedent("text block ending dedent missing") + return result +end + +Parser.parseTextBlock = function (self, sep) + local token = self:advance() + local result = string_trim(token.raw, "\n") + local indents = 0 + while self:peek() ~= nil and ( indents > 0 or not self:peekType("dedent") ) do + local newtoken = self:advance() + while token.row < newtoken.row do + result = result .. sep + token.row = token.row + 1 + end + if newtoken[1] == "indent" then + indents = indents + 1 + elseif newtoken[1] == "dedent" then + indents = indents - 1 + else + result = result .. string_trim(newtoken.raw, "\n") + end + end + return result +end + +Parser.parseHash = function (self, hash) + hash = hash or {} + local indents = 0 + + if self:isInline() then + local id = self:advanceValue() + self:expect(":", "expected semi-colon after id") + self:ignoreSpace() + if self:accept("indent") then + indents = indents + 1 + hash[id] = self:parse() + else + hash[id] = self:parse() + if self:accept("indent") then + indents = indents + 1 + end + end + self:ignoreSpace(); + end + + while self:peekType("id") do + local id = self:advanceValue() + self:expect(":","expected semi-colon after id") + self:ignoreSpace() + hash[id] = self:parse() + self:ignoreSpace(); + end + + while indents > 0 do + self:expectDedent("expected dedent") + indents = indents - 1 + end + + return hash +end + +Parser.parseInlineHash = function (self) + local id + local hash = {} + local i = 0 + + self:accept("{") + while not self:accept("}") do + self:ignoreSpace() + if i > 0 then + self:expect(",","expected comma") + end + + self:ignoreWhitespace() + if self:peekType("id") then + id = self:advanceValue() + if id then + self:expect(":","expected semi-colon after id") + self:ignoreSpace() + hash[id] = self:parse() + self:ignoreWhitespace() + end + end + + i = i + 1 + end + return hash +end + +Parser.parseList = function (self) + local list = {} + while self:accept("-") do + self:ignoreSpace() + list[#list + 1] = self:parse() + + self:ignoreSpace() + end + return list +end + +Parser.parseInlineList = function (self) + local list = {} + local i = 0 + self:accept("[") + while not self:accept("]") do + self:ignoreSpace() + if i > 0 then + self:expect(",","expected comma") + end + + self:ignoreSpace() + list[#list + 1] = self:parse() + self:ignoreSpace() + i = i + 1 + end + + return list +end + +Parser.parseTimestamp = function (self) + local capture = self:advance()[2] + + return os.time{ + year = capture[1], + month = capture[2], + day = capture[3], + hour = capture[4] or 0, + min = capture[5] or 0, + sec = capture[6] or 0, + isdst = false, + } - os.time{year=1970, month=1, day=1, hour=8} +end + +exports.eval = function (str) + return Parser:new(exports.tokenize(str)):parse() +end + +exports.dump = table_print + +return exports |