wowlib 0.0.0
Read & write World of Warcraft client files — a C++26 core
Loading...
Searching...
No Matches
csv_listfile.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <charconv>
5#include <format>
6#include <fstream>
7#include <mutex>
8#include <vector>
9
10#include <wowlib/core/path.hpp>
11
12namespace wowlib::fs {
13 namespace {
14 // Parses one "fileDataId;filepath" line; empty lines yield nullopt, malformed
15 // lines yield an error carrying the line number.
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>>{};
20
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));
25
26 std::uint32_t id = 0;
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)));
31
32 return std::optional{std::pair{id, normalizePath(line.substr(sep + 1))}};
33 }
34 }
35
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;
42 }
43
45 if (this != &other) {
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;
51 }
52 return *this;
53 }
54
55 Result<CsvListfile> CsvListfile::load(const std::filesystem::path& csv, Options options) {
56 std::ifstream file{csv};
57 if (!file)
58 return makeError(ErrorCode::ListfileIoError, std::format("cannot open listfile '{}'", csv.string()));
59
60 CsvListfile result;
61 result._source = csv;
62 result._allocator = detail::FdidAllocator{options.customFdidStart};
63
64 // The community listfile is ~2M entries and growing; reserving ahead avoids
65 // rehash storms.
66 result._pathToId.reserve(4'000'000);
67 result._idToPath.reserve(4'000'000);
68
69 std::string line;
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;
74
75 auto& [id, path] = **parsed;
76 result._idToPath.insert_or_assign(id, path);
77 result._pathToId.insert_or_assign(std::move(path), FileDataID{id});
78
79 // customizations from a previous session move the cursor past themselves
80 result._allocator.noteExisting(FileDataID{id});
81 }
82
83 if (file.bad())
84 return makeError(ErrorCode::ListfileIoError, std::format("I/O error reading listfile '{}'", csv.string()));
85
86 return result;
87 }
88
89 std::optional<FileDataID> CsvListfile::pathToFdid(std::string_view path) const {
90 const std::string canonical = normalizePath(path);
91 std::shared_lock lock{_mtx};
92 const auto it = _pathToId.find(canonical);
93 return it == _pathToId.end() ? std::nullopt : std::optional{it->second};
94 }
95
96 std::optional<std::string> CsvListfile::fdidToPath(FileDataID fdid) const {
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};
100 }
101
103 std::string canonical = normalizePath(path);
104 if (canonical.empty())
105 return makeError(ErrorCode::InvalidPath, "cannot register an empty path");
106
107 std::unique_lock lock{_mtx};
108
109 if (_pathToId.contains(canonical))
110 return makeError(ErrorCode::DuplicatePath, std::format("path '{}' already has a FileDataID", canonical));
111
112 auto id = _allocator.next();
113 if (!id) return std::unexpected(id.error());
114
115 // persist first: on failure the in-memory maps stay untouched (the allocated
116 // id is skipped — gaps in the custom range are harmless)
117 if (!_source.empty()) {
118 // a user-supplied CSV may lack a trailing newline; appending must not glue
119 // onto its last line
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) {
123 char last = '\n';
124 tail.seekg(-1, std::ios::end);
125 tail.get(last);
126 needsNewline = last != '\n';
127 }
128
129 std::ofstream file{_source, std::ios::app};
130 if (needsNewline) file << '\n';
131 if (!file || !(file << id->value << ';' << toNativeRelative(canonical) << '\n').flush())
132 return makeError(ErrorCode::ListfileIoError, std::format("cannot append to listfile '{}'", _source.string()));
133 }
134
135 _idToPath.insert_or_assign(id->value, canonical);
136 _pathToId.insert_or_assign(std::move(canonical), *id);
137 return *id;
138 }
139
140 bool CsvListfile::contains(std::string_view path) const {
141 const std::string canonical = normalizePath(path);
142 std::shared_lock lock{_mtx};
143 return _pathToId.contains(canonical);
144 }
145
147 std::shared_lock lock{_mtx};
148
149 if (_source.empty())
150 return makeError(ErrorCode::ListfileIoError, "this listfile is in-memory only; nothing to save to");
151
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);
156
157 std::ofstream file{_source, std::ios::trunc};
158 if (!file)
159 return makeError(ErrorCode::ListfileIoError, std::format("cannot write listfile '{}'", _source.string()));
160
161 for (const auto& [id, path] : entries) file << id << ';' << toNativeRelative(*path) << '\n';
162
163 if (!file.flush())
164 return makeError(ErrorCode::ListfileIoError, std::format("I/O error writing listfile '{}'", _source.string()));
165
166 return {};
167 }
168
169 std::size_t CsvListfile::size() const {
170 std::shared_lock lock{_mtx};
171 return _pathToId.size();
172 }
173}
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).
std::size_t size() const
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.
Definition path.cpp:4
std::expected< T, Error > Result
Every fallible wowlib operation returns Result<T>; bindings translate the error branch into a target-...
Definition error.hpp:100
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...
Definition path.cpp:23
std::unexpected< Error > makeError(ErrorCode code, std::string message, std::uint32_t nativeError=0)
Shorthand for constructing the error branch of a Result.
Definition error.hpp:107
@ ListfileParseError
Malformed listfile CSV content.
Definition error.hpp:25
@ InvalidPath
A path that cannot be normalized/used.
Definition error.hpp:29
@ ListfileIoError
The listfile could not be read or written.
Definition error.hpp:26
@ DuplicatePath
Registering a path that already has a FileDataID.
Definition error.hpp:28
Client-internal path canonicalization.
std::uint32_t value
The raw numeric identifier.
Definition file_key.hpp:21
FileDataID customFdidStart
First FileDataID handed to newly registered files; keep far above Blizzard's ~6-7M so future official...