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
|
#!/usr/bin/env lua
--[[--
RFC2445 parser.
--]]--
local io = require "io"
local ioopen = io.open
local string = require "string"
local stringformat = string.format
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 print_calendar
local fmt_calendar_params
do
fmt_calendar_params = function (params)
local acc = { }
local len = #params
for i = 1, len do
local params = params [i]
acc [i] = stringformat ("“%s” → “%s”", params.name, params.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
return fh:read ("*a")
end
local main = function (argv)
if #argv ~= 1 then
error "pass me a file name, I insist!"
end
local raw = loaddata (argv [1])
return parse_calendar (raw)
end
return main (arg)
|