Skip to content

Commit fa121a5

Browse files
committed
Huge rework of ZIP handling. Too many changes to mention: added more caching, better error checking, etc.
1 parent 7f5b932 commit fa121a5

17 files changed

Lines changed: 487 additions & 375 deletions

src/common/FSNodeZIP.cxx

Lines changed: 121 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
#ifdef ZIP_SUPPORT
1919

20-
#include <set>
20+
#include <unordered_set>
2121

2222
#ifdef BSPF_WINDOWS
2323
#include "HomeFinder.hxx"
@@ -45,30 +45,30 @@ FSNodeZIP::FSNodeZIP(string_view p)
4545
if(_zipFile[0] == '~')
4646
{
4747
#if defined(BSPF_UNIX) || defined(BSPF_MACOS)
48-
_zipFile.replace(0, 1, XDGPaths::instance().home());
48+
// Skip the '/' after '~' if present to avoid double slash
49+
const size_t replaceLen = (_zipFile.size() > 1 && _zipFile[1] == '/') ? 2 : 1;
50+
_zipFile.replace(0, replaceLen, XDGPaths::instance().home());
4951
#elif defined(BSPF_WINDOWS)
50-
_zipFile.replace(0, 1, HomeFinder::getHomePath());
52+
// Skip the '\\' after '~' if present to avoid double slash
53+
const size_t replaceLen = (_zipFile.size() > 1 && _zipFile[1] == '\\') ? 2 : 1;
54+
_zipFile.replace(0, replaceLen, HomeFinder::getHomePath());
5155
#endif
5256
}
5357

54-
// cerr << " => p: " << p << '\n';
55-
5658
// Open file at least once to initialize the virtual file count
5759
try
5860
{
5961
zipHandler()->open(_zipFile);
62+
63+
// Count ROMs
64+
_numFiles = zipHandler()->romFiles();
6065
}
61-
catch(const std::runtime_error&)
66+
catch(const ZipException&)
6267
{
63-
// TODO: Actually present the error passed in back to the user
64-
// For now, we just indicate that no ROMs were found
65-
_error = zip_error::NO_ROMS;
68+
return;
6669
}
67-
_numFiles = zipHandler()->romFiles();
6870
if(_numFiles == 0)
69-
{
70-
_error = zip_error::NO_ROMS;
71-
}
71+
return;
7272

7373
// We always need a virtual file/path
7474
// Either one is given, or we use the first one
@@ -80,21 +80,19 @@ FSNodeZIP::FSNodeZIP(string_view p)
8080
}
8181
else if(_numFiles == 1)
8282
{
83-
bool found = false;
84-
while(zipHandler()->hasNext() && !found)
83+
try
8584
{
86-
const auto& [name, size] = zipHandler()->next();
87-
if(Bankswitch::isValidRomName(name))
85+
if(const auto rom = zipHandler()->firstRom(); rom)
8886
{
89-
_virtualPath = name;
90-
_size = size;
87+
_virtualPath = rom->first;
88+
_size = rom->second;
9189
_isFile = true;
92-
93-
found = true;
9490
}
9591
}
96-
if(!found)
92+
catch(const ZipException&)
93+
{
9794
return;
95+
}
9896
}
9997
else if(_numFiles > 1)
10098
_isDirectory = true;
@@ -104,27 +102,23 @@ FSNodeZIP::FSNodeZIP(string_view p)
104102
// has direct access to the actual filesystem (aka, a 'System' node)
105103
// Behind the scenes, this node is actually a platform-specific object
106104
// for whatever system we are running on
107-
_realNode = FSNodeFactory::create(_zipFile,
108-
FSNodeFactory::Type::SYSTEM);
105+
_realNode = FSNodeFactory::create(_zipFile, FSNodeFactory::Type::SYSTEM);
109106

110107
setFlags(_zipFile, _virtualPath, _realNode);
111-
// cerr << "==============================================================\n";
112-
// cerr << _name << ", file: " << _isFile << ", dir: " << _isDirectory << "\n\n";
113108
}
114109

