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