summaryrefslogtreecommitdiff
path: root/Master/texmf-dist
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2024-11-18 22:29:08 +0000
committerKarl Berry <karl@freefriends.org>2024-11-18 22:29:08 +0000
commitc4e1c62bab8f131ab82c200b9f4d76b7df9d5885 (patch)
tree17e4474f0a01f396778fe73c745f57391e441f27 /Master/texmf-dist
parent46af935d0c083bbca25e26099f0226d2e512a296 (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.md81
-rwxr-xr-xMaster/texmf-dist/scripts/extractbb/extractbb-scratch.lua695
-rwxr-xr-xMaster/texmf-dist/scripts/extractbb/extractbb-wrapper.lua270
-rwxr-xr-xMaster/texmf-dist/scripts/extractbb/extractbb.lua71
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)