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 "memory.hh"
19 #include "xrange.hh"
20 #include <cassert>
21 #include <cctype>
22 #include <stdexcept>
23 
24 using std::string;
25 using std::vector;
26 using std::unique_ptr;
27 
28 namespace openmsx {
29 
30 #ifndef _MSC_EXTENSIONS
31 // #ifdef required to avoid link error with vc++, see also
32 // http://www.codeguru.com/forum/showthread.php?t=430949
33 const unsigned DiskManipulator::MAX_PARTITIONS;
34 #endif
35 
37  Reactor& reactor_)
38  : Command(commandController_, "diskmanipulator")
39  , reactor(reactor_)
40 {
41 }
42 
44 {
45  assert(drives.empty()); // all DiskContainers must be unregistered
46 }
47 
48 string DiskManipulator::getMachinePrefix() const
49 {
50  string id = reactor.getMachineID();
51  return id.empty() ? id : id + "::";
52 }
53 
55  DiskContainer& drive, const std::string& prefix)
56 {
57  assert(findDriveSettings(drive) == end(drives));
58  DriveSettings driveSettings;
59  driveSettings.drive = &drive;
60  driveSettings.driveName = prefix + drive.getContainerName();
61  driveSettings.partition = 0;
62  for (unsigned i = 0; i <= MAX_PARTITIONS; ++i) {
63  driveSettings.workingDir[i] = '/';
64  }
65  drives.push_back(driveSettings);
66 }
67 
69 {
70  auto it = findDriveSettings(drive);
71  assert(it != end(drives));
72  move_pop_back(drives, it);
73 }
74 
75 DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
76  DiskContainer& drive)
77 {
78  return find_if(begin(drives), end(drives),
79  [&](DriveSettings& ds) { return ds.drive == &drive; });
80 }
81 
82 DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
83  string_ref driveName)
84 {
85  return find_if(begin(drives), end(drives),
86  [&](DriveSettings& ds) { return ds.driveName == driveName; });
87 }
88 
89 DiskManipulator::DriveSettings& DiskManipulator::getDriveSettings(
90  string_ref diskname)
91 {
92  // first split-off the end numbers (if present)
93  // these will be used as partition indication
94  auto pos1 = diskname.find("::");
95  auto tmp1 = (pos1 == string_ref::npos) ? diskname : diskname.substr(pos1);
96  auto pos2 = tmp1.find_first_of("0123456789");
97  auto pos1b = (pos1 == string_ref::npos) ? 0 : pos1;
98  auto tmp2 = diskname.substr(0, pos2 + pos1b);
99 
100  auto it = findDriveSettings(tmp2);
101  if (it == end(drives)) {
102  it = findDriveSettings(getMachinePrefix() + tmp2);
103  if (it == end(drives)) {
104  throw CommandException("Unknown drive: " + tmp2);
105  }
106  }
107 
108  auto* disk = it->drive->getSectorAccessibleDisk();
109  if (!disk) {
110  // not a SectorBasedDisk
111  throw CommandException("Unsupported disk type.");
112  }
113 
114  if (pos2 == string_ref::npos) {
115  // whole disk
116  it->partition = 0;
117  } else {
118  try {
119  unsigned partition = fast_stou(diskname.substr(pos2));
120  DiskImageUtils::checkFAT12Partition(*disk, partition);
121  it->partition = partition;
122  } catch (std::invalid_argument&) {
123  // parse error in fast_stou()
124  throw CommandException("Invalid partition name");
125  }
126  }
127  return *it;
128 }
129 
130 unique_ptr<DiskPartition> DiskManipulator::getPartition(
131  const DriveSettings& driveData)
132 {
133  auto* disk = driveData.drive->getSectorAccessibleDisk();
134  assert(disk);
135  return make_unique<DiskPartition>(*disk, driveData.partition);
136 }
137 
138 
139 void DiskManipulator::execute(array_ref<TclObject> tokens, TclObject& result)
140 {
141  if (tokens.size() == 1) {
142  throw CommandException("Missing argument");
143  }
144 
145  string_ref subcmd = tokens[1].getString();
146  if (((tokens.size() != 4) && (subcmd == "savedsk")) ||
147  ((tokens.size() != 4) && (subcmd == "mkdir")) ||
148  ((tokens.size() != 3) && (subcmd == "dir")) ||
149  ((tokens.size() < 3 || tokens.size() > 4) && (subcmd == "format")) ||
150  ((tokens.size() < 3 || tokens.size() > 4) && (subcmd == "chdir")) ||
151  ((tokens.size() < 4) && (subcmd == "export")) ||
152  ((tokens.size() < 4) && (subcmd == "import")) ||
153  ((tokens.size() < 4) && (subcmd == "create"))) {
154  throw CommandException("Incorrect number of parameters");
155  }
156 
157  if (subcmd == "export") {
158  string_ref directory = tokens[3].getString();
159  if (!FileOperations::isDirectory(directory)) {
160  throw CommandException(directory + " is not a directory");
161  }
162  auto& settings = getDriveSettings(tokens[2].getString());
163  array_ref<TclObject> lists(std::begin(tokens) + 4, std::end(tokens));
164  exprt(settings, directory, lists);
165 
166  } else if (subcmd == "import") {
167  auto& settings = getDriveSettings(tokens[2].getString());
168  array_ref<TclObject> lists(std::begin(tokens) + 3, std::end(tokens));
169  result.setString(import(settings, lists));
170 
171  } else if (subcmd == "savedsk") {
172  auto& settings = getDriveSettings(tokens[2].getString());
173  savedsk(settings, tokens[3].getString());
174 
175  } else if (subcmd == "chdir") {
176  auto& settings = getDriveSettings(tokens[2].getString());
177  if (tokens.size() == 3) {
178  result.setString("Current directory: " +
179  settings.workingDir[settings.partition]);
180  } else {
181  result.setString(chdir(settings, tokens[3].getString()));
182  }
183 
184  } else if (subcmd == "mkdir") {
185  auto& settings = getDriveSettings(tokens[2].getString());
186  mkdir(settings, tokens[3].getString());
187 
188  } else if (subcmd == "create") {
189  create(tokens);
190 
191  } else if (subcmd == "format") {
192  bool dos1 = false;
193  string_ref drive = tokens[2].getString();
194  if (tokens.size() == 4) {
195  if (drive == "-dos1") {
196  dos1 = true;
197  drive = tokens[3].getString();
198  } else if (tokens[3] == "-dos1") {
199  dos1 = true;
200  }
201  }
202  auto& settings = getDriveSettings(drive);
203  format(settings, dos1);
204 
205  } else if (subcmd == "dir") {
206  auto& settings = getDriveSettings(tokens[2].getString());
207  result.setString(dir(settings));
208 
209  } else {
210  throw CommandException("Unknown subcommand: " + subcmd);
211  }
212 }
213 
214 string DiskManipulator::help(const vector<string>& tokens) const
215 {
216  string helptext;
217  if (tokens.size() >= 2) {
218  if (tokens[1] == "import" ) {
219  helptext=
220  "diskmanipulator import <disk name> <host directory|host file>\n"
221  "Import all files and subdirs from the host OS as specified into the <disk name> in the\n"
222  "current MSX subdirectory as was specified with the last chdir command.\n";
223  } else if (tokens[1] == "export" ) {
224  helptext=
225  "diskmanipulator export <disk name> <host directory>\n"
226  "Extract all files and subdirs from the MSX subdirectory specified with the chdir command\n"
227  "from <disk name> to the host OS in <host directory>.\n";
228  } else if (tokens[1] == "savedsk") {
229  helptext=
230  "diskmanipulator savedsk <disk name> <dskfilename>\n"
231  "Save the complete drive content to <dskfilename>, it is not possible to save just one\n"
232  "partition. The main purpose of this command is to make it possible to save a 'ramdsk' into\n"
233  "a file and to take 'live backups' of dsk-files in use.\n";
234  } else if (tokens[1] == "chdir") {
235  helptext=
236  "diskmanipulator chdir <disk name> <MSX directory>\n"
237  "Change the working directory on <disk name>. This will be the directory were the 'import',\n"
238  "'export' and 'dir' commands will work on.\n"
239  "In case of a partitioned drive, each partition has its own working directory.\n";
240  } else if (tokens[1] == "mkdir") {
241  helptext=
242  "diskmanipulator mkdir <disk name> <MSX directory>\n"
243  "Create the specified directory on <disk name>. If needed, all missing parent directories\n"
244  "are created at the same time. Accepts both absolute and relative path names.\n";
245  } else if (tokens[1] == "create") {
246  helptext=
247  "diskmanipulator create <dskfilename> <size/option> [<size/option>...]\n"
248  "Create a formatted dsk file with the given size.\n"
249  "If multiple sizes are given, a partitioned disk image will be created with each partition\n"
250  "having the size as indicated. By default the sizes are expressed in kilobyte, add the\n"
251  "postfix M for megabyte.\n"
252  "When using the -dos1 option, the boot sector of the created image will be MSX-DOS1\n"
253  "compatible.\n";
254  } else if (tokens[1] == "format") {
255  helptext=
256  "diskmanipulator format <disk name>\n"
257  "formats the current (partition on) <disk name> with a regular FAT12 MSX filesystem with an\n"
258  "MSX-DOS2 boot sector, or, when the -dos1 option is specified, with an MSX-DOS1 boot sector.\n";
259  } else if (tokens[1] == "dir") {
260  helptext=
261  "diskmanipulator dir <disk name>\n"
262  "Shows the content of the current directory on <disk name>\n";
263  } else {
264  helptext = "Unknown diskmanipulator subcommand: " + tokens[1];
265  }
266  } else {
267  helptext=
268  "diskmanipulator create <fn> <sz> [<sz> ...] : create a formatted dsk file with name <fn>\n"
269  " having the given (partition) size(s)\n"
270  "diskmanipulator savedsk <disk name> <fn> : save <disk name> as dsk file named as <fn>\n"
271  "diskmanipulator format <disk name> : format (a partition) on <disk name>\n"
272  "diskmanipulator chdir <disk name> <MSX dir> : change directory on <disk name>\n"
273  "diskmanipulator mkdir <disk name> <MSX dir> : create directory on <disk name>\n"
274  "diskmanipulator dir <disk name> : long format file listing of current\n"
275  " directory on <disk name>\n"
276  "diskmanipulator import <disk> <dir/file> ... : import files and subdirs from <dir/file>\n"
277  "diskmanipulator export <disk> <host dir> : export all files on <disk> to <host dir>\n"
278  "For more info use 'help diskmanipulator <subcommand>'.\n";
279  }
280  return helptext;
281 }
282 
283 void DiskManipulator::tabCompletion(vector<string>& tokens) const
284 {
285  if (tokens.size() == 2) {
286  static const char* const cmds[] = {
287  "import", "export", "savedsk", "dir", "create",
288  "format", "chdir", "mkdir",
289  };
290  completeString(tokens, cmds);
291 
292  } else if ((tokens.size() == 3) && (tokens[1] == "create")) {
294 
295  } else if (tokens.size() == 3) {
296  vector<string> names;
297  if ((tokens[1] == "format") || (tokens[1] == "create")) {
298  names.emplace_back("-dos1");
299  }
300  for (auto& d : drives) {
301  const auto& name1 = d.driveName; // with prexix
302  const auto& name2 = d.drive->getContainerName(); // without prefix
303  names.insert(end(names), {name1, name2});
304  // if it has partitions then we also add the partition
305  // numbers to the autocompletion
306  if (auto* disk = d.drive->getSectorAccessibleDisk()) {
307  for (unsigned i = 1; i <= MAX_PARTITIONS; ++i) {
308  try {
310  names.insert(end(names), {
311  name1 + StringOp::toString(i),
312  name2 + StringOp::toString(i)});
313  } catch (MSXException&) {
314  // skip invalid partition
315  }
316  }
317  }
318  }
319  completeString(tokens, names);
320 
321  } else if (tokens.size() >= 4) {
322  if ((tokens[1] == "savedsk") ||
323  (tokens[1] == "import") ||
324  (tokens[1] == "export")) {
326  } else if (tokens[1] == "create") {
327  static const char* const cmds[] = {
328  "360", "720", "32M", "-dos1"
329  };
330  completeString(tokens, cmds);
331  } else if (tokens[1] == "format") {
332  static const char* const cmds[] = {
333  "-dos1"
334  };
335  completeString(tokens, cmds);
336  }
337  }
338 }
339 
340 void DiskManipulator::savedsk(const DriveSettings& driveData,
341  string_ref filename)
342 {
343  auto partition = getPartition(driveData);
344  SectorBuffer buf;
345  File file(filename, File::CREATE);
346  for (auto i : xrange(partition->getNbSectors())) {
347  partition->readSector(i, buf);
348  file.write(&buf, sizeof(buf));
349  }
350 }
351 
352 void DiskManipulator::create(array_ref<TclObject> tokens)
353 {
354  vector<unsigned> sizes;
355  unsigned totalSectors = 0;
356  bool dos1 = false;
357 
358  for (unsigned i = 3; i < tokens.size(); ++i) {
359  if (tokens[i] == "-dos1") {
360  dos1 = true;
361  continue;
362  }
363 
364  if (sizes.size() >= MAX_PARTITIONS) {
366  "Maximum number of partitions is " << MAX_PARTITIONS);
367  }
368  string tok = tokens[i].getString().str();
369  char* q;
370  int sectors = strtol(tok.c_str(), &q, 0);
371  int scale = 1024; // default is kilobytes
372  if (*q) {
373  if ((q == tok.c_str()) || *(q + 1)) {
374  throw CommandException("Invalid size: " + tok);
375  }
376  switch (tolower(*q)) {
377  case 'b':
378  scale = 1;
379  break;
380  case 'k':
381  scale = 1024;
382  break;
383  case 'm':
384  scale = 1024 * 1024;
385  break;
386  case 's':
388  break;
389  default:
390  throw CommandException(
391  string("Invalid postfix: ") + 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(
449  "Please select partition number.");
450  }
451 
452  auto result = 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_ref 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_ref 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(e.getMessage());
500  }
501 }
502 
503 string DiskManipulator::import(DriveSettings& driveData,
504  array_ref<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  messages += "Ignoring " + s + '\n';
527  }
528  } catch (MSXException& e) {
529  throw CommandException(e.getMessage());
530  }
531  }
532  }
533  return messages;
534 }
535 
536 void DiskManipulator::exprt(DriveSettings& driveData, string_ref dirname,
537  array_ref<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(e.getMessage());
552  }
553 }
554 
555 } // namespace openmsx
Contains the main loop of openMSX.
Definition: Reactor.hh:63
bool isRegularFile(const Stat &st)
string_ref::const_iterator end(const string_ref &x)
Definition: string_ref.hh:161
string toString(long long a)
Definition: StringOp.cc:150
This class implements a subset of the proposal for std::string_ref (proposed for the next c++ standar...
Definition: string_ref.hh:18
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:192
mat4 scale(const vec3 &xyz)
Definition: gl_transform.hh:19
bool getStat(string_ref filename_, Stat &st)
Call stat() and return the stat structure.
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:122
const std::string & getMessage() const
Definition: MSXException.hh:13
std::string getMachineID() const
Definition: Reactor.cc:341
virtual const std::string & getContainerName() const =0
const char * data() const
Definition: string_ref.hh:68
bool startsWith(string_ref total, string_ref part)
Definition: StringOp.cc:236
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
bool hasPartitionTable(SectorAccessibleDisk &disk)
Check whether the given disk is partitioned.
unsigned fast_stou(string_ref s)
Definition: string_ref.cc:145
DiskManipulator(CommandController &commandController, Reactor &reactor)
This class represents a filename.
Definition: Filename.hh:17
std::string str() const
Definition: string_ref.cc:12
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
static const size_type npos
Definition: string_ref.hh:26
string_ref substr(size_type pos, size_type n=npos) const
Definition: string_ref.cc:32
bool empty() const
Definition: array_ref.hh:62
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.
FileContext userFileContext(string_ref savePath)
Definition: FileContext.cc:161
size_type size() const
Definition: array_ref.hh:61
Interpreter & getInterpreter() const
Definition: Command.cc:41
bool isDirectory(const Stat &st)
void setString(string_ref value)
Definition: TclObject.cc:14
size_type size() const
Definition: string_ref.hh:55
void write(const void *buffer, size_t num)
Write to file.
Definition: File.cc:84
size_type find_first_of(string_ref s) const
Definition: string_ref.cc:87
void format(SectorAccessibleDisk &disk, bool dos1)
Format the given disk (= a single partition).
string_ref::const_iterator begin(const string_ref &x)
Definition: string_ref.hh:160
bool endsWith(string_ref total, string_ref part)
Definition: StringOp.cc:245
void unregisterDrive(DiskContainer &drive)
void checkFAT12Partition(SectorAccessibleDisk &disk, unsigned partition)
Like above, but also check whether partition is of type FAT12.
XRange< T > xrange(T e)
Definition: xrange.hh:98
size_type find(string_ref s) const
Definition: string_ref.cc:38
void truncate(size_t size)
Truncate file size.
Definition: File.cc:114