cont-run.lmt /size: 9 Kb    last modification: 2025-02-21 11:03
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    local variables = interfaces.variables
213
214    tokens.setters.macro("processjob","","permanent")
215
216    environment.initializefilenames() -- todo: check if we really need to pre-prep the filename
217
218    local arguments = environment.arguments
219    local suffix    = environment.suffix
220    local filename  = environment.filename -- hm, not inputfilename !
221
222    environment.lmtxmode = CONTEXTLMTXMODE -- should be true
223
224    -- directives
225    -- silent
226    -- batchmode
227
228    if arguments.directives then
229        directives.enable(arguments.directives)
230    end
231    if arguments.experiments then
232        experiments.enable(arguments.experiments)
233    end
234
235    if arguments.exportimages then
236        arguments.synctex = false
237    end
238
239    if arguments.nosynctex then
240        luatex.synctex.setup {
241            state  = variables.never,
242        }
243    elseif arguments.synctex then
244        local state = arguments.synctex
245        if state == true then
246            state = variables.start
247        end
248        luatex.synctex.setup {
249            state  = state,
250            method = variables.max,
251        }
252    end
253
254    logs.registerfinalactions(function()
255        logs.pushtarget("log")
256        statistics.showusage("finish")
257        logs.poptarget()
258    end)
259
260    setoverloadmode(arguments.overloadmode)
261
262    if arguments.nocompactfonts then
263        report_system()
264        report_system("compact font mode disabled")
265        report_system()
266    else
267        token.expandmacro("font_enable_compact_font_mode")
268    end
269
270    if not filename or filename == "" then
271        -- skip
272    elseif suffix == "svg" or arguments.forcesvg then
273
274        report_system("processing svg output: %s",filename)
275
276        context.starttext()
277            context.startTEXpage()
278                context.externalfigure ( { filename }, { conversion = "mp" } )
279            context.stopTEXpage()
280        context.stoptext()
281
282    elseif suffix == "xml" or arguments.forcexml then
283
284        -- Maybe we should move the preamble parsing here as it
285        -- can be part of (any) loaded (sub) file. The \starttext
286        -- wrapping might go away.
287
288        report_system("processing as xml: %s",filename)
289
290        context.starttext()
291            context.xmlprocess("main",filename,"")
292        context.stoptext()
293
294    elseif suffix == "cld" or arguments.forcecld then
295
296        report_system("processing as cld: %s",filename)
297
298        context.runfile(filename)
299
300    elseif suffix == "lua" or arguments.forcelua then
301
302        -- The wrapping might go away. Why is is it there in the
303        -- first place.
304
305        report_system("processing as lua: %s",filename)
306
307        context.starttext()
308            context.ctxlua(string.format('dofile("%s")',filename))
309        context.stoptext()
310
311    elseif suffix == "mp" or arguments.forcemp then
312
313        report_system("processing as metapost: %s",filename)
314
315        context.starttext()
316            context.processMPfigurefile(filename)
317        context.stoptext()
318
319    -- elseif suffix == "prep" then
320    --
321    --     -- Why do we wrap here. Because it can be xml? Let's get rid
322    --     -- of prepping in general.
323    --
324    --     context.starttext()
325    --     context.input(filename)
326    --     context.stoptext()
327
328    elseif suffix == "mps" or arguments.forcemps then
329
330        report_system("processing metapost output: %s",filename)
331
332        context.starttext()
333            context.startTEXpage()
334                context.externalfigure { filename }
335            context.stopTEXpage()
336        context.stoptext()
337
338    else
339
340        if arguments.exportimages then
341            context.exportimages() -- maybe direct
342        elseif arguments.exportcontent then
343            context.exportcontent() -- maybe direct
344        end
345
346     -- \writestatus{system}{processing as tex}
347        -- We have a regular tex file so no \starttext yet as we can
348        -- load fonts.
349     -- context.enabletrackers { "resolvers.*" }
350        if type(arguments.forceinput) == "string" then
351            filename = arguments.forceinput or filename
352        end
353        context.input(filename)
354     -- context.disabletrackers { "resolvers.*" }
355
356    end
357
358    context.finishjob()
359
360end
361
362implement {
363    name      = "processjob",
364 -- protected = true
365    public    = true,
366    onlyonce  = true,
367    actions   = processjob,
368}
369
370texconfig.firstline = "\\processjob " -- experiment, yet undocumented
371