From dac9861a5500b0c25a4efb5469444faa101d98d7 Mon Sep 17 00:00:00 2001 From: Marcus Holland-Moritz Date: Thu, 17 Nov 2022 12:23:46 +0100 Subject: [PATCH] Add --chmod option (fixes gh #7) --- CMakeLists.txt | 1 + doc/mkdwarfs.md | 8 + include/dwarfs/chmod_transformer.h | 35 +++ src/dwarfs/chmod_transformer.cpp | 359 +++++++++++++++++++++++++++++ src/mkdwarfs.cpp | 43 +++- 5 files changed, 436 insertions(+), 10 deletions(-) create mode 100644 include/dwarfs/chmod_transformer.h create mode 100644 src/dwarfs/chmod_transformer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e80568c..47b82051 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -309,6 +309,7 @@ list( src/dwarfs/block_manager.cpp src/dwarfs/builtin_script.cpp src/dwarfs/checksum.cpp + src/dwarfs/chmod_transformer.cpp src/dwarfs/console_writer.cpp src/dwarfs/entry.cpp src/dwarfs/error.cpp diff --git a/doc/mkdwarfs.md b/doc/mkdwarfs.md index 5adebe36..11c3d81d 100644 --- a/doc/mkdwarfs.md +++ b/doc/mkdwarfs.md @@ -235,6 +235,14 @@ Most other options are concerned with compression tuning: entirely possible. Moving from second to minute resolution, for example, will save roughly 6 bits per file system entry in the metadata block. +- `--chmod=`*mode*[`,`*mode*]...|`norm`: + Recursively changes the mode bits of all entries in the generated file system. + Accepts the same mode specifications as the `chmod` utility and additionally + supports the `D` and `F` modifiers to limit a specification to directories + or non-directories. As a special case, you can pass `--chmod=norm` to + "normalize" the permissions across the file system; this is equivalent to + using `--chmod=ug-st,=Xr`. + - `--order=none`|`path`|`similarity`|`nilsimsa`[`:`*limit*[`:`*depth*[`:`*mindepth*]]]|`script`: The order in which inodes will be written to the file system. Choosing `none`, the inodes will be stored in the order in which they are discovered. With diff --git a/include/dwarfs/chmod_transformer.h b/include/dwarfs/chmod_transformer.h new file mode 100644 index 00000000..d02dd16b --- /dev/null +++ b/include/dwarfs/chmod_transformer.h @@ -0,0 +1,35 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include "dwarfs/entry_transformer.h" + +namespace dwarfs { + +std::unique_ptr +create_chmod_transformer(std::string_view spec, uint16_t umask); + +} // namespace dwarfs diff --git a/src/dwarfs/chmod_transformer.cpp b/src/dwarfs/chmod_transformer.cpp new file mode 100644 index 00000000..8aa7e34a --- /dev/null +++ b/src/dwarfs/chmod_transformer.cpp @@ -0,0 +1,359 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#include +#include +#include +#include + +#include + +#include + +#include "dwarfs/chmod_transformer.h" +#include "dwarfs/entry_interface.h" + +namespace dwarfs { + +namespace { + +uint16_t constexpr all_perm_bits = 07777; +uint16_t constexpr all_exec_bits = S_IXUSR | S_IXGRP | S_IXOTH; + +enum class oper { NONE, ADD_BITS, SUB_BITS, SET_BITS, OCT_BITS }; + +std::tuple +compute_perm_and_or(oper op, uint16_t hi_bits, uint16_t setid_bits, + uint16_t affected, uint16_t perms, uint16_t umask) { + uint16_t op_bits = hi_bits; + uint16_t perm_and; + uint16_t perm_or; + + if (affected) { + op_bits |= affected * perms; + } else { + affected = all_exec_bits; + op_bits |= (affected * perms) & ~umask; + } + + switch (op) { + case oper::ADD_BITS: + perm_and = all_perm_bits; + perm_or = op_bits; + break; + + case oper::SUB_BITS: + perm_and = all_perm_bits & ~op_bits; + perm_or = 0; + break; + + case oper::SET_BITS: + perm_and = all_perm_bits & ~((affected * S_IRWXO) | setid_bits); + perm_or = op_bits; + break; + + case oper::OCT_BITS: + perm_and = 0; + perm_or = op_bits; + break; + + default: + throw std::runtime_error("missing operation in chmod expression"); + } + + return {perm_and, perm_or}; +} + +uint16_t modify_perms(uint16_t perm, bool isdir, uint16_t perm_and, + uint16_t perm_or, bool flag_X) { + auto new_perm = perm; + + new_perm &= perm_and; + + if (!flag_X or (perm & all_exec_bits) != 0 or isdir) { + new_perm |= perm_or; + } else { + new_perm |= perm_or & ~all_exec_bits; + } + + return new_perm; +} + +class permission_modifier { + public: + virtual ~permission_modifier() = default; + + virtual uint16_t modify(uint16_t perms, bool isdir) const = 0; +}; + +class static_permission_modifier : public permission_modifier { + public: + static_permission_modifier(uint16_t perm_and, uint16_t perm_or, bool flag_X) + : perm_and_{perm_and} + , perm_or_{perm_or} + , flag_X_{flag_X} {} + + uint16_t modify(uint16_t perms, bool isdir) const override { + return modify_perms(perms, isdir, perm_and_, perm_or_, flag_X_); + } + + private: + uint16_t const perm_and_; + uint16_t const perm_or_; + bool flag_X_; +}; + +class dynamic_permission_modifier : public permission_modifier { + public: + dynamic_permission_modifier(oper op, uint16_t setid_bits, uint16_t affected, + uint16_t perms_shift, uint16_t umask) + : op_{op} + , setid_bits_{setid_bits} + , affected_{affected} + , perms_shift_{perms_shift} + , umask_{umask} {} + + uint16_t modify(uint16_t perms, bool isdir) const override { + uint16_t dyn_perms = (perms >> perms_shift_) & 07; + auto [perm_and, perm_or] = + compute_perm_and_or(op_, 0, setid_bits_, affected_, dyn_perms, umask_); + return modify_perms(perms, isdir, perm_and, perm_or, false); + } + + private: + oper const op_; + uint16_t const setid_bits_; + uint16_t const affected_; + uint16_t const perms_shift_; + uint16_t const umask_; +}; + +class chmod_transformer : public entry_transformer { + public: + chmod_transformer(std::string_view spec, uint16_t umask); + + void transform(entry_interface& ei) override; + + private: + std::unique_ptr modifier_; + bool flag_D_{false}; + bool flag_F_{false}; +}; + +chmod_transformer::chmod_transformer(std::string_view spec, uint16_t umask) { + enum class state { PARSE_WHERE, PARSE_PERMS, PARSE_OCTAL }; + state st{state::PARSE_WHERE}; + oper op{oper::NONE}; + bool flag_X{false}; + uint16_t setid_bits{0}; + uint16_t hi_bits{0}; + uint16_t affected{0}; + uint16_t perms{0}; + std::optional perms_shift; + + for (auto c : spec) { + switch (st) { + case state::PARSE_WHERE: + switch (c) { + case 'D': + if (flag_F_) { + throw std::runtime_error( + "cannot combine D and F in chmod expression"); + } + flag_D_ = true; + break; + + case 'F': + if (flag_D_) { + throw std::runtime_error( + "cannot combine D and F in chmod expression"); + } + flag_F_ = true; + break; + + case 'u': + affected |= S_IXUSR; + setid_bits |= S_ISUID; + break; + + case 'g': + affected |= S_IXGRP; + setid_bits |= S_ISGID; + break; + + case 'o': + affected |= S_IXOTH; + break; + + case 'a': + affected |= all_exec_bits; + break; + + case '+': + op = oper::ADD_BITS; + st = state::PARSE_PERMS; + break; + + case '-': + op = oper::SUB_BITS; + st = state::PARSE_PERMS; + break; + + case '=': + op = oper::SET_BITS; + st = state::PARSE_PERMS; + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + if (affected) { + throw std::runtime_error( + "unexpected octal digit in chmod expression"); + } + affected = 1; + perms = c - '0'; + op = oper::OCT_BITS; + st = state::PARSE_OCTAL; + break; + + default: + throw std::runtime_error( + fmt::format("unexpected character in chmod expression: {}", c)); + } + break; + + case state::PARSE_PERMS: + switch (c) { + case 'r': + perms |= S_IROTH; + break; + + case 'w': + perms |= S_IWOTH; + break; + + case 'X': + flag_X = true; + [[fallthrough]]; + + case 'x': + perms |= S_IXOTH; + break; + + case 's': + // default to S_ISUID unless explicitly specified + hi_bits |= setid_bits ? setid_bits : S_ISUID; + break; + + case 't': + hi_bits |= S_ISVTX; + break; + + case 'u': + case 'g': + case 'o': + if (perms_shift) { + throw std::runtime_error( + "only one of [ugo] allowed in permission specification"); + } + + switch (c) { + case 'u': + perms_shift = 6; + break; + + case 'g': + perms_shift = 3; + break; + + case 'o': + perms_shift = 0; + break; + + default: + assert(false); + } + break; + + default: + throw std::runtime_error( + fmt::format("unexpected character in chmod expression: {}", c)); + } + break; + + case state::PARSE_OCTAL: + if (c < '0' || c > '7') { + throw std::runtime_error( + fmt::format("unexpected character in chmod expression: {}", c)); + } + + perms <<= 3; + perms |= c - '0'; + + if (perms > all_perm_bits) { + throw std::runtime_error("octal chmod expression out of range"); + } + + break; + } + } + + if (perms_shift && (perms || hi_bits || flag_X)) { + throw std::runtime_error( + "[ugo] cannot be combined with other permission specifiers"); + } + + if (perms_shift) { + modifier_ = std::make_unique( + op, setid_bits, affected, *perms_shift, umask); + } else { + auto [perm_and, perm_or] = + compute_perm_and_or(op, hi_bits, setid_bits, affected, perms, umask); + + modifier_ = + std::make_unique(perm_and, perm_or, flag_X); + } +} + +void chmod_transformer::transform(entry_interface& ei) { + // skip entries for which this isn't intended + if ((flag_D_ and !ei.is_directory()) or (flag_F_ and ei.is_directory())) { + return; + } + + ei.set_permissions( + modifier_->modify(ei.get_permissions(), ei.is_directory())); +} + +} // namespace + +std::unique_ptr +create_chmod_transformer(std::string_view spec, uint16_t umask) { + return std::make_unique(spec, umask); +} + +} // namespace dwarfs diff --git a/src/mkdwarfs.cpp b/src/mkdwarfs.cpp index 838a74dc..941cd6d9 100644 --- a/src/mkdwarfs.cpp +++ b/src/mkdwarfs.cpp @@ -40,6 +40,7 @@ #include #include +#include #include #include @@ -54,6 +55,7 @@ #include "dwarfs/block_compressor.h" #include "dwarfs/block_manager.h" #include "dwarfs/builtin_script.h" +#include "dwarfs/chmod_transformer.h" #include "dwarfs/console_writer.h" #include "dwarfs/entry.h" #include "dwarfs/error.h" @@ -372,7 +374,8 @@ int mkdwarfs(int argc, char** argv) { std::string path, output, memory_limit, script_arg, compression, header, schema_compression, metadata_compression, log_level_str, timestamp, time_resolution, order, progress_mode, recompress_opts, pack_metadata, - file_hash_algo, debug_filter, max_similarity_size, input_list_str; + file_hash_algo, debug_filter, max_similarity_size, input_list_str, + chmod_str; std::vector filter; size_t num_workers, num_scanner_workers; bool no_progress = false, remove_header = false, no_section_index = false, @@ -475,6 +478,9 @@ int mkdwarfs(int argc, char** argv) { ("time-resolution", po::value(&time_resolution)->default_value("sec"), resolution_desc.c_str()) + ("chmod", + po::value(&chmod_str), + "recursively apply permission changes") ("order", po::value(&order), order_desc.c_str()) @@ -838,7 +844,7 @@ int mkdwarfs(int argc, char** argv) { } #endif - if (!filter.empty()) { + if (!filter.empty() or vm.count("chmod")) { if (script) { std::cerr << "error: scripts and filters are not simultaneously supported\n"; @@ -847,15 +853,32 @@ int mkdwarfs(int argc, char** argv) { auto bs = std::make_shared(lgr); - bs->set_root_path(path); + if (!filter.empty()) { + bs->set_root_path(path); - for (auto const& rule : filter) { - try { - bs->add_filter_rule(rule); - } catch (std::exception const& e) { - std::cerr << "error: could not parse filter rule '" << rule - << "': " << e.what() << "\n"; - return 1; + for (auto const& rule : filter) { + try { + bs->add_filter_rule(rule); + } catch (std::exception const& e) { + std::cerr << "error: could not parse filter rule '" << rule + << "': " << e.what() << "\n"; + return 1; + } + } + } + + if (vm.count("chmod")) { + if (chmod_str == "norm") { + chmod_str = "ug-st,=Xr"; + } + + std::vector chmod_exprs; + boost::split(chmod_exprs, chmod_str, boost::is_any_of(",")); + auto mask = ::umask(0); + ::umask(mask); + + for (auto expr : chmod_exprs) { + bs->add_transformer(create_chmod_transformer(expr, mask)); } }