1165 lines
29 KiB
C++
1165 lines
29 KiB
C++
/*
|
|
Copyright 2016-2024 melonDS team
|
|
|
|
This file is part of melonDS.
|
|
|
|
melonDS is free software: you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free
|
|
Software Foundation, either version 3 of the License, or (at your option)
|
|
any later version.
|
|
|
|
melonDS is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along
|
|
with melonDS. If not, see http://www.gnu.org/licenses/.
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#include <inttypes.h>
|
|
#include <vector>
|
|
|
|
#include "FATIO.h"
|
|
#include "FATStorage.h"
|
|
#include "Platform.h"
|
|
|
|
namespace melonDS
|
|
{
|
|
namespace fs = std::filesystem;
|
|
using namespace Platform;
|
|
using std::string;
|
|
|
|
FATStorage::FATStorage(const std::string& filename, u64 size, bool readonly, const std::optional<string>& sourcedir) :
|
|
FATStorage(FATStorageArgs { filename, size, readonly, sourcedir })
|
|
{
|
|
}
|
|
|
|
FATStorage::FATStorage(const FATStorageArgs& args) noexcept :
|
|
FATStorage(args.Filename, args.Size, args.ReadOnly, args.SourceDir)
|
|
{
|
|
}
|
|
|
|
FATStorage::FATStorage(FATStorageArgs&& args) noexcept :
|
|
FilePath(std::move(args.Filename)),
|
|
FileSize(args.Size),
|
|
ReadOnly(args.ReadOnly),
|
|
SourceDir(std::move(args.SourceDir))
|
|
{
|
|
Load(FilePath, FileSize, SourceDir);
|
|
}
|
|
|
|
FATStorage::FATStorage(FATStorage&& other) noexcept
|
|
{
|
|
FilePath = std::move(other.FilePath);
|
|
IndexPath = std::move(other.IndexPath);
|
|
SourceDir = std::move(other.SourceDir);
|
|
ReadOnly = other.ReadOnly;
|
|
File = other.File;
|
|
FileSize = other.FileSize;
|
|
DirIndex = std::move(other.DirIndex);
|
|
FileIndex = std::move(other.FileIndex);
|
|
|
|
other.File = nullptr;
|
|
}
|
|
|
|
FATStorage& FATStorage::operator=(FATStorage&& other) noexcept
|
|
{
|
|
if (this != &other)
|
|
{
|
|
if (File)
|
|
{ // Sync this file's contents to the host (if applicable) before closing it
|
|
if (!ReadOnly) Save();
|
|
CloseFile(File);
|
|
}
|
|
|
|
FilePath = std::move(other.FilePath);
|
|
IndexPath = std::move(other.IndexPath);
|
|
SourceDir = std::move(other.SourceDir);
|
|
ReadOnly = other.ReadOnly;
|
|
File = other.File;
|
|
FileSize = other.FileSize;
|
|
DirIndex = std::move(other.DirIndex);
|
|
FileIndex = std::move(other.FileIndex);
|
|
|
|
other.File = nullptr;
|
|
other.SourceDir = std::nullopt;
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
FATStorage::~FATStorage()
|
|
{
|
|
if (!ReadOnly) Save();
|
|
|
|
if (File) CloseFile(File);
|
|
File = nullptr;
|
|
}
|
|
|
|
bool FATStorage::InjectFile(const std::string& path, u8* data, u32 len)
|
|
{
|
|
if (!File) return false;
|
|
|
|
ff_disk_open(FF_ReadStorage(), FF_WriteStorage(), (LBA_t)(FileSize>>9));
|
|
|
|
FRESULT res;
|
|
FATFS fs;
|
|
|
|
res = f_mount(&fs, "0:", 1);
|
|
if (res != FR_OK)
|
|
{
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return false;
|
|
}
|
|
|
|
std::string prefixedPath("0:/");
|
|
prefixedPath += path;
|
|
FF_FIL file;
|
|
res = f_open(&file, prefixedPath.c_str(), FA_CREATE_ALWAYS | FA_WRITE);
|
|
if (res != FR_OK)
|
|
{
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return false;
|
|
}
|
|
|
|
u32 nwrite;
|
|
f_write(&file, data, len, &nwrite);
|
|
f_close(&file);
|
|
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return nwrite==len;
|
|
}
|
|
|
|
u32 FATStorage::ReadFile(const std::string& path, u32 start, u32 len, u8* data)
|
|
{
|
|
if (!File) return false;
|
|
|
|
ff_disk_open(FF_ReadStorage(), FF_WriteStorage(), (LBA_t)(FileSize>>9));
|
|
|
|
FRESULT res;
|
|
FATFS fs;
|
|
|
|
res = f_mount(&fs, "0:", 1);
|
|
if (res != FR_OK)
|
|
{
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return false;
|
|
}
|
|
|
|
std::string prefixedPath("0:/");
|
|
prefixedPath += path;
|
|
FF_FIL file;
|
|
res = f_open(&file, prefixedPath.c_str(), FA_READ);
|
|
if (res != FR_OK)
|
|
{
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return false;
|
|
}
|
|
|
|
u32 nread;
|
|
f_lseek(&file, start);
|
|
f_read(&file, data, len, &nread);
|
|
f_close(&file);
|
|
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return nread;
|
|
}
|
|
|
|
u32 FATStorage::ReadSectors(u32 start, u32 num, u8* data) const
|
|
{
|
|
return ReadSectorsInternal(File, FileSize, start, num, data);
|
|
}
|
|
|
|
u32 FATStorage::WriteSectors(u32 start, u32 num, const u8* data)
|
|
{
|
|
if (ReadOnly) return 0;
|
|
return WriteSectorsInternal(File, FileSize, start, num, data);
|
|
}
|
|
|
|
u64 FATStorage::GetSectorCount() const
|
|
{
|
|
return FileSize / 0x200;
|
|
}
|
|
|
|
ff_disk_read_cb FATStorage::FF_ReadStorage() const noexcept
|
|
{
|
|
return [this](BYTE* buf, LBA_t sector, UINT num) {
|
|
return ReadSectorsInternal(File, FileSize, sector, num, buf);
|
|
};
|
|
}
|
|
|
|
ff_disk_write_cb FATStorage::FF_WriteStorage() const noexcept
|
|
{
|
|
return [this](const BYTE* buf, LBA_t sector, UINT num) {
|
|
return WriteSectorsInternal(File, FileSize, sector, num, buf);
|
|
};
|
|
}
|
|
|
|
|
|
u32 FATStorage::ReadSectorsInternal(FileHandle* file, u64 filelen, u32 start, u32 num, u8* data)
|
|
{
|
|
if (!file) return 0;
|
|
|
|
u64 addr = start * 0x200ULL;
|
|
u32 len = num * 0x200;
|
|
|
|
if ((addr+len) > filelen)
|
|
{
|
|
if (addr >= filelen) return 0;
|
|
len = filelen - addr;
|
|
num = len >> 9;
|
|
}
|
|
|
|
FileSeek(file, addr, FileSeekOrigin::Start);
|
|
|
|
u32 res = FileRead(data, 0x200, num, file);
|
|
if (res < num)
|
|
{
|
|
if (IsEndOfFile(file))
|
|
{
|
|
memset(&data[0x200*res], 0, 0x200*(num-res));
|
|
return num;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
u32 FATStorage::WriteSectorsInternal(FileHandle* file, u64 filelen, u32 start, u32 num, const u8* data)
|
|
{
|
|
if (!file) return 0;
|
|
|
|
u64 addr = start * 0x200ULL;
|
|
u32 len = num * 0x200;
|
|
|
|
if ((addr+len) > filelen)
|
|
{
|
|
if (addr >= filelen) return 0;
|
|
len = filelen - addr;
|
|
num = len >> 9;
|
|
}
|
|
|
|
FileSeek(file, addr, FileSeekOrigin::Start);
|
|
|
|
u32 res = Platform::FileWrite(data, 0x200, num, file);
|
|
return res;
|
|
}
|
|
|
|
|
|
void FATStorage::LoadIndex()
|
|
{
|
|
DirIndex.clear();
|
|
FileIndex.clear();
|
|
|
|
FileHandle* f = OpenLocalFile(IndexPath, FileMode::ReadText);
|
|
if (!f) return;
|
|
|
|
char linebuf[1536];
|
|
while (!IsEndOfFile(f))
|
|
{
|
|
if (!FileReadLine(linebuf, 1536, f))
|
|
break;
|
|
|
|
if (linebuf[0] == 'S')
|
|
{
|
|
u64 fsize;
|
|
int ret = sscanf(linebuf, "SIZE %" PRIu64, &fsize);
|
|
if (ret < 1) continue;
|
|
|
|
FileSize = fsize;
|
|
}
|
|
else if (linebuf[0] == 'D')
|
|
{
|
|
u32 readonly;
|
|
char fpath[1536] = {0};
|
|
int ret = sscanf(linebuf, "DIR %u %[^\t\r\n]",
|
|
&readonly, fpath);
|
|
if (ret < 2) continue;
|
|
|
|
for (int i = 0; i < 1536 && fpath[i] != '\0'; i++)
|
|
{
|
|
if (fpath[i] == '\\')
|
|
fpath[i] = '/';
|
|
}
|
|
|
|
DirIndexEntry entry;
|
|
entry.Path = fpath;
|
|
entry.IsReadOnly = readonly!=0;
|
|
|
|
DirIndex[entry.Path] = entry;
|
|
}
|
|
else if (linebuf[0] == 'F')
|
|
{
|
|
u32 readonly;
|
|
u64 fsize;
|
|
s64 lastmodified;
|
|
u32 lastmod_internal;
|
|
char fpath[1536] = {0};
|
|
int ret = sscanf(linebuf, "FILE %u %" PRIu64 " %" PRId64 " %u %[^\t\r\n]",
|
|
&readonly, &fsize, &lastmodified, &lastmod_internal, fpath);
|
|
if (ret < 5) continue;
|
|
|
|
for (int i = 0; i < 1536 && fpath[i] != '\0'; i++)
|
|
{
|
|
if (fpath[i] == '\\')
|
|
fpath[i] = '/';
|
|
}
|
|
|
|
FileIndexEntry entry;
|
|
entry.Path = fpath;
|
|
entry.IsReadOnly = readonly!=0;
|
|
entry.Size = fsize;
|
|
entry.LastModified = lastmodified;
|
|
entry.LastModifiedInternal = lastmod_internal;
|
|
|
|
FileIndex[entry.Path] = entry;
|
|
}
|
|
}
|
|
|
|
CloseFile(f);
|
|
|
|
// ensure the indexes are sane
|
|
|
|
std::vector<std::string> removelist;
|
|
|
|
for (const auto& [key, val] : DirIndex)
|
|
{
|
|
std::string path = val.Path;
|
|
|
|
if ((path.find("/./") != std::string::npos) ||
|
|
(path.find("/../") != std::string::npos) ||
|
|
(path.substr(0,2) == "./") ||
|
|
(path.substr(0,3) == "../"))
|
|
{
|
|
removelist.push_back(key);
|
|
continue;
|
|
}
|
|
|
|
int sep = path.rfind('/');
|
|
if (sep == std::string::npos) continue;
|
|
|
|
path = path.substr(0, sep);
|
|
if (DirIndex.count(path) < 1)
|
|
{
|
|
removelist.push_back(key);
|
|
}
|
|
}
|
|
|
|
for (const auto& key : removelist)
|
|
{
|
|
DirIndex.erase(key);
|
|
}
|
|
|
|
removelist.clear();
|
|
|
|
for (const auto& [key, val] : FileIndex)
|
|
{
|
|
std::string path = val.Path;
|
|
|
|
if ((path.find("/./") != std::string::npos) ||
|
|
(path.find("/../") != std::string::npos) ||
|
|
(path.substr(0,2) == "./") ||
|
|
(path.substr(0,3) == "../"))
|
|
{
|
|
removelist.push_back(key);
|
|
continue;
|
|
}
|
|
|
|
int sep = path.rfind('/');
|
|
if (sep == std::string::npos) continue;
|
|
|
|
path = path.substr(0, sep);
|
|
if (DirIndex.count(path) < 1)
|
|
{
|
|
removelist.push_back(key);
|
|
}
|
|
}
|
|
|
|
for (const auto& key : removelist)
|
|
{
|
|
FileIndex.erase(key);
|
|
}
|
|
}
|
|
|
|
void FATStorage::SaveIndex()
|
|
{
|
|
FileHandle* f = OpenLocalFile(IndexPath, FileMode::WriteText);
|
|
if (!f) return;
|
|
|
|
FileWriteFormatted(f, "SIZE %" PRIu64 "\n", FileSize);
|
|
|
|
for (const auto& [key, val] : DirIndex)
|
|
{
|
|
FileWriteFormatted(f, "DIR %u %s\n",
|
|
val.IsReadOnly?1:0, val.Path.c_str());
|
|
}
|
|
|
|
for (const auto& [key, val] : FileIndex)
|
|
{
|
|
FileWriteFormatted(f, "FILE %u %" PRIu64 " %" PRId64 " %u %s\n",
|
|
val.IsReadOnly?1:0, val.Size, val.LastModified, val.LastModifiedInternal, val.Path.c_str());
|
|
}
|
|
|
|
CloseFile(f);
|
|
}
|
|
|
|
|
|
bool FATStorage::ExportFile(const std::string& path, fs::path out)
|
|
{
|
|
FF_FIL file;
|
|
FileHandle* fout;
|
|
FRESULT res;
|
|
|
|
res = f_open(&file, path.c_str(), FA_OPEN_EXISTING | FA_READ);
|
|
if (res != FR_OK)
|
|
return false;
|
|
|
|
u32 len = f_size(&file);
|
|
|
|
if (fs::exists(out))
|
|
{
|
|
std::error_code err;
|
|
fs::permissions(out,
|
|
fs::perms::owner_read | fs::perms::owner_write,
|
|
fs::perm_options::add,
|
|
err);
|
|
}
|
|
|
|
fout = OpenFile(out.u8string(), FileMode::Write);
|
|
if (!fout)
|
|
{
|
|
f_close(&file);
|
|
return false;
|
|
}
|
|
|
|
u8 buf[0x1000];
|
|
for (u32 i = 0; i < len; i += 0x1000)
|
|
{
|
|
u32 blocklen;
|
|
if ((i + 0x1000) > len)
|
|
blocklen = len - i;
|
|
else
|
|
blocklen = 0x1000;
|
|
|
|
u32 nread;
|
|
f_read(&file, buf, blocklen, &nread);
|
|
FileWrite(buf, blocklen, 1, fout);
|
|
}
|
|
|
|
CloseFile(fout);
|
|
f_close(&file);
|
|
|
|
return true;
|
|
}
|
|
|
|
void FATStorage::ExportDirectory(const std::string& path, const std::string& outbase, int level)
|
|
{
|
|
if (level >= 32) return;
|
|
|
|
FF_DIR dir;
|
|
FF_FILINFO info;
|
|
FRESULT res;
|
|
|
|
std::string fullpath = "0:/" + path;
|
|
res = f_opendir(&dir, fullpath.c_str());
|
|
if (res != FR_OK) return;
|
|
|
|
std::vector<std::string> subdirlist;
|
|
|
|
for (;;)
|
|
{
|
|
res = f_readdir(&dir, &info);
|
|
if (res != FR_OK) break;
|
|
if (!info.fname[0]) break;
|
|
|
|
std::string fullpath = path + info.fname;
|
|
fs::path outpath = fs::u8path(outbase + "/" + fullpath);
|
|
|
|
if (info.fattrib & AM_DIR)
|
|
{
|
|
if (DirIndex.count(fullpath) < 1)
|
|
{
|
|
std::error_code err;
|
|
fs::create_directory(outpath, err);
|
|
|
|
DirIndexEntry entry;
|
|
entry.Path = fullpath;
|
|
entry.IsReadOnly = (info.fattrib & AM_RDO) != 0;
|
|
|
|
DirIndex[entry.Path] = entry;
|
|
}
|
|
|
|
subdirlist.push_back(fullpath);
|
|
}
|
|
else
|
|
{
|
|
bool doexport = false;
|
|
|
|
if (FileIndex.count(fullpath) < 1)
|
|
{
|
|
doexport = true;
|
|
|
|
FileIndexEntry entry;
|
|
entry.Path = fullpath;
|
|
entry.IsReadOnly = (info.fattrib & AM_RDO) != 0;
|
|
entry.Size = info.fsize;
|
|
entry.LastModifiedInternal = (info.fdate << 16) | info.ftime;
|
|
|
|
FileIndex[entry.Path] = entry;
|
|
}
|
|
else
|
|
{
|
|
u32 lastmod = (info.fdate << 16) | info.ftime;
|
|
|
|
FileIndexEntry& entry = FileIndex[fullpath];
|
|
if ((info.fsize != entry.Size) || (lastmod != entry.LastModifiedInternal))
|
|
doexport = true;
|
|
|
|
entry.Size = info.fsize;
|
|
entry.LastModifiedInternal = lastmod;
|
|
}
|
|
|
|
if (doexport)
|
|
{
|
|
if (ExportFile("0:/"+fullpath, outpath))
|
|
{
|
|
fs::file_time_type modtime = fs::last_write_time(outpath);
|
|
s64 modtime_raw = std::chrono::duration_cast<std::chrono::seconds>(modtime.time_since_epoch()).count();
|
|
|
|
FileIndexEntry& entry = FileIndex[fullpath];
|
|
entry.LastModified = modtime_raw;
|
|
}
|
|
else
|
|
{
|
|
// ??????
|
|
}
|
|
}
|
|
}
|
|
|
|
std::error_code err;
|
|
fs::permissions(outpath,
|
|
fs::perms::owner_read | fs::perms::owner_write,
|
|
(info.fattrib & AM_RDO) ? fs::perm_options::remove : fs::perm_options::add,
|
|
err);
|
|
}
|
|
|
|
f_closedir(&dir);
|
|
|
|
for (auto& entry : subdirlist)
|
|
{
|
|
ExportDirectory(entry+"/", outbase, level+1);
|
|
}
|
|
}
|
|
|
|
bool FATStorage::DeleteHostDirectory(const std::string& path, const std::string& outbase, int level)
|
|
{
|
|
if (level >= 32) return false;
|
|
|
|
fs::path dirpath = fs::u8path(outbase + "/" + path);
|
|
if (!fs::is_directory(dirpath))
|
|
return true; // already deleted? oh well
|
|
|
|
std::vector<std::string> filedeletelist;
|
|
std::vector<std::string> dirdeletelist;
|
|
|
|
int outlen = outbase.length();
|
|
for (auto& entry : fs::directory_iterator(dirpath))
|
|
{
|
|
std::string fullpath = entry.path().string();
|
|
std::string innerpath = fullpath.substr(outlen);
|
|
if (innerpath[0] == '/' || innerpath[0] == '\\')
|
|
innerpath = innerpath.substr(1);
|
|
|
|
int ilen = innerpath.length();
|
|
for (int i = 0; i < ilen; i++)
|
|
{
|
|
if (innerpath[i] == '\\')
|
|
innerpath[i] = '/';
|
|
}
|
|
|
|
if (entry.is_directory())
|
|
{
|
|
dirdeletelist.push_back(innerpath);
|
|
}
|
|
else
|
|
{
|
|
filedeletelist.push_back(innerpath);
|
|
}
|
|
}
|
|
|
|
for (const auto& key : filedeletelist)
|
|
{
|
|
fs::path fullpath = fs::u8path(outbase + "/" + key);
|
|
std::error_code err;
|
|
fs::permissions(fullpath,
|
|
fs::perms::owner_read | fs::perms::owner_write,
|
|
fs::perm_options::add,
|
|
err);
|
|
if (!fs::remove(fullpath))
|
|
return false;
|
|
|
|
FileIndex.erase(key);
|
|
}
|
|
|
|
for (const auto& key : dirdeletelist)
|
|
{
|
|
if (!DeleteHostDirectory(key, outbase, level+1))
|
|
return false;
|
|
}
|
|
|
|
{
|
|
fs::path fullpath = fs::u8path(outbase + "/" + path);
|
|
|
|
std::error_code err;
|
|
fs::permissions(fullpath,
|
|
fs::perms::owner_read | fs::perms::owner_write,
|
|
fs::perm_options::add,
|
|
err);
|
|
if (!fs::remove(fullpath))
|
|
return false;
|
|
|
|
DirIndex.erase(path);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FATStorage::ExportChanges(const std::string& outbase)
|
|
{
|
|
// reflect changes in the FAT volume to the host filesystem
|
|
// * delete directories and files that exist in the index but not in the volume
|
|
// * copy files to the host FS if they exist within the index and their size or
|
|
// internal last-modified time is different
|
|
// * index and copy directories and files that exist in the volume but not in
|
|
// the index
|
|
|
|
std::vector<std::string> deletelist;
|
|
|
|
for (const auto& [key, val] : FileIndex)
|
|
{
|
|
std::string innerpath = "0:/" + val.Path;
|
|
FF_FILINFO finfo;
|
|
FRESULT res = f_stat(innerpath.c_str(), &finfo);
|
|
if (res == FR_OK)
|
|
{
|
|
if (finfo.fattrib & AM_DIR)
|
|
{
|
|
deletelist.push_back(key);
|
|
}
|
|
}
|
|
else if (res == FR_NO_FILE || res == FR_NO_PATH)
|
|
{
|
|
deletelist.push_back(key);
|
|
}
|
|
}
|
|
|
|
for (const auto& key : deletelist)
|
|
{
|
|
fs::path fullpath = fs::u8path(outbase + "/" + key);
|
|
|
|
std::error_code err;
|
|
fs::permissions(fullpath,
|
|
fs::perms::owner_read | fs::perms::owner_write,
|
|
fs::perm_options::add,
|
|
err);
|
|
fs::remove(fullpath);
|
|
|
|
FileIndex.erase(key);
|
|
}
|
|
|
|
deletelist.clear();
|
|
|
|
for (const auto& [key, val] : DirIndex)
|
|
{
|
|
std::string innerpath = "0:/" + val.Path;
|
|
FF_FILINFO finfo;
|
|
FRESULT res = f_stat(innerpath.c_str(), &finfo);
|
|
if (res == FR_OK)
|
|
{
|
|
if (!(finfo.fattrib & AM_DIR))
|
|
{
|
|
deletelist.push_back(key);
|
|
}
|
|
}
|
|
else if (res == FR_NO_FILE || res == FR_NO_PATH)
|
|
{
|
|
deletelist.push_back(key);
|
|
}
|
|
}
|
|
|
|
for (const auto& key : deletelist)
|
|
{
|
|
DeleteHostDirectory(key, outbase, 0);
|
|
}
|
|
|
|
ExportDirectory("", outbase, 0);
|
|
}
|
|
|
|
|
|
bool FATStorage::CanFitFile(u32 len)
|
|
{
|
|
FATFS* fs;
|
|
DWORD freeclusters;
|
|
FRESULT res;
|
|
|
|
res = f_getfree("0:", &freeclusters, &fs);
|
|
if (res != FR_OK) return false;
|
|
|
|
u32 clustersize = fs->csize * 0x200;
|
|
|
|
len = (len + clustersize - 1) / clustersize;
|
|
return (freeclusters >= len);
|
|
}
|
|
|
|
bool FATStorage::DeleteDirectory(const std::string& path, int level)
|
|
{
|
|
if (level >= 32) return false;
|
|
if (path.length() < 1) return false;
|
|
|
|
FF_DIR dir;
|
|
FF_FILINFO info;
|
|
FRESULT res;
|
|
|
|
std::string fullpath = "0:/" + path;
|
|
f_chmod(fullpath.c_str(), 0, AM_RDO);
|
|
res = f_opendir(&dir, fullpath.c_str());
|
|
if (res != FR_OK) return false;
|
|
|
|
std::vector<std::string> deletelist;
|
|
std::vector<std::string> subdirlist;
|
|
int survivors = 0;
|
|
|
|
for (;;)
|
|
{
|
|
res = f_readdir(&dir, &info);
|
|
if (res != FR_OK) break;
|
|
if (!info.fname[0]) break;
|
|
|
|
std::string fullpath = path + info.fname;
|
|
|
|
if (info.fattrib & AM_DIR)
|
|
{
|
|
subdirlist.push_back(fullpath);
|
|
}
|
|
else
|
|
{
|
|
deletelist.push_back(fullpath);
|
|
}
|
|
}
|
|
|
|
f_closedir(&dir);
|
|
|
|
for (auto& entry : deletelist)
|
|
{
|
|
std::string fullpath = "0:/" + entry;
|
|
f_chmod(fullpath.c_str(), 0, AM_RDO);
|
|
res = f_unlink(fullpath.c_str());
|
|
if (res != FR_OK) return false;
|
|
}
|
|
|
|
for (auto& entry : subdirlist)
|
|
{
|
|
if (!DeleteDirectory(entry+"/", level+1))
|
|
return false;
|
|
}
|
|
|
|
res = f_unlink(fullpath.c_str());
|
|
if (res != FR_OK) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
void FATStorage::CleanupDirectory(const std::string& sourcedir, const std::string& path, int level)
|
|
{
|
|
if (level >= 32) return;
|
|
|
|
FF_DIR dir;
|
|
FF_FILINFO info;
|
|
FRESULT res;
|
|
|
|
std::string fullpath = "0:/" + path;
|
|
res = f_opendir(&dir, fullpath.c_str());
|
|
if (res != FR_OK) return;
|
|
|
|
std::vector<std::string> filedeletelist;
|
|
std::vector<std::string> dirdeletelist;
|
|
std::vector<std::string> subdirlist;
|
|
|
|
for (;;)
|
|
{
|
|
res = f_readdir(&dir, &info);
|
|
if (res != FR_OK) break;
|
|
if (!info.fname[0]) break;
|
|
|
|
std::string fullpath = path + info.fname;
|
|
|
|
if (info.fattrib & AM_DIR)
|
|
{
|
|
if (DirIndex.count(fullpath) < 1)
|
|
dirdeletelist.push_back(fullpath);
|
|
else if (!fs::is_directory(fs::u8path(sourcedir+"/"+fullpath)))
|
|
{
|
|
DirIndex.erase(fullpath);
|
|
dirdeletelist.push_back(fullpath);
|
|
}
|
|
else
|
|
subdirlist.push_back(fullpath);
|
|
}
|
|
else
|
|
{
|
|
if (FileIndex.count(fullpath) < 1)
|
|
filedeletelist.push_back(fullpath);
|
|
else if (!fs::is_regular_file(fs::u8path(sourcedir+"/"+fullpath)))
|
|
{
|
|
FileIndex.erase(fullpath);
|
|
filedeletelist.push_back(fullpath);
|
|
}
|
|
}
|
|
}
|
|
|
|
f_closedir(&dir);
|
|
|
|
for (auto& entry : filedeletelist)
|
|
{
|
|
std::string fullpath = "0:/" + entry;
|
|
f_chmod(fullpath.c_str(), 0, AM_RDO);
|
|
f_unlink(fullpath.c_str());
|
|
}
|
|
|
|
for (auto& entry : dirdeletelist)
|
|
{
|
|
DeleteDirectory(entry+"/", level+1);
|
|
}
|
|
|
|
for (auto& entry : subdirlist)
|
|
{
|
|
CleanupDirectory(sourcedir, entry+"/", level+1);
|
|
}
|
|
}
|
|
|
|
bool FATStorage::ImportFile(const std::string& path, fs::path in)
|
|
{
|
|
FF_FIL file;
|
|
FileHandle* fin;
|
|
FRESULT res;
|
|
|
|
fin = Platform::OpenFile(in.u8string(), FileMode::Read);
|
|
if (!fin)
|
|
return false;
|
|
|
|
u32 len = FileLength(fin);
|
|
|
|
if (!CanFitFile(len))
|
|
{
|
|
CloseFile(fin);
|
|
return false;
|
|
}
|
|
|
|
res = f_open(&file, path.c_str(), FA_CREATE_ALWAYS | FA_WRITE);
|
|
if (res != FR_OK)
|
|
{
|
|
CloseFile(fin);
|
|
return false;
|
|
}
|
|
|
|
u8 buf[0x1000];
|
|
for (u32 i = 0; i < len; i += 0x1000)
|
|
{
|
|
u32 blocklen;
|
|
if ((i + 0x1000) > len)
|
|
blocklen = len - i;
|
|
else
|
|
blocklen = 0x1000;
|
|
|
|
u32 nwrite;
|
|
FileRead(buf, blocklen, 1, fin);
|
|
f_write(&file, buf, blocklen, &nwrite);
|
|
}
|
|
|
|
CloseFile(fin);
|
|
f_close(&file);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FATStorage::ImportDirectory(const std::string& sourcedir)
|
|
{
|
|
// remove whatever isn't in the index
|
|
CleanupDirectory(sourcedir, "", 0);
|
|
|
|
int srclen = sourcedir.length();
|
|
|
|
// iterate through the host directory:
|
|
// * directories will be added if they aren't in the index
|
|
// * files will be added if they aren't in the index, or if the size or last-modified-date don't match
|
|
for (auto& entry : fs::recursive_directory_iterator(fs::u8path(sourcedir)))
|
|
{
|
|
std::string fullpath = entry.path().u8string();
|
|
std::string innerpath = fullpath.substr(srclen);
|
|
if (innerpath[0] == '/' || innerpath[0] == '\\')
|
|
innerpath = innerpath.substr(1);
|
|
|
|
int ilen = innerpath.length();
|
|
for (int i = 0; i < ilen; i++)
|
|
{
|
|
if (innerpath[i] == '\\')
|
|
innerpath[i] = '/';
|
|
}
|
|
|
|
bool readonly = (entry.status().permissions() & fs::perms::owner_write) == fs::perms::none;
|
|
|
|
if (entry.is_directory())
|
|
{
|
|
if (DirIndex.count(innerpath) < 1)
|
|
{
|
|
DirIndexEntry ientry;
|
|
ientry.Path = innerpath;
|
|
ientry.IsReadOnly = readonly;
|
|
|
|
innerpath = "0:/" + innerpath;
|
|
FRESULT res = f_mkdir(innerpath.c_str());
|
|
if (res == FR_OK)
|
|
{
|
|
DirIndex[ientry.Path] = ientry;
|
|
}
|
|
}
|
|
}
|
|
else if (entry.is_regular_file())
|
|
{
|
|
u64 filesize = entry.file_size();
|
|
|
|
auto lastmodified = entry.last_write_time();
|
|
s64 lastmodified_raw = std::chrono::duration_cast<std::chrono::seconds>(lastmodified.time_since_epoch()).count();
|
|
|
|
bool import = false;
|
|
if (FileIndex.count(innerpath) < 1)
|
|
{
|
|
import = true;
|
|
}
|
|
else
|
|
{
|
|
FileIndexEntry& chk = FileIndex[innerpath];
|
|
if (chk.Size != filesize) import = true;
|
|
if (chk.LastModified != lastmodified_raw) import = true;
|
|
}
|
|
|
|
if (import)
|
|
{
|
|
FileIndexEntry ientry;
|
|
ientry.Path = innerpath;
|
|
ientry.IsReadOnly = readonly;
|
|
ientry.Size = filesize;
|
|
ientry.LastModified = lastmodified_raw;
|
|
|
|
innerpath = "0:/" + innerpath;
|
|
if (ImportFile(innerpath, entry.path()))
|
|
{
|
|
FF_FILINFO finfo;
|
|
f_stat(innerpath.c_str(), &finfo);
|
|
|
|
ientry.LastModifiedInternal = (finfo.fdate << 16) | finfo.ftime;
|
|
|
|
FileIndex[ientry.Path] = ientry;
|
|
}
|
|
}
|
|
}
|
|
|
|
f_chmod(innerpath.c_str(), readonly?AM_RDO:0, AM_RDO);
|
|
}
|
|
|
|
SaveIndex();
|
|
|
|
return true;
|
|
}
|
|
|
|
u64 FATStorage::GetDirectorySize(fs::path sourcedir) const
|
|
{
|
|
u64 ret = 0;
|
|
u32 csize = 0x1000; // this is an estimate
|
|
|
|
for (auto& entry : fs::recursive_directory_iterator(sourcedir))
|
|
{
|
|
if (entry.is_directory())
|
|
{
|
|
ret += csize;
|
|
}
|
|
else if (entry.is_regular_file())
|
|
{
|
|
u64 filesize = entry.file_size();
|
|
|
|
filesize = (filesize + (csize-1)) & ~(csize-1);
|
|
ret += filesize;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool FATStorage::Load(const std::string& filename, u64 size, const std::optional<string>& sourcedir)
|
|
{
|
|
bool hasdir = sourcedir && !sourcedir->empty();
|
|
if (sourcedir)
|
|
{
|
|
if (!fs::is_directory(fs::u8path(*sourcedir)))
|
|
{
|
|
hasdir = false;
|
|
SourceDir = std::nullopt;
|
|
}
|
|
}
|
|
|
|
// 'auto' size management: (size=0)
|
|
// * if an index exists: the size from the index is used
|
|
// * if no index, and an image file exists: the file size is used
|
|
// * if no image: if sourcing from a directory, size is calculated from that
|
|
// with a minimum 128MB extra, otherwise size is defaulted to 512MB
|
|
|
|
bool isnew = !Platform::LocalFileExists(filename);
|
|
File = Platform::OpenLocalFile(filename, static_cast<FileMode>(FileMode::ReadWrite | FileMode::Preserve));
|
|
if (!File)
|
|
return false;
|
|
|
|
IndexPath = FilePath + ".idx";
|
|
if (isnew)
|
|
{
|
|
DirIndex.clear();
|
|
FileIndex.clear();
|
|
SaveIndex();
|
|
}
|
|
else
|
|
{
|
|
LoadIndex();
|
|
|
|
if (FileSize == 0)
|
|
{
|
|
FileSize = FileLength(File);
|
|
}
|
|
}
|
|
|
|
bool needformat = false;
|
|
FATFS fs;
|
|
FRESULT res;
|
|
|
|
if (FileSize == 0)
|
|
{
|
|
needformat = true;
|
|
}
|
|
else
|
|
{
|
|
ff_disk_open(FF_ReadStorage(), FF_WriteStorage(), (LBA_t)(FileSize>>9));
|
|
|
|
res = f_mount(&fs, "0:", 1);
|
|
if (res != FR_OK)
|
|
{
|
|
needformat = true;
|
|
}
|
|
else if (size > 0 && size != FileSize)
|
|
{
|
|
needformat = true;
|
|
}
|
|
}
|
|
|
|
if (needformat)
|
|
{
|
|
FileSize = size;
|
|
if (FileSize == 0)
|
|
{
|
|
if (hasdir)
|
|
{
|
|
FileSize = GetDirectorySize(fs::u8path(*sourcedir));
|
|
FileSize += 0x8000000ULL; // 128MB leeway
|
|
|
|
// make it a power of two
|
|
FileSize |= (FileSize >> 1);
|
|
FileSize |= (FileSize >> 2);
|
|
FileSize |= (FileSize >> 4);
|
|
FileSize |= (FileSize >> 8);
|
|
FileSize |= (FileSize >> 16);
|
|
FileSize |= (FileSize >> 32);
|
|
FileSize++;
|
|
}
|
|
else
|
|
FileSize = 0x20000000ULL; // 512MB
|
|
}
|
|
|
|
ff_disk_close();
|
|
ff_disk_open(FF_ReadStorage(), FF_WriteStorage(), (LBA_t)(FileSize>>9));
|
|
|
|
DirIndex.clear();
|
|
FileIndex.clear();
|
|
SaveIndex();
|
|
|
|
FF_MKFS_PARM fsopt;
|
|
|
|
// FAT type: we force it to FAT32 for any volume that is 1GB or more
|
|
// libfat attempts to determine the FAT type from the volume size and other parameters
|
|
// which can lead to it trying to interpret a FAT16 volume as FAT32
|
|
if (FileSize >= 0x40000000ULL)
|
|
fsopt.fmt = FM_FAT32;
|
|
else
|
|
fsopt.fmt = FM_FAT;
|
|
|
|
fsopt.au_size = 0;
|
|
fsopt.align = 1;
|
|
fsopt.n_fat = 1;
|
|
fsopt.n_root = 512;
|
|
|
|
BYTE workbuf[FF_MAX_SS];
|
|
res = f_mkfs("0:", &fsopt, workbuf, sizeof(workbuf));
|
|
|
|
if (res == FR_OK)
|
|
res = f_mount(&fs, "0:", 1);
|
|
}
|
|
|
|
if (res == FR_OK)
|
|
{
|
|
if (hasdir)
|
|
ImportDirectory(*sourcedir);
|
|
}
|
|
|
|
f_unmount("0:");
|
|
|
|
ff_disk_close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FATStorage::Save()
|
|
{
|
|
if (!SourceDir)
|
|
{ // If we're not syncing the SD card image to a host directory...
|
|
return true; // Not an error.
|
|
}
|
|
|
|
ff_disk_open(FF_ReadStorage(), FF_WriteStorage(), (LBA_t)(FileSize>>9));
|
|
|
|
FRESULT res;
|
|
FATFS fs;
|
|
|
|
res = f_mount(&fs, "0:", 1);
|
|
if (res != FR_OK)
|
|
{
|
|
f_unmount("0:");
|
|
ff_disk_close();
|
|
return false;
|
|
}
|
|
|
|
ExportChanges(*SourceDir);
|
|
|
|
SaveIndex();
|
|
|
|
f_unmount("0:");
|
|
|
|
ff_disk_close();
|
|
|
|
return true;
|
|
}
|
|
|
|
} |