115110
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
116-
FSNodeZIP::FSNodeZIP(const string& zipfile, const string& virtualpath,
111+
FSNodeZIP::FSNodeZIP(string_view zipfile, string_view virtualpath,
117112
const AbstractFSNodePtr& realnode, size_t size, bool isdir)
118113
: _size{size},
119114
_isDirectory{isdir},
120115
_isFile{!isdir}
121116
{
122-
// cerr << "=> c'tor 2: " << zipfile << ", " << virtualpath << ", " << isdir << '\n';
123117
setFlags(zipfile, virtualpath, realnode);
124118
}
125119

126120
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
127-
void FSNodeZIP::setFlags(const string& zipfile, const string& virtualpath,
121+
void FSNodeZIP::setFlags(string_view zipfile, string_view virtualpath,
128122
const AbstractFSNodePtr& realnode)
129123
{
130124
_zipFile = zipfile;
@@ -137,77 +131,108 @@ void FSNodeZIP::setFlags(const string& zipfile, const string& virtualpath,
137131
// Is a file component present?
138132
if(!_virtualPath.empty())
139133
{
140-
_path += ("/" + _virtualPath);
141-
_shortPath += ("/" + _virtualPath);
134+
_path += '/';
135+
_path += _virtualPath;
136+
_shortPath += '/';
137+
_shortPath += _virtualPath;
142138
}
143139
_name = AsciiFold::toAscii(lastPathComponent(_path));
144140

145141
if(!_realNode->isFile())
146-
_error = zip_error::NOT_A_FILE;
142+
throw ZipException(ZipError::FILE_ERROR);
147143
if(!_realNode->isReadable())
148-
_error = zip_error::NOT_READABLE;
144+
throw ZipException(ZipError::FILE_ERROR);
149145
}
150146

151147
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
152148
bool FSNodeZIP::exists() const
153149
{
154-
if(_realNode && _realNode->exists())
150+
if(!_realNode || !_realNode->exists())
151+
return false;
152+
153+
// An empty virtual path means the ZIP itself is the node (directory case)
154+
if(_virtualPath.empty() || _isDirectory)
155+
return true;
156+
157+
// We need to inspect the actual path, not just the ZIP file itself
158+
try
155159
{
156-
// We need to inspect the actual path, not just the ZIP file itself
157-
try
158-
{
159-
zipHandler()->open(_zipFile);
160-
while(zipHandler()->hasNext())
161-
{
162-
const auto& [name, size] = zipHandler()->next();
163-
if(BSPF::startsWithIgnoreCase(name, _virtualPath))
164-
return true;
165-
}
166-
}
167-
catch(const std::runtime_error&)
168-
{
169-
// TODO: Actually present the error passed in back to the user
170-
cerr << "ERROR: FSNodeZIP::exists()\n";
171-
}
160+
zipHandler()->open(_zipFile);
161+
return zipHandler()->find(_virtualPath).second;
162+
}
163+
catch(const ZipException&)
164+
{
165+
return false;
172166
}
173-
174-
return false;
175167
}
176168

177169
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
178-
bool FSNodeZIP::getChildren(AbstractFSList& myList, ListMode mode) const
170+
bool FSNodeZIP::getChildren(AbstractFSList& myList, ListMode) const
179171
{
180172
// Files within ZIP archives don't contain children
181-
if(!isDirectory() || _error != zip_error::NONE)
173+
if(!isDirectory())
182174
return false;
183175

184-
std::set<string> dirs;
185-
zipHandler()->open(_zipFile);
186-
while(zipHandler()->hasNext())
176+
std::unordered_set<string> dirs;
177+
178+
try
187179
{
188-
// Only consider entries that start with '_virtualPath'
189-
// Ignore empty filenames and '__MACOSX' virtual directories
190-
const auto& [name, size] = zipHandler()->next();
191-
if(BSPF::startsWithIgnoreCase(name, "__MACOSX") || name == EmptyString())
192-
continue;
193-
if(BSPF::startsWithIgnoreCase(name, _virtualPath))
180+
zipHandler()->open(_zipFile);
181+
zipHandler()->forEachEntry([&](string_view name, uInt64 size)
194182
{
195-
// First strip off the leading directory
196-
const string& curr = name.substr(
197-
_virtualPath.empty() ? 0 : _virtualPath.size()+1);
183+
// Ignore empty filenames and '__MACOSX' virtual directories
184+
if(name.empty() || BSPF::startsWithIgnoreCase(name, "__MACOSX"))
185+
return;
186+
187+
string_view remainder = name;
188+
189+
// Handle virtual path cleanly
190+
if(!_virtualPath.empty())
191+
{
192+
if(!BSPF::startsWithIgnoreCase(name, _virtualPath))
193+
return;
194+
195+
const size_t baseLen = _virtualPath.size();
196+
197+
// Must be exact match or followed by '/'
198+
if(name.size() == baseLen)
199+
return; // directory itself
200+
201+
if(name[baseLen] != '/' && name[baseLen] != '\\')
202+
return;
203+
204+
remainder = name.substr(baseLen + 1);
205+
}
206+
207+
if(remainder.empty())
208+
return;
198209

199210
// Only add sub-directory entries once
200-
const auto pos = curr.find_first_of("/\\");
201-
if(pos != string::npos)
202-
dirs.emplace(curr.substr(0, pos));
211+
const auto pos = remainder.find_first_of("/\\");
212+
if(pos != string_view::npos)
213+
dirs.emplace(remainder.substr(0, pos));
203214
else
204215
myList.emplace_back(new FSNodeZIP(_zipFile, name, _realNode, size, false));
205-
}
216+
});
206217
}
218+
catch(const ZipException&)
219+
{
220+
return false;
221+
}
222+
207223
for(const auto& dir: dirs)
208224
{
209-
// Prepend previous path
210-
const string& vpath = !_virtualPath.empty() ? _virtualPath + "/" + dir : dir;
225+
string vpath;
226+
if(_virtualPath.empty())
227+
vpath = dir;
228+
else
229+
{
230+
vpath.reserve(_virtualPath.size() + 1 + dir.size());
231+
vpath = _virtualPath;
232+
vpath += '/';
233+
vpath += dir;
234+
}
235+
211236
myList.emplace_back(new FSNodeZIP(_zipFile, vpath, _realNode, 0, true));
212237
}
213238

@@ -217,26 +242,8 @@ bool FSNodeZIP::getChildren(AbstractFSList& myList, ListMode mode) const
217242
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
218243
size_t FSNodeZIP::read(ByteBuffer& buffer, size_t) const
219244
{
220-
switch(_error)
221-
{
222-
using enum zip_error;
223-
case NONE: break;
224-
case NOT_A_FILE: throw std::runtime_error("ZIP file contains errors/not found");
225-
case NOT_READABLE: throw std::runtime_error("ZIP file not readable");
226-
case NO_ROMS: throw std::runtime_error("ZIP file doesn't contain any ROMs");
227-
default: throw std::runtime_error("FSNodeZIP::read default case hit");
228-
}
229-
230245
zipHandler()->open(_zipFile);
231-
232-
bool found = false;
233-
while(zipHandler()->hasNext() && !found)
234-
{
235-
const auto& [name, size] = zipHandler()->next();
236-
found = name == _virtualPath;
237-
}
238-
239-
return found ? zipHandler()->decompress(buffer) : 0;
246+
return zipHandler()->decompress(_virtualPath, buffer);
240247
}
241248

242249
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -258,19 +265,30 @@ AbstractFSNodePtr FSNodeZIP::getParent() const
258265
if(_virtualPath.empty())
259266
return _realNode ? _realNode->getParent() : nullptr;
260267

261-
// TODO: For some reason, getting the stem for normal paths and zip paths
262-
// behaves differently. For now, we'll use the old method here.
263-
auto STEM_FOR_ZIP = [](string_view s) {
264-
const char* const start = s.data();
265-
const char* cur = start + s.size() - 2;
266-
267-
while (cur >= start && !(*cur == '/' || *cur == '\\'))
268-
--cur;
269-
270-
return s.substr(0, (cur + 1) - start - 1);
271-
};
268+
const auto stem = stemPathComponent(_path);
269+
// stemPathComponent includes trailing slash; strip it for ZIP constructor
270+
return std::make_unique<FSNodeZIP>(
271+
stem.empty() ? stem : stem.substr(0, stem.size() - 1));
272+
}
272273

273-
return std::make_unique<FSNodeZIP>(STEM_FOR_ZIP(_path));
274+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
275+
AbstractFSNodePtr FSNodeZIP::getSiblingNode(string_view ext) const
276+
{
277+
// Replace extension of the virtual path component
278+
const string_view stem = stemPathComponent(_virtualPath);
279+
const string_view name = lastPathComponent(_virtualPath);
280+
const size_t dot = name.find_last_of('.');
281+
282+
string newVirtual;
283+
newVirtual.reserve(stem.size() + name.size() + ext.size());
284+
newVirtual = stem;
285+
if(dot != string_view::npos)
286+
newVirtual += name.substr(0, dot);
287+
else
288+
newVirtual += name;
289+
newVirtual += ext;
290+
291+
return AbstractFSNodePtr(new FSNodeZIP(_zipFile, newVirtual, _realNode, 0, false));
274292
}
275293

276294
#endif // ZIP_SUPPORT

0 commit comments

Comments
 (0)