Serenity Operating System
at master 285 lines 11 kB view raw
1/* 2 * Copyright (c) 2020, Peter Elliott <pelliott@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/Assertions.h> 8#include <AK/LexicalPath.h> 9#include <AK/Span.h> 10#include <AK/Vector.h> 11#include <LibArchive/TarStream.h> 12#include <LibCompress/Gzip.h> 13#include <LibCore/ArgsParser.h> 14#include <LibCore/DeprecatedFile.h> 15#include <LibCore/DirIterator.h> 16#include <LibCore/Directory.h> 17#include <LibCore/System.h> 18#include <LibMain/Main.h> 19#include <fcntl.h> 20#include <stdio.h> 21#include <sys/stat.h> 22#include <unistd.h> 23 24constexpr size_t buffer_size = 4096; 25 26ErrorOr<int> serenity_main(Main::Arguments arguments) 27{ 28 bool create = false; 29 bool extract = false; 30 bool list = false; 31 bool verbose = false; 32 bool gzip = false; 33 bool no_auto_compress = false; 34 StringView archive_file; 35 bool dereference; 36 StringView directory; 37 Vector<DeprecatedString> paths; 38 39 Core::ArgsParser args_parser; 40 args_parser.add_option(create, "Create archive", "create", 'c'); 41 args_parser.add_option(extract, "Extract archive", "extract", 'x'); 42 args_parser.add_option(list, "List contents", "list", 't'); 43 args_parser.add_option(verbose, "Print paths", "verbose", 'v'); 44 args_parser.add_option(gzip, "Compress or decompress file using gzip", "gzip", 'z'); 45 args_parser.add_option(no_auto_compress, "Do not use the archive suffix to select the compression algorithm", "no-auto-compress", 0); 46 args_parser.add_option(directory, "Directory to extract to/create from", "directory", 'C', "DIRECTORY"); 47 args_parser.add_option(archive_file, "Archive file", "file", 'f', "FILE"); 48 args_parser.add_option(dereference, "Follow symlinks", "dereference", 'h'); 49 args_parser.add_positional_argument(paths, "Paths", "PATHS", Core::ArgsParser::Required::No); 50 args_parser.parse(arguments); 51 52 if (create + extract + list != 1) { 53 warnln("exactly one of -c, -x, and -t can be used"); 54 return 1; 55 } 56 57 if (!no_auto_compress && !archive_file.is_empty()) { 58 if (archive_file.ends_with(".gz"sv) || archive_file.ends_with(".tgz"sv)) 59 gzip = true; 60 } 61 62 if (list || extract) { 63 if (!directory.is_empty()) 64 TRY(Core::System::chdir(directory)); 65 66 NonnullOwnPtr<Stream> input_stream = TRY(Core::File::open_file_or_standard_stream(archive_file, Core::File::OpenMode::Read)); 67 68 if (gzip) 69 input_stream = make<Compress::GzipDecompressor>(move(input_stream)); 70 71 auto tar_stream = TRY(Archive::TarInputStream::construct(move(input_stream))); 72 73 HashMap<DeprecatedString, DeprecatedString> global_overrides; 74 HashMap<DeprecatedString, DeprecatedString> local_overrides; 75 76 auto get_override = [&](StringView key) -> Optional<DeprecatedString> { 77 Optional<DeprecatedString> maybe_local = local_overrides.get(key); 78 79 if (maybe_local.has_value()) 80 return maybe_local; 81 82 Optional<DeprecatedString> maybe_global = global_overrides.get(key); 83 84 if (maybe_global.has_value()) 85 return maybe_global; 86 87 return {}; 88 }; 89 90 while (!tar_stream->finished()) { 91 Archive::TarFileHeader const& header = tar_stream->header(); 92 93 // Handle meta-entries earlier to avoid consuming the file content stream. 94 if (header.content_is_like_extended_header()) { 95 switch (header.type_flag()) { 96 case Archive::TarFileType::GlobalExtendedHeader: { 97 TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) { 98 if (value.length() == 0) 99 global_overrides.remove(key); 100 else 101 global_overrides.set(key, value); 102 })); 103 break; 104 } 105 case Archive::TarFileType::ExtendedHeader: { 106 TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) { 107 local_overrides.set(key, value); 108 })); 109 break; 110 } 111 default: 112 warnln("Unknown extended header type '{}' of {}", (char)header.type_flag(), header.filename()); 113 VERIFY_NOT_REACHED(); 114 } 115 116 TRY(tar_stream->advance()); 117 continue; 118 } 119 120 Archive::TarFileStream file_stream = tar_stream->file_contents(); 121 122 // Handle other header types that don't just have an effect on extraction. 123 switch (header.type_flag()) { 124 case Archive::TarFileType::LongName: { 125 StringBuilder long_name; 126 127 Array<u8, buffer_size> buffer; 128 129 while (!file_stream.is_eof()) { 130 auto slice = TRY(file_stream.read_some(buffer)); 131 long_name.append(reinterpret_cast<char*>(slice.data()), slice.size()); 132 } 133 134 local_overrides.set("path", long_name.to_deprecated_string()); 135 TRY(tar_stream->advance()); 136 continue; 137 } 138 default: 139 // None of the relevant headers, so continue as normal. 140 break; 141 } 142 143 LexicalPath path = LexicalPath(header.filename()); 144 if (!header.prefix().is_empty()) 145 path = path.prepend(header.prefix()); 146 DeprecatedString filename = get_override("path"sv).value_or(path.string()); 147 148 if (list || verbose) 149 outln("{}", filename); 150 151 if (extract) { 152 DeprecatedString absolute_path = Core::DeprecatedFile::absolute_path(filename); 153 auto parent_path = LexicalPath(absolute_path).parent(); 154 auto header_mode = TRY(header.mode()); 155 156 switch (header.type_flag()) { 157 case Archive::TarFileType::NormalFile: 158 case Archive::TarFileType::AlternateNormalFile: { 159 MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes)); 160 161 int fd = TRY(Core::System::open(absolute_path, O_CREAT | O_WRONLY, header_mode)); 162 163 Array<u8, buffer_size> buffer; 164 while (!file_stream.is_eof()) { 165 auto slice = TRY(file_stream.read_some(buffer)); 166 TRY(Core::System::write(fd, slice)); 167 } 168 169 TRY(Core::System::close(fd)); 170 break; 171 } 172 case Archive::TarFileType::SymLink: { 173 MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes)); 174 175 TRY(Core::System::symlink(header.link_name(), absolute_path)); 176 break; 177 } 178 case Archive::TarFileType::Directory: { 179 MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes)); 180 181 auto result_or_error = Core::System::mkdir(absolute_path, header_mode); 182 if (result_or_error.is_error() && result_or_error.error().code() != EEXIST) 183 return result_or_error.release_error(); 184 break; 185 } 186 default: 187 // FIXME: Implement other file types 188 warnln("file type '{}' of {} is not yet supported", (char)header.type_flag(), header.filename()); 189 VERIFY_NOT_REACHED(); 190 } 191 } 192 193 // Non-global headers should be cleared after every file. 194 local_overrides.clear(); 195 196 TRY(tar_stream->advance()); 197 } 198 199 return 0; 200 } 201 202 if (create) { 203 if (paths.size() == 0) { 204 warnln("you must provide at least one path to be archived"); 205 return 1; 206 } 207 208 NonnullOwnPtr<Stream> output_stream = TRY(Core::File::standard_output()); 209 210 if (!archive_file.is_empty()) 211 output_stream = TRY(Core::File::open(archive_file, Core::File::OpenMode::Write)); 212 213 if (!directory.is_empty()) 214 TRY(Core::System::chdir(directory)); 215 216 if (gzip) 217 output_stream = TRY(try_make<Compress::GzipCompressor>(move(output_stream))); 218 219 Archive::TarOutputStream tar_stream(move(output_stream)); 220 221 auto add_file = [&](DeprecatedString path) -> ErrorOr<void> { 222 auto file = Core::DeprecatedFile::construct(path); 223 if (!file->open(Core::OpenMode::ReadOnly)) { 224 warnln("Failed to open {}: {}", path, file->error_string()); 225 return {}; 226 } 227 228 auto statbuf = TRY(Core::System::lstat(path)); 229 auto canonicalized_path = TRY(String::from_deprecated_string(LexicalPath::canonicalized_path(path))); 230 TRY(tar_stream.add_file(canonicalized_path, statbuf.st_mode, file->read_all())); 231 if (verbose) 232 outln("{}", canonicalized_path); 233 234 return {}; 235 }; 236 237 auto add_link = [&](DeprecatedString path) -> ErrorOr<void> { 238 auto statbuf = TRY(Core::System::lstat(path)); 239 240 auto canonicalized_path = TRY(String::from_deprecated_string(LexicalPath::canonicalized_path(path))); 241 TRY(tar_stream.add_link(canonicalized_path, statbuf.st_mode, TRY(Core::System::readlink(path)))); 242 if (verbose) 243 outln("{}", canonicalized_path); 244 245 return {}; 246 }; 247 248 auto add_directory = [&](DeprecatedString path, auto handle_directory) -> ErrorOr<void> { 249 auto statbuf = TRY(Core::System::lstat(path)); 250 251 auto canonicalized_path = TRY(String::from_deprecated_string(LexicalPath::canonicalized_path(path))); 252 TRY(tar_stream.add_directory(canonicalized_path, statbuf.st_mode)); 253 if (verbose) 254 outln("{}", canonicalized_path); 255 256 Core::DirIterator it(path, Core::DirIterator::Flags::SkipParentAndBaseDir); 257 while (it.has_next()) { 258 auto child_path = it.next_full_path(); 259 if (!dereference && Core::DeprecatedFile::is_link(child_path)) { 260 TRY(add_link(child_path)); 261 } else if (!Core::DeprecatedFile::is_directory(child_path)) { 262 TRY(add_file(child_path)); 263 } else { 264 TRY(handle_directory(child_path, handle_directory)); 265 } 266 } 267 268 return {}; 269 }; 270 271 for (auto const& path : paths) { 272 if (Core::DeprecatedFile::is_directory(path)) { 273 TRY(add_directory(path, add_directory)); 274 } else { 275 TRY(add_file(path)); 276 } 277 } 278 279 TRY(tar_stream.finish()); 280 281 return 0; 282 } 283 284 return 0; 285}