/* 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 namespace dwarfs::writer::internal { namespace fs = std::filesystem; namespace { using mode_type = chmod_transformer::mode_type; constexpr mode_type const kSetUidBit{ static_cast(fs::perms::set_uid)}; constexpr mode_type const kSetGidBit{ static_cast(fs::perms::set_gid)}; constexpr mode_type const kStickyBit{ static_cast(fs::perms::sticky_bit)}; constexpr mode_type const kUserReadBit{ static_cast(fs::perms::owner_read)}; constexpr mode_type const kUserWriteBit{ static_cast(fs::perms::owner_write)}; constexpr mode_type const kUserExecBit{ static_cast(fs::perms::owner_exec)}; constexpr mode_type const kGroupReadBit{ static_cast(fs::perms::group_read)}; constexpr mode_type const kGroupWriteBit{ static_cast(fs::perms::group_write)}; constexpr mode_type const kGroupExecBit{ static_cast(fs::perms::group_exec)}; constexpr mode_type const kOtherReadBit{ static_cast(fs::perms::others_read)}; constexpr mode_type const kOtherWriteBit{ static_cast(fs::perms::others_write)}; constexpr mode_type const kOtherExecBit{ static_cast(fs::perms::others_exec)}; constexpr mode_type const kAllUidBits{kSetUidBit | kSetGidBit}; constexpr mode_type const kAllUserBits{kUserReadBit | kUserWriteBit | kUserExecBit}; constexpr mode_type const kAllGroupBits{kGroupReadBit | kGroupWriteBit | kGroupExecBit}; constexpr mode_type const kAllOtherBits{kOtherReadBit | kOtherWriteBit | kOtherExecBit}; constexpr mode_type const kAllReadBits{kUserReadBit | kGroupReadBit | kOtherReadBit}; constexpr mode_type const kAllWriteBits{kUserWriteBit | kGroupWriteBit | kOtherWriteBit}; constexpr mode_type const kAllExecBits{kUserExecBit | kGroupExecBit | kOtherExecBit}; constexpr mode_type const kAllRWXBits{kAllReadBits | kAllWriteBits | kAllExecBits}; constexpr mode_type const kAllModeBits{kAllUidBits | kStickyBit | kAllUserBits | kAllGroupBits | kAllOtherBits}; enum class opmode { normal, promote_exec, copy_from }; struct modifier { char oper; opmode mode; mode_type whom; mode_type bits; mode_type mask; }; class chmod_transformer_ : public chmod_transformer::impl { public: chmod_transformer_(std::string_view spec, mode_type umask); std::optional transform(mode_type mode, bool isdir) const override; private: std::optional parse_oct(std::string_view& spec); std::optional parse_whom(std::string_view& spec); static constexpr bool is_op(char c) { return c == '=' or c == '+' or c == '-'; } static constexpr bool is_ugo(char c) { return c == 'u' or c == 'g' or c == 'o'; } std::vector modifiers_; bool flag_D_{false}; bool flag_F_{false}; mode_type const umask_; }; chmod_transformer_::chmod_transformer_(std::string_view spec, mode_type umask) : umask_{umask} { // This is roughly following the implementation of chmod(1) from GNU coreutils if (spec.empty()) { throw std::invalid_argument("empty mode"); } auto orig_spec{spec}; if ('0' <= spec[0] and spec[0] <= '7') { // octal mode auto mode = parse_oct(spec); if (!mode or !spec.empty()) { throw std::invalid_argument(fmt::format("invalid mode: {}", orig_spec)); } mode_type mask{spec.size() > 4 ? kAllModeBits : (mode.value() & kAllUidBits) | kStickyBit | kAllRWXBits}; modifiers_.push_back( {'=', opmode::normal, kAllModeBits, mode.value(), mask}); return; } // symbolic mode auto whom = parse_whom(spec); if (!whom) { throw std::invalid_argument(fmt::format("invalid mode: {}", orig_spec)); } mode_type const mask{whom.value() ? whom.value() : kAllModeBits}; while (!spec.empty() and is_op(spec.front())) { auto op = spec.front(); spec.remove_prefix(1); if (spec.empty()) { throw std::invalid_argument(fmt::format("invalid mode: {}", orig_spec)); } if (auto mode = parse_oct(spec); mode) { if (whom.value() or !spec.empty()) { throw std::invalid_argument(fmt::format("invalid mode: {}", orig_spec)); } modifiers_.push_back( {op, opmode::normal, kAllModeBits, mode.value(), kAllModeBits}); break; } if (is_ugo(spec.front())) { mode_type bits{}; switch (spec.front()) { case 'u': bits = kAllUserBits; break; case 'g': bits = kAllGroupBits; break; case 'o': bits = kAllOtherBits; break; } modifiers_.push_back( {op, opmode::copy_from, whom.value(), bits, bits & mask}); spec.remove_prefix(1); } else { auto mode{opmode::normal}; mode_type bits{}; bool more{true}; while (!spec.empty() and more) { switch (spec.front()) { case 'r': bits |= kAllReadBits; break; case 'w': bits |= kAllWriteBits; break; case 'x': bits |= kAllExecBits; break; case 's': bits |= kAllUidBits; break; case 't': bits |= kStickyBit; break; case 'X': mode = opmode::promote_exec; break; default: more = false; break; } if (more) { spec.remove_prefix(1); } } modifiers_.push_back({op, mode, whom.value(), bits, bits & mask}); } } if (!spec.empty()) { throw std::invalid_argument(fmt::format("invalid mode: {}", orig_spec)); } } auto chmod_transformer_::parse_oct(std::string_view& spec) -> std::optional { mode_type mode; if (auto [p, ec] = std::from_chars(spec.data(), spec.data() + spec.size(), mode, 8); ec == std::errc{} and mode <= kAllModeBits) { spec.remove_prefix(p - spec.data()); return mode; } return std::nullopt; } auto chmod_transformer_::parse_whom(std::string_view& spec) -> std::optional { mode_type whom{}; while (!spec.empty()) { switch (spec.front()) { case 'u': whom |= kSetUidBit | kAllUserBits; break; case 'g': whom |= kSetGidBit | kAllGroupBits; break; case 'o': whom |= kStickyBit | kAllOtherBits; break; case 'a': whom = kAllModeBits; break; case 'D': flag_D_ = true; break; case 'F': flag_F_ = true; break; case '=': case '+': case '-': return whom; default: return std::nullopt; } spec.remove_prefix(1); } return std::nullopt; } std::optional chmod_transformer_::transform(mode_type mode, bool isdir) const { // skip entries for which this isn't intended if ((flag_D_ and !isdir) or (flag_F_ and isdir)) { return std::nullopt; } // This is roughly following the implementation of chmod(1) from GNU coreutils for (auto const& m : modifiers_) { mode_type omit{isdir ? kAllUidBits & ~m.mask : 0}; auto bits = m.bits; switch (m.mode) { case opmode::normal: break; case opmode::promote_exec: if (isdir or (mode & kAllExecBits)) { bits |= kAllExecBits; } break; case opmode::copy_from: bits &= mode; if (bits & kAllReadBits) { bits |= kAllReadBits; } if (bits & kAllWriteBits) { bits |= kAllWriteBits; } if (bits & kAllExecBits) { bits |= kAllExecBits; } break; } bits &= (m.whom ? m.whom : ~umask_) & ~omit; switch (m.oper) { case '=': mode = (mode & ((m.whom ? ~m.whom : 0) | omit)) | bits; break; case '+': mode |= bits; break; case '-': mode &= ~bits; break; } } return mode; } } // namespace chmod_transformer::chmod_transformer(std::string_view spec, mode_type umask) : impl_{std::make_unique(spec, umask)} {} } // namespace dwarfs::writer::internal