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