yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
graphics_scratch_storage.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <iterator>
6
7#include "core/project.h"
9#include "util/macro.h"
10
12namespace {
13
14constexpr uint64_t kMaxBinContainerBytes = 64 * 1024 * 1024;
15
16absl::StatusOr<std::filesystem::path> ResolveBinPath(const std::string& path) {
17 if (path.empty() || path.find('\0') != std::string::npos)
18 return absl::InvalidArgumentError("Select a BIN path");
19 std::error_code ec;
20 const auto absolute = std::filesystem::absolute(path, ec);
21 if (ec)
22 return absl::InvalidArgumentError("Cannot resolve BIN path: " +
23 ec.message());
24 auto resolved = std::filesystem::weakly_canonical(absolute, ec);
25 if (ec)
26 return absl::InvalidArgumentError("Cannot resolve BIN path: " +
27 ec.message());
28 return resolved;
29}
30
31bool PathsAlias(const std::filesystem::path& a,
32 const std::filesystem::path& b) {
33 std::error_code ec;
34 if (std::filesystem::equivalent(a, b, ec) && !ec)
35 return true;
36 ec.clear();
37 const auto first = std::filesystem::weakly_canonical(a, ec);
38 if (ec)
39 return false;
40 const auto second = std::filesystem::weakly_canonical(b, ec);
41 return !ec && first == second;
42}
43
44absl::StatusOr<std::optional<std::string>> ReadBinContainer(
45 const std::filesystem::path& path) {
46 std::error_code ec;
47 const bool exists = std::filesystem::exists(path, ec);
48 if (ec)
49 return absl::PermissionDeniedError("Cannot inspect BIN destination: " +
50 ec.message());
51 if (!exists)
52 return std::optional<std::string>{};
53 if (!std::filesystem::is_regular_file(path, ec) || ec)
54 return absl::FailedPreconditionError(
55 "BIN destination is not a regular file");
56 std::ifstream input(path, std::ios::binary | std::ios::ate);
57 if (!input)
58 return absl::PermissionDeniedError("Cannot read BIN destination");
59 const auto end = input.tellg();
60 if (end < 0 || static_cast<uint64_t>(end) > kMaxBinContainerBytes)
61 return absl::ResourceExhaustedError(
62 "BIN container exceeds the 64 MiB export limit");
63 std::string bytes(static_cast<size_t>(end), '\0');
64 input.seekg(0);
65 if ((!bytes.empty() && !input.read(bytes.data(), bytes.size())) ||
66 input.peek() != std::char_traits<char>::eof())
67 return absl::DataLossError("BIN destination changed while being read");
68 return std::optional<std::string>(std::move(bytes));
69}
70
71absl::StatusOr<std::string> ReadLibrary(const std::string& path) {
72 std::error_code ec;
73 const auto size = std::filesystem::file_size(path, ec);
74 if (ec)
75 return absl::InternalError("Cannot inspect graphics scratch library: " +
76 ec.message());
77 if (size > 4 * 1024 * 1024)
78 return absl::ResourceExhaustedError(
79 "Graphics scratch library exceeds 4 MiB");
80 std::ifstream file(path, std::ios::binary);
81 if (!file)
82 return absl::InternalError("Cannot read graphics scratch library");
83 std::string bytes((std::istreambuf_iterator<char>(file)), {});
84 if (file.bad())
85 return absl::DataLossError(
86 "Cannot finish reading graphics scratch library");
87 return bytes;
88}
89
90} // namespace
91
92absl::StatusOr<LibrarySnapshot> LoadLibrary(const std::string& path) {
93 std::error_code ec;
94 const bool exists = std::filesystem::exists(path, ec);
95 if (ec)
96 return absl::InternalError("Cannot inspect graphics scratch path: " +
97 ec.message());
98 LibrarySnapshot result;
99 if (exists) {
100 ASSIGN_OR_RETURN(auto bytes, ReadLibrary(path));
102 result.bytes = std::move(bytes);
103 }
104 return result;
105}
106
107absl::StatusOr<std::string> SaveLibrary(
108 const std::string& path, const GraphicsScratchLibrary& library,
109 const std::optional<std::string>& expected_bytes) {
110#ifndef __EMSCRIPTEN__
112 {std::filesystem::path(path)}));
113#endif
114 std::error_code ec;
115 const bool exists = std::filesystem::exists(path, ec);
116 if (ec || exists != expected_bytes.has_value())
117 return absl::FailedPreconditionError(
118 "Graphics scratch library changed outside this editor; export pending "
119 "assets before reloading");
120 if (exists) {
121 ASSIGN_OR_RETURN(const auto bytes, ReadLibrary(path));
122 if (bytes != *expected_bytes)
123 return absl::FailedPreconditionError(
124 "Graphics scratch library changed outside this editor; export "
125 "pending assets before reloading");
126 }
127 const auto bytes = library.Serialize();
128#ifndef __EMSCRIPTEN__
130#else
131 return absl::UnimplementedError(
132 "Scratch library persistence requires the desktop editor");
133#endif
134 return bytes;
135}
136
137absl::StatusOr<GraphicsScratchAsset> ImportBin(const std::string& path, int bpp,
138 size_t offset,
139 const std::string& name) {
140 if (bpp < 2 || bpp > 4)
141 return absl::InvalidArgumentError("Select 2, 3 or 4 bpp");
142 std::ifstream input(path, std::ios::binary | std::ios::ate);
143 if (!input)
144 return absl::NotFoundError("Cannot open BIN file");
145 const auto size = input.tellg();
146 const size_t count = 64 * 8 * bpp;
147 if (size < 0 || offset > static_cast<uint64_t>(size) ||
148 count > static_cast<uint64_t>(size) - offset)
149 return absl::OutOfRangeError(
150 "BIN range does not contain 64 complete tiles");
151 input.seekg(offset);
152 std::vector<uint8_t> bytes(count);
153 if (!input.read(reinterpret_cast<char*>(bytes.data()), count))
154 return absl::DataLossError("Cannot finish reading BIN tiles");
155 auto asset = DecodeScratchBin(bytes, bpp, name);
156 if (!asset.ok())
157 return asset.status();
158 asset->source = path + " @ " + std::to_string(offset);
159 ASSIGN_OR_RETURN(const auto resolved, ResolveBinPath(path));
160 asset->source_path = resolved.string();
161 asset->source_offset = offset;
162 asset->original_chunk = bytes;
163 return std::move(*asset);
164}
165
166absl::StatusOr<BinExportResult> ExportBin(
167 const GraphicsScratchAsset& asset, const std::string& path,
168 const std::string& library_path,
169 const std::function<absl::Status(const std::string&)>& validator) {
170 ASSIGN_OR_RETURN(const auto encoded, EncodeScratchBin(asset));
171 ASSIGN_OR_RETURN(const auto target, ResolveBinPath(path));
172 if (PathsAlias(target, library_path))
173 return absl::FailedPreconditionError(
174 "BIN export cannot replace the scratch library");
175 if (validator)
176 RETURN_IF_ERROR(validator(target.string()));
177#ifndef __EMSCRIPTEN__
179 {target}, {"graphics BIN", "exported BIN"}));
180 ASSIGN_OR_RETURN(const auto before, ReadBinContainer(target));
181 const bool same_source =
182 !asset.source_path.empty() && PathsAlias(target, asset.source_path);
183 if (same_source) {
184 ASSIGN_OR_RETURN(const auto original_path,
185 ResolveBinPath(asset.source_path));
186 if (target != original_path)
187 return absl::FailedPreconditionError(
188 "BIN destination is a hard-link alias of the source; select the "
189 "original BIN path");
190 }
191 std::string after(reinterpret_cast<const char*>(encoded.data()),
192 encoded.size());
193 if (same_source) {
194 if (!before || asset.source_offset > before->size() ||
195 encoded.size() > before->size() - asset.source_offset)
196 return absl::FailedPreconditionError(
197 "Original BIN source range is missing or truncated; asset retained");
198 const std::string original(
199 reinterpret_cast<const char*>(asset.original_chunk.data()),
200 asset.original_chunk.size());
201 if (before->compare(asset.source_offset, original.size(), original) != 0)
202 return absl::AbortedError(
203 "BIN source chunk changed since import/export; reimport to review "
204 "it");
205 after = *before;
206 after.replace(asset.source_offset, encoded.size(),
207 reinterpret_cast<const char*>(encoded.data()),
208 encoded.size());
209 } else if (before && before->size() != encoded.size()) {
210 return absl::FailedPreconditionError(
211 "Standalone BIN export cannot replace a differently sized file; choose "
212 "a new BIN path");
213 }
214 if (before) {
216 *lock, {{target, before, after}},
218 } else {
220 project::WriteProjectFileAtomically(target.string(), after, false));
221 }
222 return BinExportResult{same_source, encoded};
223#else
224 return absl::UnimplementedError("BIN export requires the desktop editor");
225#endif
226}
227
228} // namespace yaze::editor::graphics_scratch_storage
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
absl::StatusOr< std::filesystem::path > ResolveBinPath(const std::string &path)
absl::StatusOr< std::optional< std::string > > ReadBinContainer(const std::filesystem::path &path)
absl::StatusOr< LibrarySnapshot > LoadLibrary(const std::string &path)
absl::StatusOr< BinExportResult > ExportBin(const GraphicsScratchAsset &asset, const std::string &path, const std::string &library_path, const std::function< absl::Status(const std::string &)> &validator)
absl::StatusOr< std::string > SaveLibrary(const std::string &path, const GraphicsScratchLibrary &library, const std::optional< std::string > &expected_bytes)
absl::StatusOr< GraphicsScratchAsset > ImportBin(const std::string &path, int bpp, size_t offset, const std::string &name)
absl::StatusOr< GraphicsScratchAsset > DecodeScratchBin(const std::vector< uint8_t > &bytes, int bpp, std::string name)
absl::StatusOr< std::vector< uint8_t > > EncodeScratchBin(const GraphicsScratchAsset &asset)
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:453
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
static absl::StatusOr< GraphicsScratchLibrary > Parse(const std::string &text)