Serenity Operating System
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}