cont-run.lmt /size: 9467 b    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['cont-run'] = {
2    version   = 1.001,
3    comment   = "companion to cont-yes.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- When a style is loaded there is a good change that we never enter
10-- this code.
11
12
13local type, tostring = type, tostring
14
15local report_sandbox = logs.reporter("sandbox","call")
16local report_system  = logs.reporter("system")
17local fastserialize  = table.fastserialize
18local quoted         = string.quoted
19local possiblepath   = sandbox.possiblepath
20
21local context        = context
22local implement      = interfaces.implement
23
24local texset         = tex.set
25
26local qualified      = { }
27local writeable      = { }
28local readable       = { }
29local blocked        = { }
30local trace_files    = false
31local trace_calls    = false
32local nofcalls       = 0
33local nofrejected    = 0
34local logfilename    = "sandbox.log"
35
36local function registerstats()
37    statistics.register("sandboxing", function()
38        if trace_files then
39            return string.format("%i calls, %i rejected, logdata in '%s'",nofcalls,nofrejected,logfilename)
40        else
41            return string.format("%i calls, %i rejected",nofcalls,nofrejected)
42        end
43    end)
44    registerstats = false
45end
46
47local function logsandbox(details)
48    local comment   = details.comment
49    local result    = details.result
50    local arguments = details.arguments
51    for i=1,#arguments do
52        local argument = arguments[i]
53        local t = type(argument)
54        if t == "string" then
55            arguments[i] = quoted(argument)
56            if trace_files and possiblepath(argument) then
57                local q = qualified[argument]
58                if q then
59                    local c = q[comment]
60                    if c then
61                        local r = c[result]
62                        if r then
63                            c[result] = r + 1
64                        else
65                            c[result] = r
66                        end
67                    else
68                        q[comment] = {
69                            [result] = 1
70                        }
71                    end
72                else
73                    qualified[argument] = {
74                        [comment] = {
75                            [result] = 1
76                        }
77                    }
78                end
79            end
80        elseif t == "table" then
81            arguments[i] = fastserialize(argument)
82        else
83            arguments[i] = tostring(argument)
84        end
85    end
86    if trace_calls then
87        report_sandbox("%s(%,t) => %l",details.comment,arguments,result)
88    end
89    nofcalls = nofcalls + 1
90    if not result then
91        nofrejected = nofrejected + 1
92    end
93end
94
95local ioopen = sandbox.original(io.open) -- dummy call
96
97local function logsandboxfiles(name,what,asked,okay)
98    -- we're only interested in permitted access
99    if not okay then
100        blocked  [asked] = blocked  [asked] or 0 + 1
101    elseif what == "*" or what == "w" then
102        writeable[asked] = writeable[asked] or 0 + 1
103    else
104        readable [asked] = readable [asked] or 0 + 1
105    end
106end
107
108function sandbox.logcalls()
109    if not trace_calls then
110        trace_calls = true
111        sandbox.setlogger(logsandbox)
112        if registerstats then
113            registerstats()
114        end
115    end
116end
117
118function sandbox.logfiles()
119    if not trace_files then
120        trace_files = true
121        sandbox.setlogger(logsandbox)
122        sandbox.setfilenamelogger(logsandboxfiles)
123        luatex.registerstopactions(function()
124            table.save(logfilename,{
125                calls = {
126                    nofcalls    = nofcalls,
127                    nofrejected = nofrejected,
128                    filenames   = qualified,
129                },
130                checkednames = {
131                    readable  = readable,
132                    writeable = writeable,
133                    blocked   = blocked,
134                },
135            })
136        end)
137        if registerstats then
138            registerstats()
139        end
140    end
141end
142
143trackers.register("sandbox.tracecalls",sandbox.logcalls)
144trackers.register("sandbox.tracefiles",sandbox.logfiles)
145
146local sandboxing = environment.arguments.sandbox
147local debugging  = environment.arguments.debug
148
149if sandboxing then
150
151    report_system("enabling sandbox")
152
153    sandbox.enable()
154
155    if type(sandboxing) == "string" then
156        sandboxing = utilities.parsers.settings_to_hash(sandboxing)
157        if sandboxing.calls then
158            sandbox.logcalls()
159        end
160        if sandboxing.files then
161            sandbox.logfiles()
162        end
163    end
164
165    -- Nicer would be if we could just disable write 18 and keep os.execute
166    -- which in fact we can do by defining write18 as macro instead of
167    -- primitive ... todo ... well, it has been done now.
168
169    -- We block some potential escapes from protection.
170
171    context [[\let\primitive\relax\let\normalprimitive\relax]]
172
173    debug = {
174        traceback = debug.traceback,
175    }
176
177    package.loaded.debug = debug
178
179elseif debugging then
180
181    -- we keep debug
182
183else
184
185    debug = {
186        traceback = debug.traceback,
187        getinfo   = debug.getinfo,
188        sethook   = debug.sethook,
189    }
190
191    package.loaded.debug = debug
192
193end
194
195local function setoverloadmode(overloadmode)
196    if overloadmode == "warning" then
197        overloadmode = 3 -- 5
198    elseif overloadmode == "error" then
199        overloadmode = 4 -- 6
200    else
201        overloadmode = tonumber(overloadmode)
202    end
203    if overloadmode then
204        texset("overloadmode",overloadmode)
205    end
206end
207
208directives.register("overloadmode", setoverloadmode)
209
210local function processjob()
211
212    tokens.setters.macro("processjob","","permanent")
213
214    environment.initializefilenames() -- todo: check if we really need to pre-prep the filename
215
216    local arguments = environment.arguments
217    local suffix    = environment.suffix
218    local filename  = environment.filename -- hm, not inputfilename !
219
220    environment.lmtxmode = CONTEXTLMTXMODE -- should be true
221
222    -- directives
223    -- silent
224    -- batchmode
225
226if arguments.directives then
227    directives.enable(arguments.directives)
228end
229
230    if arguments.nosynctex then
231        luatex.synctex.setup {
232            state  = interfaces.variables.never,
233        }
234    elseif arguments.synctex then
235        luatex.synctex.setup {
236            state  = interfaces.variables.start,
237            method = interfaces.variables.max,
238        }
239    end
240
241    logs.registerfinalactions(function()
242        logs.pushtarget("log")
243        statistics.showusage("finish")
244        logs.poptarget()
245    end)
246
247    setoverloadmode(arguments.overloadmode)
248
249    if not filename or filename == "" then
250        -- skip
251    elseif suffix == "svg" or arguments.forcesvg then
252
253        report_system("processing svg output: %s",filename)
254
255        context.starttext()
256            context.startTEXpage()
257                context.externalfigure ( { filename }, { conversion = "mp" } )
258            context.stopTEXpage()
259        context.stoptext()
260
261    elseif suffix == "xml" or arguments.forcexml then
262
263        -- Maybe we should move the preamble parsing here as it
264        -- can be part of (any) loaded (sub) file. The \starttext
265        -- wrapping might go away.
266
267        report_system("processing as xml: %s",filename)
268
269        context.starttext()
270            context.xmlprocess("main",filename,"")
271        context.stoptext()
272
273    elseif suffix == "cld" or arguments.forcecld then
274
275        report_system("processing as cld: %s",filename)
276
277        context.runfile(filename)
278
279    elseif suffix == "lua" or arguments.forcelua then
280
281        -- The wrapping might go away. Why is is it there in the
282        -- first place.
283
284        report_system("processing as lua: %s",filename)
285
286        context.starttext()
287            context.ctxlua(string.format('dofile("%s")',filename))
288        context.stoptext()
289
290    elseif suffix == "mp" or arguments.forcemp then
291
292        report_system("processing as metapost: %s",filename)
293
294        context.starttext()
295            context.processMPfigurefile(filename)
296        context.stoptext()
297
298    -- elseif suffix == "prep" then
299    --
300    --     -- Why do we wrap here. Because it can be xml? Let's get rid
301    --     -- of prepping in general.
302    --
303    --     context.starttext()
304    --     context.input(filename)
305    --     context.stoptext()
306
307    elseif suffix == "mps" or arguments.forcemps then
308
309        report_system("processing metapost output: %s",filename)
310
311        context.starttext()
312            context.startTEXpage()
313                context.externalfigure { filename }
314            context.stopTEXpage()
315        context.stoptext()
316
317    else
318
319     -- \writestatus{system}{processing as tex}
320        -- We have a regular tex file so no \starttext yet as we can
321        -- load fonts.
322     -- context.enabletrackers { "resolvers.*" }
323        if type(arguments.forceinput) == "string" then
324            filename = arguments.forceinput or filename
325        end
326        context.input(filename)
327     -- context.disabletrackers { "resolvers.*" }
328
329    end
330
331    context.finishjob()
332
333end
334
335implement {
336    name      = "processjob",
337 -- protected = true
338    public    = true,
339    onlyonce  = true,
340    actions   = processjob,
341}
342
343texconfig.firstline = "\\processjob " -- experiment, yet undocumented
344