stella/src/common/FSNodeZIP.cxx

289 lines
8.6 KiB
C++

//============================================================================
//
// SSSS tt lll lll
// SS SS tt ll ll
// SS tttttt eeee ll ll aaaa
// SSSS tt ee ee ll ll aa
// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
// SS SS tt ee ll ll aa aa
// SSSS ttt eeeee llll llll aaaaa
//
// Copyright (c) 1995-2025 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================
#if defined(ZIP_SUPPORT)
#include <set>
#include "bspf.hxx"
#include "Bankswitch.hxx"
#include "FSNodeFactory.hxx"
#include "FSNodeZIP.hxx"
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FSNodeZIP::FSNodeZIP(string_view p)
{
// Extract ZIP file and virtual file (if specified)
const size_t pos = BSPF::findIgnoreCase(p, ".zip");
if(pos == string::npos)
return;
_zipFile = p.substr(0, pos+4);
// Expand '~' to the users 'home' directory
if(_zipFile[0] == '~')
{
#if defined(BSPF_UNIX) || defined(BSPF_MACOS)
const char* const home = std::getenv("HOME"); // NOLINT (not thread safe)
if(home != nullptr)
_zipFile.replace(0, 1, home);
#elif defined(BSPF_WINDOWS)
_zipFile.replace(0, 1, HomeFinder::getHomePath());
#endif
}
// cerr << " => p: " << p << '\n';
// Open file at least once to initialize the virtual file count
try
{
myZipHandler->open(_zipFile);
}
catch(const runtime_error&)
{
// TODO: Actually present the error passed in back to the user
// For now, we just indicate that no ROMs were found
_error = zip_error::NO_ROMS;
}
_numFiles = myZipHandler->romFiles();
if(_numFiles == 0)
{
_error = zip_error::NO_ROMS;
}
// We always need a virtual file/path
// Either one is given, or we use the first one
if(pos+5 < p.length()) // if something comes after '.zip'
{
_virtualPath = p.substr(pos+5);
_isFile = Bankswitch::isValidRomName(_virtualPath);
_isDirectory = !_isFile;
}
else if(_numFiles == 1)
{
bool found = false;
while(myZipHandler->hasNext() && !found)
{
const auto& [name, size] = myZipHandler->next();
if(Bankswitch::isValidRomName(name))
{
_virtualPath = name;
_size = size;
_isFile = true;
found = true;
}
}
if(!found)
return;
}
else if(_numFiles > 1)
_isDirectory = true;
// Create a concrete FSNode to use
// This *must not* be a ZIP file; it must be a real FSNode object that
// has direct access to the actual filesystem (aka, a 'System' node)
// Behind the scenes, this node is actually a platform-specific object
// for whatever system we are running on
_realNode = FSNodeFactory::create(_zipFile,
FSNodeFactory::Type::SYSTEM);
setFlags(_zipFile, _virtualPath, _realNode);
// cerr << "==============================================================\n";
// cerr << _name << ", file: " << _isFile << ", dir: " << _isDirectory << "\n\n";
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FSNodeZIP::FSNodeZIP(const string& zipfile, const string& virtualpath,
const AbstractFSNodePtr& realnode, size_t size, bool isdir)
: _size{size},
_isDirectory{isdir},
_isFile{!isdir}
{
// cerr << "=> c'tor 2: " << zipfile << ", " << virtualpath << ", " << isdir << '\n';
setFlags(zipfile, virtualpath, realnode);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void FSNodeZIP::setFlags(const string& zipfile, const string& virtualpath,
const AbstractFSNodePtr& realnode)
{
_zipFile = zipfile;
_virtualPath = virtualpath;
_realNode = realnode;
_path = _realNode->getPath();
_shortPath = _realNode->getShortPath();
// Is a file component present?
if(!_virtualPath.empty())
{
_path += ("/" + _virtualPath);
_shortPath += ("/" + _virtualPath);
}
_name = lastPathComponent(_path);
if(!_realNode->isFile())
_error = zip_error::NOT_A_FILE;
if(!_realNode->isReadable())
_error = zip_error::NOT_READABLE;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FSNodeZIP::exists() const
{
if(_realNode && _realNode->exists())
{
// We need to inspect the actual path, not just the ZIP file itself
try
{
myZipHandler->open(_zipFile);
while(myZipHandler->hasNext())
{
const auto& [name, size] = myZipHandler->next();
if(BSPF::startsWithIgnoreCase(name, _virtualPath))
return true;
}
}
catch(const runtime_error&)
{
// TODO: Actually present the error passed in back to the user
cerr << "ERROR: FSNodeZIP::exists()\n";
}
}
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool FSNodeZIP::getChildren(AbstractFSList& myList, ListMode mode) const
{
// Files within ZIP archives don't contain children
if(!isDirectory() || _error != zip_error::NONE)
return false;
std::set<string> dirs;
myZipHandler->open(_zipFile);
while(myZipHandler->hasNext())
{
// Only consider entries that start with '_virtualPath'
// Ignore empty filenames and '__MACOSX' virtual directories
const auto& [name, size] = myZipHandler->next();
if(BSPF::startsWithIgnoreCase(name, "__MACOSX") || name == EmptyString)
continue;
if(BSPF::startsWithIgnoreCase(name, _virtualPath))
{
// First strip off the leading directory
const string& curr = name.substr(
_virtualPath.empty() ? 0 : _virtualPath.size()+1);
// Only add sub-directory entries once
const auto pos = curr.find_first_of("/\\");
if(pos != string::npos)
dirs.emplace(curr.substr(0, pos));
else
myList.emplace_back(new FSNodeZIP(_zipFile, name, _realNode, size, false));
}
}
for(const auto& dir: dirs)
{
// Prepend previous path
const string& vpath = !_virtualPath.empty() ? _virtualPath + "/" + dir : dir;
myList.emplace_back(new FSNodeZIP(_zipFile, vpath, _realNode, 0, true));
}
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
size_t FSNodeZIP::read(ByteBuffer& buffer, size_t) const
{
switch(_error)
{
using enum zip_error;
case NONE: break;
case NOT_A_FILE: throw runtime_error("ZIP file contains errors/not found");
case NOT_READABLE: throw runtime_error("ZIP file not readable");
case NO_ROMS: throw runtime_error("ZIP file doesn't contain any ROMs");
default: throw runtime_error("FSNodeZIP::read default case hit");
}
myZipHandler->open(_zipFile);
bool found = false;
while(myZipHandler->hasNext() && !found)
{
const auto& [name, size] = myZipHandler->next();
found = name == _virtualPath;
}
return found ? myZipHandler->decompress(buffer) : 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
size_t FSNodeZIP::read(stringstream& buffer) const
{
// For now, we just read into a buffer and store in the stream
// TODO: maybe there's a more efficient way to do this?
ByteBuffer read_buf;
const size_t size = read(read_buf, 0);
if(size > 0)
buffer.write(reinterpret_cast<char*>(read_buf.get()), size);
return size;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
size_t FSNodeZIP::write(const ByteBuffer& buffer, size_t) const
{
// TODO: Not yet implemented
throw runtime_error("ZIP file not writable");
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
size_t FSNodeZIP::write(const stringstream& buffer) const
{
// TODO: Not yet implemented
throw runtime_error("ZIP file not writable");
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AbstractFSNodePtr FSNodeZIP::getParent() const
{
if(_virtualPath.empty())
return _realNode ? _realNode->getParent() : nullptr;
// TODO: For some reason, getting the stem for normal paths and zip paths
// behaves differently. For now, we'll use the old method here.
auto STEM_FOR_ZIP = [](string_view s) {
const char* const start = s.data();
const char* cur = start + s.size() - 2;
while (cur >= start && !(*cur == '/' || *cur == '\\'))
--cur;
return s.substr(0, (cur + 1) - start - 1);
};
return make_unique<FSNodeZIP>(STEM_FOR_ZIP(_path));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
unique_ptr<ZipHandler> FSNodeZIP::myZipHandler = make_unique<ZipHandler>();
#endif // ZIP_SUPPORT