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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
152148bool 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// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
218243size_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