]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - libexec/nuageinit/yaml.lua
nuageinit: add basic support for cloudinit.
[FreeBSD/FreeBSD.git] / libexec / nuageinit / yaml.lua
1 -- SPDX-License-Identifier: MIT
2 --
3 -- Copyright (c) 2017 Dominic Letz dominicletz@exosite.com
4
5 local table_print_value
6 table_print_value = function(value, indent, done)
7   indent = indent or 0
8   done = done or {}
9   if type(value) == "table" and not done [value] then
10     done [value] = true
11
12     local list = {}
13     for key in pairs (value) do
14       list[#list + 1] = key
15     end
16     table.sort(list, function(a, b) return tostring(a) < tostring(b) end)
17     local last = list[#list]
18
19     local rep = "{\n"
20     local comma
21     for _, key in ipairs (list) do
22       if key == last then
23         comma = ''
24       else
25         comma = ','
26       end
27       local keyRep
28       if type(key) == "number" then
29         keyRep = key
30       else
31         keyRep = string.format("%q", tostring(key))
32       end
33       rep = rep .. string.format(
34         "%s[%s] = %s%s\n",
35         string.rep(" ", indent + 2),
36         keyRep,
37         table_print_value(value[key], indent + 2, done),
38         comma
39       )
40     end
41
42     rep = rep .. string.rep(" ", indent) -- indent it
43     rep = rep .. "}"
44
45     done[value] = false
46     return rep
47   elseif type(value) == "string" then
48     return string.format("%q", value)
49   else
50     return tostring(value)
51   end
52 end
53
54 local table_print = function(tt)
55   print('return '..table_print_value(tt))
56 end
57
58 local table_clone = function(t)
59   local clone = {}
60   for k,v in pairs(t) do
61     clone[k] = v
62   end
63   return clone
64 end
65
66 local string_trim = function(s, what)
67   what = what or " "
68   return s:gsub("^[" .. what .. "]*(.-)["..what.."]*$", "%1")
69 end
70
71 local push = function(stack, item)
72   stack[#stack + 1] = item
73 end
74
75 local pop = function(stack)
76   local item = stack[#stack]
77   stack[#stack] = nil
78   return item
79 end
80
81 local context = function (str)
82   if type(str) ~= "string" then
83     return ""
84   end
85
86   str = str:sub(0,25):gsub("\n","\\n"):gsub("\"","\\\"");
87   return ", near \"" .. str .. "\""
88 end
89
90 local Parser = {}
91 function Parser.new (self, tokens)
92   self.tokens = tokens
93   self.parse_stack = {}
94   self.refs = {}
95   self.current = 0
96   return self
97 end
98
99 local exports = {version = "1.2"}
100
101 local word = function(w) return "^("..w..")([%s$%c])" end
102
103 local tokens = {
104   {"comment",   "^#[^\n]*"},
105   {"indent",    "^\n( *)"},
106   {"space",     "^ +"},
107   {"true",      word("enabled"),  const = true, value = true},
108   {"true",      word("true"),     const = true, value = true},
109   {"true",      word("yes"),      const = true, value = true},
110   {"true",      word("on"),      const = true, value = true},
111   {"false",     word("disabled"), const = true, value = false},
112   {"false",     word("false"),    const = true, value = false},
113   {"false",     word("no"),       const = true, value = false},
114   {"false",     word("off"),      const = true, value = false},
115   {"null",      word("null"),     const = true, value = nil},
116   {"null",      word("Null"),     const = true, value = nil},
117   {"null",      word("NULL"),     const = true, value = nil},
118   {"null",      word("~"),        const = true, value = nil},
119   {"id",    "^\"([^\"]-)\" *(:[%s%c])"},
120   {"id",    "^'([^']-)' *(:[%s%c])"},
121   {"string",    "^\"([^\"]-)\"",  force_text = true},
122   {"string",    "^'([^']-)'",    force_text = true},
123   {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?):(%d%d)"},
124   {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)%s+(%-?%d%d?)"},
125   {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d):(%d%d)"},
126   {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?):(%d%d)"},
127   {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)%s+(%d%d?)"},
128   {"timestamp", "^(%d%d%d%d)-(%d%d?)-(%d%d?)"},
129   {"doc",       "^%-%-%-[^%c]*"},
130   {",",         "^,"},
131   {"string",    "^%b{} *[^,%c]+", noinline = true},
132   {"{",         "^{"},
133   {"}",         "^}"},
134   {"string",    "^%b[] *[^,%c]+", noinline = true},
135   {"[",         "^%["},
136   {"]",         "^%]"},
137   {"-",         "^%-", noinline = true},
138   {":",         "^:"},
139   {"pipe",      "^(|)(%d*[+%-]?)", sep = "\n"},
140   {"pipe",      "^(>)(%d*[+%-]?)", sep = " "},
141   {"id",        "^([%w][%w %-_]*)(:[%s%c])"},
142   {"string",    "^[^%c]+", noinline = true},
143   {"string",    "^[^,%]}%c ]+"}
144 };
145 exports.tokenize = function (str)
146   local token
147   local row = 0
148   local ignore
149   local indents = 0
150   local lastIndents
151   local stack = {}
152   local indentAmount = 0
153   local inline = false
154   str = str:gsub("\r\n","\010")
155
156   while #str > 0 do
157     for i in ipairs(tokens) do
158       local captures = {}
159       if not inline or tokens[i].noinline == nil then
160         captures = {str:match(tokens[i][2])}
161       end
162
163       if #captures > 0 then
164         captures.input = str:sub(0, 25)
165         token = table_clone(tokens[i])
166         token[2] = captures
167         local str2 = str:gsub(tokens[i][2], "", 1)
168         token.raw = str:sub(1, #str - #str2)
169         str = str2
170
171         if token[1] == "{" or token[1] == "[" then
172           inline = true
173         elseif token.const then
174           -- Since word pattern contains last char we're re-adding it
175           str = token[2][2] .. str
176           token.raw = token.raw:sub(1, #token.raw - #token[2][2])
177         elseif token[1] == "id" then
178           -- Since id pattern contains last semi-colon we're re-adding it
179           str = token[2][2] .. str
180           token.raw = token.raw:sub(1, #token.raw - #token[2][2])
181           -- Trim
182           token[2][1] = string_trim(token[2][1])
183         elseif token[1] == "string" then
184           -- Finding numbers
185           local snip = token[2][1]
186           if not token.force_text then
187             if snip:match("^(-?%d+%.%d+)$") or snip:match("^(-?%d+)$") then
188               token[1] = "number"
189             end
190           end
191
192         elseif token[1] == "comment" then
193           ignore = true;
194         elseif token[1] == "indent" then
195           row = row + 1
196           inline = false
197           lastIndents = indents
198           if indentAmount == 0 then
199             indentAmount = #token[2][1]
200           end
201
202           if indentAmount ~= 0 then
203             indents = (#token[2][1] / indentAmount);
204           else
205             indents = 0
206           end
207
208           if indents == lastIndents then
209             ignore = true;
210           elseif indents > lastIndents + 2 then
211             error("SyntaxError: invalid indentation, got " .. tostring(indents)
212               .. " instead of " .. tostring(lastIndents) .. context(token[2].input))
213           elseif indents > lastIndents + 1 then
214             push(stack, token)
215           elseif indents < lastIndents then
216             local input = token[2].input
217             token = {"dedent", {"", input = ""}}
218             token.input = input
219             while lastIndents > indents + 1 do
220               lastIndents = lastIndents - 1
221               push(stack, token)
222             end
223           end
224         end -- if token[1] == XXX
225         token.row = row
226         break
227       end -- if #captures > 0
228     end
229
230     if not ignore then
231       if token then
232         push(stack, token)
233         token = nil
234       else
235         error("SyntaxError " .. context(str))
236       end
237     end
238
239     ignore = false;
240   end
241
242   return stack
243 end
244
245 Parser.peek = function (self, offset)
246   offset = offset or 1
247   return self.tokens[offset + self.current]
248 end
249
250 Parser.advance = function (self)
251   self.current = self.current + 1
252   return self.tokens[self.current]
253 end
254
255 Parser.advanceValue = function (self)
256   return self:advance()[2][1]
257 end
258
259 Parser.accept = function (self, type)
260   if self:peekType(type) then
261     return self:advance()
262   end
263 end
264
265 Parser.expect = function (self, type, msg)
266   return self:accept(type) or
267     error(msg .. context(self:peek()[1].input))
268 end
269
270 Parser.expectDedent = function (self, msg)
271   return self:accept("dedent") or (self:peek() == nil) or
272     error(msg .. context(self:peek()[2].input))
273 end
274
275 Parser.peekType = function (self, val, offset)
276   return self:peek(offset) and self:peek(offset)[1] == val
277 end
278
279 Parser.ignore = function (self, items)
280   local advanced
281   repeat
282     advanced = false
283     for _,v in pairs(items) do
284       if self:peekType(v) then
285         self:advance()
286         advanced = true
287       end
288     end
289   until advanced == false
290 end
291
292 Parser.ignoreSpace = function (self)
293   self:ignore{"space"}
294 end
295
296 Parser.ignoreWhitespace = function (self)
297   self:ignore{"space", "indent", "dedent"}
298 end
299
300 Parser.parse = function (self)
301
302   local ref = nil
303   if self:peekType("string") and not self:peek().force_text then
304     local char = self:peek()[2][1]:sub(1,1)
305     if char == "&" then
306       ref = self:peek()[2][1]:sub(2)
307       self:advanceValue()
308       self:ignoreSpace()
309     elseif char == "*" then
310       ref = self:peek()[2][1]:sub(2)
311       return self.refs[ref]
312     end
313   end
314
315   local result
316   local c = {
317     indent = self:accept("indent") and 1 or 0,
318     token = self:peek()
319   }
320   push(self.parse_stack, c)
321
322   if c.token[1] == "doc" then
323     result = self:parseDoc()
324   elseif c.token[1] == "-" then
325     result = self:parseList()
326   elseif c.token[1] == "{" then
327     result = self:parseInlineHash()
328   elseif c.token[1] == "[" then
329     result = self:parseInlineList()
330   elseif c.token[1] == "id" then
331     result = self:parseHash()
332   elseif c.token[1] == "string" then
333     result = self:parseString("\n")
334   elseif c.token[1] == "timestamp" then
335     result = self:parseTimestamp()
336   elseif c.token[1] == "number" then
337     result = tonumber(self:advanceValue())
338   elseif c.token[1] == "pipe" then
339     result = self:parsePipe()
340   elseif c.token.const == true then
341     self:advanceValue();
342     result = c.token.value
343   else
344     error("ParseError: unexpected token '" .. c.token[1] .. "'" .. context(c.token.input))
345   end
346
347   pop(self.parse_stack)
348   while c.indent > 0 do
349     c.indent = c.indent - 1
350     local term = "term "..c.token[1]..": '"..c.token[2][1].."'"
351     self:expectDedent("last ".. term .." is not properly dedented")
352   end
353
354   if ref then
355     self.refs[ref] = result
356   end
357   return result
358 end
359
360 Parser.parseDoc = function (self)
361   self:accept("doc")
362   return self:parse()
363 end
364
365 Parser.inline = function (self)
366   local current = self:peek(0)
367   if not current then
368     return {}, 0
369   end
370
371   local inline = {}
372   local i = 0
373
374   while self:peek(i) and not self:peekType("indent", i) and current.row == self:peek(i).row do
375     inline[self:peek(i)[1]] = true
376     i = i - 1
377   end
378   return inline, -i
379 end
380
381 Parser.isInline = function (self)
382   local _, i = self:inline()
383   return i > 0
384 end
385
386 Parser.parent = function(self, level)
387   level = level or 1
388   return self.parse_stack[#self.parse_stack - level]
389 end
390
391 Parser.parentType = function(self, type, level)
392   return self:parent(level) and self:parent(level).token[1] == type
393 end
394
395 Parser.parseString = function (self)
396   if self:isInline() then
397     local result = self:advanceValue()
398
399     --[[
400       - a: this looks
401         flowing: but is
402         no: string
403     --]]
404     local types = self:inline()
405     if types["id"] and types["-"] then
406       if not self:peekType("indent") or not self:peekType("indent", 2) then
407         return result
408       end
409     end
410
411     --[[
412       a: 1
413       b: this is
414         a flowing string
415         example
416       c: 3
417     --]]
418     if self:peekType("indent") then
419       self:expect("indent", "text block needs to start with indent")
420       local addtl = self:accept("indent")
421
422       result = result .. "\n" .. self:parseTextBlock("\n")
423
424       self:expectDedent("text block ending dedent missing")
425       if addtl then
426         self:expectDedent("text block ending dedent missing")
427       end
428     end
429     return result
430   else
431     --[[
432       a: 1
433       b:
434         this is also
435         a flowing string
436         example
437       c: 3
438     --]]
439     return self:parseTextBlock("\n")
440   end
441 end
442
443 Parser.parsePipe = function (self)
444   local pipe = self:expect("pipe")
445   self:expect("indent", "text block needs to start with indent")
446   local result = self:parseTextBlock(pipe.sep)
447   self:expectDedent("text block ending dedent missing")
448   return result
449 end
450
451 Parser.parseTextBlock = function (self, sep)
452   local token = self:advance()
453   local result = string_trim(token.raw, "\n")
454   local indents = 0
455   while self:peek() ~= nil and ( indents > 0 or not self:peekType("dedent") ) do
456     local newtoken = self:advance()
457     while token.row < newtoken.row do
458       result = result .. sep
459       token.row = token.row + 1
460     end
461     if newtoken[1] == "indent" then
462       indents = indents + 1
463     elseif newtoken[1] == "dedent" then
464       indents = indents - 1
465     else
466       result = result .. string_trim(newtoken.raw, "\n")
467     end
468   end
469   return result
470 end
471
472 Parser.parseHash = function (self, hash)
473   hash = hash or {}
474   local indents = 0
475
476   if self:isInline() then
477     local id = self:advanceValue()
478     self:expect(":", "expected semi-colon after id")
479     self:ignoreSpace()
480     if self:accept("indent") then
481       indents = indents + 1
482       hash[id] = self:parse()
483     else
484       hash[id] = self:parse()
485       if self:accept("indent") then
486         indents = indents + 1
487       end
488     end
489     self:ignoreSpace();
490   end
491
492   while self:peekType("id") do
493     local id = self:advanceValue()
494     self:expect(":","expected semi-colon after id")
495     self:ignoreSpace()
496     hash[id] = self:parse()
497     self:ignoreSpace();
498   end
499
500   while indents > 0 do
501     self:expectDedent("expected dedent")
502     indents = indents - 1
503   end
504
505   return hash
506 end
507
508 Parser.parseInlineHash = function (self)
509   local id
510   local hash = {}
511   local i = 0
512
513   self:accept("{")
514   while not self:accept("}") do
515     self:ignoreSpace()
516     if i > 0 then
517       self:expect(",","expected comma")
518     end
519
520     self:ignoreWhitespace()
521     if self:peekType("id") then
522       id = self:advanceValue()
523       if id then
524         self:expect(":","expected semi-colon after id")
525         self:ignoreSpace()
526         hash[id] = self:parse()
527         self:ignoreWhitespace()
528       end
529     end
530
531     i = i + 1
532   end
533   return hash
534 end
535
536 Parser.parseList = function (self)
537   local list = {}
538   while self:accept("-") do
539     self:ignoreSpace()
540     list[#list + 1] = self:parse()
541
542     self:ignoreSpace()
543   end
544   return list
545 end
546
547 Parser.parseInlineList = function (self)
548   local list = {}
549   local i = 0
550   self:accept("[")
551   while not self:accept("]") do
552     self:ignoreSpace()
553     if i > 0 then
554       self:expect(",","expected comma")
555     end
556
557     self:ignoreSpace()
558     list[#list + 1] = self:parse()
559     self:ignoreSpace()
560     i = i + 1
561   end
562
563   return list
564 end
565
566 Parser.parseTimestamp = function (self)
567   local capture = self:advance()[2]
568
569   return os.time{
570     year  = capture[1],
571     month = capture[2],
572     day   = capture[3],
573     hour  = capture[4] or 0,
574     min   = capture[5] or 0,
575     sec   = capture[6] or 0,
576     isdst = false,
577   } - os.time{year=1970, month=1, day=1, hour=8}
578 end
579
580 exports.eval = function (str)
581   return Parser:new(exports.tokenize(str)):parse()
582 end
583
584 exports.dump = table_print
585
586 return exports