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 "xrange.hh"
22 #include <cassert>
23 #include <cctype>
24 #include <memory>
25 #include <stdexcept>
26 
27 using std::string;
28 using std::string_view;
29 using std::unique_ptr;
30 using std::vector;
31 
32 namespace openmsx {
33 
35  Reactor& reactor_)
36  : Command(commandController_, "diskmanipulator")
37  , reactor(reactor_)
38 {
39 }
40 
42 {
43  assert(drives.empty()); // all DiskContainers must be unregistered
44 }
45 
46 string DiskManipulator::getMachinePrefix() const
47 {
48  string_view id = reactor.getMachineID();
49  return id.empty() ? string{} : strCat(id, "::");
50 }
51 
53  DiskContainer& drive, const std::string& prefix)
54 {
55  assert(findDriveSettings(drive) == end(drives));
56  DriveSettings driveSettings;
57  driveSettings.drive = &drive;
58  driveSettings.driveName = prefix + drive.getContainerName();
59  driveSettings.partition = 0;
60  for (unsigned i = 0; i <= MAX_PARTITIONS; ++i) {
61  driveSettings.workingDir[i] = '/';
62  }
63  drives.push_back(driveSettings);
64 }
65 
67 {
68  auto it = findDriveSettings(drive);
69  assert(it != end(drives));
70  move_pop_back(drives, it);
71 }
72 
73 DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
74  DiskContainer& drive)
75 {
76  return ranges::find_if(drives,
77  [&](auto& ds) { return ds.drive == &drive; });
78 }
79 
80 DiskManipulator::Drives::iterator DiskManipulator::findDriveSettings(
81  string_view driveName)
82 {
83  return ranges::find_if(drives,
84  [&](auto& ds) { return ds.driveName == driveName; });
85 }
86 
87 DiskManipulator::DriveSettings& DiskManipulator::getDriveSettings(
88  string_view diskname)
89 {
90  // first split-off the end numbers (if present)
91  // these will be used as partition indication
92  auto pos1 = diskname.find("::");
93  auto tmp1 = (pos1 == string_view::npos) ? diskname : diskname.substr(pos1);
94  auto pos2 = tmp1.find_first_of("0123456789");
95  auto pos1b = (pos1 == string_view::npos) ? 0 : pos1;
96  auto tmp2 = diskname.substr(0, pos2 + pos1b);
97 
98  auto it = findDriveSettings(tmp2);
99  if (it == end(drives)) {
100  it = findDriveSettings(strCat(getMachinePrefix(), tmp2));
101  if (it == end(drives)) {
102  throw CommandException("Unknown drive: ", tmp2);
103  }
104  }
105 
106  auto* disk = it->drive->getSectorAccessibleDisk();
107  if (!disk) {
108  // not a SectorBasedDisk
109  throw CommandException("Unsupported disk type.");
110  }
111 
112  if (pos2 == string_view::npos) {
113  // whole disk
114  it->partition = 0;
115  } else {
116  try {
117  unsigned partition = StringOp::fast_stou(diskname.substr(pos2));
119  it->partition = partition;
120  } catch (std::invalid_argument&) {
121  // parse error in fast_stou()
122  throw CommandException("Invalid partition name");
123  }
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 directory = tokens[3].getString();
153  if (!FileOperations::isDirectory(directory)) {
154  throw CommandException(directory, " is not a directory");
155  }
156  auto& settings = getDriveSettings(tokens[2].getString());
157  span<const TclObject> lists(std::begin(tokens) + 4, std::end(tokens));
158  exprt(settings, directory, lists);
159 
160  } else if (subcmd == "import") {
161  auto& settings = getDriveSettings(tokens[2].getString());
162  span<const TclObject> lists(std::begin(tokens) + 3, std::end(tokens));
163  result = import(settings, lists);
164 
165  } else if (subcmd == "savedsk") {
166  auto& settings = getDriveSettings(tokens[2].getString());
167  savedsk(settings, tokens[3].getString());
168 
169  } else if (subcmd == "chdir") {
170  auto& settings = getDriveSettings(tokens[2].getString());
171  if (tokens.size() == 3) {
172  result = "Current directory: " +
173  settings.workingDir[settings.partition];
174  } else {
175  result = chdir(settings, tokens[3].getString());
176  }
177 
178  } else if (subcmd == "mkdir") {
179  auto& settings = getDriveSettings(tokens[2].getString());
180  mkdir(settings, tokens[3].getString());
181 
182  } else if (subcmd == "create") {
183  create(tokens);
184 
185  } else if (subcmd == "format") {
186  bool dos1 = false;
187  string_view drive = tokens[2].getString();
188  if (tokens.size() == 4) {
189  if (drive == "-dos1") {
190  dos1 = true;
191  drive = tokens[3].getString();
192  } else if (tokens[3] == "-dos1") {
193  dos1 = true;
194  }
195  }
196  auto& settings = getDriveSettings(drive);
197  format(settings, dos1);
198 
199  } else if (subcmd == "dir") {
200  auto& settings = getDriveSettings(tokens[2].getString());
201  result = dir(settings);
202 
203  } else {
204  throw CommandException("Unknown subcommand: ", subcmd);
205  }
206 }
207 
208 string DiskManipulator::help(const vector<string>& tokens) const
209 {
210  string helptext;
211  if (tokens.size() >= 2) {
212  if (tokens[1] == "import" ) {
213  helptext=
214  "diskmanipulator import <disk name> <host directory|host file>\n"
215  "Import all files and subdirs from the host OS as specified into the <disk name> in the\n"
216  "current MSX subdirectory as was specified with the last chdir command.\n";
217  } else if (tokens[1] == "export" ) {
218  helptext=
219  "diskmanipulator export <disk name> <host directory>\n"
220  "Extract all files and subdirs from the MSX subdirectory specified with the chdir command\n"
221  "from <disk name> to the host OS in <host directory>.\n";
222  } else if (tokens[1] == "savedsk") {
223  helptext=
224  "diskmanipulator savedsk <disk name> <dskfilename>\n"
225  "Save the complete drive content to <dskfilename>, it is not possible to save just one\n"
226  "partition. The main purpose of this command is to make it possible to save a 'ramdsk' into\n"
227  "a file and to take 'live backups' of dsk-files in use.\n";
228  } else if (tokens[1] == "chdir") {
229  helptext=
230  "diskmanipulator chdir <disk name> <MSX directory>\n"
231  "Change the working directory on <disk name>. This will be the directory were the 'import',\n"
232  "'export' and 'dir' commands will work on.\n"
233  "In case of a partitioned drive, each partition has its own working directory.\n";
234  } else if (tokens[1] == "mkdir") {
235  helptext=
236  "diskmanipulator mkdir <disk name> <MSX directory>\n"
237  "Create the specified directory on <disk name>. If needed, all missing parent directories\n"
238  "are created at the same time. Accepts both absolute and relative path names.\n";
239  } else if (tokens[1] == "create") {
240  helptext=
241  "diskmanipulator create <dskfilename> <size/option> [<size/option>...]\n"
242  "Create a formatted dsk file with the given size.\n"
243  "If multiple sizes are given, a partitioned disk image will be created with each partition\n"
244  "having the size as indicated. By default the sizes are expressed in kilobyte, add the\n"
245  "postfix M for megabyte.\n"
246  "When using the -dos1 option, the boot sector of the created image will be MSX-DOS1\n"
247  "compatible.\n";
248  } else if (tokens[1] == "format") {
249  helptext=
250  "diskmanipulator format <disk name>\n"
251  "formats the current (partition on) <disk name> with a regular FAT12 MSX filesystem with an\n"
252  "MSX-DOS2 boot sector, or, when the -dos1 option is specified, with an MSX-DOS1 boot sector.\n";
253  } else if (tokens[1] == "dir") {
254  helptext=
255  "diskmanipulator dir <disk name>\n"
256  "Shows the content of the current directory on <disk name>\n";
257  } else {
258  helptext = "Unknown diskmanipulator subcommand: " + tokens[1];
259  }
260  } else {
261  helptext=
262  "diskmanipulator create <fn> <sz> [<sz> ...] : create a formatted dsk file with name <fn>\n"
263  " having the given (partition) size(s)\n"
264  "diskmanipulator savedsk <disk name> <fn> : save <disk name> as dsk file named as <fn>\n"
265  "diskmanipulator format <disk name> : format (a partition) on <disk name>\n"
266  "diskmanipulator chdir <disk name> <MSX dir> : change directory on <disk name>\n"
267  "diskmanipulator mkdir <disk name> <MSX dir> : create directory on <disk name>\n"
268  "diskmanipulator dir <disk name> : long format file listing of current\n"
269  " directory on <disk name>\n"
270  "diskmanipulator import <disk> <dir/file> ... : import files and subdirs from <dir/file>\n"
271  "diskmanipulator export <disk> <host dir> : export all files on <disk> to <host dir>\n"
272  "For more info use 'help diskmanipulator <subcommand>'.\n";
273  }
274  return helptext;
275 }
276 
277 void DiskManipulator::tabCompletion(vector<string>& tokens) const
278 {
279  if (tokens.size() == 2) {
280  static constexpr const char* const cmds[] = {
281  "import", "export", "savedsk", "dir", "create",
282  "format", "chdir", "mkdir",
283  };
284  completeString(tokens, cmds);
285 
286  } else if ((tokens.size() == 3) && (tokens[1] == "create")) {
288 
289  } else if (tokens.size() == 3) {
290  vector<string> names;
291  if (tokens[1] == one_of("format", "create")) {
292  names.emplace_back("-dos1");
293  }
294  for (auto& d : drives) {
295  const auto& name1 = d.driveName; // with prexix
296  const auto& name2 = d.drive->getContainerName(); // without prefix
297  append(names, {name1, name2});
298  // if it has partitions then we also add the partition
299  // numbers to the autocompletion
300  if (auto* disk = d.drive->getSectorAccessibleDisk()) {
301  for (unsigned i = 1; i <= MAX_PARTITIONS; ++i) {
302  try {
304  append(names,
305  {strCat(name1, i), strCat(name2, i)});
306  } catch (MSXException&) {
307  // skip invalid partition
308  }
309  }
310  }
311  }
312  completeString(tokens, names);
313 
314  } else if (tokens.size() >= 4) {
315  if (tokens[1] == one_of("savedsk", "import", "export")) {
317  } else if (tokens[1] == "create") {
318  static constexpr const char* const cmds[] = {
319  "360", "720", "32M", "-dos1"
320  };
321  completeString(tokens, cmds);
322  } else if (tokens[1] == "format") {
323  static constexpr const char* const cmds[] = {
324  "-dos1"
325  };
326  completeString(tokens, cmds);
327  }
328  }
329 }
330 
331 void DiskManipulator::savedsk(const DriveSettings& driveData,
332  string_view filename)
333 {
334  auto partition = getPartition(driveData);
335  SectorBuffer buf;
336  File file(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 (size_t i = 3; i < tokens.size(); ++i) {
350  if (tokens[i] == "-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  string tok(tokens[i].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 (= bootsecor + minimal fat +
394  // minial 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(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 (auto& l : lists) {
501  for (auto i : xrange(l.getListLength(interp))) {
502  auto s = 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(string(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 (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
openmsx::DiskImageUtils::checkFAT12Partition
void checkFAT12Partition(SectorAccessibleDisk &disk, unsigned partition)
Like above, but also check whether partition is of type FAT12.
Definition: DiskImageUtils.cc:57
DiskManipulator.hh
one_of.hh
DSKDiskImage.hh
FileException.hh
DiskContainer.hh
span::empty
constexpr bool empty() const noexcept
Definition: span.hh:300
xrange
auto xrange(T e)
Definition: xrange.hh:170
openmsx::DiskImageUtils::format
void format(SectorAccessibleDisk &disk, bool dos1)
Format the given disk (= a single partition).
Definition: DiskImageUtils.cc:182
StringOp::startsWith
bool startsWith(string_view total, string_view part)
Definition: StringOp.cc:71
openmsx::DiskManipulator::registerDrive
void registerDrive(DiskContainer &drive, const std::string &prefix)
Definition: DiskManipulator.cc:52
openmsx::CommandController
Definition: CommandController.hh:17
openmsx::Completer::completeFileName
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:139
openmsx::DiskContainer
Definition: DiskContainer.hh:14
TclObject.hh
openmsx::FileOperations::getStat
bool getStat(string_view filename_, Stat &st)
Call stat() and return the stat structure.
Definition: FileOperations.cc:635
openmsx::FileOperations::Stat
struct stat Stat
Definition: FileOperations.hh:212
openmsx::FileOperations::isRegularFile
bool isRegularFile(const Stat &st)
Definition: FileOperations.cc:654
openmsx::SectorAccessibleDisk::SECTOR_SIZE
static constexpr size_t SECTOR_SIZE
Definition: SectorAccessibleDisk.hh:18
StringOp::fast_stou
unsigned fast_stou(string_view s)
Definition: StringOp.cc:265
ranges.hh
openmsx::userFileContext
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:161
Filename.hh
openmsx::File::CREATE
@ CREATE
Definition: File.hh:21
strAppend
void strAppend(std::string &result, Ts &&...ts)
Definition: strCat.hh:644
SectorBasedDisk.hh
span
Definition: span.hh:34
openmsx::Reactor
Contains the main loop of openMSX.
Definition: Reactor.hh:66
openmsx::Completer::completeString
static void completeString(std::vector< std::string > &tokens, ITER begin, ITER end, bool caseSensitive=true)
Definition: Completer.hh:125
StringOp::endsWith
bool endsWith(string_view total, string_view part)
Definition: StringOp.cc:81
Reactor.hh
openmsx::DiskManipulator::DiskManipulator
DiskManipulator(CommandController &commandController, Reactor &reactor)
Definition: DiskManipulator.cc:34
move_pop_back
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition: stl.hh:177
DiskImageUtils.hh
MSXtar.hh
openmsx::Reactor::getMachineID
std::string_view getMachineID() const
Definition: Reactor.cc:374
File.hh
one_of
Definition: one_of.hh:7
openmsx::filename
constexpr const char *const filename
Definition: FirmwareSwitch.cc:10
openmsx::DiskImageUtils::hasPartitionTable
bool hasPartitionTable(SectorAccessibleDisk &disk)
Check whether the given disk is partitioned.
Definition: DiskImageUtils.cc:21
FileContext.hh
openmsx::DiskContainer::getContainerName
virtual const std::string & getContainerName() const =0
openmsx::FileOperations::isDirectory
bool isDirectory(const Stat &st)
Definition: FileOperations.cc:664
FileOperations.hh
span::size
constexpr index_type size() const noexcept
Definition: span.hh:296
StringOp.hh
gl::scale
mat4 scale(const vec3 &xyz)
Definition: gl_transform.hh:19
openmsx::Filename
Filename
Definition: Filename.cc:50
DiskPartition.hh
strCat.hh
openmsx::Command
Definition: Command.hh:40
detail::append
void append(Result &)
Definition: stl.hh:340
openmsx::DiskManipulator::unregisterDrive
void unregisterDrive(DiskContainer &drive)
Definition: DiskManipulator.cc:66
stl.hh
ranges::find_if
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition: ranges.hh:113
openmsx::DiskImageUtils::partition
void partition(SectorAccessibleDisk &disk, const std::vector< unsigned > &sizes)
Write a partition table to the given disk and format each partition.
Definition: DiskImageUtils.cc:225
strCat
std::string strCat(Ts &&...ts)
Definition: strCat.hh:573
CommandException.hh
openmsx::DiskManipulator::~DiskManipulator
~DiskManipulator()
Definition: DiskManipulator.cc:41
openmsx
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
openmsx::CommandCompleter::getInterpreter
Interpreter & getInterpreter() const final
Definition: Command.cc:41
xrange.hh