diff --git a/etc/mtree/BSD.usr.dist b/etc/mtree/BSD.usr.dist --- a/etc/mtree/BSD.usr.dist +++ b/etc/mtree/BSD.usr.dist @@ -376,6 +376,8 @@ firmware .. flua + mtree + .. .. games fortune diff --git a/libexec/flua/share/Makefile b/libexec/flua/share/Makefile new file mode 100644 --- /dev/null +++ b/libexec/flua/share/Makefile @@ -0,0 +1,8 @@ + +BINDIR?= ${SHAREDIR} + +SUBDIR+= examples +SUBDIR+= mtree + +.include "Makefile.inc" +.include diff --git a/libexec/flua/share/Makefile.inc b/libexec/flua/share/Makefile.inc new file mode 100644 --- /dev/null +++ b/libexec/flua/share/Makefile.inc @@ -0,0 +1 @@ +SHAREDIR?= /usr/share/flua diff --git a/libexec/flua/share/examples/METALOG b/libexec/flua/share/examples/METALOG new file mode 100644 --- /dev/null +++ b/libexec/flua/share/examples/METALOG @@ -0,0 +1,71 @@ +#mtree 2.0 +. type=dir uname=root gname=wheel mode=0755 +./bin type=dir uname=root gname=wheel mode=0755 +./boot type=dir uname=root gname=wheel mode=0755 +./boot/defaults type=dir uname=root gname=wheel mode=0755 +./boot/dtb type=dir uname=root gname=wheel mode=0755 +./boot/dtb/allwinner type=dir uname=root gname=wheel mode=0755 tags=package=runtime +./boot/dtb/overlays type=dir uname=root gname=wheel mode=0755 tags=package=runtime +./boot/dtb/rockchip type=dir uname=root gname=wheel mode=0755 tags=package=runtime +./boot/efi type=dir uname=root gname=wheel mode=0755 +./boot/firmware type=dir uname=root gname=wheel mode=0755 +./boot/fonts type=dir uname=root gname=wheel mode=0755 +./boot/images type=dir uname=root gname=wheel mode=0755 +./boot/kernel type=dir uname=root gname=wheel mode=0755 +./boot/loader.conf.d type=dir uname=root gname=wheel mode=0755 tags=package=bootloader +./boot/lua type=dir uname=root gname=wheel mode=0755 +./boot/modules type=dir uname=root gname=wheel mode=0755 +./boot/uboot type=dir uname=root gname=wheel mode=0755 +./boot/zfs type=dir uname=root gname=wheel mode=0755 +./dev type=dir uname=root gname=wheel mode=0555 +./etc type=dir uname=root gname=wheel mode=0755 +./etc/X11 type=dir uname=root gname=wheel mode=0755 +./etc/authpf type=dir uname=root gname=wheel mode=0755 +./etc/autofs type=dir uname=root gname=wheel mode=0755 +./etc/bluetooth type=dir uname=root gname=wheel mode=0755 +./etc/cron.d type=dir uname=root gname=wheel mode=0755 +./etc/defaults type=dir uname=root gname=wheel mode=0755 +./etc/devd type=dir uname=root gname=wheel mode=0755 +./etc/dma type=dir uname=root gname=wheel mode=0755 +./etc/gss type=dir uname=root gname=wheel mode=0755 +./etc/jail.conf.d type=dir uname=root gname=wheel mode=0755 +./etc/kyua type=dir uname=root gname=wheel mode=0755 tags=package=tests +./etc/mail type=dir uname=root gname=wheel mode=0755 +./etc/mtree type=dir uname=root gname=wheel mode=0755 +./etc/newsyslog.conf.d type=dir uname=root gname=wheel mode=0755 +./etc/ntp type=dir uname=root gname=wheel mode=0700 +./etc/pam.d type=dir uname=root gname=wheel mode=0755 +./etc/periodic type=dir uname=root gname=wheel mode=0755 +./etc/periodic/daily type=dir uname=root gname=wheel mode=0755 +./etc/periodic/monthly type=dir uname=root gname=wheel mode=0755 +./etc/periodic/security type=dir uname=root gname=wheel mode=0755 +./etc/periodic/weekly type=dir uname=root gname=wheel mode=0755 +./etc/pkg type=dir uname=root gname=wheel mode=0755 +./etc/ppp type=dir uname=root gname=wheel mode=0755 +./etc/profile.d type=dir uname=root gname=wheel mode=0755 +./etc/rc.conf.d type=dir uname=root gname=wheel mode=0755 +./etc/rc.d type=dir uname=root gname=wheel mode=0755 +./etc/security type=dir uname=root gname=wheel mode=0755 +./etc/ssh type=dir uname=root gname=wheel mode=0755 +./etc/ssl type=dir uname=root gname=wheel mode=0755 +./etc/ssl/certs type=dir uname=root gname=wheel mode=0755 +./etc/ssl/untrusted type=dir uname=root gname=wheel mode=0755 +./etc/sysctl.kld.d type=dir uname=root gname=wheel mode=0755 +./etc/syslog.d type=dir uname=root gname=wheel mode=0755 +./etc/zfs type=dir uname=root gname=wheel mode=0755 +./etc/zfs/compatibility.d type=dir uname=root gname=wheel mode=0755 +./etc/gss type=dir uname=root gname=wheel mode=0755 tags=package=utilities +./etc/gss/mech type=file uname=root gname=wheel mode=0444 size=239 tags=package=utilities +./etc/gss/qop type=file uname=root gname=wheel mode=0444 size=89 tags=package=utilities +./etc/mtree type=dir uname=root gname=wheel mode=0755 tags=package=mtree +./etc/mtree/BSD.debug.dist type=file uname=root gname=wheel mode=0444 size=1435 tags=package=mtree +./etc/mtree/BSD.root.dist type=file uname=root gname=wheel mode=0444 size=2121 tags=package=mtree +./etc/mtree/BSD.include.dist type=file uname=root gname=wheel mode=0444 size=5351 tags=package=mtree +./etc/mtree/BSD.lib32.dist type=file uname=root gname=wheel mode=0444 size=377 tags=package=mtree +./etc/mtree/BSD.tests.dist type=file uname=root gname=wheel mode=0444 size=24891 tags=package=mtree +./etc/mtree/BSD.var.dist type=file uname=root gname=wheel mode=0444 size=2357 tags=package=mtree +./etc/mtree/BSD.usr.dist type=file uname=root gname=wheel mode=0444 size=17353 tags=package=mtree +./etc/termcap type=link uname=root gname=wheel mode=0755 link=/usr/share/misc/termcap tags=package=runtime +./etc/rmt type=link uname=root gname=wheel mode=0755 link=../usr/sbin/rmt tags=package=utilities +./etc/os-release type=link uname=root gname=wheel mode=0755 link=../var/run/os-release tags=package=runtime +./etc/unbound type=link uname=root gname=wheel mode=0755 link=../var/unbound tags=package=unbound diff --git a/libexec/flua/share/examples/Makefile b/libexec/flua/share/examples/Makefile new file mode 100644 --- /dev/null +++ b/libexec/flua/share/examples/Makefile @@ -0,0 +1,10 @@ + +PACKAGE?= examples +BINDIR?= /usr/share/examples/flua + +FILES+= libjail.lua +FILES+= mtree-read.lua +FILES+= mtree-alter.lua +FILES+= METALOG + +.include diff --git a/share/examples/flua/libjail.lua b/libexec/flua/share/examples/libjail.lua rename from share/examples/flua/libjail.lua rename to libexec/flua/share/examples/libjail.lua diff --git a/libexec/flua/share/examples/mtree-alter.lua b/libexec/flua/share/examples/mtree-alter.lua new file mode 100644 --- /dev/null +++ b/libexec/flua/share/examples/mtree-alter.lua @@ -0,0 +1,37 @@ +#!/usr/libexec/flua + +local mtree = require('mtree') + +local path = arg[0] +-- Turn path into something useful for us +local dirlen = #path - (string.find(path:reverse(), "/") or #path) +path = path:sub(1, dirlen) +if #path == 0 then + path = "." +end + +local tree = assert(mtree.load(path .. "/METALOG")) + +-- waffles gets tagged as a dir automatically when we start adding entries +-- beneath it. We set the mode to demonstrate that that type-setting doesn't +-- override our dir mode with the auto-deduced mode. +local leaf = tree:append("/etc/waffles") +leaf:keyword("mode", "01777") +leaf:keyword("tags", "package=breakfast") + +-- Adding new entries by absolute path may be the most convenient; append() +-- will return the leaf node that we just added. +leaf = tree:append("/etc/waffles/chocolate") +leaf:keyword("type", "file") +-- Note that any slash makes it an absolute path, so ./ is not from "pwd". +leaf = tree:append("./etc/waffles/strawberry") +leaf:keyword("type", "device") + +-- We can also chdir() to the path if we're going to be doing a lot of additions +-- in a row to it. +tree:chdir("/etc/waffles") +leaf = tree:append("cherry") +leaf:keyword("type", "link") +leaf:keyword("link", "/nonexistent") + +print(tree:dump(true)) diff --git a/libexec/flua/share/examples/mtree-read.lua b/libexec/flua/share/examples/mtree-read.lua new file mode 100644 --- /dev/null +++ b/libexec/flua/share/examples/mtree-read.lua @@ -0,0 +1,12 @@ +#!/usr/libexec/flua + +local filename = arg[1] +local mtree = require('mtree') + +if not filename then + io.stderr:write("usage: " .. arg[0] .. " [file]\n") + os.exit(1) +end + +local tree = assert(mtree.load(filename)) +print(tree:dump(true)) diff --git a/libexec/flua/share/mtree/Makefile b/libexec/flua/share/mtree/Makefile new file mode 100644 --- /dev/null +++ b/libexec/flua/share/mtree/Makefile @@ -0,0 +1,7 @@ +BINDIR?= ${SHAREDIR}/mtree + +FILES+= init.lua +FILES+= parser.lua +FILES+= tree.lua + +.include diff --git a/libexec/flua/share/mtree/init.lua b/libexec/flua/share/mtree/init.lua new file mode 100644 --- /dev/null +++ b/libexec/flua/share/mtree/init.lua @@ -0,0 +1,20 @@ +-- +-- Copyright (c) 2025 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +local mtree = {} +local parser = require('mtree.parser') + +mtree.parse = parser.parse +function mtree.load(filename) + local file, err = io.open(filename, "r") + if not file then + return nil, err + end + local contents = file:read("a") + return mtree.parse(contents) +end + +return mtree diff --git a/libexec/flua/share/mtree/parser.lua b/libexec/flua/share/mtree/parser.lua new file mode 100644 --- /dev/null +++ b/libexec/flua/share/mtree/parser.lua @@ -0,0 +1,127 @@ +-- +-- Copyright (c) 2025 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +local tree = require('mtree.tree') +local parser = {} + +function parser.parse(mtree) + local count = 0 + local defaults = {} + local parsed_tree = tree:new() + + -- Returns key, value pair + local function parse_keyword(keyword) + local key = keyword:match("^[^=]+") + local value = keyword:sub(#key + 2) + + if not key then + return nil, "Malformed keyword: " .. keyword + end + + return key, value + end + + -- Processes an entry, returns the directory name part of it so that the + -- parent can chdir() into it. + local function process_entry(line) + local dir = line:match("[^%s]+") + local dirent, err = parsed_tree:append(dir) + local kwords_set = {} + + if not dirent then + return nil, err + end + + line = line:sub(#dir + 1) + + for kword in line:gmatch("[^%s]+") do + local k, v = parse_keyword(kword) + if not k then + return nil, v + end + + kwords_set[k] = 1 + dirent:keyword(k, v) + end + + for k, v in pairs(defaults) do + if not kwords_set[k] then + dirent:keyword(k, v) + end + end + + return dir + end + + for line in mtree:gmatch("[^\n]+") do + local dir, err, indent, ok + + count = count + 1 + + -- First line may be a version spec. + if count == 1 and line:match("^#mtree") then + tree:version(line:match("mtree v([%d]+%.[%d]+)")) + goto next + end + + -- Strip off any comments and any leading whitespace. Internal + -- whitespace is significant, so we'll leave that alone. + line = line:match("^[^#]+") or "" + indent = line:match("^[%s]+") or "" + line = line:sub(#indent + 1) + + -- Ignore empty lines + if #line == 0 then + goto next + end + + if line:match("^/set") or line:match("^/unset") then + local command = line:match("^/u?n?set") + local is_set = command == "/set" + + line = line:sub(#command + 1) + + for kword in line:gmatch("[^%s]+") do + if not is_set then + if kword:match("=") then + return nil, "Malformed /unset" + end + + defaults[kword] = nil + else + local k, v = parse_keyword(kword, true) + if not k then + return nil, v + end + + defaults[k] = v + end + end + + goto next + end + + dir = line + if line ~= ".." then + dir, err = process_entry(line) + if not dir then + return nil, err + end + end + + ok, err = parsed_tree:chdir(dir) + if not ok then + return nil, err + end + + ::next:: + end + +-- io.stderr:write(tostring(parsed_tree) .. "\n") + return parsed_tree +end + +return parser diff --git a/libexec/flua/share/mtree/tree.lua b/libexec/flua/share/mtree/tree.lua new file mode 100644 --- /dev/null +++ b/libexec/flua/share/mtree/tree.lua @@ -0,0 +1,284 @@ +-- +-- Copyright (c) 2025 Kyle Evans +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +local inherited = { + -- Almost certainly we want the same ownership as the parent, + -- the caller can override that. + "uname", "gname", "uid", "gid", + + -- We use tags mostly for the package, but also to signify + -- config files. If we're a leaf entry, the parent should only + -- really have package tags if anything, and it doesn't hurt to + -- inherit those. + "tags", +} + +local function encoded_name(name) + local function needescape(c) + -- Printable ASCII do not need escaping, with exception to a + -- backslash. + return not (c ~= 0x5c and c >= 0x20 and c < 0x7f) + end + + local encoded = "" + for i = 1, #name do + local c = name:byte(i) + + if not needescape(c) then + encoded = encoded .. string.char(c) + else + encoded = encoded .. string.format("\\%.3o", c) + end + end + + return encoded +end +local function dirent_name(name) + for oct in name:gmatch("\\[0-7][0-7][0-7]") do + local rep = tonumber(oct:sub(2), 8) + + name = name:gsub(oct, string.char(rep)) + end + + return name +end + +local dirent = {} +-- XXX Note that new dirents inherit the parent uname/gname/tags by default. +function dirent:new(tree, parent, name) + local obj = setmetatable({}, self) + self.__index = self + + if name:match("/") then + return nil, "Illegal filename: " .. name + end + obj.name = dirent_name(name) + obj.mtree_name = encoded_name(obj.name) + obj.parent = parent + obj.tree = tree + obj.children = {} + obj._keywords = {} + + if parent then + for _, kword in ipairs(inherited) do + obj:keyword(kword, parent:keyword(kword)) + end + + local ptype = parent:keyword("type") + if not ptype then + parent:keyword("type", "dir") + elseif ptype ~= "dir" then + return nil, "Entry added to non-directory type" + end + end + + return obj +end +function dirent:add(de) + self.children[de.name] = de +end +function dirent:child(path) + return self.children[path] +end +function dirent:keyword(key, ...) + local args = { ... } + + -- A bit of a hack: dirent:keyword("foo") should return the value of + -- foo, but dirent:keyword("foo", nil) should unset it. + if #args == 0 then + return self._keywords[key] + end + + local value = args[1] + self._keywords[key] = value + + -- Set a mode if they're setting a type, but let's not override an + -- existing mode. These are just reasonable defaults. + if key == "type" and not self._keywords["mode"] then + if value == "dir" then + self._keywords["mode"] = "0755" + else + self._keywords["mode"] = "0644" + end + end + return true +end +function dirent:dump(absolute_path, pretty) + local strv = self.mtree_name + + if absolute_path and self.parent ~= self then + local parent = self + local cmp + + repeat + parent = parent.parent + cmp = parent.mtree_name + strv = cmp .. "/" .. strv + until cmp == "." + end + if self._keywords then + local count = 0 + local pathsep = " " + local kwdelim = (pretty and ", ") or " " + + if not absolute_path then + pathsep = string.rep(" ", 8) + end + for k, v in pairs(self._keywords) do + if count == 0 then + if not pretty then + strv = strv .. pathsep + else + strv = strv .. "[" + end + else + strv = strv .. kwdelim + end + + strv = strv .. k .. "=" .. v + count = count + 1 + end + + if count > 0 and pretty then + strv = strv .. "]" + end + end + + return strv +end +function dirent:__tostring() + return self:dump(false, true) +end + +local tree = {} +function tree:new() + local obj = setmetatable({}, self) + self.__index = self + + local root = assert(dirent:new(obj, nil, ".")) + root.parent = root + + obj._version = nil + obj.root = root + obj.pwd = root + + return obj +end +local function traverse(self, path, create_missing) + local last_created + local is_abs = path:match("/") + local pwd = (is_abs and self.root) or self.pwd + + for cmp in path:gmatch("[^/]+") do + local next + + if cmp == ".." then + next = pwd.parent + elseif cmp == "." then + next = pwd + else + next = pwd:child(cmp) + end + + -- Just construct the missing entry + if not next and create_missing then + local de, err = dirent:new(self, pwd, cmp) + + if not de then + return nil, err + end + + -- We now know that the last dirent we created was not + -- a leaf, so we can assume it's a directory and set the + -- type appropriately. + if last_created and not last_created:keyword("type") then + last_created:keyword("type", "dir") + end + + pwd:add(de) + last_created = de + next = de + elseif not next then + return nil, "Missing component" + end + + pwd = next + end + + return pwd +end + +function tree:append(path) + return traverse(self, path, true) +end +function tree:chdir(path) + local new_pwd, err = traverse(self, path) + if not new_pwd then + return nil, err + end + + self.pwd = new_pwd + return true +end +function tree:get(path) + local getdir, err = traverse(self, path) + if not getdir then + return nil, err + end + return getdir +end +function tree:version(newvers) + if newvers then + self._version = newvers + end + + return self._version or "2.0" +end +function tree:dump(pretty) + local pwd = self.root + local version = self:version() + local strv + + if not pretty then + strv = "#mtree " .. version .. "\n" + else + strv = "" + end + + local major_version = tonumber(version:match("^([0-9]+)")) + local absolute_path = not pretty and major_version >= 2 + local function walk(node, indentlvl) + local sorted = {} + local indentation = "" + + if not absolute_path then + indentation = string.rep("\t", indentlvl) + end + + for name in pairs(node.children) do + sorted[#sorted + 1] = name + end + + table.sort(sorted) + strv = strv .. indentation .. + node:dump(absolute_path, pretty) .. "\n" + for _, name in ipairs(sorted) do + local de = node.children[name] + walk(de, indentlvl + 1) + end + if not pretty and not absolute_path then + strv = strv .. indentation .. "..\n" + end + end + + walk(pwd, 0) + return strv +end +function tree:__tostring() + return self:dump(false) +end + +return tree diff --git a/share/examples/Makefile b/share/examples/Makefile --- a/share/examples/Makefile +++ b/share/examples/Makefile @@ -13,7 +13,6 @@ drivers \ etc \ find_interface \ - flua \ indent \ ipfw \ jails \ @@ -91,9 +90,6 @@ README \ find_interface.c -SE_DIRS+= flua -SE_FLUA= libjail.lua - SE_DIRS+= indent SE_INDENT= indent.pro