aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/purl.lua
diff options
context:
space:
mode:
authorWilliam Boman <william@redwill.se>2022-12-10 23:01:16 +0100
committerGitHub <noreply@github.com>2022-12-10 23:01:16 +0100
commite8bf53119572622f9c45c82f4ef9443a4d37df4b (patch)
treefae78a9d2dc7283bddc0e611a8aa3a43ee329fdb /lua/mason-core/purl.lua
parentfeat(functional): add some more functions (#755) (diff)
downloadmason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.tar
mason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.tar.gz
mason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.tar.bz2
mason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.tar.lz
mason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.tar.xz
mason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.tar.zst
mason-e8bf53119572622f9c45c82f4ef9443a4d37df4b.zip
feat: add purl parser (#756)
Diffstat (limited to 'lua/mason-core/purl.lua')
-rw-r--r--lua/mason-core/purl.lua283
1 files changed, 283 insertions, 0 deletions
diff --git a/lua/mason-core/purl.lua b/lua/mason-core/purl.lua
new file mode 100644
index 00000000..e2430baf
--- /dev/null
+++ b/lua/mason-core/purl.lua
@@ -0,0 +1,283 @@
+local _ = require "mason-core.functional"
+local Result = require "mason-core.result"
+local Optional = require "mason-core.optional"
+
+local M = {}
+
+-- Fully spec-compliant parser for purls (https://github.com/package-url/purl-spec)
+
+---@param str string
+local function parse_hex(str)
+ return tonumber(str, 16)
+end
+
+---@param char string
+local function percent_encode(char)
+ return ("%%%x"):format(string.byte(char, 1, 1))
+end
+
+local decode_percent_encoding = _.gsub("%%([A-F0-9][A-F0-9])", _.compose(string.char, parse_hex))
+local encode_percent_encoding = _.gsub("[!#$&'%(%)%*%+;=%?@%[%] ]", percent_encode)
+
+local function validate_conan(purl)
+ if purl.namespace and not _.path({ "qualifiers", "channel" }, purl) then
+ return Result.failure "Missing channel qualifier."
+ elseif not purl.namespace and _.path({ "qualifiers", "channel" }, purl) then
+ return Result.failure "Missing namespace."
+ end
+ return Result.success(purl)
+end
+
+local function validate_cran(purl)
+ if not purl.version then
+ return Result.failure "Missing version."
+ end
+ return Result.success(purl)
+end
+
+local function validate_swift(purl)
+ if not purl.namespace then
+ return Result.failure "Missing namespace."
+ end
+ if not purl.version then
+ return Result.failure "Missing version."
+ end
+ return Result.success(purl)
+end
+
+---@class Purl
+---@field scheme '"pkg"'
+---@field type string
+---@field namespace string?
+---@field name string
+---@field version string?
+---@field qualifiers table<string, string>?
+---@field subpath string?
+
+---@param str string
+local function split_once_right(str, char)
+ for i = #str, 1, -1 do
+ if str:sub(i, i) == char then
+ local segment = str:sub(i + 1, #str)
+ return str:sub(1, i - 1), segment
+ end
+ end
+ return str
+end
+
+---@param str string
+local function split_once_left(str, char)
+ for i = 1, #str do
+ if str:sub(i, i) == char then
+ local segment = str:sub(1, i - 1)
+ return segment, str:sub(i + 1)
+ end
+ end
+ return str
+end
+
+local function left_trim(char, str)
+ for i = 1, #str do
+ if str:sub(i, i) ~= char then
+ return i
+ end
+ end
+ return #str + 1
+end
+
+local function right_trim(char, str)
+ for i = #str, 1, -1 do
+ if str:sub(i, i) ~= char then
+ return i
+ end
+ end
+ return #str + 1
+end
+
+---@param char string
+---@param str string
+local function trim(char, str)
+ return str:sub(left_trim(char, str), right_trim(char, str))
+end
+
+local parse_subpath = _.compose(
+ _.join "/",
+ _.filter_map(function(segment)
+ if segment == "." or segment == ".." or segment == "" then
+ return Optional.empty()
+ end
+ return Optional.of(decode_percent_encoding(segment))
+ end),
+ _.split "/",
+ _.partial(trim, "/")
+)
+
+local parse_qualifiers = _.compose(
+ _.evolve {
+ checksum = _.split ",",
+ },
+ _.from_pairs,
+ _.filter_map(function(pair)
+ local key, value = split_once_left(pair, "=")
+ if value ~= nil and value ~= "" then
+ return Optional.of { _.to_lower(key), decode_percent_encoding(value) }
+ else
+ return Optional.empty()
+ end
+ end),
+ _.split "&"
+)
+
+local parse_namespace = _.compose(
+ _.join "/",
+ _.filter_map(function(segment)
+ if segment == "" then
+ return Optional.empty()
+ end
+ return Optional.of(decode_percent_encoding(segment))
+ end),
+ _.split "/"
+)
+
+local pypi = _.evolve {
+ name = _.compose(_.to_lower, _.gsub("_", "-")),
+}
+
+local huggingface = _.evolve {
+ version = _.to_lower,
+}
+
+local azuredatabricks = _.evolve {
+ name = _.to_lower,
+ namespace = _.to_lower,
+}
+
+local bitbucket = _.evolve {
+ name = _.to_lower,
+ namespace = _.to_lower,
+}
+
+local github = _.evolve {
+ name = _.to_lower,
+ namespace = _.to_lower,
+}
+
+local is_mlflow_azuredatabricks = _.all_pass {
+ _.prop_eq("type", "mlflow"),
+ _.path_satisfies(_.matches "^https?://.*azuredatabricks%.net", { "qualifiers", "repository_url" }),
+}
+
+local type_validations = _.cond {
+ { _.prop_eq("type", "conan"), validate_conan },
+ { _.prop_eq("type", "cran"), validate_cran },
+ { _.prop_eq("type", "swift"), validate_swift },
+ { _.T, Result.success },
+}
+
+local type_transforms = _.cond {
+ { _.prop_eq("type", "bitbucket"), bitbucket },
+ { _.prop_eq("type", "github"), github },
+ { _.prop_eq("type", "pypi"), pypi },
+ { _.prop_eq("type", "huggingface"), huggingface },
+ { is_mlflow_azuredatabricks, azuredatabricks },
+ { _.T, _.identity },
+}
+
+local type_specific_transforms = _.compose(type_validations, type_transforms)
+
+---@param raw_purl string
+---@return Result # Result<Purl>
+function M.parse(raw_purl)
+ -- Implementation of recommended parsing algo
+ -- https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#how-to-parse-a-purl-string-in-its-components
+ local remainder, subpath = split_once_right(raw_purl, "#")
+ if subpath then
+ subpath = parse_subpath(subpath)
+ end
+
+ local remainder, qualifiers = split_once_right(remainder, "?")
+ if qualifiers then
+ qualifiers = parse_qualifiers(qualifiers)
+ if not _.all(_.matches "^[a-zA-Z%-_%.][0-9a-zA-Z%-_%.]*$", _.keys(qualifiers)) then
+ return Result.failure "Malformed purl (invalid qualifier names)."
+ end
+ end
+
+ local scheme, remainder = split_once_left(remainder, ":")
+ if not remainder then
+ return Result.failure "Malformed purl (missing type, namespace, name, version components)."
+ end
+ if scheme ~= "pkg" then
+ return Result.failure "Malformed purl (invalid scheme)."
+ end
+ remainder = trim("/", remainder)
+
+ local type, remainder = split_once_left(remainder, "/")
+ if not remainder then
+ return Result.failure "Malformed purl (missing namespace, name, version components)"
+ end
+ type = _.to_lower(type)
+
+ local remainder, version = split_once_right(remainder, "@")
+ if version then
+ version = decode_percent_encoding(version)
+ end
+
+ local remainder, name = split_once_right(remainder, "/")
+ if not name then
+ name = remainder
+ remainder = nil
+ end
+ if name == "" then
+ return Result.failure "Malformed purl (missing name)."
+ end
+ name = decode_percent_encoding(name)
+
+ local namespace = remainder
+ if namespace then
+ namespace = parse_namespace(namespace)
+ end
+
+ return type_specific_transforms {
+ scheme = scheme,
+ type = type,
+ namespace = namespace,
+ name = name,
+ version = version,
+ qualifiers = qualifiers,
+ subpath = subpath,
+ }
+end
+
+local stringify_qualifiers = _.compose(
+ _.join "&",
+ _.sort_by(_.identity),
+ _.map(_.compose(_.join "=", _.evolve { _.identity, encode_percent_encoding })),
+ _.to_pairs,
+ _.evolve {
+ checksum = _.join ",",
+ }
+)
+
+---@param purl Purl
+---@return string
+function M.compile(purl)
+ local str = "pkg:"
+ str = str .. purl.type .. "/"
+ if purl.namespace then
+ str = str .. encode_percent_encoding(purl.namespace) .. "/"
+ end
+ str = str .. purl.name
+ if purl.version then
+ str = str .. "@" .. encode_percent_encoding(purl.version)
+ end
+ if purl.qualifiers then
+ str = str .. "?" .. stringify_qualifiers(purl.qualifiers)
+ end
+ if purl.subpath then
+ str = str .. "#" .. purl.subpath
+ end
+ return str
+end
+
+return M