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));
}
}