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