17 parseLine(std::string_view line, std::size_t lineNo) {
18 if (!line.empty() && line.back() ==
'\r') line.remove_suffix(1);
19 if (line.empty())
return std::optional<std::pair<std::uint32_t, std::string>>{};
21 const auto sep = line.find(
';');
22 if (sep == std::string_view::npos || sep == 0 || sep + 1 >= line.size())
24 std::format(
"listfile line {}: expected 'fileDataId;filepath'", lineNo));
27 const auto [ptr, ec] = std::from_chars(line.data(), line.data() + sep,
id);
28 if (ec != std::errc{} || ptr != line.data() + sep)
30 std::format(
"listfile line {}: bad fileDataId '{}'", lineNo, line.substr(0, sep)));
32 return std::optional{std::pair{id,
normalizePath(line.substr(sep + 1))}};
37 std::unique_lock lock{other._mtx};
38 _source = std::move(other._source);
39 _pathToId = std::move(other._pathToId);
40 _idToPath = std::move(other._idToPath);
41 _allocator = other._allocator;
46 std::scoped_lock lock{_mtx, other._mtx};
47 _source = std::move(other._source);
48 _pathToId = std::move(other._pathToId);
49 _idToPath = std::move(other._idToPath);
50 _allocator = other._allocator;
56 std::ifstream file{csv};
66 result._pathToId.reserve(4'000'000);
67 result._idToPath.reserve(4'000'000);
70 for (std::size_t lineNo = 1; std::getline(file, line); ++lineNo) {
71 auto parsed = parseLine(line, lineNo);
72 if (!parsed)
return std::unexpected(parsed.error());
73 if (!*parsed)
continue;
75 auto& [id, path] = **parsed;
76 result._idToPath.insert_or_assign(
id, path);
77 result._pathToId.insert_or_assign(std::move(path),
FileDataID{
id});
91 std::shared_lock lock{_mtx};
92 const auto it = _pathToId.find(canonical);
93 return it == _pathToId.end() ? std::nullopt : std::optional{it->second};
97 std::shared_lock lock{_mtx};
98 const auto it = _idToPath.find(fdid.
value);
99 return it == _idToPath.end() ? std::nullopt : std::optional{it->second};
104 if (canonical.empty())
107 std::unique_lock lock{_mtx};
109 if (_pathToId.contains(canonical))
112 auto id = _allocator.next();
113 if (!
id)
return std::unexpected(
id.error());
117 if (!_source.empty()) {
120 bool needsNewline =
false;
121 if (std::ifstream tail{_source, std::ios::binary | std::ios::ate}; tail)
122 if (
const auto size = tail.tellg();
size > 0) {
124 tail.seekg(-1, std::ios::end);
126 needsNewline = last !=
'\n';
129 std::ofstream file{_source, std::ios::app};
130 if (needsNewline) file <<
'\n';
131 if (!file || !(file << id->value <<
';' <<
toNativeRelative(canonical) <<
'\n').flush())
135 _idToPath.insert_or_assign(id->value, canonical);
136 _pathToId.insert_or_assign(std::move(canonical), *
id);
142 std::shared_lock lock{_mtx};
143 return _pathToId.contains(canonical);
147 std::shared_lock lock{_mtx};
152 std::vector<std::pair<std::uint32_t, const std::string*>> entries;
153 entries.reserve(_idToPath.size());
154 for (
const auto& [
id, path] : _idToPath) entries.emplace_back(
id, &path);
155 std::ranges::sort(entries, {}, &std::pair<std::uint32_t, const std::string*>::first);
157 std::ofstream file{_source, std::ios::trunc};
161 for (
const auto& [
id, path] : entries) file <<
id <<
';' <<
toNativeRelative(*path) <<
'\n';
170 std::shared_lock lock{_mtx};
171 return _pathToId.size();
std::optional< std::string > fdidToPath(FileDataID fdid) const
Look up the canonical path of a FileDataID.
static Result< CsvListfile > load(const std::filesystem::path &csv, Options options={})
Load a listfile CSV; the file becomes the working database that registerPath appends to.
Result< void > save() const
Rewrite the whole working file canonically (ids ascending).
CsvListfileOptions Options
Result< FileDataID > registerPath(std::string_view path)
Allocate a fresh custom FileDataID for a new path, remember the mapping and append it to the working ...
CsvListfile & operator=(const CsvListfile &)=delete
std::optional< FileDataID > pathToFdid(std::string_view path) const
Look up the FileDataID of a path (any spelling; canonicalized here).
bool contains(std::string_view path) const
Whether a path is known.
Monotonic allocator for custom (non-Blizzard) FileDataIDs.
void noteExisting(FileDataID id)
Bump the allocator past an id seen in a loaded listfile, so re-opening a project never re-hands-out a...
The CSV-backed listfile provider: one working file ('fileDataId;filepath' per line) that is both read...
std::string normalizePath(std::string_view path)
Canonicalize a client-internal file path.
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
std::string toNativeRelative(std::string_view canonical)
Convert a canonical path to a forward-slash relative path for use on the native filesystem (project-d...
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
@ ListfileParseError
Malformed listfile CSV content.
@ InvalidPath
A path that cannot be normalized/used.
@ ListfileIoError
The listfile could not be read or written.
@ DuplicatePath
Registering a path that already has a FileDataID.
Client-internal path canonicalization.
std::uint32_t value
The raw numeric identifier.
FileDataID customFdidStart
First FileDataID handed to newly registered files; keep far above Blizzard's ~6-7M so future official...