diff options
author | Karl Berry <karl@freefriends.org> | 2024-11-18 22:29:08 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2024-11-18 22:29:08 +0000 |
commit | c4e1c62bab8f131ab82c200b9f4d76b7df9d5885 (patch) | |
tree | 17e4474f0a01f396778fe73c745f57391e441f27 /Master/texmf-dist | |
parent | 46af935d0c083bbca25e26099f0226d2e512a296 (diff) |
extractbb lua reimplementation, with TEXLIVE_EXTRACTBB envvar (18nov24)
git-svn-id: svn://tug.org/texlive/trunk@72890 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist')
-rw-r--r-- | Master/texmf-dist/doc/support/extractbb/README.md | 81 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua | 695 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua | 270 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/extractbb/extractbb.lua | 71 |
4 files changed, 1117 insertions, 0 deletions
diff --git a/Master/texmf-dist/doc/support/extractbb/README.md b/Master/texmf-dist/doc/support/extractbb/README.md new file mode 100644 index 00000000000..0bbecfd6391 --- /dev/null +++ b/Master/texmf-dist/doc/support/extractbb/README.md @@ -0,0 +1,81 @@ +<!-- extractbb-lua + https://github.com/gucci-on-fleek/extractbb + SPDX-License-Identifier: MPL-2.0+ OR CC-BY-SA-4.0+ + SPDX-FileCopyrightText: 2024 Max Chernoff +--> + +`extractbb-lua` +=============== + +A reimplementation of +[`extractbb`](https://texdoc.org/serve/extractbb/0), written in Lua. + + +Variants +-------- + +There are two variants of `extractbb-lua`: + +- **`wrapper`**: A wrapper script around the original `xdvipdmfx`-based + `extractbb` that is used to fix a security vulernability in + `xdvipdfmx`. + +- **`scratch`**: A standalone implementation of `extractbb`, written in + Lua from scratch, with no dependencies on `xdvipdfmx`. + +Currently, the script `extractbb` defaults to the `wrapper` variant, but +you can manually select any specific variant by setting the +`TEXLIVE_EXTRACTBB` environment variable to either `wrapper` or +`scratch`. + +> [!WARNING] +> The `scratch` variant is still in development and may be buggy or +> insecure. + + +### Secret Developer Options + +If you set `TEXLIVE_EXTRACTBB` to the full path of an executable, it +will run that directly. And if you set +`TEXLIVE_EXTRACTBB_UNSAFE=unsafe`, then it will ignore some of the +security checks. + + +Support +------- + +If you have any problems with this tool, please report it (in order of +preference): + +1. By [opening a new issue on + GitHub](https://github.com/gucci-on-fleek/extractbb/issues/new). + +2. Via email to the public [`tex-live@tug.org` mailing list](https://tug.org/mailman/listinfo/tex-live). + +3. By contacting the author directly at `tex@maxchernoff.ca`. + + +Building +-------- + +Please see the file +[`BUILDING.md`](https://github.com/gucci-on-fleek/extractbb/blob/master/BUILDING.md) +on GitHub. + + +Licence +------- + +`extractbb-lua` is licensed under the [_Mozilla Public License_, version +2.0](https://www.mozilla.org/en-US/MPL/2.0/) or greater. The +documentation is additionally licensed under [CC-BY-SA, version +4.0](https://creativecommons.org/licenses/by-sa/4.0/legalcode) or +greater. + +The test files have various licences, please see the file +[`tests/LICENCE.md`](tests/LICENCE.md) for more information. (Since +these files are not distributed with the package, neither is this +licence file.) + +--- +_v1.0.0 (2024-11-17)_ <!--%%version %%dashdate--> diff --git a/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua b/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua new file mode 100755 index 00000000000..3b1cd6948be --- /dev/null +++ b/Master/texmf-dist/scripts/extractbb/extractbb-scratch.lua @@ -0,0 +1,695 @@ +#!/usr/bin/env texlua +-- extractbb-lua +-- https://github.com/gucci-on-fleek/extractbb +-- SPDX-License-Identifier: MPL-2.0+ +-- SPDX-FileCopyrightText: 2024 Max Chernoff +-- +-- Inclusion Methods +-- ================= +-- +-- This script can use two different methods to extract bounding boxes from +-- images: the "img" module and the "pdfe" module. The "img" module will be +-- automatically selected in most cases and supports all image types that are +-- supported by the original "extractbb" program. If and only if the "img" +-- module fails to load, the "pdfe" module will be used as a fallback. However, +-- the "pdfe" module only supports PDF files. Both modules are built in to the +-- LuaTeX binaries, however due to some technical issues, the "img" module may +-- fail to load on some more exotic platforms. +-- +-- +-- Compatibility +-- ============= +-- +-- Based off of my testing, this Lua script is 100% compatible with the original +-- C-based "extractbb" program, with the following exceptions: +-- +-- * When running in "img" mode, the PDF version is always reported as "1.5". +-- +-- * When running in "img" mode, if the requested bounding box is not found, +-- the script will fallback to the Crop box or the Media box, instead of +-- following the original fallback order. (In practice, almost all PDFs set +-- all their bounding boxes equal to each other, and even if the boxes are +-- set to different values, the script will still return the requested box, +-- provided that it is set in the PDF.) +-- +-- * When running in "pdfe" mode, only PDF files are supported. +-- +-- All of these issues are very unlikely to affect any real-world documents. +-- +-- +-- Security +-- ======== +-- +-- This script is designed to be safely ran from restricted shell escape. A few +-- security features: +-- +-- * The majority of this script runs inside a sandboxed Lua environment, +-- which only exposes a very restricted set of functions. +-- +-- * All file-related functions available inside the sandbox first check with +-- kpathsea to ensure that the file is allowed to be opened. +-- +-- * In the event of any errors, the script immediately exits. +-- +-- * This script does not run (fork/exec) any external programs. +-- +-- * This script is written entirely in Lua, so overflow/use-after-free +-- vulnerabilities are not possible. +-- +-- Some potential security concerns: +-- +-- * This script has not been audited or reviewed by anyone other than myself. +-- +-- * Using the "ffi" module to load the "img" library is technically undefined +-- behaviour, and as such may potentially lead to unforeseen security +-- issues. +-- +-- * The underlying LuaTeX modules may themselves have security +-- vulnerabilities, which would be inherited by this script. + + +---------------------- +--- Initialization --- +---------------------- + +-- Pre-sandbox variables/constants +local show_errors = true +local SOURCE_DATE_EPOCH = tonumber(os.getenv("SOURCE_DATE_EPOCH")) +local version = "extractbb.lua v1.0.0 (2024-11-17)" --%%version %%dashdate + +-- Required for any kpathsea calls to work. +kpse.set_program_name("texlua", "extractbb") + + +-------------------------- +--- Questionable Hacks --- +-------------------------- + +-- LuaTeX doesn't load the "img" library in "texlua" mode, so we need this +-- questionable hack to load it manually. We do it inside of "pcall" since there +-- are some exotic platforms where the "ffi" module is unsupported. +pcall(function() + local ffi = package.loaded.ffi + + ffi.cdef[[ + typedef struct lua_State lua_State; + typedef int (*lua_CFunction) (lua_State *L); + + lua_State *Luas; + void luaL_requiref(lua_State *L, const char *modname, + lua_CFunction openf, int glb); + int luaopen_img(lua_State * L); + + int lua_only; + ]] + + -- Basic initialization + ffi.C.lua_only = 0 + tex.initialize() + + -- "tex" module + _G.tex = package.loaded.tex + tex.enableprimitives("", tex.extraprimitives()) + tex.outputmode = 1 + tex.interactionmode = 0 + + -- "pdf" module + _G.pdf = package.loaded.pdf + pdf.setignoreunknownimages(1) + pdf.setmajorversion(2) + pdf.setminorversion(0) + + -- "img" module + ffi.C.luaL_requiref(ffi.C.Luas, "img", ffi.C.luaopen_img, 1) +end) + +-- In case of failure, define an empty "img" table. +if not img then + _G.img = {} +end + + +------------------ +--- Sandboxing --- +------------------ + +-- Prepare the sandbox for the rest of the script. +local env = { + arg = arg, + io = { stdout = io.stdout, }, + ipairs = ipairs, + math = math, + os = { date = os.date, exit = os.exit, }, + pairs = pairs, + pdfe = pdfe, + print = print, + select = select, + table = table, + tonumber = tonumber, + type = type, +} + +do + -- Saved global functions + local debug_traceback = debug.traceback + local find_file = kpse.find_file + local img_scan = img.scan + local io_open = io.open + local io_stderr = io.stderr + local kpse_in_name_ok = kpse.in_name_ok + local kpse_out_name_ok = kpse.out_name_ok + local kpse_var_value = kpse.var_value + local lfs_attributes = lfs.attributes + local os_exit = os.exit + local os_setenv = os.setenv + local pdfe_open = pdfe.open + local select = select + local tostring = tostring + + -- Error messages + local function error(...) + if show_errors then + -- Header + io_stderr:write("! extractbb ERROR: ") + + -- Message + for i = 1, select("#", ...) do + io_stderr:write(tostring(select(i, ...)), " ") + end + + -- Traceback + io_stderr:write("\n", "\n") + io_stderr:write(debug_traceback(nil, 2), "\n") + end + + -- Flush and exit + io_stderr:flush() + os_exit(1) + end + + env.error = error + + -- Make sure that "openin_any" is at least "restricted", and that + -- "openout_any" is at least "paranoid". + local initial_openin = kpse_var_value("openin_any") + local initial_openout = kpse_var_value("openout_any") + + if (initial_openin ~= "r") or (initial_openout ~= "p") then + os_setenv("openin_any", "r") + end + + if (initial_openout ~= "p") then + os_setenv("openout_any", "p") + end + + -- Check the input paths. + local function resolve_input_name(file_name) + local file_path = find_file(file_name, "graphic/figure", true) + if not file_path then + error("Cannot find input file:", file_name) + end + + local allowed = kpse_in_name_ok(file_path) + if not allowed then + error("Input file is not allowed:", file_path) + end + + local mode = lfs_attributes(file_path, "mode") + if mode ~= "file" then + error("Input file is not a regular file:", file_path) + end + + return file_path + end + + -- Check the output paths. + local function resolve_output_name(file_name) + local allowed = kpse_out_name_ok(file_name) + if not allowed then + error("Output file is not allowed:", file_name) + end + + local name, extension = file_name:match("(.+)%.([^.]-)$") + + if (not name) or (not extension) or + (name == "") or (extension == "") + then + error("Output file has no extension:", file_name) + end + + if (extension ~= "xbb") and (extension ~= "bb") then + error("Output file has an invalid extension:", file_name) + end + + -- We shouldn't allow files with weird characters in their names. + if name:match("[%c%%\t\r\n><*|]") then + error("Output file has an invalid name:", file_name) + end + + return file_name + end + + -- Opens a file. + function env.open_file(file_name, read_write, binary_text) + local file_path, mode + if read_write == "read" then + file_path = resolve_input_name(file_name) + mode = "r" + elseif read_write == "write" then + file_path = resolve_output_name(file_name) + mode = "w" + else + error("Invalid read/write mode:", read_write) + end + + if binary_text == "binary" then + mode = mode .. "b" + elseif binary_text == "text" then + mode = mode .. "" + else + error("Invalid binary/text mode:", binary_text) + end + + local file, message = io_open(file_path, mode) + + if not file then + error("Cannot open file:", file_path, message) + end + + return file + end + + -- Open an PDF file. + function env.pdfe.open(file_name) + local file_path = resolve_input_name(file_name) + return pdfe_open(file_path) + end + + -- Open an image file. + function env.open_image(file_name, page, box) + local file_path = resolve_input_name(file_name) + return img_scan { + filename = file_path, + filepath = file_path, + page = page, + pagebox = box, + } + end + + if not img_scan then + env.open_image = false + end +end + +-- Prevent trying to change the environment. +local function bad_index(...) + env.error("Attempt to access an undefined index:", select(2, ...)) +end + +setmetatable(env, { + __index = bad_index, + __metatable = false, + __newindex = bad_index, +}) + +-- Set the environment. +_ENV = env + + +----------------------------------- +--- Post-Sandbox Initialization --- +----------------------------------- + +-- Constants +local BP_TO_SP = 65781.76 +local IN_TO_BP = 72 + +-- Save often-used globals for a slight speed boost. +local floor = math.floor +local insert = table.insert +local remove = table.remove +local script_arguments = arg +local unpack = table.unpack + +-- General-purpose functions +local function round(number) + return floor(number +0.5) +end + + +------------------------- +--- Argument Handling --- +------------------------- + +-- Define the argument handling functions. +local process_arguments = {} + +-- > Specify a PDF pagebox for bounding box +-- > pagebox=cropbox, mediabox, artbox, trimbox, bleedbox +local bbox_option = "auto" +function process_arguments.B(script_arguments) + bbox_option = remove(script_arguments, 1) +end + +-- > Show this help message and exit +function process_arguments.h(script_arguments) + print [[ +Usage: extractbb [-B pagebox] [-p page] [-q|-v] [-O] [-m|-x] FILE... + extractbb --help|--version +Extract bounding box from PDF, PNG, JPEG, JP2, or BMP file; default output below. + +Options: + -B pagebox Specify a PDF pagebox for bounding box + pagebox=cropbox, mediabox, artbox, trimbox, bleedbox + -h | --help Show this help message and exit + --version Output version information and exit + -p page Specify a PDF page to extract bounding box + -q Be quiet + -v Be verbose + -O Write output to stdout + -m Output .bb file used in DVIPDFM (default) + -x Output .xbb file used in DVIPDFMx +]] + os.exit(0) +end + +process_arguments["-help"] = process_arguments.h + +-- > Output version information and exit +function process_arguments.V(script_arguments) + print(version) + os.exit(0) +end + +process_arguments["-version"] = process_arguments.V + +-- > Specify a PDF page to extract bounding box +local page_number = 1 +function process_arguments.p(script_arguments) + page_number = tonumber(remove(script_arguments, 1)) +end + +-- > Be quiet +function process_arguments.q(script_arguments) + show_errors = false +end + +-- > Be verbose +function process_arguments.v(script_arguments) + show_errors = true +end + +-- > Write output to stdout +local output_file +function process_arguments.O(script_arguments) + output_file = io.stdout +end + +-- Output format +local output_format = "xbb" + +if script_arguments[0]:match("ebb") then + output_format = "bb" +end + +-- > Output .bb file used in DVIPDFM (default) +function process_arguments.m(script_arguments) + output_format = "bb" +end + +-- > Output .xbb file used in DVIPDFMx +function process_arguments.x(script_arguments) + output_format = "xbb" +end + +-- Get the input file name. +local input_name +function process_arguments.i(script_arguments) + input_name = remove(script_arguments, 1) +end + +process_arguments["-input-name"] = process_arguments.i + +-- Clear the interpreter and script names. +script_arguments[-1] = nil +script_arguments[0] = nil + +-- Process the arguments. +while script_arguments[1] do + -- Get the next argument. + local arg = remove(script_arguments, 1) + local cmd = arg:match("^%-(.*)$") + + -- Default to "--input-name" if no command is given. + if not cmd then + insert(script_arguments, 1, arg) + cmd = "-input-name" + end + + -- Handle multi-character arguments. + if (cmd:len() >= 2) and (not cmd:match("^%-")) then + local i = 0 + for char in cmd:gmatch(".") do + i = i + 1 + insert(script_arguments, i, "-" .. char) + end + + goto continue + end + + -- Get the function to process the argument and run it. + local func = process_arguments[cmd] + + if not func then + error("Invalid argument:", arg) + end + + func(script_arguments) + + ::continue:: +end + +-- Validate the arguments. +if not type(page_number) == "number" then + error("Invalid page number:", page_number) +end + +if not input_name then + error("No input file specified.") +end + +-- Validate the bounding box type. We need this rather crazy fallback scheme +-- to match the behaviour of "extractbb". +local bbox_orders = {} +bbox_orders.mediabox = { + { img = "media", pdfe = "MediaBox" }, +} +bbox_orders.cropbox = { + { img = "crop", pdfe = "CropBox" }, unpack(bbox_orders.mediabox) +} +bbox_orders.artbox = { + { img = "art", pdfe = "ArtBox" }, unpack(bbox_orders.cropbox) +} +bbox_orders.trimbox = { + { img = "trim", pdfe = "TrimBox" }, unpack(bbox_orders.artbox) +} +bbox_orders.bleedbox = { + { img = "bleed", pdfe = "BleedBox" }, unpack(bbox_orders.trimbox) +} +bbox_orders.auto = { + bbox_orders.cropbox[1], bbox_orders.artbox[1], bbox_orders.trimbox[1], + bbox_orders.bleedbox[1], bbox_orders.mediabox[1], +} + +local bbox_order = bbox_orders[bbox_option] + +if not bbox_order then + error("Invalid PDF box type:", bbox_option) +end + +-- Set the default pixel resolution. +local default_dpi +if output_format == "xbb" then + default_dpi = 72 +elseif output_format == "bb" then + default_dpi = 100 +else + error("Invalid output format:", output_format) +end + +-- Open the output file. +if not output_file then + local base_name = input_name:match("(.+)%.([^.]-)$") or input_name + local output_name = base_name .. "." .. output_format + output_file = open_file(output_name, "write", "text") +end + + +------------------------ +--- Image Processing --- +------------------------ + +local x_min, y_min, x_max, y_max +local num_pages, image_type +local pdf_major_version, pdf_minor_version + +if open_image then + -- Check the number of pages. + local image = open_image(input_name) + num_pages = image.pages + + if page_number > num_pages then + error("Invalid page number:", page_number) + end + + -- Open the image to the specified page and bounding box. If the requested + -- bounding box is not available, LuaTeX will fall back to the crop box + -- or the media box. + image = open_image(input_name, page_number, bbox_order[1].img) + + if not image then + error("Cannot open image:", input_name) + end + + -- Get the image metadata. + image_type = image.imagetype + local bounding_box = image.bbox + + if not bounding_box then + error("Cannot get bounding box:", page_number) + end + + local x_resolution = image.xres + local y_resolution = image.yres + + if (x_resolution or 0) == 0 then + x_resolution = default_dpi + end + + if (y_resolution or 0) == 0 then + y_resolution = default_dpi + end + + -- Convert the bounding box to PostScript points. + for i, dimen in ipairs(bounding_box) do + if image_type == "pdf" then + dimen = dimen / BP_TO_SP + else + if i % 2 == 1 then + dimen = dimen / x_resolution * IN_TO_BP + else + dimen = dimen / y_resolution * IN_TO_BP + end + end + + bounding_box[i] = dimen + end + + -- Save the bounding box. + x_min, y_min, x_max, y_max = unpack(bounding_box) + + -- We can't get the PDF version with the "img" library, so we'll just + -- pretend that it's v1.5 (which supports most features). + pdf_major_version = 1 + pdf_minor_version = 5 +else + -- Fallback to PDFs only. + image_type = "pdf" + local document = pdfe.open(input_name) + + if pdfe.getstatus(document) ~= 0 then + error("Cannot open PDF file:", input_name) + end + + -- Check the number of pages. + num_pages = pdfe.getnofpages(document) + + if type(num_pages) ~= "number" then + error("Invalid number of pages:", num_pages) + end + + if page_number > num_pages then + error("Invalid page number:", page_number) + end + + -- Get the page. + local page = pdfe.getpage(document, page_number) + + if not page then + error("Cannot get page:", page_number) + end + + -- Get the bounding box. Here, we check the boxes in the exact same order + -- that "extractbb" does. + local bounding_box + for _, bbox in ipairs(bbox_order) do + bounding_box = pdfe.getbox(page, bbox.pdfe) + + if bounding_box then + break + end + end + + if not bounding_box then + error("Cannot get bounding box:", page_number) + end + + -- Save the bounding box. + x_min, y_min, x_max, y_max = unpack(bounding_box) + + -- Get the PDF version. + pdf_major_version, pdf_minor_version = pdfe.getversion(document) +end + +-- Validate the bounding box. +for _, dimen in ipairs { x_min, y_min, x_max, y_max } do + if type(dimen) ~= "number" then + error("Invalid bounding box:", x_min, y_min, x_max, y_max) + end +end + + +-------------- +--- Output --- +-------------- + +-- Get the output fields and values. +local lines = {} + +insert(lines, ("Title: %s"):format(input_name)) +insert(lines, ("Creator: %s"):format(version)) +insert(lines, + ("BoundingBox: %d %d %d %d") + :format(round(x_min), round(y_min), round(x_max), round(y_max))) + +if output_format == "xbb" then + insert(lines, + ("HiResBoundingBox: %0.6f %0.6f %0.6f %0.6f") + :format(x_min, y_min, x_max, y_max)) + + if image_type == "pdf" then + insert(lines, + ("PDFVersion: %d.%d") + :format(pdf_major_version, pdf_minor_version)) + + insert(lines, ("Pages: %d"):format(num_pages)) + end + +end + +insert(lines, ("CreationDate: %s"):format(os.date("%c", SOURCE_DATE_EPOCH))) + +-- Create the output text. +local begin_line = "%%" +local end_line = "\n" + +local text = begin_line .. + table.concat(lines, end_line .. begin_line) .. + end_line .. end_line + +-- Write the output text. +output_file:write(text) +output_file:close() + +-- Everything is done, so now we can exit. +os.exit(0) diff --git a/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua b/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua new file mode 100755 index 00000000000..4d5bfc6d91a --- /dev/null +++ b/Master/texmf-dist/scripts/extractbb/extractbb-wrapper.lua @@ -0,0 +1,270 @@ +#!/usr/bin/env texlua +-- extractbb-lua +-- https://github.com/gucci-on-fleek/extractbb +-- SPDX-License-Identifier: MPL-2.0+ +-- SPDX-FileCopyrightText: 2024 Max Chernoff +-- +-- A generic wrapper to make commands safe to run with restricted shell escape. +-- +-- Originally created for extractbb, which is listed in shell_escape_commands, +-- but can be run as dvipdfm(x), which in turn can run arbitrary commands +-- using its -D option. +-- +-- The idea is to exec "ebb --ebb <other args>", since only argv[1] is +-- used by dvipdfmx to determine its behavior. +-- +-- Note: This script can only adjust the paths and arguments of the target +-- executable; it *CANNOT* make an arbitrary program safe to run with +-- restricted shell escape. + +-- A shorter, less paranoid version. +-- (Prepend a hyphen to the line below to enable). +--[=[ +arg[0] = arg[0]:gsub("extractbb", "ebb") +table.insert(arg, 1, "ebb") +table.insert(arg, 2, "--extractbb") +os.exec(arg) +os.exit(1) +--]=] + +--------------------- +--- Configuration --- +--------------------- + +-- The base name of this script. (Example: ``extractbb'') +local SCRIPT_NAME = "extractbb" + +-- The base name of the path to the target program. (Example: ``xdvipdfmx'') +local TARGET_PATH_NAME = "xdvipdfmx" + +-- The name to use when calling the target program. Equivalent to ``argv[0]'' +-- in C. (Example: ``extractbb'') +local TARGET_EXEC_NAME = "ebb" + +-- Any extra arguments to be prepended to the target program, before any +-- user-supplied arguments. Equivalent to ``argv[1], ...'' in C. +-- (Example: ``--extractbb'') +local TARGET_PREPEND_ARGS = { "--extractbb" } + +-- Any extra arguments to be appended to the target program, after any +-- user-supplied arguments. Equivalent to ``..., argv[argc]'' in C. +local TARGET_APPEND_ARGS = {} + +-- Sets the value of ``openin_any'' to this value. If ``nil'', then the value +-- will be left unchanged. (Example: ``r'') +local READ_PERMS = "r" + +-- Sets the value of ``openout_any'' to this value. If ``nil'', then the value +-- will be left unchanged. (Example: ``p'') +local WRITE_PERMS = "p" + +-- The name of the Lua interpreter. (Example: ``texlua'') +local INTERPRETER_NAME = "texlua" + +-- The extension of the interpreter. Extensionless-names are also permitted. +-- (Example: ``exe'') +local INTERPRETER_EXT = "exe" + + +---------------------- +--- Initialization --- +---------------------- + +-- Save often-used globals for a slight speed boost. +local insert = table.insert + +-- Set the kpathsea program name +kpse.set_program_name(INTERPRETER_NAME, SCRIPT_NAME) + +-- Rename the input arguments so we don't get confused +local script_args = arg + + +---------------------------- +--- Function Definitions --- +---------------------------- + +-- Error messages +local function error(title, details) + -- Header + io.stderr:write("! extractbb ERROR: ") + io.stderr:write(title) + io.stderr:write(".\n\nTechnical Details:\n") + + -- Messages + for key, value in pairs(details) do + io.stderr:write(tostring(key), ": ") + io.stderr:write("(", type(value), ") ") + io.stderr:write(tostring(value), "\n") + end + + -- Traceback + io.stderr:write("\n") + io.stderr:write(debug.traceback(nil, 2), "\n") + + -- Flush and exit + io.stderr:flush() + os.exit(1) +end + +-- Get the directory, name, and extension from a full path. We'll split on +-- either a forward or backward slash---Windows can use either, and we don't +-- need to support Unix systems with TL installed to a directory with +-- backslashes in its name. +local split_dir_pattern = "^(.*)[/\\]([^/\\]-)$" +local split_ext_pattern = "(.*)%.([^.]-)$" + +local function split_path(path) + -- Make sure that we were given a string + if type(path) ~= "string" then + return nil, nil, nil + end + + -- Split the (directory) from the (name and extension) + local dir, name_ext = path:match(split_dir_pattern) + + -- No directory + if not dir then + dir = nil + name_ext = path + + -- A bare directory (with a trailing slash) + elseif name_ext == "" then + return dir, nil, nil + end + + -- Split the (name) from the (extension) + local name, ext = name_ext:match(split_ext_pattern) + + -- No extension (or a dotfile) + if (not name) or (name == "") then + name = name_ext + ext = nil + end + + return dir, name, ext +end + +-- See if a file exists +local function file_exists(path) + local mode = lfs.attributes(path, "mode") + return (mode == "file") or (mode == "link") +end + + +--------------------- +--- Safety Checks --- +--------------------- + +-- Make sure that we're running unrestricted. +if status.shell_escape ~= 1 then + error("Shell escape has been disabled", { + shell_escape = status.shell_escape, + }) +end + +if status.safer_option ~= 0 then + error("The ``safer'' option has been enabled", { + safer_option = status.safer_option, + }) +end + +-- Set the file permissions. +if READ_PERMS then + os.setenv("openin_any", READ_PERMS) +end + +if WRITE_PERMS then + os.setenv("openout_any", WRITE_PERMS) +end + +-- Get the location of the interpreter +local interpreter_dir = os.selfdir or kpse.var_value("SELFAUTOLOC") +local _, interpreter_name, interpreter_ext = split_path(script_args[-1]) + +if os.type == "windows" then + interpreter_ext = INTERPRETER_EXT +end + +-- Error details +local error_details = { + interpreter_dir = interpreter_dir or "<nil>", + interpreter_name = interpreter_name or "<nil>", + interpreter_ext = interpreter_ext or "<nil>", + os_type = os.type or "<nil>", + os_name = os.name or "<nil>", +} + +-- Get the path to the target program +local target_ext = interpreter_ext and ("." .. interpreter_ext) or "" +local target_path = interpreter_dir .. "/" .. TARGET_PATH_NAME .. target_ext + +error_details.target_path = target_path or "<nil>" +error_details.target_ext = target_ext or "<nil>" + +-- Make sure that the target program exists +if not file_exists(target_path) then + error("The target program does not exist", error_details) +end + + +---------------------- +--- Run the target --- +---------------------- + +-- Generate the target arguments +local target_args = { + [0] = target_path, -- Path to the executable + [1] = TARGET_EXEC_NAME, -- argv[0] +} + +-- argv[2] through argv[n] +for _, arg in ipairs(TARGET_PREPEND_ARGS) do + insert(target_args, arg) +end + +for i = 1, #script_args do + -- We use a numeric iterator here to avoid ``arg[-1]'' and ``arg[0]''. + local this_arg = script_args[i] + insert(target_args, this_arg) + + -- Show version information + if this_arg:match("%-version") then + print("[Wrapped by extractbb.lua v1.0.0 (2024-11-17)]") --%%version %%dashdate + end +end + +for _, arg in ipairs(TARGET_APPEND_ARGS) do + insert(target_args, arg) +end + +-- For Unixish OSs, argv is passed as an array into a spawned process, so no +-- further tokenization is done by the C runtime, therefore arguments containing +-- special characters won't cause any issues. For Windows, argv is concatenated +-- into a single string when spawning a new process, then retokenized by the C +-- runtime in the new program, so we need to be careful with special characters +-- here since they might be interpreted as token separators. +if os.type == "windows" then + for i, arg in ipairs(target_args) do + -- Hmm, which characters do we need to protect against? + if arg:match('"') then + -- Already contains a quote, so let's hope that this means that + -- someone already quoted it correctly. + elseif arg:match("[^-_%l%u%d]") then + -- Contains a special character, so let's quote it. + target_args[i] = '"' .. arg .. '"' + end + -- Any other cases and things should be fine. Probably. + end +end + +-- Run the target program, replacing the current process +local _, err = os.exec(target_args) + +-- Unreachable except in the case of a failed exec +for key, value in ipairs(target_args) do + error_details["target_args[" .. key .. "]"] = value +end + +error_details.exec_message = err or "<nil>" +error("The target program failed to run", error_details) diff --git a/Master/texmf-dist/scripts/extractbb/extractbb.lua b/Master/texmf-dist/scripts/extractbb/extractbb.lua new file mode 100755 index 00000000000..15e9909fb70 --- /dev/null +++ b/Master/texmf-dist/scripts/extractbb/extractbb.lua @@ -0,0 +1,71 @@ +#!/usr/bin/env texlua +-- extractbb-lua +-- https://github.com/gucci-on-fleek/extractbb +-- SPDX-License-Identifier: MPL-2.0+ +-- SPDX-FileCopyrightText: 2024 Max Chernoff +-- +-- A wrapper script to allow you to choose which implementation of extractbb to +-- use. Should hopefully be replaced with the ``scratch'' file in TeX Live 2025. +-- +-- v1.0.0 (2024-11-17) %%version %%dashdate + +--------------------- +--- Configuration --- +--------------------- +-- Choose which implementation of extractbb to use. +local DEFAULT = "wrapper" + + +----------------- +--- Execution --- +----------------- + +-- Get the value of the environment variable that decides which version to run. +local env_choice = os.env["TEXLIVE_EXTRACTBB"] + +-- If the environment variable is set to a file path, run that directly. +local env_mode = lfs.attributes(env_choice or "", "mode") +if (env_mode == "file") or (env_mode == "link") then + arg[0] = env_choice + table.insert(arg, 1, env_choice) + arg[-1] = nil + return os.exec(arg) +end + +-- Map the choice names to file names. +kpse.set_program_name("texlua", "extractbb") +local choice_mapping = { + wrapper = kpse.find_file("extractbb-wrapper.lua", "lua", true), + scratch = kpse.find_file("extractbb-scratch.lua", "lua", true), +} + +-- Choose the implementation to run. +local choice = choice_mapping[env_choice] or choice_mapping[DEFAULT] + +if not choice then + print("No implementation of extractbb found. Exiting.") + os.exit(1) +end + +-- Make sure that the script is not writable. +if kpse.out_name_ok_silent_extended(choice) then + if os.env["TEXLIVE_EXTRACTBB_UNSAFE"] == "unsafe" then + -- If we're running in development mode, then we can allow this. + else + print("Refusing to run a writable script. Exiting.") + os.exit(1) + end +end + +-- Make sure that the script is beside this one, just to be safe +local split_dir_pattern = "^(.*)[/\\]([^/\\]-)$" +local current_dir, current_name = arg[0]:match(split_dir_pattern) +local choice_dir, choice_name = choice:match(split_dir_pattern) + +if current_dir ~= choice_dir then + print("Refusing to run a script from a different directory. Exiting.") + os.exit(1) +end + +-- And run it. +dofile(choice) |