LibArchive/Zip+Utilities: Support unix permissions

Implements reading and writing unix permissions for LibArchive/Zip and
the zip,unzip utilities.
This commit is contained in:
Eduardo Casadei
2025-10-08 14:14:05 -04:00
committed by Tim Schumacher
parent c649e7074d
commit 8abb4806ae
4 changed files with 46 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2022-2025, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -94,7 +94,10 @@ ErrorOr<bool> Zip::for_each_member(Function<ErrorOr<IterationDecision>(ZipMember
member.crc32 = central_directory_record.crc32;
member.modification_time = central_directory_record.modification_time;
member.modification_date = central_directory_record.modification_date;
member.is_directory = central_directory_record.external_attributes & zip_directory_external_attribute || member.name.bytes_as_string_view().ends_with('/'); // FIXME: better directory detection
member.is_directory = central_directory_record.external_attributes.msdos & zip_directory_msdos_attribute || member.name.bytes_as_string_view().ends_with('/'); // FIXME: better directory detection
if (central_directory_record.made_by_version.made_by == ZipMadeBy::Unix) {
member.mode = static_cast<mode_t>(central_directory_record.external_attributes.unix);
}
if (TRY(callback(member)) == IterationDecision::Break)
return false;
@@ -158,7 +161,7 @@ ErrorOr<void> ZipOutputStream::add_member(ZipMember const& member)
return local_file_header.write(*m_stream);
}
ErrorOr<ZipOutputStream::MemberInformation> ZipOutputStream::add_member_from_stream(StringView path, Stream& stream, Optional<Core::DateTime> const& modification_time)
ErrorOr<ZipOutputStream::MemberInformation> ZipOutputStream::add_member_from_stream(StringView path, Stream& stream, Optional<Core::DateTime> const& modification_time, Optional<mode_t> mode)
{
auto buffer = TRY(stream.read_until_eof());
@@ -190,13 +193,14 @@ ErrorOr<ZipOutputStream::MemberInformation> ZipOutputStream::add_member_from_str
Crypto::Checksum::CRC32 checksum { buffer.bytes() };
member.crc32 = checksum.digest();
member.is_directory = false;
member.mode = mode;
TRY(add_member(member));
return MemberInformation { compression_ratio, compressed_size };
}
ErrorOr<void> ZipOutputStream::add_directory(StringView name, Optional<Core::DateTime> const& modification_time)
ErrorOr<void> ZipOutputStream::add_directory(StringView name, Optional<Core::DateTime> const& modification_time, Optional<mode_t> mode)
{
Archive::ZipMember member {};
member.name = TRY(String::from_utf8(name));
@@ -205,6 +209,7 @@ ErrorOr<void> ZipOutputStream::add_directory(StringView name, Optional<Core::Dat
member.uncompressed_size = 0;
member.crc32 = 0;
member.is_directory = true;
member.mode = mode;
if (modification_time.has_value()) {
member.modification_date = to_packed_dos_date(modification_time->year(), modification_time->month(), modification_time->day());
@@ -224,7 +229,7 @@ ErrorOr<void> ZipOutputStream::finish()
for (ZipMember const& member : m_members) {
auto zip_version = minimum_version_needed(member.compression_method);
CentralDirectoryRecord central_directory_record {
.made_by_version = zip_version,
.made_by_version = { .version = static_cast<u8>(zip_version), .made_by = ZipMadeBy::Unix },
.minimum_version = zip_version,
.general_purpose_flags = { .flags = 0 },
.compression_method = member.compression_method,
@@ -238,7 +243,10 @@ ErrorOr<void> ZipOutputStream::finish()
.comment_length = 0,
.start_disk = 0,
.internal_attributes = 0,
.external_attributes = member.is_directory ? zip_directory_external_attribute : 0,
.external_attributes = {
.msdos = static_cast<u16>(member.is_directory ? zip_directory_msdos_attribute : 0),
.unix = static_cast<u16>(member.mode.value_or(0)),
},
.local_file_header_offset = file_header_offset, // FIXME: we assume the wrapped output stream was never written to before us
.name = reinterpret_cast<u8 const*>(member.name.bytes_as_string_view().characters_without_null_termination()),
.extra_data = nullptr,

View File

@@ -1,6 +1,6 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2022-2025, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -33,6 +33,7 @@ static bool read_helper(ReadonlyBytes buffer, T* self)
}
// NOTE: Due to the format of zip files compression is streamed and decompression is random access.
// Zip format specification: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
static constexpr auto signature_length = 4;
@@ -109,10 +110,18 @@ union ZipGeneralPurposeFlags {
};
static_assert(sizeof(ZipGeneralPurposeFlags) == sizeof(u16));
enum class ZipMadeBy : u8 {
MSDOS = 0,
Unix = 3,
};
struct [[gnu::packed]] CentralDirectoryRecord {
static constexpr Array<u8, signature_length> signature = { 0x50, 0x4b, 0x01, 0x02 }; // 'PK\x01\x02'
u16 made_by_version;
struct {
u8 version;
ZipMadeBy made_by;
} made_by_version;
u16 minimum_version;
ZipGeneralPurposeFlags general_purpose_flags;
ZipCompressionMethod compression_method;
@@ -126,7 +135,10 @@ struct [[gnu::packed]] CentralDirectoryRecord {
u16 comment_length;
u16 start_disk;
u16 internal_attributes;
u32 external_attributes;
struct {
u16 msdos;
u16 unix;
} external_attributes;
u32 local_file_header_offset;
u8 const* name;
u8 const* extra_data;
@@ -182,7 +194,7 @@ struct [[gnu::packed]] CentralDirectoryRecord {
return signature.size() + (sizeof(CentralDirectoryRecord) - (sizeof(u8*) * 3)) + name_length + extra_data_length + comment_length;
}
};
static constexpr u32 zip_directory_external_attribute = 1 << 4;
static constexpr u16 zip_directory_msdos_attribute = 1 << 4;
struct [[gnu::packed]] LocalFileHeader {
static constexpr Array<u8, signature_length> signature = { 0x50, 0x4b, 0x03, 0x04 }; // 'PK\x03\x04'
@@ -250,6 +262,7 @@ struct ZipMember {
bool is_directory;
DOSPackedTime modification_time;
DOSPackedDate modification_date;
Optional<mode_t> mode;
};
class Zip {
@@ -282,11 +295,11 @@ public:
ZipOutputStream(NonnullOwnPtr<Stream>);
ErrorOr<void> add_member(ZipMember const&);
ErrorOr<MemberInformation> add_member_from_stream(StringView, Stream&, Optional<Core::DateTime> const& = {});
ErrorOr<MemberInformation> add_member_from_stream(StringView, Stream&, Optional<Core::DateTime> const& = {}, Optional<mode_t> mode = {});
// NOTE: This does not add any of the files within the directory,
// it just adds an entry for it.
ErrorOr<void> add_directory(StringView, Optional<Core::DateTime> const& = {});
ErrorOr<void> add_directory(StringView, Optional<Core::DateTime> const& = {}, Optional<mode_t> mode = {});
ErrorOr<void> finish();

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Andrés Vieira <anvieiravazquez@gmail.com>
* Copyright (c) 2025, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -44,7 +45,8 @@ static bool unpack_zip_member(Archive::ZipMember zip_member, bool quiet)
return true;
}
MUST(Core::Directory::create(LexicalPath(zip_member.name.to_byte_string()).parent(), Core::Directory::CreateDirectories::Yes));
auto new_file_or_error = Core::File::open(zip_member.name.to_byte_string(), Core::File::OpenMode::Write);
mode_t file_permissions = zip_member.mode.value_or(0644) & 0777;
auto new_file_or_error = Core::File::open(zip_member.name.to_byte_string(), Core::File::OpenMode::Write, file_permissions);
if (new_file_or_error.is_error()) {
warnln("Can't write file {}: {}", zip_member.name, new_file_or_error.release_error());
return false;
@@ -197,6 +199,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
warnln("Failed setting modification time for directory {}", directory.name);
return 1;
}
if (directory.mode.has_value()) {
if (Core::System::chmod(directory.name, directory.mode.value() & 0777).is_error()) {
warnln("Failed setting permissions for directory {}", directory.name);
return 1;
}
}
}
return success ? 0 : 1;

View File

@@ -1,5 +1,6 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2025, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -56,7 +57,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto stat = TRY(Core::System::fstat(file->fd()));
auto date = Core::DateTime::from_timestamp(stat.st_mtim.tv_sec);
auto information = TRY(zip_stream.add_member_from_stream(canonicalized_path, *file, date));
auto information = TRY(zip_stream.add_member_from_stream(canonicalized_path, *file, date, stat.st_mode));
if (information.compression_ratio < 1.f) {
outln(" adding: {} (deflated {}%)", canonicalized_path, (int)(information.compression_ratio * 100));
} else {
@@ -71,7 +72,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
auto stat = TRY(Core::System::stat(path));
auto date = Core::DateTime::from_timestamp(stat.st_mtim.tv_sec);
TRY(zip_stream.add_directory(canonicalized_path, date));
TRY(zip_stream.add_directory(canonicalized_path, date, stat.st_mode));
if (!recurse)
return {};