summaryrefslogtreecommitdiff
path: root/cal.lua
blob: bb48c68dbcdcd729533debd14a54e412fa49a867 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env lua
--[[--

    RFC2445 parser / printer.

--]]--

local io            = require "io"
local ioopen        = io.open
local iostdin       = io.stdin
local iostdout      = io.stdout

local math          = require "math"
local mathmin       = math.min

local string        = require "string"
local stringformat  = string.format
local stringsub     = string.sub

local table         = require "table"
local tableconcat   = table.concat

local verboselvl    = 3

local mk_out = function (stream, threshold, newlinep)
  local out = io.stdout
  if stream == "err" or stream == "stderr" then out = io.stderr end
  return function (...)
    if verboselvl >= threshold then
      local ok, msg = pcall (stringformat, ...)
      if ok then
        out:write (msg)
        if newlinep == true then out:write "\n" end
      ---else silently ignore
      end
    end
  end
end

local println       = mk_out ("stdout", 0, true)
local errorln       = mk_out ("stderr", 0, true)
local noiseln       = mk_out ("stderr", 1, true)
local debugln       = mk_out ("stderr", 2, true)

local fmt_calendar do
  local fold_rfc2445    = 75  --- B
  local fold_wsp        = " " --- could be tab as well
  local crlf            = "\r\n"

  local fmt_line = function (ln)
    local params = ""
    local lparams = ln.params
    if lparams ~= nil then
      local acc = { }
      for i = 1, #lparams do
        local param = lparams [i]
        local value = param.value
        acc [#acc + 1] = ";"
        acc [#acc + 1] = param.name
        acc [#acc + 1] = "="
        for j = 1, #value do
          if j > 1 then
            acc [#acc + 1] = ","
          end
          acc [#acc + 1] = value [j]
        end
      end
      params = tableconcat (acc)
    end
    return stringformat ("%s%s:%s", ln.name, params, ln.value)
  end

  local fold_line = function (line, len, newline)
    local acc = { }
    local pos = 1

    while pos < #line do
      local wsp  = ""
      local pos1
      if pos == 1 then
        pos1 = mathmin (#line, pos + len)
      else
        pos1 = mathmin (#line, pos + len - 1) --- account for leading whitespace
        wsp = fold_wsp
      end
      acc [#acc + 1] = wsp .. stringsub (line, pos, pos1)
      pos = pos1
    end

    return tableconcat (acc, newline)
  end

  fmt_calendar = function (cal, fold, newline, i, acc)
    if i == nil then
      return fmt_calendar (cal,
                           fold or fold_rfc2445,
                           newline or crlf,
                           1, { })
    end
    if i > #cal then return tableconcat (acc, newline) end

    local cur  = cal [i]
    local line = fmt_line (cur)

    if #line > fold then
      line = fold_line (line, fold, newline)
    end

    acc [#acc + 1] = line

    return fmt_calendar (cal, fold, newline, i + 1, acc)
  end
end

local fmt_calendar_params do
  fmt_calendar_params = function (params)
    local acc = { }
    local len = #params

    for i = 1, len do
      local param = params [i]
      acc [i] = stringformat ("“%s” → [%s]",
                              param.name,
                              tableconcat (param.value, ", "))
    end

    return tableconcat (acc, ", ")
  end
end

local parse_calendar do
  local lpeg            = require "lpeg"
  local lpegmatch       = lpeg.match
  local C               = lpeg.C
  local Cf              = lpeg.Cf
  local Cg              = lpeg.Cg
  local Cp              = lpeg.Cp
  local Cs              = lpeg.Cs
  local Ct              = lpeg.Ct
  local P               = lpeg.P
  local R               = lpeg.R
  local S               = lpeg.S

  local p_space         = P" "
  local p_cr            = P"\r"
  local p_lf            = P"\n"

  local p_wsp           = S" \t"
  local p_white_fold    = p_wsp
  local p_white         = S" \n\r\t\v"
  local p_eof           = P(-1)
  local p_eol           = p_eof + p_cr * p_lf + p_lf
  local p_noeol         = P(1) - p_eol

  local p_comma         = P","
  local p_colon         = P":"
  local p_semicolon     = P";"
  local p_dash          = P"-"
  local p_equals        = P"="
  local p_dquote        = P"\""

  local p_alpha         = R("az", "AZ")
  local p_digit         = R"09"

--[[--
     NON-US-ASCII       = %x80-F8
     QSAFE-CHAR         = WSP / %x21 / %x23-7E / NON-US-ASCII
     SAFE-CHAR          = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E
                        / NON-US-ASCII
     VALUE-CHAR         = WSP / %x21-7E / NON-US-ASCII

--]]--

  local p_non_ascii     = R"\x7f\xff"
  local p_value_char    = p_wsp
                        + R"#~"       -- 0x23–0x7e
                        + p_non_ascii
  local p_qsafe_char    = p_wsp
                        + P"!"        -- 0x21
                        + R"#~"       -- 0x23–0x7e
                        + p_non_ascii
  local p_safe_char     = p_wsp --[[ printable range excluding {",:;} ]]
                        + P"!"        -- 0x21
                        + R"#+"       -- 0x23–0x2b
                        + R"-9"       -- 0x2d–0x39
                        + R"<~"       -- 0x3c–0x7e
                        + p_non_ascii
  ----- p_safe_char     = p_wsp + (R"!~" - S",:;")

  --[[--

    RFC2445: Long content lines SHOULD be split into a multiple line
    representations using a line "folding" technique. That is, a long line can
    be split between any two characters by inserting a CRLF immediately
    followed by a single linear white space character (i.e., SPACE, US-ASCII
    decimal 32 or HTAB, US-ASCII decimal 9).

  --]]--

  local p_folded_line_1 = p_noeol^1 * (p_eol / "")
  local p_folded_line_c = p_white_fold/"" * p_folded_line_1

  local p_folded_line   = Cs(p_folded_line_1 * p_folded_line_c^0) * Cp()

  local p_skip_line     = p_noeol^0 * p_eol * Cp()

  --[[--

     contentline        = name *(";" param ) ":" value CRLF
     name               = x-name / iana-token
     iana-token         = 1*(ALPHA / DIGIT / "-")
     x-name             = "X-" [vendorid "-"] 1*(ALPHA / DIGIT / "-")
     vendorid           = 3*(ALPHA / DIGIT)     ;Vendor identification
     param              = param-name "=" param-value
                          *("," param-value)
     param-name         = iana-token / x-token
     param-value        = paramtext / quoted-string
     paramtext          = *SAFE-CHAR
     value              = *VALUE-CHAR
     quoted-string      = DQUOTE *QSAFE-CHAR DQUOTE

  --]]--

  local add_param = function (t, k, v)
    debugln ("»»»» add_param ({%s}, %s, %s)", t, tostring (k), tostring (v))

    t [#t + 1] = { name = k, value = v }

    return t
  end

  local p_quoted_string = p_dquote * p_qsafe_char^0 * p_dquote
  local p_ianatok       = (p_alpha + p_digit + p_dash)^1
  local p_xtok          = nil --[[ XXX rule missing from RFC ]]
  local p_param_name    = p_ianatok -- + p_xtok
  local p_param_text    = p_safe_char^0
  local p_value         = p_value_char^0
  local p_param_value   = p_param_text + p_quoted_string
  local p_param         = Cg (  C(p_param_name)
                              * p_equals
                              * Ct(C(p_param_value) * (p_comma * C(p_param_value))^0))
  local p_params        = Cf (Ct"" * (p_semicolon * p_param)^0, add_param)
  local p_vendorid      = (p_alpha + p_digit)^3
  local p_xname         = P"X" * p_dash * (p_vendorid * p_dash)^-1
                        * (p_ianatok)
  local p_name          = p_xname + p_ianatok
  local p_content_line  = C(p_name)
                        * p_params^-1
                        * p_colon * C(p_value) * Cp()

  local parse_content_line = function (raw, pos0)
    local tmp, pos1 = lpegmatch (p_folded_line, raw, pos0)
    if tmp == nil then return false end

    local name, params, value, epos
    name, params, value, epos = lpegmatch (p_content_line, tmp)

    if name == nil or value == nil then return false end

    if epos ~= #tmp + 1 then
      noiseln ("parsing unfolded line stopped %d characters short \z
                of EOL [%d]“%s”", epos - #tmp - 1, #tmp, tmp)
    end

    return true, pos1, name, params, value
  end

  local skip_line = function (raw, pos0)
    return lpegmatch (p_skip_line, raw, pos0)
  end

  parse_calendar = function (raw, pos0, acc, consumed, nline, nskipped)
    if pos0 == nil then return parse_calendar (raw, 1, { }, 0, 1, 0) end

    local ok, pos1, name, params, value = parse_content_line (raw, pos0)

    if ok == false then
      pos1 = skip_line (raw, pos0)
      if pos0 == pos1 then
        noiseln ("»»» [%d] reached EOF, terminating after %d bytes, \z
                  %d calendar lines",
                 pos0, consumed, nline)
        return acc
      end
      errorln ("[%d–%d] %d bad content line; skipping", pos0, pos1, nline)
      nskipped = nskipped + 1
    else
      noiseln ("»»» [%d–%d] “%s” [%s] “%s”",
               pos0, pos1, name, fmt_calendar_params (params), value)
      acc [#acc + 1] = { pos = { pos0, pos1 }
                       , name = name, params = params, value = value }
    end

    return parse_calendar (raw, pos1, acc, consumed + (pos1 - pos0), nline + 1,
                           nskipped)
  end
end

local loaddata = function (fname)
  local fh = ioopen (fname, "r")

  if fh == nil then
    error ("could not open file " .. fname .. " for reading")
  end

  local ret = fh:read ("*a")
  fh:close ()

  return ret
end

-------------------------------------------------------------------------------
--- API
-------------------------------------------------------------------------------

local set_verbosity = function (n) verboselvl = n end

local parse = function (raw)
  local ok, obj = pcall (parse_calendar, raw)
  return ok and obj
end

local parse_handle = function (fh)
  local raw = fh:read ("*a")
  return raw ~= nil and parse (raw)
end

local parse_file = function (fname)
  local fh = ioopen (fname, "r")
  local cal = parse_handle (fh)
  fh:close ()
  return cal
end

local parse_stdin = function ()
  return parse_handle (iostdin)
end

local format = function (cal, newline)
  return fmt_calendar (cal, nil, newline or "\n")
end

local output_handle = function (cal, fh)
  local str = fmt_calendar (cal, nil, newline or "\n")
  fh:write (str)
end

local output = function (cal)
  return output_handle (cal, iostdout)
end

return { set_verbosity = set_verbosity
       , parse         = parse
       , parse_handle  = parse_handle
       , parse_file    = parse_file
       , parse_stdin   = parse_stdin
       , format        = format
       , output        = output
       , output_handle = output_handle
       }