openMSX
DiskManipulator.cc
Go to the documentation of this file.
1#include "DiskManipulator.hh"
2#include "DiskContainer.hh"
3#include "MSXtar.hh"
4#include "DiskImageUtils.hh"
5#include "DSKDiskImage.hh"
6#include "DiskPartition.hh"
7#include "CommandException.hh"
8#include "Reactor.hh"
9#include "File.hh"
10#include "Filename.hh"
11#include "FileContext.hh"
12#include "FileException.hh"
13#include "FileOperations.hh"
14#include "SectorBasedDisk.hh"
15#include "StringOp.hh"
16#include "TclObject.hh"
17#include "one_of.hh"
18#include "ranges.hh"
19#include "static_vector.hh"
20#include "stl.hh"
21#include "strCat.hh"
22#include "view.hh"
23#include "xrange.hh"
24#include <array>
25#include <cassert>
26#include <cctype>
27#include <memory>
28
29using std::string;
30using std::string_view;
31
32namespace openmsx {
33
35 Reactor& reactor_)
36 : Command(commandController_, "diskmanipulator")
37 , reactor(reactor_)
38{
39}
40
42{
43 assert(drives.empty()); // all DiskContainers must be unregistered
44}
45
46string DiskManipulator::getMachinePrefix() const
47{
48 string_view id = reactor.getMachineID();
49 return id.empty() ? string{} : strCat(id, "::");
50}
51
53 DiskContainer& drive, std::string_view prefix)
54{
55 assert(findDriveSettings(drive) == end(drives));
56 DriveSettings driveSettings;
57 driveSettings.drive = &drive;
58 driveSettings.driveName = strCat(prefix, drive.getContainerName());
59 driveSettings.partition = 0;
60 for (unsigned i = 0; i <= MAX_PARTITIONS; ++i) {
61 driveSettings.workingDir[i] = '/';
62 }
63 drives.push_back(driveSettings);
64}
65
67{
68 auto it = findDriveSettings(drive);
69 assert(it != end(drives));
70 move_pop_back(drives, it);
71}
72
73DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
74 DiskContainer& drive)
75{
76 return ranges::find(drives, &drive, &DriveSettings::drive);
77}
78
79DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
80 string_view driveName)
81{
82 return ranges::find(drives, driveName, &DriveSettings::driveName);
83}
84
85DiskManipulator::DriveSettings& DiskManipulator::getDriveSettings(
86 string_view diskname)
87{
88 // first split-off the end numbers (if present)
89 // these will be used as partition indication
90 auto pos1 = diskname.find("::");
91 auto tmp1 = (pos1 == string_view::npos) ? diskname : diskname.substr(pos1);
92 auto pos2 = tmp1.find_first_of("0123456789");
93 auto pos1b = (pos1 == string_view::npos) ? 0 : pos1;
94 auto tmp2 = diskname.substr(0, pos2 + pos1b);
95
96 auto it = findDriveSettings(tmp2);
97 if (it == end(drives)) {
98 it = findDriveSettings(tmpStrCat(getMachinePrefix(), tmp2));
99 if (it == end(drives)) {
100 throw CommandException("Unknown drive: ", tmp2);
101 }
102 }
103
104 auto* disk = it->drive->getSectorAccessibleDisk();
105 if (!disk) {
106 // not a SectorBasedDisk
107 throw CommandException("Unsupported disk type.");
108 }
109
110 if (pos2 == string_view::npos) {
111 // whole disk
112 it->partition = 0;
113 } else {
114 auto partitionName = diskname.substr(pos2);
115 auto partition = StringOp::stringToBase<10, unsigned>(partitionName);
116 if (!partition) {
117 throw CommandException("Invalid partition name: ", partitionName);
118 }
120 it->partition = *partition;
121 }
122 return *it;
123}
124
125DiskPartition DiskManipulator::getPartition(
126 const DriveSettings& driveData)
127{
128 auto* disk = driveData.drive->getSectorAccessibleDisk();
129 assert(disk);
130 return {*disk, driveData.partition};
131}
132
133
134void DiskManipulator::execute(std::span<const TclObject> tokens, TclObject& result)
135{
136 if (tokens.size() == 1) {
137 throw CommandException("Missing argument");
138 }
139
140 string_view subcmd = tokens[1].getString();
141 if (((tokens.size() != 4) && (subcmd == one_of("savedsk", "mkdir"))) ||
142 ((tokens.size() != 3) && (subcmd == "dir" )) ||
143 ((tokens.size() < 3 || tokens.size() > 4) && (subcmd == one_of("format", "chdir"))) ||
144 ((tokens.size() < 4) && (subcmd == one_of("export", "import", "create")))) {
145 throw CommandException("Incorrect number of parameters");
146 }
147
148 if (subcmd == "export") {
149 string_view dir = tokens[3].getString();
150 auto directory = FileOperations::expandTilde(string(dir));
151 if (!FileOperations::isDirectory(directory)) {
152 throw CommandException(dir, " is not a directory");
153 }
154 auto& settings = getDriveSettings(tokens[2].getString());
155 // Workaround clang-13/libc++ bug
156 //std::span<const TclObject> lists(std::begin(tokens) + 4, std::end(tokens));
157 std::span<const TclObject> lists(&*(std::begin(tokens) + 4), std::end(tokens) - std::begin(tokens) + 4);
158 exprt(settings, directory, lists);
159
160 } else if (subcmd == "import") {
161 auto& settings = getDriveSettings(tokens[2].getString());
162 // Workaround clang-13/libc++ bug
163 //std::span<const TclObject> lists(std::begin(tokens) + 3, std::end(tokens));
164 std::span<const TclObject> lists(&*(std::begin(tokens) + 3), std::end(tokens) - std::begin(tokens) + 3);
165 result = import(settings, lists);
166
167 } else if (subcmd == "savedsk") {
168 auto& settings = getDriveSettings(tokens[2].getString());
169 savedsk(settings, FileOperations::expandTilde(string(tokens[3].getString())));
170
171 } else if (subcmd == "chdir") {
172 auto& settings = getDriveSettings(tokens[2].getString());
173 if (tokens.size() == 3) {
174 result = tmpStrCat("Current directory: ",
175 settings.workingDir[settings.partition]);
176 } else {
177 result = chdir(settings, tokens[3].getString());
178 }
179
180 } else if (subcmd == "mkdir") {
181 auto& settings = getDriveSettings(tokens[2].getString());
182 mkdir(settings, tokens[3].getString());
183
184 } else if (subcmd == "create") {
185 create(tokens);
186
187 } else if (subcmd == "format") {
188 bool dos1 = false;
189 string_view drive = tokens[2].getString();
190 if (tokens.size() == 4) {
191 if (drive == "-dos1") {
192 dos1 = true;
193 drive = tokens[3].getString();
194 } else if (tokens[3] == "-dos1") {
195 dos1 = true;
196 }
197 }
198 auto& settings = getDriveSettings(drive);
199 format(settings, dos1);
200
201 } else if (subcmd == "dir") {
202 auto& settings = getDriveSettings(tokens[2].getString());
203 result = dir(settings);
204
205 } else {
206 throw CommandException("Unknown subcommand: ", subcmd);
207 }
208}
209
210string DiskManipulator::help(std::span<const TclObject> tokens) const
211{
212 string helptext;
213 if (tokens.size() >= 2) {
214 if (tokens[1] == "import") {
215 helptext =
216 "diskmanipulator import <disk name> <host directory|host file>\n"
217 "Import all files and subdirs from the host OS as specified into the <disk name> in the\n"
218 "current MSX subdirectory as was specified with the last chdir command.\n";
219 } else if (tokens[1] == "export") {
220 helptext =
221 "diskmanipulator export <disk name> <host directory>\n"
222 "Extract all files and subdirs from the MSX subdirectory specified with the chdir command\n"
223 "from <disk name> to the host OS in <host directory>.\n";
224 } else if (tokens[1] == "savedsk") {
225 helptext =
226 "diskmanipulator savedsk <disk name> <dskfilename>\n"
227 "Save the complete drive content to <dskfilename>, it is not possible to save just one\n"
228 "partition. The main purpose of this command is to make it possible to save a 'ramdsk' into\n"
229 "a file and to take 'live backups' of dsk-files in use.\n";
230 } else if (tokens[1] == "chdir") {
231 helptext =
232 "diskmanipulator chdir <disk name> <MSX directory>\n"
233 "Change the working directory on <disk name>. This will be the directory were the 'import',\n"
234 "'export' and 'dir' commands will work on.\n"
235 "In case of a partitioned drive, each partition has its own working directory.\n";
236 } else if (tokens[1] == "mkdir") {
237 helptext =
238 "diskmanipulator mkdir <disk name> <MSX directory>\n"
239 "Create the specified directory on <disk name>. If needed, all missing parent directories\n"
240 "are created at the same time. Accepts both absolute and relative path names.\n";
241 } else if (tokens[1] == "create") {
242 helptext =
243 "diskmanipulator create <dskfilename> <size/option> [<size/option>...]\n"
244 "Create a formatted dsk file with the given size.\n"
245 "If multiple sizes are given, a partitioned disk image will be created with each partition\n"
246 "having the size as indicated. By default the sizes are expressed in kilobyte, add the\n"
247 "postfix M for megabyte.\n"
248 "When using the -dos1 option, the boot sector of the created image will be MSX-DOS1\n"
249 "compatible.\n";
250 } else if (tokens[1] == "format") {
251 helptext =
252 "diskmanipulator format <disk name>\n"
253 "formats the current (partition on) <disk name> with a regular FAT12 MSX filesystem with an\n"
254 "MSX-DOS2 boot sector, or, when the -dos1 option is specified, with an MSX-DOS1 boot sector.\n";
255 } else if (tokens[1] == "dir") {
256 helptext =
257 "diskmanipulator dir <disk name>\n"
258 "Shows the content of the current directory on <disk name>\n";
259 } else {
260 helptext = strCat("Unknown diskmanipulator subcommand: ", tokens[1].getString());
261 }
262 } else {
263 helptext =
264 "diskmanipulator create <fn> <sz> [<sz> ...] : create a formatted dsk file with name <fn>\n"
265 " having the given (partition) size(s)\n"
266 "diskmanipulator savedsk <disk name> <fn> : save <disk name> as dsk file named as <fn>\n"
267 "diskmanipulator format <disk name> : format (a partition) on <disk name>\n"
268 "diskmanipulator chdir <disk name> <MSX dir> : change directory on <disk name>\n"
269 "diskmanipulator mkdir <disk name> <MSX dir> : create directory on <disk name>\n"
270 "diskmanipulator dir <disk name> : long format file listing of current\n"
271 " directory on <disk name>\n"
272 "diskmanipulator import <disk> <dir/file> ... : import files and subdirs from <dir/file>\n"
273 "diskmanipulator export <disk> <host dir> : export all files on <disk> to <host dir>\n"
274 "For more info use 'help diskmanipulator <subcommand>'.\n";
275 }
276 return helptext;
277}
278
279void DiskManipulator::tabCompletion(std::vector<string>& tokens) const
280{
281 using namespace std::literals;
282 if (tokens.size() == 2) {
283 static constexpr std::array cmds = {
284 "import"sv, "export"sv, "savedsk"sv, "dir"sv, "create"sv,
285 "format"sv, "chdir"sv, "mkdir"sv,
286 };
287 completeString(tokens, cmds);
288
289 } else if ((tokens.size() == 3) && (tokens[1] == "create")) {
291
292 } else if (tokens.size() == 3) {
293 std::vector<string> names;
294 if (tokens[1] == one_of("format", "create")) {
295 names.emplace_back("-dos1");
296 }
297 for (const auto& d : drives) {
298 const auto& name1 = d.driveName; // with prexix
299 const auto& name2 = d.drive->getContainerName(); // without prefix
300 append(names, {name1, std::string(name2)});
301 // if it has partitions then we also add the partition
302 // numbers to the autocompletion
303 if (auto* disk = d.drive->getSectorAccessibleDisk()) {
304 for (unsigned i = 1; i <= MAX_PARTITIONS; ++i) {
305 try {
307 append(names,
308 {strCat(name1, i), strCat(name2, i)});
309 } catch (MSXException&) {
310 // skip invalid partition
311 }
312 }
313 }
314 }
315 completeString(tokens, names);
316
317 } else if (tokens.size() >= 4) {
318 if (tokens[1] == one_of("savedsk", "import", "export")) {
320 } else if (tokens[1] == "create") {
321 static constexpr std::array cmds = {
322 "360"sv, "720"sv, "32M"sv, "-dos1"sv,
323 };
324 completeString(tokens, cmds);
325 } else if (tokens[1] == "format") {
326 static constexpr std::array cmds = {"-dos1"sv};
327 completeString(tokens, cmds);
328 }
329 }
330}
331
332void DiskManipulator::savedsk(const DriveSettings& driveData,
333 string filename)
334{
335 auto partition = getPartition(driveData);
336 SectorBuffer buf;
337 File file(std::move(filename), File::CREATE);
338 for (auto i : xrange(partition.getNbSectors())) {
339 partition.readSector(i, buf);
340 file.write(&buf, sizeof(buf));
341 }
342}
343
344void DiskManipulator::create(std::span<const TclObject> tokens)
345{
347 unsigned totalSectors = 0;
348 bool dos1 = false;
349
350 for (const auto& token : view::drop(tokens, 3)) {
351 if (token == "-dos1") {
352 dos1 = true;
353 continue;
354 }
355
356 if (sizes.size() >= MAX_PARTITIONS) {
357 throw CommandException(
358 "Maximum number of partitions is ", MAX_PARTITIONS);
359 }
360 auto tok = token.getString();
361 char* q;
362 int sectors = strtol(tok.c_str(), &q, 0);
363 int scale = 1024; // default is kilobytes
364 if (*q) {
365 if ((q == tok.c_str()) || *(q + 1)) {
366 throw CommandException("Invalid size: ", tok);
367 }
368 switch (tolower(*q)) {
369 case 'b':
370 scale = 1;
371 break;
372 case 'k':
373 scale = 1024;
374 break;
375 case 'm':
376 scale = 1024 * 1024;
377 break;
378 case 's':
380 break;
381 default:
382 throw CommandException("Invalid suffix: ", q);
383 }
384 }
385 sectors = (sectors * scale) / SectorBasedDisk::SECTOR_SIZE;
386 // for a 32MB disk or greater the sectors would be >= 65536
387 // since MSX use 16 bits for this, in case of sectors = 65536
388 // the truncated word will be 0 -> formatted as 320 Kb disk!
389 if (sectors > 65535) sectors = 65535; // this is the max size for fat12 :-)
390
391 // TEMP FIX: the smallest bootsector we create in MSXtar is for
392 // a normal single sided disk.
393 // TODO: MSXtar must be altered and this temp fix must be set to
394 // the real smallest dsk possible (= bootsector + minimal fat +
395 // minimal dir + minimal data clusters)
396 if (sectors < 720) sectors = 720;
397
398 sizes.push_back(sectors);
399 totalSectors += sectors;
400 }
401 if (sizes.empty()) {
402 throw CommandException("No size(s) given.");
403 }
404 if (sizes.size() > 1) {
405 // extra sector for partition table
406 ++totalSectors;
407 }
408
409 // create file with correct size
410 Filename filename(FileOperations::expandTilde(string(tokens[2].getString())));
411 try {
412 File file(filename, File::CREATE);
413 file.truncate(totalSectors * SectorBasedDisk::SECTOR_SIZE);
414 } catch (FileException& e) {
415 throw CommandException("Couldn't create image: ", e.getMessage());
416 }
417
418 // initialize (create partition tables and format partitions)
419 DSKDiskImage image(filename);
420 if (sizes.size() > 1) {
422 } else {
423 // only one partition specified, don't create partition table
425 }
426}
427
428void DiskManipulator::format(DriveSettings& driveData, bool dos1)
429{
430 auto partition = getPartition(driveData);
432 driveData.workingDir[driveData.partition] = '/';
433}
434
435MSXtar DiskManipulator::getMSXtar(
436 SectorAccessibleDisk& disk, DriveSettings& driveData)
437{
439 throw CommandException("Please select partition number.");
440 }
441
442 MSXtar result(disk);
443 try {
444 result.chdir(driveData.workingDir[driveData.partition]);
445 } catch (MSXException&) {
446 driveData.workingDir[driveData.partition] = '/';
447 throw CommandException(
448 "Directory ", driveData.workingDir[driveData.partition],
449 " doesn't exist anymore. Went back to root "
450 "directory. Command aborted, please retry.");
451 }
452 return result;
453}
454
455string DiskManipulator::dir(DriveSettings& driveData)
456{
457 auto partition = getPartition(driveData);
458 auto workhorse = getMSXtar(partition, driveData);
459 return workhorse.dir();
460}
461
462string DiskManipulator::chdir(DriveSettings& driveData, string_view filename)
463{
464 auto partition = getPartition(driveData);
465 auto workhorse = getMSXtar(partition, driveData);
466 try {
467 workhorse.chdir(filename);
468 } catch (MSXException& e) {
469 throw CommandException("chdir failed: ", e.getMessage());
470 }
471 // TODO clean-up this temp hack, used to enable relative paths
472 string& cwd = driveData.workingDir[driveData.partition];
473 if (filename.starts_with('/')) {
474 cwd = filename;
475 } else {
476 if (!cwd.ends_with('/')) cwd += '/';
477 cwd.append(filename.data(), filename.size());
478 }
479 return "New working directory: " + cwd;
480}
481
482void DiskManipulator::mkdir(DriveSettings& driveData, string_view filename)
483{
484 auto partition = getPartition(driveData);
485 auto workhorse = getMSXtar(partition, driveData);
486 try {
487 workhorse.mkdir(filename);
488 } catch (MSXException& e) {
489 throw CommandException(std::move(e).getMessage());
490 }
491}
492
493string DiskManipulator::import(DriveSettings& driveData,
494 std::span<const TclObject> lists)
495{
496 auto partition = getPartition(driveData);
497 auto workhorse = getMSXtar(partition, driveData);
498
499 string messages;
500 auto& interp = getInterpreter();
501 for (const auto& l : lists) {
502 for (auto i : xrange(l.getListLength(interp))) {
503 auto s = FileOperations::expandTilde(string(l.getListIndex(interp, i).getString()));
504 try {
506 if (!FileOperations::getStat(s, st)) {
507 throw CommandException(
508 "Non-existing file ", s);
509 }
511 messages += workhorse.addDir(s);
512 } else if (FileOperations::isRegularFile(st)) {
513 messages += workhorse.addFile(s);
514 } else {
515 // ignore other stuff (sockets, device nodes, ..)
516 strAppend(messages, "Ignoring ", s, '\n');
517 }
518 } catch (MSXException& e) {
519 throw CommandException(std::move(e).getMessage());
520 }
521 }
522 }
523 return messages;
524}
525
526void DiskManipulator::exprt(DriveSettings& driveData, string_view dirname,
527 std::span<const TclObject> lists)
528{
529 auto partition = getPartition(driveData);
530 auto workhorse = getMSXtar(partition, driveData);
531 try {
532 if (lists.empty()) {
533 // export all
534 workhorse.getDir(dirname);
535 } else {
536 for (const auto& l : lists) {
537 workhorse.getItemFromDir(dirname, l.getString());
538 }
539 }
540 } catch (MSXException& e) {
541 throw CommandException(std::move(e).getMessage());
542 }
543}
544
545} // namespace openmsx
std::string image
Definition: HDImageCLI.cc:13
Definition: one_of.hh:7
Interpreter & getInterpreter() const final
Definition: Command.cc:38
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:147
static void completeString(std::vector< std::string > &tokens, ITER begin, ITER end, bool caseSensitive=true)
Definition: Completer.hh:133
virtual std::string_view getContainerName() const =0
void unregisterDrive(DiskContainer &drive)
DiskManipulator(CommandController &commandController, Reactor &reactor)
void registerDrive(DiskContainer &drive, std::string_view prefix)
Contains the main loop of openMSX.
Definition: Reactor.hh:68
std::string_view getMachineID() const
Definition: Reactor.cc:380
static constexpr size_t SECTOR_SIZE
constexpr size_t size() const
constexpr bool empty() const
constexpr void push_back(const T &a)
constexpr double e
Definition: Math.hh:18
void append(Result &)
Definition: stl.hh:290
constexpr mat4 scale(const vec3 &xyz)
Definition: gl_transform.hh:19
bool hasPartitionTable(SectorAccessibleDisk &disk)
Check whether the given disk is partitioned.
void partition(SectorAccessibleDisk &disk, std::span< const unsigned > sizes)
Write a partition table to the given disk and format each partition.
void format(SectorAccessibleDisk &disk, bool dos1)
Format the given disk (= a single partition).
void checkFAT12Partition(SectorAccessibleDisk &disk, unsigned partition)
Like above, but also check whether partition is of type FAT12.
string expandTilde(string path)
Expand the '~' character to the users home directory.
bool getStat(zstring_view filename, Stat &st)
Call stat() and return the stat structure.
bool isRegularFile(const Stat &st)
bool isDirectory(const Stat &st)
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr const char *const filename
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:171
auto find(InputRange &&range, const T &value)
Definition: ranges.hh:144
constexpr auto drop(Range &&range, size_t n)
Definition: view.hh:360
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:125
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:617
std::string strCat(Ts &&...ts)
Definition: strCat.hh:549
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:627
constexpr auto xrange(T e)
Definition: xrange.hh:133
constexpr auto begin(const zstring_view &x)
constexpr auto end(const zstring_view &x)