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