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 
29 using std::string;
30 using std::string_view;
31 
32 namespace 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 
46 string 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 
73 DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
74  DiskContainer& drive)
75 {
76  return ranges::find(drives, &drive, &DriveSettings::drive);
77 }
78 
79 DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
80  string_view driveName)
81 {
82  return ranges::find(drives, driveName, &DriveSettings::driveName);
83 }
84 
85 DiskManipulator::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 
125 DiskPartition DiskManipulator::getPartition(
126  const DriveSettings& driveData)
127 {
128  auto* disk = driveData.drive->getSectorAccessibleDisk();
129  assert(disk);
130  return {*disk, driveData.partition};
131 }
132 
133 
134 void 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 
210 string 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 
279 void 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 
332 void 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 
344 void 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 
428 void DiskManipulator::format(DriveSettings& driveData, bool dos1)
429 {
430  auto partition = getPartition(driveData);
432  driveData.workingDir[driveData.partition] = '/';
433 }
434 
435 MSXtar 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 
455 string DiskManipulator::dir(DriveSettings& driveData)
456 {
457  auto partition = getPartition(driveData);
458  auto workhorse = getMSXtar(partition, driveData);
459  return workhorse.dir();
460 }
461 
462 string 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 
482 void 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 
493 string 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  }
510  if (FileOperations::isDirectory(st)) {
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 
526 void 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)