-
Notifications
You must be signed in to change notification settings - Fork 134
/
Copy pathdisk.js
2260 lines (2155 loc) · 101 KB
/
disk.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @fileoverview Implements Disk support for FDC and HDC components
* @author Jeff Parsons <[email protected]>
* @copyright © 2012-2024 Jeff Parsons
* @license MIT <https://www.pcjs.org/LICENSE.txt>
*
* This file is part of PCjs, a computer emulation software project at <https://www.pcjs.org>.
*/
import MESSAGE from "./message.js";
import Component from "../../../modules/v2/component.js";
import DiskAPI from "../../../modules/v2/diskapi.js";
import DumpAPI from "../../../modules/v2/dumpapi.js";
import StrLib from "../../../modules/v2/strlib.js";
import WebLib from "../../../modules/v2/weblib.js";
import { BACKTRACK, DEBUG, SYMBOLS } from "./defines.js";
/**
* The Disk component provides methods for:
*
* 1) creating an empty disk: create()
* 2) loading a disk image: load()
* 3) getting disk information: info()
* 4) seeking a disk sector: seek()
* 5) reading data from a sector: read()
* 6) writing data to a sector: write()
* 7) save disk deltas: save()
* 8) restore disk deltas: restore()
* 9) converting disk contents: convertToJSON()
*
* More functionality may be factored out of the FDC and HDC components later and moved here, to
* further reduce some of the duplication between them, but the above functionality is a good start.
*/
/**
* Client/Server Disk I/O
*
* To support large disks without consuming large amounts of client-side memory, and to push
* client-side disk changes back the server, we need a DiskIO API that can be used in place of
* the DiskDump API.
*
* Use of the DiskIO API and any associated disk images must be tightly coupled to per-user
* storage and specific machine configurations, to prevent the disk images from being corrupted
* by inconsistent I/O operations. Our basic User API (userapi.js) already provides some
* per-user storage that we can use to get the design rolling.
*
* The DiskIO API must also provide the ability to create new (empty) hard disk images in per-user
* storage and automatically associate them with the machine configurations that requested them.
*/
/**
* Principles
*
* Originally, when the Disk class was given a disk image to load and mount, it would request the
* ENTIRE disk image from the DiskDump module. That works well for small (floppy) disk images, but
* for larger disks -- let's just say anything stored on the server as an "img" file -- we'd prefer
* to interact with that disk using "On-Demand I/O". Any "img" file on the same server as the PCjs
* application should be a candidate for on-demand access.
*
* On-Demand I/O means that nothing is initially transferred from the server. As sectors are
* requested by the PCx86 machine, PCx86 requests them from the server, and maintains an MRU cache
* of sectors, periodically discarding the least-used clean sectors above a certain memory limit.
* Dirty sectors (ie, those that the PCx86 machine has written to) must be periodically sent
* back to the server and then marked as clean, so that they can be discarded like any other
* sector.
*
* We also support "local" init-only disk images, which means that dirty sectors are never sent
* back to the server and are instead retained by the client for the lifetime of the app; such
* images are "read-only" as far as the server is concerned, but "read-write" as far as the client
* is concerned. Reloading/restarting an app with an "local" disk will return the disk to its
* initial state.
*
* Practice
*
* Let's first look at what we *already* do for the HDC component:
*
* 1) Creating new (empty) disk images
* 2) Pre-loading pre-built JSON-encoded disk images (converting them to JSON on the fly as needed)
*
* An example of #1 is in /devices/pc/machine/5160/cga/256kb/demo/machine.xml:
*
* <hdc id="hdcXT" drives='[{name:"10Mb Hard Drive",type:3}]'/>
*
* and an example of #2 is in /disks/pc/fixed/win101.xml:
*
* <hdc id="hdcXT" drives='[{name:"10Mb Hard Drive",path:"/disks/pc/fixed/win101/10mb.json",type:3}]'/>
*
* The HDC component expects an array of drive entries. Array position determines drive numbering
* (the first entry is drive 0, the second is drive 1, etc), and each entry contains the following
* properties:
*
* 'name': user-friendly name for the disk, if any
* 'path': URL of the disk image, if any
* 'type': a drive type
*
* Of those properties, only 'type' is required, which provides an index into an HDC "Drive Type"
* table that determines disk geometry and therefore disk size. As we add support for larger disks and
* newer disk controllers, the 'type' parameter will be superseded by either a user-defined 'geometry'
* parameter that will define number of heads, cylinders, tracks, sectors per track, and (max) bytes per
* sector, or perhaps a generic 'size' parameter that leaves geometry choices to the HDC component,
* which will then pass those decisions on to the Disk component.
*
* We will enable on-demand I/O for a disk image with a new 'mode' parameter that looks like:
*
* 'mode': one of "local", "preload", "demandrw", "demandro"
*
* "preload" means the disk image will be completely preloaded, exactly as before; "demandrw" enables
* full on-demand I/O support; and "demandro" enables on-demand I/O for reads only (all writes are retained
* and never written back to the server).
*
* "ro" will be the fallback for "rw" unless TWO other important criteria are met: 1) the user has a
* private user key, and therefore per-user storage; and 2) the disk image 'path' contains an asterisk (*)
* that the server can internally remap to a directory in the user's storage; eg:
*
* 'path': <asterisk>/10mb.img (path components following the asterisk are optional)
*
* If the disk image does not already exist, it will be created (but not formatted).
*
* This preserves the promise that EVERYTHING a user does within a PCx86 machine is private (ie, not
* visible to any other PCjs users). I don't want to be in the business of saving any user machine
* states or disk changes, but at least those operations are limited to users who have asked for (and
* received) a private user key.
*
* Another important consideration at this stage is dealing with multiple machines writing to the same
* disk image; even though we're limiting the "demandrw" mode to per-user images, a single user may still
* inadvertently start up multiple machines that refer to the same disk image.
*
* So, every PCx86 machine needs to generate a unique token and include that token with every Disk I/O API
* operation, so that the server can revoke a previous machine's "rw" access to a disk image when a new
* machine requests "rw" access to the same disk image.
*
* From the client's perspective, revocation can be quietly dealt with by reverting to "demandro" mode;
* that client becomes stuck with all their dirty sectors until they can reclaim "rw" access, which should
* only happen if no intervening writes to the disk image on the server have occurred (if I bother allowing
* reclamation at all).
*
* The real challenge here is avoiding revocation of a machine that still has critical changes to commit,
* but since we can't even solve the problem of a user closing their browser at an inopportune time
* and potentially leaving a disk image in an inconsistent state, premature revocation is the least of
* our problems. Since a real hard drive could suffer the same fate if the machine's power was turned off
* at the wrong time, you could say that we're simply providing a faithful simulation of reality.
*/
/**
* Every Sector object (once loaded, parsed, and "normalized") should have ALL of the following properties:
*
* [ID]: sector ID
* [LENGTH]: size of the sector, in bytes
* [DATA]: array of dwords
* pattern: dword pattern to use for empty or partial sectors (or null if sector still needs to be loaded)
*
* initSector() also sets the following properties, to help us quickly identify its location within diskData:
*
* iCylinder
* iHead
*
* In addition, we will maintain the following information on a per-sector basis, as sectors are modified:
*
* iModify: index of first modified dword in sector
* cModify: number of modified dwords in sector
* fDirty: true if sector is dirty, false if clean (or cleaning in progress)
*
* fDirty is used in conjunction with "demandrw" disks; it is set to true whenever the sector is modified, and is
* set to false whenever the sector has been sent to the server. If the server write succeeds and fDirty is still
* false, then the sector modifications are removed (cModify is set to zero). If the write succeeds but fDirty was
* set to true again in the meantime, then all the sector modifications (even those that were just written) remain
* in place (since we don't keep track of more than one modification range within a sector). And if the write failed,
* then fDirty is set back to true and again all modifications remain in place; the best we can do is schedule another
* write attempt.
*
* TODO: Perhaps we should also maintain a failure count and stop trying to write sectors that reach a certain
* threshold. Error-handling, as usual, is the thorniest problem.
*
* @typedef {Object} Sector
* @property {number} c (cylinder #)
* @property {number} h (head #)
* @property {number} s (sector ID)
* @property {number} l (length of sector, in bytes)
* @property {Array.<number>} d (array of 32-bit values)
* @property {number} f (index into the disk's file table)
* @property {number} o (offset of this sector within the file's data stream)
* @property {number} iCylinder (deprecated; see c)
* @property {number} iHead (deprecated; see h)
* @property {number} sector (deprecated; see s)
* @property {number} length (deprecated; see l)
* @property {Array.<number>} data (deprecated; see d)
* @property {number|null} pattern (deprecated)
* @property {FileInfo} file
* @property {number} offFile
* @property {number} dataCRC
* @property {boolean} dataError
* @property {number} dataMark
* @property {number} headCRC
* @property {boolean} headError
* @property {number} iModify (for internal use only)
* @property {number} cModify (for internal use only)
* @property {boolean} fDirty (for internal use only)
*/
/**
* @typedef {Object} FileDesc
* @property {string} hash
* @property {string} path
* @property {string} attr
* @property {string} date
* @property {number} size
* @property {FileModule} [module]
*/
/**
* @typedef {Object} FileModule
* @property {string} name
* @property {string} description
* @property {Object.<FileSegment>} [segments]
*/
/**
* @typedef {Object} FileSegment
* @property {number} offStart
* @property {number} offEnd
* @property {Object.<FileOrdinal>} [ordinals]
*/
/**
* @typedef {Object} FileOrdinal
* @property {number} o (offset within segment)
* @property {string} s (name of symbol, if any)
*/
/**
* @class Disk
* @unrestricted (allows the class to define properties, both dot and named, outside of the constructor)
*/
export default class Disk extends Component {
/**
* Sector object "public" properties.
*/
static SECTOR = {
CYLINDER: 'c', // cylinder number (0-based) [formerly iCylinder]
HEAD: 'h', // head number (0-based) [formerly iHead]
ID: 's', // sector ID (generally 1-based, except for unusual/copy-protected disks) [formerly 'sector']
LENGTH: 'l', // sector length, in bytes (generally 512, except for unusual/copy-protected disks) [formerly 'length']
DATA: 'd', // array of signed 32-bit values (if less than length/4, the last value is repeated) [formerly 'data']
FILE_INDEX: 'f', // "extended" JSON disk images only [formerly file]
FILE_OFFSET:'o', // "extended" JSON disk images only [formerly offFile]
PATTERN: 'pattern', // deprecated (no longer used in external images, still used internally)
/**
* The following properties occur very infrequently (and usually only in copy-protected or damaged disk images),
* hence the longer, more meaningful IDs.
*/
DATA_CRC: 'dataCRC',
DATA_ERROR: 'dataError',
DATA_MARK: 'dataMark',
HEAD_CRC: 'headCRC',
HEAD_ERROR: 'headError'
};
/**
* The default number of milliseconds to wait before writing a dirty sector back to a remote disk image
*
* @const {number}
*/
static REMOTE_WRITE_DELAY = 2000; // 2-second delay
/**
* A global disk count, used to form unique Disk component IDs (totally optional; for debugging purposes only)
*/
static nDisks = 0;
/**
* Disk(controller, drive, mode)
*
* Disk contents are stored as an array (diskData) of cylinders, each of which is an array of
* heads, each of which is an array of sector objects; the latter contain sector numbers and
* sector data, where sector data is an array of dwords. The format does not impose any
* limitations on number of cylinders, number of heads, sectors per track, or bytes per sector.
*
* WARNING: All accesses to disk sector properties must be via their string names, not their
* "dot" names, otherwise code will break after it's been processed by the Closure Compiler,
* and any dumped disks may be unmountable. This is a side-effect of how we mount and dump
* disk images (ie, as JSON-encoded streams).
*
* This means, for example, that all references to "track[iSector].data" must actually appear as
* "track[iSector][Disk.SECTOR.DATA]".
*
* @this {Disk}
* @param {HDC|FDC} controller
* @param {Object} drive
* @param {string} mode
*/
constructor(controller, drive, mode)
{
super("Disk", {'id': controller.idMachine + ".disk" + StrLib.toHex(++Disk.nDisks, 4)}, MESSAGE.DISK);
this.controller = controller;
/**
* Route all printing through this.controller (eg, controller.print()), because
* the Computer component is unaware of any Disk objects and therefore will not set
* up the usual overrides when a Control Panel is installed.
*/
this.print = controller.print;
this.cmp = controller.cmp;
this.dbg = controller.dbg;
this.drive = drive;
/**
* We pull out a number of drive properties that we may or may not need as defaults
*/
this.sDiskName = drive.name;
this.fRemovable = drive.fRemovable;
this.fOnDemand = this.fRemote = false;
/**
* Initialize the disk contents
*/
this.create(mode, drive.nCylinders, drive.nHeads, drive.nSectors, drive.cbSector);
/**
* The following dirty sector and timer properties are used only with fOnDemand disks,
* assuming fRemote was successfully set.
*/
this.aDirtySectors = [];
this.aDirtyTimestamps = []; // this array is parallel to aDirtySectors
this.timerWrite = null; // REMOTE_WRITE_DELAY timer in effect, if any
this.msTimerWrite = 0; // the time that the write timer, if any, is set to fire
this.fWriteInProgress = false;
/**
* To make getModuleInfo() more reliable, we use aModules to cache any modules we see as
* sectors are read.
*/
this.aModules = {};
this.setReady();
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* We have no real interest in this notification, other than to obtain a reference to the Debugger
* for every disk loaded BEFORE the initBus() phase; any disk loaded AFTER that point will get its Debugger
* reference, if any, from the disk controller passed to the Disk() constructor.
*
* @this {Disk}
* @param {Computer} cmp
* @param {Busx86} bus
* @param {CPUx86} cpu
* @param {Debuggerx86} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.dbg = dbg;
}
/**
* isRemote()
*
* @this {Disk}
* @returns {boolean} true if remote disk, false if not
*/
isRemote()
{
/**
* Ironically, we can't rely on fRemote, because that is cleared and set across disconnect and
* reconnect operations. fOnDemand is the next best thing.
*/
return this.fOnDemand;
}
/**
* powerUp(data, fRepower)
*
* As with powerDown(), our sole concern here is for REMOTE disks: if a powerDown() call disconnected an
* "on-demand" disk, we need to get reconnected. Calling our own load() function should get the job done.
*
* The HDC component could have triggered this as well, but its powerUp() function only calls autoMount()
* in case of page (ie, application) reload, which is fine for local disks but insufficient for remote disks,
* which have a server connection that must be re-established.
*
* @this {Disk}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @returns {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (this.fOnDemand && !this.fRemote) {
this.setReady(false);
this.load(this.sDiskName, this.sDiskPath, null, this.donePowerUp, this);
}
}
return true;
}
/**
* donePowerUp(drive, disk, sDiskName, sDiskPath)
*
* This is a callback issued by the Disk component once the load() from powerUp() has finished.
*
* @this {Disk}
* @param {Object} drive
* @param {Disk} disk is set if the disk was successfully mounted, null if not
* @param {string} sDiskName
* @param {string} sDiskPath
*/
donePowerUp(drive, disk, sDiskName, sDiskPath)
{
this.setReady(true);
}
/**
* powerDown(fSave, fShutdown)
*
* Our sole concern here is for REMOTE disks, making sure any unwritten changes get flushed to
* the server during a shutdown. No local state is ever returned, so fSave is ignored.
*
* Local disks are managed by the controller (ie, FDC or HDC) that mounted them; the controller's
* powerDown() handler will take care of calling save() as needed.
*
* TODO: Consider taking responsibility for saving the state of local disks as well; the only reason
* the controllers still take care of them is historical, because this component originally didn't
* exist, and even after it was created, it didn't originally receive powerDown() notifications.
*
* @this {Disk}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @returns {Object|boolean}
*/
powerDown(fSave, fShutdown)
{
/**
* If we're connected to a remote disk, take this opportunity to flush any remaining unwritten
* changes and then close the connection.
*/
if (this.fRemote) {
let response;
let nErrorCode = 0;
if (this.fWriteInProgress) {
/**
* TODO: Verify that the Computer's powerOff() handler will actually honor a false return value.
*/
if (!Component.confirmUser("Disk writes are still in progress, shut down anyway?")) {
return false;
}
}
while ((response = this.findDirtySectors(false))) {
if ((nErrorCode = response[0])) {
this.printf(MESSAGE.NOTICE, "Unable to save \"%s\" (error %d)\n", this.sDiskName, nErrorCode);
break;
}
}
if (fShutdown) {
this.disconnectRemoteDisk();
}
/**
* I only report that changes to the disk have been "saved" if fSave is true, to avoid confusing
* users who might not understand the difference between discarding local changes (which should restore
* all diskettes to their original state) and discarding remote changes (which could leave the remote disk
* in a bad state).
*/
if (!nErrorCode && fSave) this.printf(MESSAGE.NOTICE, "\"%s\" saved\n", this.sDiskName);
}
return true;
}
/**
* create()
*
* @this {Disk}
* @param {string} mode
* @param {number} nCylinders
* @param {number} nHeads
* @param {number} nSectors (per track)
* @param {number} cbSector
*
* Initializes the disk contents according to the current drive mode and parameters.
*/
create(mode, nCylinders, nHeads, nSectors, cbSector)
{
this.mode = mode;
this.nCylinders = nCylinders;
this.nHeads = nHeads;
this.nSectors = nSectors;
this.cbSector = cbSector;
this.diskData = [];
/**
* If the drive is using PRELOAD mode, then it will use the load()/mount() process to initialize the disk contents;
* it wouldn't hurt to let create() do its thing, too, but it's a waste of time.
*/
if (this.mode != DiskAPI.MODE.PRELOAD) {
this.printf(MESSAGE.DISK, "blank disk for \"%s\": %d cylinders, %d head(s)\n", this.sDiskName, this.nCylinders, this.nHeads);
let aCylinders = new Array(this.nCylinders);
for (let iCylinder = 0; iCylinder < aCylinders.length; iCylinder++) {
let aHeads = new Array(this.nHeads);
for (let iHead = 0; iHead < aHeads.length; iHead++) {
let aSectors = new Array(this.nSectors);
for (let iSector = 1; iSector <= aSectors.length; iSector++) {
/**
* Now that our read() and write() functions can deal with unallocated data
* arrays, and can read/write the specified pattern on-the-fly, we no longer need
* to pre-allocate and pre-initialize the DATA array.
*
* For "local" disks, we can assume a 'pattern' of 0, but for "demandrw" and "demandro"
* disks, 'pattern' is set to null, as yet another indication that I/O is required to load
* the sector from the server (or to write it back to the server).
*/
aSectors[iSector - 1] = this.initSector(null, iCylinder, iHead, iSector, this.cbSector, (this.mode == DiskAPI.MODE.LOCAL? 0 : null));
}
aHeads[iHead] = aSectors;
}
aCylinders[iCylinder] = aHeads;
}
this.diskData = aCylinders;
}
this.dwChecksum = null;
}
/**
* load(sDiskName, sDiskPath, file, fnNotify)
*
* TODO: Figure out how we can strongly type fnNotify, because the Closure Compiler has issues with:
*
* param {function(Component,Object,Disk,string,string)} fnNotify
*
* for:
*
* this.fnNotify.call(this.controller, this.drive, disk, this.sDiskName, this.sDiskPath);
*
* Also, while we're at it, learn if there are ways to:
*
* 1) declare a function taking NO parameters (ie, generate a warning if any parameters are specified)
* 2) declare a type for a function's return value
*
* @this {Disk}
* @param {string} sDiskName
* @param {string} sDiskPath
* @param {File} [file] is set if there's an associated File object
* @param {function(...)} [fnNotify]
* @param {Component} [controller]
* @returns {boolean} true if load completed (successfully or not), false if queued
*/
load(sDiskName, sDiskPath, file, fnNotify, controller)
{
let sDiskURL = sDiskPath;
this.printf(MESSAGE.DISK, 'load("%s","%s")\n', sDiskName, sDiskPath);
if (this.fnNotify) {
this.printf(MESSAGE.DISK, 'too many load requests for "%s" (%s)\n', sDiskName, sDiskPath);
return true;
}
this.sDiskName = sDiskName;
this.sDiskPath = sDiskPath;
this.sDiskFile = StrLib.getBaseName(sDiskPath);
this.sFormat = "json";
let disk = this;
this.fnNotify = fnNotify;
this.controllerNotify = controller || this.controller;
if (file) {
let reader = new FileReader();
if (file.type == "application/json") {
reader.onload = function() {
disk.doneLoad(sDiskURL, /** @type {string} */ (reader.result), 0);
};
reader.onerror = function() {
disk.buildDisk(null, false, reader.error.message);
};
reader.readAsText(file);
} else {
reader.onload = function() {
disk.buildDisk(/** @type {ArrayBuffer} */ (reader.result), true);
};
reader.onerror = function() {
disk.buildDisk(null, false, reader.error.message);
};
reader.readAsArrayBuffer(file);
}
return true;
}
/**
* If there's an occurrence of API_ENDPOINT anywhere in the path, we assume we can use it as-is;
* ie, that the user has already formed a URL of the type we use ourselves for unconverted disk images.
*/
if (sDiskPath.indexOf(DumpAPI.ENDPOINT) < 0) {
/**
* If the selected disk image has a "json" extension, then we assume it's a pre-converted
* JSON-encoded disk image, so we load it as-is; otherwise, we ask our server-side disk image
* converter to return the corresponding JSON-encoded data.
*/
let sDiskExt = StrLib.getExtension(sDiskPath);
if (sDiskExt != DumpAPI.FORMAT.JSON && sDiskExt != DumpAPI.FORMAT.JSON_GZ) {
if (this.mode == DiskAPI.MODE.DEMANDRW || this.mode == DiskAPI.MODE.DEMANDRO) {
sDiskURL = this.connectRemoteDisk(sDiskPath);
this.fOnDemand = true;
} else {
this.sFormat = "arraybuffer";
}
}
}
let sProgress = "Loading " + sDiskURL + "...";
return !!WebLib.getResource(sDiskURL, this.sFormat, true, function loadDone(sURL, sResponse, nErrorCode) {
disk.doneLoad(sURL, sResponse, nErrorCode);
}, function(nState) {
disk.printf(MESSAGE.PROGRESS, "%s\n", sProgress);
});
}
/**
* buildDisk(buffer, fModified, message)
*
* Builds a disk image from an ArrayBuffer (eg, from a FileReader object), rather than from JSON-encoded data.
*
* @this {Disk}
* @param {ArrayBuffer|null} buffer
* @param {boolean} [fModified] is true if we should mark the entire disk modified (to ensure that we save/restore it)
* @param {string} [message] (usually only set if there was an error, and therefore buffer is null or undefined)
*/
buildDisk(buffer, fModified, message)
{
let cbDiskData = 0, dv = null, disk;
if (buffer) {
cbDiskData = buffer.byteLength;
dv = new DataView(buffer, 0, cbDiskData);
}
/**
* Hard drive images using the PCJS MBR will have a special signature, and if that MBR also contains
* a non-zero DiskInfo.MBR.DRIVE0PARMS.CYLS value, then we'll use the geometry stored in the MBR.
*/
let nCylinders = 0;
if (dv && dv.getUint32(0x199, true) == 0x534a4350) { // if DiskInfo.MBR.PCJS_SIG == PCJS_VALUE
nCylinders = dv.getUint16(0x19E, true); // DiskInfo.MBR.DRIVE0PARMS.CYLS
}
if (nCylinders) {
this.nCylinders = nCylinders;
this.nHeads = dv.getUint8(0x1A0); // DiskInfo.MBR.DRIVE0PARMS.HEADS
this.nSectors = dv.getUint8(0x1AC); // DiskInfo.MBR.DRIVE0PARMS.SECTORS
this.cbSector = 512;
}
else {
let diskFormat = DiskAPI.GEOMETRIES[cbDiskData];
if (diskFormat) {
/**
* This geometry lookup is primarily intended for diskette images, because there are a wide variety
* of diskette formats that can work within a drive's parameters. So, I used to assert the number
* of cylinders match, but the assertion has been relaxed (we require only that the image have no
* MORE than the number of cylinders and heads than the drive can handle).
*
* For example, a 40-cylinder diskette image should be fine with an 80-cylinder high-capacity drive.
*
* There are also a couple of standard hard drive formats that PCjs likes to use (10Mb and 20Mb), which
* I could treat specially (based on diskFormat[4]), but since PCjs can use its own MBR for non-standard
* hard disk images now, I'd rather not do that.
*/
if (diskFormat[0] <= this.nCylinders && diskFormat[1] <= this.nHeads /* || !diskFormat[4] */) {
this.nCylinders = diskFormat[0];
this.nHeads = diskFormat[1];
this.nSectors = diskFormat[2];
this.cbSector = (diskFormat[3] || 512);
} else {
this.nCylinders = 0; // we don't know what's going on here...
}
}
}
if (dv && this.nCylinders) {
let ib = 0;
let cdw = this.cbSector >> 2, dwPattern = 0, dwChecksum = 0;
this.diskData = new Array(this.nCylinders);
for (let iCylinder = 0; iCylinder < this.diskData.length; iCylinder++) {
let cylinder = this.diskData[iCylinder] = new Array(this.nHeads);
for (let iHead = 0; iHead < cylinder.length; iHead++) {
let head = cylinder[iHead] = new Array(this.nSectors);
for (let iSector = 0; iSector < head.length; iSector++) {
let sector = this.initSector(null, iCylinder, iHead, iSector + 1, this.cbSector, dwPattern);
let adw = sector[Disk.SECTOR.DATA];
for (let idw = 0; idw < cdw; idw++, ib += 4) {
let dw = adw[idw] = dv.getInt32(ib, true);
dwChecksum = (dwChecksum + dw) & (0xffffffff|0);
}
if (fModified) sector.cModify = cdw;
head[iSector] = sector;
}
}
}
this.dwChecksum = dwChecksum;
disk = this;
} else {
this.printf(MESSAGE.NOTICE, "%s\n", message || ("Unrecognized disk format (" + cbDiskData + " bytes)"));
}
if (this.fnNotify) {
this.fnNotify.call(this.controller, this.drive, disk, this.sDiskName, this.sDiskPath);
this.fnNotify = null;
}
}
/**
* doneLoad(sURL, imageData, nErrorCode)
*
* This function was originally called mount(). If the mount is successful, we pass the Disk object to the
* caller's fnNotify handler; otherwise, we pass null.
*
* @this {Disk}
* @param {string} sURL
* @param {string|ArrayBuffer} imageData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
doneLoad(sURL, imageData, nErrorCode)
{
let disk = null;
this.fWriteProtected = false;
let idMessage = (nErrorCode < 0 && this.cmp && !this.cmp.flags.powered)? MESSAGE.STATUS : MESSAGE.NOTICE;
if (this.fOnDemand) {
if (!nErrorCode) {
disk = this;
this.printf(MESSAGE.DISK, "doneLoad(\"%s\")\n", this.sDiskPath);
this.fRemote = true;
} else {
this.printf(idMessage, "Unable to connect to disk \"%s\" (error %d: %s)\n", this.sDiskPath, nErrorCode, imageData);
}
}
else if (nErrorCode) {
/**
* This can happen for innocuous reasons, such as the user switching away too quickly, forcing
* the request to be cancelled. And unfortunately, the browser cancels XMLHttpRequest requests
* BEFORE it notifies any page event handlers, so if the Computer's being powered down, we won't know
* that yet. For now, we rely on the lack of a specific error (nErrorCode < 0), and suppress the
* notify() alert if there's no specific error AND the computer is not powered up yet.
*/
this.printf(idMessage, "Unable to load disk \"%s\" (error %d: %s)\n", this.sDiskName, nErrorCode, sURL);
} else {
this.printf(MESSAGE.DISK, "doneLoad(\"%s\")\n", this.sDiskPath);
/**
* If we received binary data instead of JSON, we can use the same buildDisk() function that
* our FileReader code uses.
*/
if (typeof imageData != "string") {
this.buildDisk(imageData);
return;
}
try {
/**
* The following code was a hack to turn on write-protection for a disk image if there was
* an initial comment line containing the string "write-protected". However, since comments
* are technically not allowed in JSON, I needed an alternative solution. So, if the basename
* contains the suffix "-readonly", then I'll turn on write-protection for that disk as well.
*
* TODO: Provide some UI for turning write-protection on/off for disks at will, and provide
* an XML-based solution (ie, a per-disk XML configuration option) for controlling it as well.
*/
let sBaseName = StrLib.getBaseName(this.sDiskFile, true).toLowerCase();
if (sBaseName.indexOf("-readonly") > 0) {
this.fWriteProtected = true;
} else {
let iEOL = imageData.indexOf("\n");
if (iEOL > 0 && iEOL < 1024) {
let sConfig = imageData.substring(0, iEOL);
if (sConfig.indexOf("write-protected") > 0) {
this.fWriteProtected = true;
}
}
}
let diskData, fileTable, imageInfo;
/**
* The most likely source of any exception will be here, where we're parsing the disk data.
*/
if (imageData.substr(0, 1) == "<") { // if the "data" begins with a "<"...
/**
* Early server configs reported an error (via the nErrorCode parameter) if a disk URL was invalid,
* but more recent server configs now display a somewhat friendlier HTML error page. The downside,
* however, is that the original error has been buried, and we've received "data" that isn't actually
* disk data.
*
* So, if the data we've received appears to be "HTML-like", all we can really do is assume that the
* disk image is missing. And so we pretend we received an error message to that effect.
*/
diskData = ["Missing disk image: " + this.sDiskName];
} else {
/**
* TODO: IE9 is rather unfriendly and restrictive with regard to how much data it's willing to
* eval(). In particular, the 10Mb disk image we use for the Windows 1.01 demo config fails in
* IE9 with an "Out of memory" exception. One work-around would be to chop the data into chunks
* (perhaps one track per chunk, using regular expressions) and then manually re-assemble it.
*
* However, it turns out that using JSON.parse(imageData) instead of eval("(" + imageData + ")")
* is a much easier fix. The only drawback is that we must first quote any unquoted property names
* and remove any comments, because while eval() was cool with them, JSON.parse() is more particular;
* the following RegExp replacements take care of those requirements.
*
* The use of hex values is something else that eval() was OK with, but JSON.parse() is not, and
* while I've stopped using hex values in DumpAPI responses (at least when "format=json" is specified),
* I can't guarantee they won't show up in "legacy" images, and there's no simple RegExp replacement
* for transforming hex values into decimal values, so I cop out and fall back to eval() if I detect
* any hex prefixes ("0x") in the sequence. Ditto for error messages, which appear like so:
*
* ["unrecognized disk path: test.img"]
*/
if (imageData[0] == '{') {
let image = JSON.parse(imageData);
diskData = image['diskData'];
fileTable = image['fileTable'];
imageInfo = image['imageInfo'];
} else if (imageData.indexOf("0x") < 0 && imageData.substr(0, 2) != "[\"") {
diskData = JSON.parse(imageData.replace(/([a-z]+):/gm, "\"$1\":").replace(/\/\/[^\n]*/gm, ""));
} else {
diskData = eval("(" + imageData + ")");
}
}
if (!diskData.length) {
Component.error("Empty disk image: " + this.sDiskName);
}
else if (diskData.length == 1) {
Component.error(diskData[0]);
}
/**
* diskData is an array of cylinders, each of which is an array of heads, each of which
* is an array of sector objects. The format does not impose any limitations on number of
* cylinders, number of heads, or number of bytes in any of the sector object byte-arrays.
*
* WARNING: All accesses to sector object properties must be via their string names, not their
* "dot" names, otherwise code will break after it's been processed by the Closure Compiler.
*
* Sector object properties (from Disk.SECTOR) include:
*
* [ID] the sector ID (1-based, not required to be sequential)
* [LENGTH] the byte-length (ie, formatted length) of the sector
* [DATA] the dword-array containing the sector data
*
* We still support the older JSON encoding, where sector data was encoded as an array of 'bytes'
* rather than a dword DATA array. However, our support is strictly limited to an on-the-fly
* conversion to a forward-compatible DATA array.
*/
else {
if (DEBUG && this.messageEnabled(MESSAGE.DISK + MESSAGE.DATA)) {
let sCylinders = diskData.length + " track" + (diskData.length > 1 ? "s" : "");
let nHeads = diskData[0].length;
let sHeads = nHeads + " head" + (nHeads > 1 ? "s" : "");
let nSectorsPerTrack = diskData[0][0].length;
let sSectorsPerTrack = nSectorsPerTrack + " sector" + (nSectorsPerTrack > 1 ? "s" : "") + "/track";
this.printf("%s, %s, %s\n", sCylinders, sHeads, sSectorsPerTrack);
}
/**
* Before the image is usable, we must "normalize" all the sectors. In the past, this meant
* "inflating" them all. However, that's no longer strictly necessary. Mainly, it just means
* setting LENGTH and DATA properties, so that all the sectors are well-defined.
*
* This includes detecting sector data in older formats (eg, the old array of 'bytes' instead
* of the new DATA array of dwords) and converting them on-the-fly to the current format.
*/
this.nCylinders = diskData.length;
this.nHeads = diskData[0].length;
this.nSectors = diskData[0][0].length;
let sector = diskData[0][0][0];
this.cbSector = (sector && (sector[Disk.SECTOR.LENGTH] || sector['length'])) || 512;
let dwChecksum = 0;
for (let iCylinder = 0; iCylinder < this.nCylinders; iCylinder++) {
for (let iHead = 0; iHead < this.nHeads; iHead++) {
for (let iSector = 0; iSector < this.nSectors; iSector++) {
sector = diskData[iCylinder][iHead][iSector];
if (!sector) continue; // non-standard (eg, XDF) disk images may have "unused" (null) sectors
/**
* "Upgrade" all sector object properties.
*/
let idSector = sector[Disk.SECTOR.ID];
if (idSector == undefined) {
idSector = sector['sector'];
sector[Disk.SECTOR.ID] = idSector;
delete sector['sector'];
}
let length = sector[Disk.SECTOR.LENGTH];
if (length == undefined) {
length = sector['length'] || 512;
sector[Disk.SECTOR.LENGTH] = length;
delete sector['length'];
}
let dwPattern = sector['pattern'];
if (dwPattern == undefined) {
dwPattern = 0;
} else {
delete sector['pattern'];
}
let adw = sector[Disk.SECTOR.DATA];
if (adw == undefined) {
adw = sector['data'];
if (adw != undefined) {
sector[Disk.SECTOR.DATA] = adw;
delete sector['data'];
}
else {
let ab = sector['bytes'];
if (ab === undefined || !ab.length) {
/**
* If there is neither a 'bytes' nor 'data' array, then our job is simple:
* create an empty 'data' array; it will be filled in with the dword pattern
* as needed later.
*
* The only wrinkle is if there *is* a 'bytes' array but it's empty, in which
* case we must assume that the pattern was a byte pattern, so convert it to a
* dword pattern.
*/
sector[Disk.SECTOR.DATA] = adw = [];
if (ab) {
this.assert((dwPattern & 0xff) == dwPattern);
dwPattern = (dwPattern | (dwPattern << 8) | (dwPattern << 16) | (dwPattern << 24));
}
}
else {
/**
* To keep the conversion code simple, we'll do any necessary pattern-filling first,
* to fully "inflate" the sector, eliminating the possibility of partial dwords and
* saving any code downstream from dealing with byte-size patterns.
*/
this.assert((dwPattern & 0xff) == dwPattern);
for (let ib = ab.length; ib < length; ib++) {
ab[ib] = dwPattern; // the pattern for byte-arrays was only a byte
}
this.fill(sector, ab, 0);
}
delete sector['bytes'];
}
}
else {
if (adw.length < (length >> 2)) {
/**
* To minimize breakage and changes, I opted to convert new data arrays to the old format,
* where the data array is just the non-repeating data and dwPattern is the repeating value,
* like so:
*
* dwPattern = adw[--adw.length];
*
* But that was a bone-headed move, because that line will ALWAYS return undefined, since the
* array gets shortened BEFORE the fetch of the final value.
*/
dwPattern = adw[adw.length - 1];
if (adw.length) adw.length--;
}
}
this.initSector(sector, iCylinder, iHead, idSector, this.cbSector, dwPattern);
/**
* For the disk as a whole, we maintain a checksum of the original unmodified data:
*
* dwChecksum: summation of all dwords in all non-empty sectors
*
* Pattern-filling of sectors is deferred until absolutely necessary (eg, when a sector is
* being written). So all we need to do at this point is checksum all the initial sector data.
*/
for (let idw = 0; idw < adw.length; idw++) {
dwChecksum = (dwChecksum + adw[idw]) & (0xffffffff|0);
}
}
}
}
this.diskData = diskData;
this.dwChecksum = dwChecksum;
this.imageInfo = imageInfo;
if (BACKTRACK || SYMBOLS) this.buildFileTable(fileTable);
disk = this;
}
} catch (e) {
Component.error("Disk image error (" + sURL + "): " + e.message);
imageData = null;
}
if (imageData) {
Component.addMachineResource(this.controller.idMachine, sURL, imageData);
}
}
if (this.fnNotify) {
this.fnNotify.call(this.controllerNotify, this.drive, disk, this.sDiskName, this.sDiskPath, nErrorCode);
this.fnNotify = null;
}
}
/**
* buildFileTable(fileTable)
*
* This function builds a table of FileInfo objects from any and all file descriptors present in the
* "extended" JSON disk image, and updates all the sector objects to point back to the corresponding FileInfo.
* Used for BACKTRACK and SYMBOLS support.
*
* @this {Disk}
* @param {Array.<FileDesc>} [fileTable] (array of FileDescs, if any, stored in the JSON disk image)
*/
buildFileTable(fileTable)