summaryrefslogtreecommitdiff
path: root/src/luaotfload-diagnostics.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/luaotfload-diagnostics.lua')
-rw-r--r--src/luaotfload-diagnostics.lua684
1 files changed, 684 insertions, 0 deletions
diff --git a/src/luaotfload-diagnostics.lua b/src/luaotfload-diagnostics.lua
new file mode 100644
index 0000000..d9b13f5
--- /dev/null
+++ b/src/luaotfload-diagnostics.lua
@@ -0,0 +1,684 @@
+#!/usr/bin/env texlua
+-----------------------------------------------------------------------
+-- FILE: luaotfload-diagnostics.lua
+-- DESCRIPTION: functionality accessible by the --diagnose option
+-- REQUIREMENTS: luaotfload-tool.lua
+-- AUTHOR: Philipp Gesang (Phg), <phg42.2a@gmail.com>
+-- VERSION: 2.5
+-- MODIFIED: 2014-01-02 21:23:06+0100
+-----------------------------------------------------------------------
+--
+local names = fonts.names
+
+local kpse = require "kpse"
+local kpseexpand_path = kpse.expand_path
+local kpseexpand_var = kpse.expand_var
+local kpsefind_file = kpse.find_file
+
+local lfs = require "lfs"
+local lfsattributes = lfs.attributes
+local lfsisfile = lfs.isfile
+local lfsreadlink = lfs.readlink
+
+local md5 = require "md5"
+local md5sumhexa = md5.sumhexa
+
+local ioopen = io.open
+
+local osgetenv = os.getenv
+local osname = os.name
+local osremove = os.remove
+local ostype = os.type
+local stringformat = string.format
+local stringlower = string.lower
+local stringsub = string.sub
+
+local fileisreadable = file.isreadable
+local fileiswritable = file.iswritable
+local filesplitpath = file.splitpath
+local filesuffix = file.suffix
+local ioloaddata = io.loaddata
+local lua_of_json = utilities.json.tolua
+local tableconcat = table.concat
+local tablesortedkeys = table.sortedkeys
+local tabletohash = table.tohash
+
+local lpeg = require "lpeg"
+local C, Cg, Ct = lpeg.C, lpeg.Cg, lpeg.Ct
+local lpegmatch = lpeg.match
+
+local report = luaotfload.log.report
+local out = function (...)
+ report (false, 0, "diagnose", ...)
+end
+
+local parsers = luaotfload.parsers
+local stripslashes = parsers.stripslashes
+local splitcomma = parsers.splitcomma
+
+local check_index = function (errcnt)
+
+ out "================= font names =================="
+ local namedata = names.data()
+
+ if not namedata then
+ namedata = names.load ()
+ end
+
+ local mappings = namedata.mappings
+
+ if not namedata and namedata.formats and namedata.version then
+ out "Database corrupt."
+ return errcnt + 1
+ end
+
+ out ("Database version: %.3f.", namedata.meta.version)
+ out ("Font formats indexed: %s.",
+ tableconcat (namedata.meta.formats, ", "))
+ out ("%d font files indexed.", #mappings)
+
+ local by_format = { }
+ for i = 1, #mappings do
+ local record = mappings[i]
+ local format = stringlower (filesuffix (record.filename))
+ local count = by_format[format]
+ if count then
+ by_format[format] = count + 1
+ else
+ by_format[format] = 1
+ end
+ end
+
+ local formats = tablesortedkeys (by_format)
+ for i = 1, #formats do
+ local format = formats[i]
+ out ("%20s: %5d", format, by_format[format])
+ end
+ return errcnt
+end
+
+local verify_files = function (errcnt)
+ out "================ verify files ================="
+ local status = config.luaotfload.status
+ local hashes = status.hashes
+ local notes = status.notes
+ if not hashes or #hashes == 0 then
+ out ("FAILED: cannot read checksums from %s.", status_file)
+ return 1/0
+ elseif not notes then
+ out ("FAILED: cannot read commit metadata from %s.",
+ status_file)
+ return 1/0
+ end
+
+ out ("Luaotfload revision %s.", notes.revision)
+ out ("Committed by %s.", notes.committer)
+ out ("Timestamp %s.", notes.timestamp)
+
+ local nhashes = #hashes
+ out ("Testing %d files for integrity.", nhashes)
+ for i = 1, nhashes do
+ local fname, canonicalsum = unpack (hashes[i])
+ local location = kpsefind_file (fname)
+ or kpsefind_file (fname, "texmfscripts")
+ if not location then
+ errcnt = errcnt + 1
+ out ("FAILED: file %s missing.", fname)
+ else
+ out ("File: %s.", location)
+ local raw = ioloaddata (location)
+ if not raw then
+ errcnt = errcnt + 1
+ out ("FAILED: file %d not readable.", fname)
+ else
+ local sum = md5sumhexa (raw)
+ if sum ~= canonicalsum then
+ errcnt = errcnt + 1
+ out ("FAILED: checksum mismatch for file %s.",
+ fname)
+ out ("Expected %s.", canonicalsum)
+ out ("Got %s.", sum)
+ else
+ out ("Ok, %s passed.", fname)
+ end
+ end
+ end
+ end
+ return errcnt
+end
+
+local get_tentative_attributes = function (file)
+ if not lfsisfile (file) then
+ local chan = ioopen (file, "w")
+ if chan then
+ chan:close ()
+ local attributes = lfsattributes (file)
+ os.remove (file)
+ return attributes
+ end
+ end
+end
+
+local p_permissions = Ct(Cg(Ct(C(1) * C(1) * C(1)), "u")
+ * Cg(Ct(C(1) * C(1) * C(1)), "g")
+ * Cg(Ct(C(1) * C(1) * C(1)), "o"))
+
+local analyze_permissions = function (raw)
+ return lpegmatch (p_permissions, raw)
+end
+
+local get_permissions = function (t, location)
+ if stringsub (location, #location) == "/" then
+ --- strip trailing slashes (lfs idiosyncrasy on Win)
+ location = lpegmatch (stripslashes, location)
+ end
+ local attributes = lfsattributes (location)
+
+ if not attributes and t == "f" then
+ attributes = get_tentative_attributes (location)
+ if not attributes then
+ return false
+ end
+ end
+
+ local permissions
+
+ if fileisreadable (location) then
+ --- link handling appears to be unnecessary because
+ --- lfs.attributes() will return the information on
+ --- the link target.
+ if mode == "link" then --follow and repeat
+ location = lfsreadlink (location)
+ attributes = lfsattributes (location)
+ end
+ end
+
+ permissions = analyze_permissions (attributes.permissions)
+
+ return {
+ location = location,
+ mode = attributes.mode,
+ owner = attributes.uid, --- useless on windows
+ permissions = permissions,
+ attributes = attributes,
+ }
+end
+
+local check_conformance = function (spec, permissions, errcnt)
+ local uid = permissions.attributes.uid
+ local gid = permissions.attributes.gid
+ local raw = permissions.attributes.permissions
+
+ out ("Owner: %d, group %d, permissions %s.", uid, gid, raw)
+ if ostype == "unix" then
+ if uid == 0 or gid == 0 then
+ out "Owned by the superuser, permission conflict likely."
+ errcnt = errcnt + 1
+ end
+ end
+
+ local user = permissions.permissions.u
+ if spec.r == true then
+ if user[1] == "r" then
+ out "Readable: ok."
+ else
+ out "Not readable: permissions need fixing."
+ errcnt = errcnt + 1
+ end
+ end
+
+ if spec.w == true then
+ if user[2] == "w"
+ or fileiswritable (permissions.location) then
+ out "Writable: ok."
+ else
+ out "Not writable: permissions need fixing."
+ errcnt = errcnt + 1
+ end
+ end
+
+ return errcnt
+end
+
+local desired_permissions
+local init_desired_permissions = function ()
+ inspect(config.luaotfload.paths)
+ local paths = config.luaotfload.paths
+ desired_permissions = {
+ { "d", {"r","w"}, function () return caches.getwritablepath () end },
+ { "d", {"r","w"}, paths.prefix },
+ { "f", {"r","w"}, paths.index_path_lua .. ".gz" },
+ { "f", {"r","w"}, paths.index_path_luc },
+ { "f", {"r","w"}, paths.lookup_path_lua },
+ { "f", {"r","w"}, paths.lookup_path_luc },
+ }
+end
+
+local check_permissions = function (errcnt)
+ out [[=============== file permissions ==============]]
+ if not desired_permissions then init_desired_permissions () end
+ for i = 1, #desired_permissions do
+ local t, spec, path = unpack (desired_permissions[i])
+ if type (path) == "function" then
+ path = path ()
+ end
+
+ spec = tabletohash (spec)
+
+ out ("Checking permissions of %s.", path)
+
+ local permissions = get_permissions (t, path)
+ if permissions then
+ --inspect (permissions)
+ errcnt = check_conformance (spec, permissions, errcnt)
+ else
+ errcnt = errcnt + 1
+ end
+ end
+ return errcnt
+end
+
+local check_upstream
+
+if kpsefind_file ("https.lua", "lua") == nil then
+ check_upstream = function (errcnt)
+ out [[============= upstream repository =============
+ WARNING: Cannot retrieve repository data.
+ Github API access requires the luasec library.
+ Grab it from <https://github.com/brunoos/luasec>
+ and retry.]]
+ return errcnt
+ end
+else
+--- github api stuff begin
+ local https = require "ssl.https"
+
+ local gh_api_root = [[https://api.github.com]]
+ local release_url = [[https://github.com/lualatex/luaotfload/releases]]
+ local luaotfload_repo = [[lualatex/luaotfload]]
+ local user_agent = [[lualatex/luaotfload integrity check]]
+ local shortbytes = 8
+
+ local gh_shortrevision = function (rev)
+ return stringsub (rev, 1, shortbytes)
+ end
+
+ local gh_encode_parameters = function (parameters)
+ local acc = {}
+ for field, value in next, parameters do
+ --- unsafe, non-urlencoded coz it’s all ascii chars
+ acc[#acc+1] = field .. "=" .. value
+ end
+ return "?" .. tableconcat (acc, "&")
+ end
+
+ local gh_make_url = function (components, parameters)
+ local url = tableconcat ({ gh_api_root,
+ unpack (components) },
+ "/")
+ if parameters then
+ url = url .. gh_encode_parameters (parameters)
+ end
+ return url
+ end
+
+ local alright = [[HTTP/1.1 200 OK]]
+
+ local gh_api_request = function (...)
+ local args = {...}
+ local nargs = #args
+ local final = args[nargs]
+ local request = {
+ url = "",
+ headers = { ["user-agent"] = user_agent },
+ }
+ if type (final) == "table" then
+ args[nargs] = nil
+ request = gh_make_url (args, final)
+ else
+ request = gh_make_url (args)
+ end
+
+ out ("Requesting <%s>.", request)
+ local response, code, headers, status = https.request (request)
+ if status ~= alright then
+ out "Request failed!"
+ return false
+ end
+ return response
+ end
+
+ local gh_api_checklimit = function (headers)
+ local rawlimit = gh_api_request "rate_limit"
+ local limitdata = lua_of_json (rawlimit)
+ if not limitdata and limitdata.rate then
+ out "Cannot parse API rate limit."
+ return false
+ end
+ limitdata = limitdata.rate
+
+ local limit = tonumber (limitdata.limit)
+ local left = tonumber (limitdata.remaining)
+ local reset = tonumber (limitdata.reset)
+
+ out ("%d of %d Github API requests left.", left, limit)
+ if left == 0 then
+ out ("Cannot make any more API requests.")
+ if ostype == "unix" then
+ out ("Try again later at %s.", osdate ("%F %T", reset))
+ else --- windows doesn’t C99
+ out ("Try again later at %s.",
+ osdate ("%Y-%m-d %H:%M:%S", reset))
+ end
+ end
+ return true
+ end
+
+ local gh_tags = function ()
+ out "Fetching tags from repository, please stand by."
+ local rawtags = gh_api_request ("repos",
+ luaotfload_repo,
+ "tags")
+ local taglist = lua_of_json (rawtags)
+ if not taglist or #taglist == 0 then
+ out "Cannot parse response."
+ return false
+ end
+
+ local ntags = #taglist
+ out ("Repository contains %d tags.", ntags)
+ local _idx, latest = next (taglist)
+ out ("The most recent release is %s (revision %s).",
+ latest.name,
+ gh_shortrevision (latest.commit.sha))
+ return latest
+ end
+
+ local gh_compare = function (head, base)
+ if base == nil then
+ base = "HEAD"
+ end
+ out ("Fetching comparison between %s and %s, \z
+ please stand by.",
+ gh_shortrevision (head),
+ gh_shortrevision (base))
+ local comparison = base .. "..." .. head
+ local rawstatus = gh_api_request ("repos",
+ luaotfload_repo,
+ "compare",
+ comparison)
+ local status = lua_of_json (rawstatus)
+ if not status then
+ out "Cannot parse response for status request."
+ return false
+ end
+ return status
+ end
+
+ local gh_news = function (since)
+ local compared = gh_compare (since)
+ if not compared then
+ return false
+ end
+ local behind_by = compared.behind_by
+ local ahead_by = compared.ahead_by
+ local status = compared.status
+ out ("Comparison state: %s.", status)
+ if behind_by > 0 then
+ out ("Your Luaotfload is %d \z
+ revisions behind upstream.",
+ behind_by)
+ return behind_by
+ elseif status == "ahead" then
+ out "Since you are obviously from the future \z
+ I assume you already know the repository state."
+ else
+ out "Everything up to date. \z
+ Luaotfload is in sync with upstream."
+ end
+ return false
+ end
+
+ local gh_catchup = function (current, latest)
+ local compared = gh_compare (latest, current)
+ local ahead_by = tonumber (compared.ahead_by)
+ if ahead_by > 0 then
+ local permalink_url = compared.permalink_url
+ out ("Your Luaotfload is %d revisions \z
+ behind the most recent release.",
+ ahead_by)
+ out ("To view the commit log, visit <%s>.",
+ permalink_url)
+ out ("You can grab an up to date tarball at <%s>.",
+ release_url)
+ return true
+ else
+ out "There weren't any new releases in the meantime."
+ out "Luaotfload is up to date."
+ end
+ return false
+ end
+
+ check_upstream = function (current)
+ out "============= upstream repository ============="
+ local _succ = gh_api_checklimit ()
+ local behind = gh_news (current)
+ if behind then
+ local latest = gh_tags ()
+ local _behind = gh_catchup (current,
+ latest.commit.sha,
+ latest.name)
+ end
+ end
+
+ --- trivium: diff since the first revision as pushed by Élie
+ --- in 2009
+ --- local firstrevision = "c3ccb3ee07e0a67171c24960966ae974e0dd8e98"
+ --- check_upstream (firstrevision)
+end
+--- github api stuff end
+
+local print_envvar = function (var)
+ local val = osgetenv (var)
+ if val then
+ out ("%20s: %q", stringformat ("$%s", var), val)
+ return val
+ else
+ out ("%20s: <unset>", stringformat ("$%s", var))
+ end
+end
+
+local print_path = function (var)
+ local val = osgetenv (var)
+ if val then
+ local paths = filesplitpath (val)
+ if paths then
+ local npaths = #paths
+ if npaths == 1 then
+ out ("%20s: %q", stringformat ("$%s", var), val)
+ elseif npaths > 1 then
+ out ("%20s: <%d items>", stringformat ("$%s", var), npaths)
+ for i = 1, npaths do
+ out (" +: %q", paths[i])
+ end
+ else
+ out ("%20s: <empty>")
+ end
+ end
+ else
+ out ("%20s: <unset>", stringformat ("$%s", var))
+ end
+end
+
+local print_kpsevar = function (var)
+ var = "$" .. var
+ local val = kpseexpand_var (var)
+ if val and val ~= var then
+ out ("%20s: %q", var, val)
+ return val
+ else
+ out ("%20s: <empty or unset>", var)
+ end
+end
+
+local print_kpsepath = function (var)
+ var = "$" .. var
+ local val = kpseexpand_path (var)
+ if val and val ~= "" then
+ local paths = filesplitpath (val)
+ if paths then
+ local npaths = #paths
+ if npaths == 1 then
+ out ("%20s: %q", var, paths[1])
+ elseif npaths > 1 then
+ out ("%20s: <%d items>", var, npaths)
+ for i = 1, npaths do
+ out (" +: %q", paths[i])
+ end
+ else
+ out ("%20s: <empty>")
+ end
+ end
+ else
+ out ("%20s: <empty or unset>", var)
+ end
+end
+
+--- this test first if a variable is set and then expands the
+--- paths; this is necessitated by the fact that expand-path will
+--- return the empty string both if the variable is unset and if
+--- the directory does not exist
+
+local print_kpsepathvar = function (var)
+ local vvar = "$" .. var
+ local val = kpseexpand_var (vvar)
+ if val and val ~= vvar then
+ out ("%20s: %q", vvar, val)
+ print_kpsepath (var)
+ else
+ out ("%20s: <empty or unset>", var)
+ end
+end
+
+local check_environment = function (errcnt)
+ out "============ environment settings ============="
+ out ("system: %s/%s", ostype, osname)
+ if ostype == "unix" and io.popen then
+ local chan = io.popen ("uname -a", "r")
+ if chan then
+ out ("info: %s", chan:read "*all")
+ chan:close ()
+ end
+ end
+
+ out "1) *shell environment*"
+ print_envvar "SHELL"
+ print_path "PATH"
+ print_path "OSFONTDIR"
+ print_envvar "USER"
+ if ostype == "windows" then
+ print_envvar "WINDIR"
+ print_envvar "CD"
+ print_path "TEMP"
+ elseif ostype == "unix" then
+ print_envvar "HOME"
+ print_envvar "PWD"
+ print_path "TMPDIR"
+ end
+
+ out "2) *kpathsea*"
+ print_kpsepathvar "OPENTYPEFONTS"
+ print_kpsepathvar "TTFONTS"
+
+ print_kpsepathvar "TEXMFCACHE"
+ print_kpsepathvar "TEXMFVAR"
+
+ --- the expansion of these can be quite large; as they aren’t
+ --- usually essential to luaotfload, we won’t dump every single
+ --- path
+ print_kpsevar "LUAINPUTS"
+ print_kpsevar "CLUAINPUTS"
+
+ return errcnt
+end
+
+local anamneses = {
+ "environment",
+ "files",
+ "index",
+ "repository",
+ "permissions"
+}
+
+local diagnose = function (job)
+ local errcnt = 0
+ local asked = job.asked_diagnostics
+ if asked == "all" or asked == "thorough" then
+ asked = tabletohash (anamneses, true)
+ else
+ asked = lpegmatch (splitcomma, asked)
+ asked = tabletohash (asked, true)
+ end
+
+ if asked.index == true then
+ errcnt = check_index (errcnt)
+ asked.index = nil
+ end
+
+ if asked.environment == true then
+ errcnt = check_environment (errcnt)
+ asked.environment = nil
+ end
+
+ if asked.files == true then
+ errcnt = verify_files (errcnt)
+ asked.files = nil
+ end
+
+ if asked.permissions == true then
+ errcnt = check_permissions (errcnt)
+ asked.permissions = nil
+ end
+
+ if asked.repository == true then
+ check_upstream (status.notes.revision)
+ asked.repository = nil
+ end
+
+ local rest = next (asked)
+ if rest ~= nil then --> something unknown
+ out ("Unknown diagnostic %q.", rest)
+ end
+ if errcnt == 0 then --> success
+ out ("Everything appears to be in order, \z
+ you may sleep well.")
+ return true, false
+ end
+ out ( [[===============================================
+ WARNING
+ ===============================================
+
+ The diagnostic detected %d errors.
+
+ This version of luaotfload may have been
+ tampered with. Modified versions of the
+ luaotfload source are unsupported. Read the log
+ carefully and get a clean version from CTAN or
+ github:
+
+ × http://ctan.org/tex-archive/macros/luatex/generic/luaotfload
+ × https://github.com/lualatex/luaotfload/releases
+
+ If you are uncertain as to how to proceed, then
+ ask on the lualatex mailing list:
+
+ http://www.tug.org/mailman/listinfo/lualatex-dev
+
+ ===============================================
+]], errcnt)
+ return true, false
+end
+
+return diagnose
+
+-- vim:tw=71:sw=4:ts=4:expandtab