Repository navigation
Expand file tree
/
Copy pathfiles.zig
More file actions
200 lines (176 loc) · 9.75 KB
/
Copy pathfiles.zig
File metadata and controls
200 lines (176 loc) · 9.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! The game's file names, as it hands them to Windows: folders are split by `\` or `/`, and each
//! name matches in any case. `find`, `readFile` and `writeFile` treat them the same way on every
//! system, so a file a player drops into the game's folders is found however its name is spelled.
const std = @import("std");
const Allocator = std.mem.Allocator;
const Io = std.Io;
/// The longest path the game builds, Windows' `MAX_PATH`.
pub const max_path = 260;
/// The most OpenReliant reads of one of the game's files into memory, far past the largest.
pub const max_file_size = std.fmt.parseIntSizeSuffix("256MiB", 10) catch unreachable;
/// The characters that split the folders of a game path. Windows takes both.
pub const separators = "\\/";
/// The last name in the game path `path`: what follows its last `\` or `/`, as Windows splits it.
pub fn leaf(path: []const u8) []const u8 {
const start = if (std.mem.findLastAny(u8, path, separators)) |at| at + 1 else 0;
return path[start..];
}
/// The extension of `path`'s last name, from its last `.` and with the dot; empty when the name has
/// no dot, or only a leading one.
pub fn extension(path: []const u8) []const u8 {
const name = leaf(path);
const dot = std.mem.findScalarLast(u8, name, '.') orelse return name[name.len..];
return if (dot == 0) name[name.len..] else name[dot..];
}
/// `path`'s last name without its extension.
pub fn stem(path: []const u8) []const u8 {
const name = leaf(path);
return name[0 .. name.len - extension(name).len];
}
/// The file or folder `path` names under `dir`, spelled as it is on disk and with `/` between its
/// names, in `buffer`; null where none does. `.` names the folder itself, and `\` and `/` both
/// split the names.
pub fn find(io: Io, dir: Io.Dir, path: []const u8, buffer: *[max_path]u8) ?[]const u8 {
var found: usize = 0;
var names = std.mem.tokenizeAny(u8, path, separators);
while (names.next()) |name| {
if (std.mem.eql(u8, name, ".")) continue;
var folder = dir.openDir(io, if (found == 0) "." else buffer[0..found], .{ .iterate = true }) catch return null;
defer folder.close(io);
const spelled = entryNamed(io, folder, name) orelse return null;
const separator: usize = @intFromBool(found > 0);
if (found + separator + spelled.len > buffer.len) return null;
if (separator > 0) buffer[found] = '/';
@memcpy(buffer[found + separator ..][0..spelled.len], spelled);
found += separator + spelled.len;
}
return buffer[0..found];
}
/// The name of the entry of `folder` spelled `name` whatever its case, which lives as long as the
/// iteration's buffer: until `folder` is closed.
fn entryNamed(io: Io, folder: Io.Dir, name: []const u8) ?[]const u8 {
var entries = folder.iterate();
while (entries.next(io) catch return null) |entry| {
if (std.ascii.eqlIgnoreCase(entry.name, name)) return entry.name;
}
return null;
}
/// Whether `path` names a file or folder under `dir`, as the game asks with `_access`.
pub fn exists(io: Io, dir: Io.Dir, path: []const u8) bool {
var buffer: [max_path]u8 = undefined;
return find(io, dir, path, &buffer) != null;
}
/// The whole of the file `path` names under `dir`, made in `gpa`, found as `find` finds it; null
/// where there is none.
pub fn readFile(io: Io, gpa: Allocator, dir: Io.Dir, path: []const u8, limit: Io.Limit) Io.Dir.ReadFileAllocError!?[]u8 {
var buffer: [max_path]u8 = undefined;
const spelled = find(io, dir, path, &buffer) orelse return null;
return try dir.readFileAlloc(io, spelled, gpa, limit);
}
/// Reads the start of the game's file `path` under `dir`, found as `readFile` finds it, into
/// `buffer`: as much as fits, or the whole file where it is shorter. Null where there's no such
/// file.
pub fn readHead(io: Io, dir: Io.Dir, path: []const u8, buffer: []u8) Io.Dir.ReadFileError!?[]u8 {
var name_buffer: [max_path]u8 = undefined;
const spelled = find(io, dir, path, &name_buffer) orelse return null;
return try dir.readFile(io, spelled, buffer);
}
/// Writes `data` to the game's file `path` under `dir`, as Windows does: over the file `find` finds,
/// whatever the case of its name, or else as a new file named by `path`'s last name in the folder
/// `find` finds. error.FileNotFound when that folder isn't there. The file is written safely
/// (`writeAtomic`).
pub fn writeFile(io: Io, dir: Io.Dir, path: []const u8, data: []const u8) (WriteAtomicError || error{ FileNotFound, NameTooLong })!void {
var buffer: [max_path]u8 = undefined;
if (find(io, dir, path, &buffer)) |spelled| return writeAtomic(io, dir, spelled, data);
const name = leaf(path);
const folder = path[0 .. path.len - name.len];
const found = if (std.mem.trim(u8, folder, separators).len == 0) "" else find(io, dir, folder, &buffer) orelse return error.FileNotFound;
var joined: [max_path]u8 = undefined;
const sub_path = if (found.len == 0) name else std.mem.print(&joined, "{s}/{s}", .{ found, name }) catch return error.NameTooLong;
return writeAtomic(io, dir, sub_path, data);
}
/// The ways `writeAtomic` can fail.
pub const WriteAtomicError = Io.Dir.CreateFileAtomicError || Io.File.Writer.Error || Io.File.SyncError || Io.File.Atomic.ReplaceError;
/// Writes `data` to the file `sub_path` under `dir` without risking the file there: into a new
/// file in the same folder, flushed to the disk, which then takes the old file's place in one
/// step. A crash, a power cut or a full disk during the write leaves the old file as it was. A
/// write that fails deletes its new file; one cut short by a crash can leave it in the folder,
/// named with hex digits, where nothing reads it.
///
/// **Improvement:** the original writes its settings, the pilot's profile and the saved games
/// over the old files, so a write cut short leaves them broken.
pub fn writeAtomic(io: Io, dir: Io.Dir, sub_path: []const u8, data: []const u8) WriteAtomicError!void {
var file = try dir.createFileAtomic(io, sub_path, .{ .replace = true });
defer file.deinit(io);
try file.file.writeStreamingAll(io, data);
try file.file.sync(io);
try file.replace(io);
}
test leaf {
try std.testing.expectEqualStrings("trooper.fm8", leaf("pilots\\trooper.fm8"));
try std.testing.expectEqualStrings("trooper.fm8", leaf("pilots/trooper.fm8"));
try std.testing.expectEqualStrings("c.png", leaf("a\\b/c.png"));
try std.testing.expectEqualStrings("trooper", leaf("trooper"));
}
test stem {
try std.testing.expectEqualStrings("peel_shot", stem("art\\peel_shot.png"));
try std.testing.expectEqualStrings("keep", stem("keep."));
try std.testing.expectEqualStrings(".png", stem(".png"));
}
test extension {
// A dot in a folder's name is no extension, on any system.
try std.testing.expectEqualStrings("", extension("v1.0\\readme"));
try std.testing.expectEqualStrings(".FM8", extension("static.FM8"));
try std.testing.expectEqualStrings(".c", extension("a.b.c"));
}
test writeFile {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{ .iterate = true });
defer tmp.cleanup();
try tmp.dir.createDirPath(io, "Saves");
try tmp.dir.writeFile(io, .{ .sub_path = "Saves/OLD.SAV", .data = "old" });
try tmp.dir.writeFile(io, .{ .sub_path = "STARLANCER.INI", .data = "old" });
// A file found in any case is written over, so the folder keeps one of it.
try writeFile(io, tmp.dir, "saves\\old.sav", "new");
try writeFile(io, tmp.dir, "starlancer.ini", "new");
var buffer: [max_path]u8 = undefined;
try std.testing.expectEqualStrings("Saves/OLD.SAV", find(io, tmp.dir, "saves/old.sav", &buffer).?);
const saved = (try readFile(io, std.testing.allocator, tmp.dir, "saves\\old.sav", .unlimited)).?;
defer std.testing.allocator.free(saved);
try std.testing.expectEqualStrings("new", saved);
var count: usize = 0;
var top = try tmp.dir.openDir(io, ".", .{ .iterate = true });
defer top.close(io);
var entries = top.iterate();
while (try entries.next(io)) |entry| {
if (std.ascii.eqlIgnoreCase(entry.name, "starlancer.ini")) count += 1;
}
try std.testing.expectEqual(1, count);
// Nothing is left beside the files written, such as a new file that took no file's place.
var all: usize = 0;
entries = top.iterate();
while (try entries.next(io)) |_| all += 1;
try std.testing.expectEqual(2, all);
// A new file goes into the folder as it is spelled; a missing folder is an error.
try writeFile(io, tmp.dir, "saves\\new.sav", "made");
try std.testing.expectEqualStrings("Saves/new.sav", find(io, tmp.dir, "SAVES\\NEW.SAV", &buffer).?);
try std.testing.expectError(error.FileNotFound, writeFile(io, tmp.dir, "missing\\x", "x"));
}
test find {
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{ .iterate = true });
defer tmp.cleanup();
try tmp.dir.createDirPath(io, "Missions");
try tmp.dir.writeFile(io, .{ .sub_path = "Missions/Mission1.DTE", .data = "mission" });
// Found whatever the case, from the game's own spelling.
var buffer: [max_path]u8 = undefined;
try std.testing.expectEqualStrings("Missions/Mission1.DTE", find(io, tmp.dir, ".\\missions\\mission1.dte", &buffer).?);
try std.testing.expect(exists(io, tmp.dir, "missions/MISSION1.dte"));
try std.testing.expect(exists(io, tmp.dir, "missions"));
try std.testing.expect(!exists(io, tmp.dir, "missions\\mission2.dte"));
try std.testing.expect(!exists(io, tmp.dir, "other\\mission1.dte"));
const bytes = (try readFile(io, std.testing.allocator, tmp.dir, "MISSIONS\\mission1.dte", .unlimited)).?;
defer std.testing.allocator.free(bytes);
try std.testing.expectEqualStrings("mission", bytes);
try std.testing.expectEqual(null, try readFile(io, std.testing.allocator, tmp.dir, "missions\\mission2.dte", .unlimited));
}