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