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