Skip to content

Commit 396dff5

Browse files
authored
Merge pull request #3468 from Gwitr/tarsupport
Add support for TAR archives
2 parents 7464311 + 9a4a7c1 commit 396dff5

File tree

13 files changed

+398
-5
lines changed

13 files changed

+398
-5
lines changed

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ add_library(${PROJECT_NAME} OBJECT
141141
src/filesystem_stream.h
142142
src/filesystem_zip.cpp
143143
src/filesystem_zip.h
144+
src/filesystem_tar.cpp
145+
src/filesystem_tar.h
144146
src/flash.h
145147
src/flat_map.h
146148
src/font.cpp

Makefile.am

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ libeasyrpg_player_a_SOURCES = \
112112
src/filesystem_stream.h \
113113
src/filesystem_zip.cpp \
114114
src/filesystem_zip.h \
115+
src/filesystem_tar.cpp \
116+
src/filesystem_tar.h \
115117
src/flash.h \
116118
src/flat_map.h \
117119
src/font.cpp \
@@ -750,6 +752,7 @@ test_runner_SOURCES = \
750752
tests/filefinder.cpp \
751753
tests/filesystem.cpp \
752754
tests/filesystem_zip.cpp \
755+
tests/filesystem_tar.cpp \
753756
tests/flat_map.cpp \
754757
tests/font.cpp \
755758
tests/game_actor.cpp \

src/filefinder.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ bool FileFinder::IsSupportedArchiveExtension(std::string path) {
296296
}
297297
#endif
298298

299-
return EndsWith(pv, ".zip") || EndsWith(pv, ".easyrpg");
299+
return EndsWith(pv, ".zip") || EndsWith(pv, ".tar") || EndsWith(pv, ".easyrpg");
300300
}
301301

302302
void FileFinder::Quit() {

src/filesystem.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "filesystem_native.h"
2020
#include "filesystem_lzh.h"
2121
#include "filesystem_zip.h"
22+
#include "filesystem_tar.h"
2223
#include "filesystem_stream.h"
2324
#include "filefinder.h"
2425
#include "utils.h"
@@ -129,18 +130,26 @@ FilesystemView Filesystem::Create(std::string_view path) const {
129130
}
130131
}
131132

133+
if (!handle_internal) {
134+
// No supported archive type found
135+
return {};
136+
}
137+
132138
if (!internal_path.empty()) {
133139
internal_path.pop_back();
134140
}
135141

136142
std::shared_ptr<Filesystem> filesystem = std::make_shared<ZipFilesystem>(path_prefix, Subtree(dir_of_file));
137-
if (!filesystem->IsValid()) {
138143
#if HAVE_LHASA
144+
if (!filesystem->IsValid()) {
139145
filesystem = std::make_shared<LzhFilesystem>(path_prefix, Subtree(dir_of_file));
146+
}
140147
#endif
141-
if (!filesystem->IsValid()) {
142-
return {};
143-
}
148+
if (!filesystem->IsValid()) {
149+
filesystem = std::make_shared<TarFilesystem>(path_prefix, Subtree(dir_of_file));
150+
}
151+
if (!filesystem->IsValid()) {
152+
return {};
144153
}
145154
if (!internal_path.empty()) {
146155
auto fs_view = filesystem->Create(internal_path);

src/filesystem_tar.cpp

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#include "output.h"
2+
#include "filesystem.h"
3+
#include "filesystem_stream.h"
4+
5+
#include <fmt/core.h>
6+
#include <string>
7+
#include <variant>
8+
#include <string_view>
9+
#include <iostream>
10+
11+
#include "filesystem_tar.h"
12+
13+
static std::string cstr_to_string_safe(const char *s, size_t n) {
14+
std::string target;
15+
target.resize(n);
16+
strncat(target.data(), s, n);
17+
if (auto nul = target.find('\0'); nul != std::string::npos) {
18+
target.resize(nul);
19+
target.shrink_to_fit();
20+
}
21+
return target;
22+
}
23+
24+
TarFilesystem::Entry::Entry() {}
25+
26+
TarFilesystem::Entry::Entry(std::string dirname) : name(dirname), data(Children()) {}
27+
28+
TarFilesystem::Entry::Entry(long offs, tar_entry_raw from, long *skip) {
29+
name = cstr_to_string_safe(from.data.ustar.path_prefix, sizeof(from.data.ustar.path_prefix)) + cstr_to_string_safe(from.data.filename, sizeof(from.data.filename));
30+
31+
std::string sizestr = cstr_to_string_safe(from.data.size, sizeof(from.data.size));
32+
char *end;
33+
long filesize = strtol(sizestr.c_str(), &end, 8);
34+
35+
auto magic = cstr_to_string_safe(from.data.ustar.magic, 5);
36+
if (magic != "ustar") {
37+
Output::Debug("TarFilesystem: invalid magic '{}' (offset {})", magic, offs);
38+
invalidate();
39+
return;
40+
}
41+
42+
switch (from.data.type) {
43+
case '0':
44+
case '\0':
45+
// BSD manpage tar(5): Numeric fields must be terminated with either space or NUL characters
46+
if (*end != 0 && *end != ' ') {
47+
Output::Debug("TarFilesystem: couldn't parse file size (file {}, offset {})", name, offs);
48+
invalidate();
49+
return;
50+
}
51+
data = FileEntryInfo{offs, filesize};
52+
if ((*skip = filesize) % 512 != 0)
53+
*skip += 512 - *skip % 512;
54+
break;
55+
case '5':
56+
if (filesize != 0 || (*end != 0 && *end != ' ')) {
57+
Output::Debug("TarFilesystem: invalid size for directory (file {}, offset {})", name, offs);
58+
invalidate();
59+
return;
60+
}
61+
data = Children();
62+
*skip = 0;
63+
break;
64+
default:
65+
Output::Debug("TarFilesystem: unknown format '{}' (file {})", from.data.type, name);
66+
invalidate();
67+
}
68+
}
69+
70+
bool TarFilesystem::Entry::valid() const {
71+
return !std::get_if<std::monostate>(&data);
72+
}
73+
74+
void TarFilesystem::Entry::invalidate() {
75+
data = std::monostate();
76+
}
77+
78+
bool TarFilesystem::Entry::add(Entry &&entry) {
79+
assert(!std::get_if<std::monostate>(&entry.data));
80+
81+
auto slashloc = entry.name.rfind('/');
82+
auto dirname = slashloc == std::string::npos ? "" : entry.name.substr(0, slashloc);
83+
entry.name = entry.name.substr(slashloc + 1);
84+
85+
auto parent_entry = locate(dirname, true);
86+
if (!parent_entry) {
87+
Output::Debug("TarFilesystem: invalid path to archive entry");
88+
return false;
89+
}
90+
91+
if (entry.name.empty()) {
92+
// Is a directory (name becomes empty when ending on /)
93+
// "locate" already adds the directory
94+
return true;
95+
}
96+
97+
std::get<Children>(parent_entry->data)[entry.name] = std::move(entry);
98+
return true;
99+
}
100+
101+
TarFilesystem::Entry *TarFilesystem::Entry::locate(std::string path, bool auto_create_dirs) {
102+
if (path == "")
103+
return this;
104+
105+
if (name != "") {
106+
assert(path.substr(0, path.find('/')) == name);
107+
path = path.substr(1 + path.find('/'));
108+
}
109+
110+
auto *dirdata = std::get_if<Children>(&data);
111+
if (!dirdata)
112+
return nullptr;
113+
114+
auto subdir = path.substr(0, path.find('/'));
115+
if (dirdata->find(subdir) == dirdata->end() && auto_create_dirs)
116+
(*dirdata)[subdir] = Entry(subdir);
117+
118+
auto iter = dirdata->find(subdir);
119+
if (iter == dirdata->end())
120+
return nullptr;
121+
return path.find('/') == std::string::npos ? &iter->second : iter->second.locate(std::move(path), auto_create_dirs);
122+
}
123+
124+
const TarFilesystem::Entry *TarFilesystem::Entry::locate(std::string path) const {
125+
return const_cast<Entry *>(this)->locate(path, false);
126+
}
127+
128+
TarFilesystem::TarFilesystem(std::string base_path, FilesystemView parent_fs) : Filesystem(base_path, parent_fs), stream(parent_fs.OpenInputStream(GetPath())) {
129+
if (!stream) {
130+
Output::Warning("Failed to open {}", base_path);
131+
rootdir.invalidate();
132+
return;
133+
}
134+
135+
tar_entry_raw data;
136+
int blank = 0, n = 0;
137+
static const char key[32] = { 0 };
138+
while (stream.ReadIntoObj(data)) {
139+
if (memcmp(&data, key, sizeof(key)) == 0 && memcmp(&data, sizeof(key) + (const char *)&data, sizeof(data) - sizeof(key)) == 0) {
140+
if (++blank == 2)
141+
return;
142+
continue;
143+
}
144+
blank = 0;
145+
146+
++n;
147+
148+
long skip;
149+
Entry entry { (long)stream.tellg(), data, &skip };
150+
if (!entry.valid()) {
151+
rootdir.invalidate();
152+
return;
153+
}
154+
if (skip)
155+
stream.seekg(skip, std::ios_base::cur);
156+
157+
if (entry.name == "")
158+
continue;
159+
if (!rootdir.add(std::move(entry))) {
160+
rootdir.invalidate();
161+
return;
162+
}
163+
}
164+
Output::Warning("TarFilesystem: tar file has no terminator");
165+
}
166+
167+
bool TarFilesystem::IsFile(std::string_view path) const {
168+
auto entry = rootdir.locate(std::string(path));
169+
if (!entry) {
170+
return false;
171+
}
172+
return !!std::get_if<Entry::FileEntryInfo>(&entry->data);
173+
}
174+
175+
bool TarFilesystem::IsDirectory(std::string_view path, bool follow_symlinks) const {
176+
(void)follow_symlinks;
177+
178+
auto entry = rootdir.locate(std::string(path));
179+
if (!entry)
180+
return false;
181+
return !!std::get_if<Entry::Children>(&entry->data);
182+
}
183+
184+
bool TarFilesystem::Exists(std::string_view path) const {
185+
if (!rootdir.valid() && path == "")
186+
return false; // Filesystem::IsValid requires this behavior
187+
return !!rootdir.locate(std::string(path));
188+
}
189+
190+
int64_t TarFilesystem::GetFilesize(std::string_view path) const {
191+
auto entry = rootdir.locate(std::string(path));
192+
if (!entry)
193+
return 0;
194+
auto info = std::get_if<Entry::FileEntryInfo>(&entry->data);
195+
if (!info)
196+
return 0;
197+
return info->size;
198+
}
199+
200+
std::streambuf* TarFilesystem::CreateInputStreambuffer(std::string_view path, std::ios_base::openmode) const {
201+
std::vector<uint8_t> data;
202+
203+
auto entry = rootdir.locate(std::string(path));
204+
if (!entry) {
205+
return nullptr;
206+
}
207+
auto info = std::get_if<Entry::FileEntryInfo>(&entry->data);
208+
if (!info) {
209+
return nullptr;
210+
}
211+
data.resize(info->size);
212+
213+
stream.seekg(info->offs, std::ios_base::beg);
214+
if (stream.read(reinterpret_cast<char*>(data.data()), info->size).gcount() != info->size) {
215+
Output::Warning("TarFilesystem: error reading archive");
216+
return nullptr;
217+
}
218+
219+
return new Filesystem_Stream::InputMemoryStreamBuf(std::move(data));
220+
}
221+
222+
bool TarFilesystem::GetDirectoryContent(std::string_view path, std::vector<DirectoryTree::Entry>& entries) const {
223+
auto entry = rootdir.locate(std::string(path));
224+
if (!entry) {
225+
Output::Debug("TarFilesystem: no file exists at path {}", path);
226+
return false;
227+
}
228+
auto contents = std::get_if<Entry::Children>(&entry->data);
229+
if (!contents) {
230+
Output::Debug("TarFilesystem: {} is a file", path);
231+
return false;
232+
}
233+
for (const auto &pair : *contents)
234+
entries.emplace_back(std::string(pair.first), !std::get_if<Entry::FileEntryInfo>(&pair.second.data) ? DirectoryTree::FileType::Directory : DirectoryTree::FileType::Regular);
235+
return true;
236+
}
237+
238+
std::string TarFilesystem::Describe() const {
239+
return fmt::format("[Tar] {}", GetPath());
240+
}

src/filesystem_tar.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#ifndef FILESYSTEM_TAR
2+
#define FILESYSTEM_TAR
3+
4+
#include <map>
5+
#include <string>
6+
#include <cstdint>
7+
#include <variant>
8+
#include <string_view>
9+
10+
#include "filesystem.h"
11+
#include "filesystem_stream.h"
12+
13+
union tar_entry_raw {
14+
struct {
15+
char filename[100];
16+
char perms[8];
17+
char uid[8];
18+
char gid[8];
19+
char size[12];
20+
char modify_time[12];
21+
char checksum[8];
22+
char type;
23+
char symlink_to[100];
24+
struct {
25+
char magic[6];
26+
char version[2];
27+
char username[32];
28+
char groupname[32];
29+
char device_major[8];
30+
char device_minor[8];
31+
char path_prefix[155];
32+
} ustar;
33+
} data;
34+
char pack[512];
35+
};
36+
37+
class TarFilesystem : public Filesystem {
38+
struct Entry {
39+
struct FileEntryInfo {
40+
long offs, size;
41+
};
42+
std::string name = "";
43+
using Children = std::map<std::string, Entry>;
44+
std::variant<std::monostate, Children, FileEntryInfo> data = std::monostate();
45+
46+
Entry();
47+
Entry(std::string dirname);
48+
Entry(long offs, tar_entry_raw from, long *skip);
49+
50+
bool valid() const;
51+
void invalidate();
52+
bool add(Entry &&entry);
53+
// Entry &locate(std::string path);
54+
Entry *locate(std::string path, bool auto_create_dirs = false);
55+
const Entry *locate(std::string path) const;
56+
};
57+
58+
mutable Filesystem_Stream::InputStream stream;
59+
60+
Entry rootdir = { "" };
61+
62+
protected:
63+
bool IsFile(std::string_view path) const override;
64+
bool IsDirectory(std::string_view path, bool follow_symlinks) const override;
65+
bool Exists(std::string_view path) const override;
66+
int64_t GetFilesize(std::string_view path) const override;
67+
std::streambuf* CreateInputStreambuffer(std::string_view path, std::ios_base::openmode mode) const override;
68+
bool GetDirectoryContent(std::string_view path, std::vector<DirectoryTree::Entry>& entries) const override;
69+
std::string Describe() const override;
70+
71+
public:
72+
TarFilesystem(std::string base_path, FilesystemView parent_fs);
73+
};
74+
75+
#endif // FILESYSTEM_TAR

0 commit comments

Comments
 (0)