5 * Copyright (C) Andreas Neuper, Sep 1998.
6 * This file may be modified and redistributed under
7 * the terms of the GNU Public License.
9 * 1999-03-20 Arnaldo Carvalho de Melo <acme@conectiva.com.br>
10 * Internationalization
12 * 2003-03-20 Phillip Kesling <pkesling@sgi.com>
15 #include <stdio.h> /* stderr */
16 #include <stdlib.h> /* exit */
17 #include <string.h> /* strstr */
18 #include <unistd.h> /* write */
19 #include <sys/ioctl.h> /* ioctl */
20 #include <sys/stat.h> /* stat */
21 #include <assert.h> /* assert */
30 #include "fdisksgilabel.h"
32 static int other_endian = 0;
34 static short volumes=1;
37 * only dealing with free blocks here
40 typedef struct { unsigned int first; unsigned int last; } freeblocks;
41 static freeblocks freelist[17]; /* 16 partitions can produce 17 vacant slots */
44 setfreelist(int i, unsigned int f, unsigned int l) {
45 freelist[i].first = f;
50 add2freelist(unsigned int f, unsigned int l) {
53 if (freelist[i].last == 0)
62 for (i = 0; i < 17 ; i++)
67 isinfreelist(unsigned int b) {
70 for (i = 0; i < 17 ; i++)
71 if (freelist[i].first <= b && freelist[i].last >= b)
72 return freelist[i].last;
75 /* return last vacant block of this stride (never 0). */
76 /* the '>=' is not quite correct, but simplifies the code */
78 * end of free blocks section
80 struct systypes sgi_sys_types[] = {
81 {SGI_VOLHDR, N_("SGI volhdr")},
82 {0x01, N_("SGI trkrepl")},
83 {0x02, N_("SGI secrepl")},
84 {SGI_SWAP, N_("SGI raw")},
85 {0x04, N_("SGI bsd")},
86 {0x05, N_("SGI sysv")},
87 {ENTIRE_DISK, N_("SGI volume")},
88 {SGI_EFS, N_("SGI efs")},
89 {0x08, N_("SGI lvol")},
90 {0x09, N_("SGI rlvol")},
91 {SGI_XFS, N_("SGI xfs")},
92 {SGI_XFSLOG, N_("SGI xfslog")},
93 {SGI_XLV, N_("SGI xlv")},
94 {SGI_XVM, N_("SGI xvm")},
95 {LINUX_SWAP, N_("Linux swap")},
96 {LINUX_NATIVE, N_("Linux native")},
97 {LINUX_LVM, N_("Linux LVM")},
98 {LINUX_RAID, N_("Linux RAID")},
102 static inline unsigned short
103 __swap16(unsigned short x) {
104 return (((uint16_t)(x) & 0xFF) << 8) | (((uint16_t)(x) & 0xFF00) >> 8);
107 static inline uint32_t
108 __swap32(uint32_t x) {
109 return (((x & 0xFF) << 24) |
110 ((x & 0xFF00) << 8) |
111 ((x & 0xFF0000) >> 8) |
112 ((x & 0xFF000000) >> 24));
116 sgi_get_nsect(void) {
117 return SSWAP16(sgilabel->devparam.nsect);
121 sgi_get_ntrks(void) {
122 return SSWAP16(sgilabel->devparam.ntrks);
127 sgi_get_head_vol0(void) {
128 return SSWAP16(sgilabel->devparam.head_vol0);
132 sgi_get_bytes(void) {
133 return SSWAP16(sgilabel->devparam.bytes);
137 sgi_get_pcylcount(void) {
138 return SSWAP16(sgilabel->devparam.pcylcount);
150 two_s_complement_32bit_sum(unsigned int *base, int size /* in bytes */) {
152 unsigned int sum = 0;
154 size /= sizeof(unsigned int);
155 for (i = 0; i < size; i++)
156 sum -= SSWAP32(base[i]);
162 if (sizeof(sgilabel) > 512) {
164 _("According to MIPS Computer Systems, Inc the "
165 "Label must not contain more than 512 bytes\n"));
169 if (sgilabel->magic != SGI_LABEL_MAGIC &&
170 sgilabel->magic != SGI_LABEL_MAGIC_SWAPPED) {
176 other_endian = (sgilabel->magic == SGI_LABEL_MAGIC_SWAPPED);
178 * test for correct checksum
180 if (two_s_complement_32bit_sum((unsigned int*)sgilabel,
181 sizeof(*sgilabel))) {
183 _("Detected sgi disklabel with wrong checksum.\n"));
193 sgi_list_table(int xtra) {
195 int kpi = 0; /* kernel partition ID */
198 w = strlen(disk_device);
201 printf(_("\nDisk %s (SGI disk label): %d heads, %llu sectors\n"
202 "%d cylinders, %d physical cylinders\n"
203 "%d extra sects/cyl, interleave %d:1\n"
205 "Units = %s of %d * %d bytes\n\n"),
206 disk_device, heads, sectors, cylinders,
207 SSWAP16(sgiparam.pcylcount),
208 SSWAP16(sgiparam.sparecyl),
209 SSWAP16(sgiparam.ilfact),
211 str_units(PLURAL), units_per_sector,
214 printf(_("\nDisk %s (SGI disk label): "
215 "%d heads, %llu sectors, %d cylinders\n"
216 "Units = %s of %d * %d bytes\n\n"),
217 disk_device, heads, sectors, cylinders,
218 str_units(PLURAL), units_per_sector,
221 printf(_("----- partitions -----\n"
222 "Pt# %*s Info Start End Sectors Id System\n"),
224 for (i = 0 ; i < partitions; i++) {
225 if (sgi_get_num_sectors(i) || debug) {
226 uint32_t start = sgi_get_start_sector(i);
227 uint32_t len = sgi_get_num_sectors(i);
228 kpi++; /* only count nonempty partitions */
230 "%2d: %s %4s %9ld %9ld %9ld %2x %s\n",
231 /* fdisk part number */ i+1,
232 /* device */ partname(disk_device, kpi, w+2),
233 /* flags */ (sgi_get_swappartition() == i) ? "swap" :
234 /* flags */ (sgi_get_bootpartition() == i) ? "boot" : " ",
235 /* start */ (long) scround(start),
236 /* end */ (long) scround(start+len)-1,
237 /* no odd flag on end */ (long) len,
238 /* type id */ sgi_get_sysid(i),
239 /* type name */ (type = partition_type(sgi_get_sysid(i)))
240 ? type : _("Unknown"));
243 printf(_("----- Bootinfo -----\nBootfile: %s\n"
244 "----- Directory Entries -----\n"),
245 sgilabel->boot_file);
246 for (i = 0 ; i < volumes; i++) {
247 if (sgilabel->directory[i].vol_file_size) {
248 uint32_t start = SSWAP32(sgilabel->directory[i].vol_file_start);
249 uint32_t len = SSWAP32(sgilabel->directory[i].vol_file_size);
250 unsigned char *name = sgilabel->directory[i].vol_file_name;
251 printf(_("%2d: %-10s sector%5u size%8u\n"),
252 i, name, (unsigned int) start,
259 sgi_get_start_sector(int i) {
260 return SSWAP32(sgilabel->partitions[i].start_sector);
264 sgi_get_num_sectors(int i) {
265 return SSWAP32(sgilabel->partitions[i].num_sectors);
271 return SSWAP32(sgilabel->partitions[i].id);
275 sgi_get_bootpartition(void)
277 return SSWAP16(sgilabel->boot_part);
281 sgi_get_swappartition(void)
283 return SSWAP16(sgilabel->swap_part);
287 sgi_set_bootpartition(int i)
289 sgilabel->boot_part = SSWAP16(((short)i));
293 sgi_get_lastblock(void) {
294 return heads * sectors * cylinders;
298 sgi_set_swappartition(int i) {
299 sgilabel->swap_part = SSWAP16(((short)i));
303 sgi_check_bootfile(const char* aFile) {
304 if (strlen(aFile) < 3) /* "/a\n" is minimum */ {
305 printf(_("\nInvalid Bootfile!\n"
306 "\tThe bootfile must be an absolute non-zero pathname,\n"
307 "\te.g. \"/unix\" or \"/unix.save\".\n"));
310 if (strlen(aFile) > 16) {
311 printf(_("\n\tName of Bootfile too long: "
312 "16 bytes maximum.\n"));
315 if (aFile[0] != '/') {
316 printf(_("\n\tBootfile must have a "
317 "fully qualified pathname.\n"));
322 if (strncmp(aFile, (char *) sgilabel->boot_file, 16)) {
323 printf(_("\n\tBe aware, that the bootfile is not checked for existence.\n\t"
324 "SGI's default is \"/unix\" and for backup \"/unix.save\".\n"));
325 /* filename is correct and did change */
328 return 0; /* filename did not change */
332 sgi_get_bootfile(void) {
333 return (char *) sgilabel->boot_file;
337 sgi_set_bootfile(const char* aFile) {
340 if (sgi_check_bootfile(aFile)) {
342 if ((aFile[i] != '\n') /* in principle caught again by next line */
343 && (strlen(aFile) > i))
344 sgilabel->boot_file[i] = aFile[i];
346 sgilabel->boot_file[i] = 0;
349 printf(_("\n\tBootfile is changed to \"%s\".\n"),
350 sgilabel->boot_file);
355 create_sgiinfo(void) {
356 /* I keep SGI's habit to write the sgilabel to the second block */
357 sgilabel->directory[0].vol_file_start = SSWAP32(2);
358 sgilabel->directory[0].vol_file_size = SSWAP32(sizeof(sgiinfo));
359 strncpy((char *) sgilabel->directory[0].vol_file_name, "sgilabel", 8);
362 sgiinfo *fill_sgiinfo(void);
365 sgi_write_table(void) {
367 sgilabel->csum = SSWAP32(two_s_complement_32bit_sum(
368 (unsigned int*)sgilabel,
370 assert(two_s_complement_32bit_sum(
371 (unsigned int*)sgilabel, sizeof(*sgilabel)) == 0);
372 if (lseek(fd, 0, SEEK_SET) < 0)
373 fatal(unable_to_seek);
374 if (write(fd, sgilabel, SECTOR_SIZE) != SECTOR_SIZE)
375 fatal(unable_to_write);
376 if (! strncmp((char *) sgilabel->directory[0].vol_file_name, "sgilabel", 8)) {
378 * keep this habit of first writing the "sgilabel".
379 * I never tested whether it works without (AN 981002).
381 sgiinfo *info = fill_sgiinfo();
382 int infostartblock = SSWAP32(sgilabel->directory[0].vol_file_start);
383 if (lseek(fd, (off_t) infostartblock*
384 SECTOR_SIZE, SEEK_SET) < 0)
385 fatal(unable_to_seek);
386 if (write(fd, info, SECTOR_SIZE) != SECTOR_SIZE)
387 fatal(unable_to_write);
393 compare_start(int *x, int *y) {
395 * sort according to start sectors
396 * and prefers largest partition:
397 * entry zero is entire disk entry
401 unsigned int a = sgi_get_start_sector(i);
402 unsigned int b = sgi_get_start_sector(j);
403 unsigned int c = sgi_get_num_sectors(i);
404 unsigned int d = sgi_get_num_sectors(j);
407 return (d > c) ? 1 : (d == c) ? 0 : -1;
408 return (a > b) ? 1 : -1;
415 * = 0 : disk is properly filled to the rim
416 * < 0 : there is an overlap
417 * > 0 : there is still some vacant space
419 return verify_sgi(0);
423 verify_sgi(int verbose)
425 int Index[16]; /* list of valid partitions */
426 int sortcount = 0; /* number of used partitions, i.e. non-zero lengths */
427 int entire = 0, i = 0;
428 unsigned int start = 0;
429 long long gap = 0; /* count unused blocks */
430 unsigned int lastblock = sgi_get_lastblock();
433 for (i=0; i<16; i++) {
434 if (sgi_get_num_sectors(i) != 0) {
435 Index[sortcount++]=i;
436 if (sgi_get_sysid(i) == ENTIRE_DISK) {
439 printf(_("More than one entire disk entry present.\n"));
444 if (sortcount == 0) {
446 printf(_("No partitions defined\n"));
447 return (lastblock > 0) ? 1 : (lastblock == 0) ? 0 : -1;
449 qsort(Index, sortcount, sizeof(Index[0]), (void*)compare_start);
450 if (sgi_get_sysid(Index[0]) == ENTIRE_DISK) {
451 if ((Index[0] != 10) && verbose)
452 printf(_("IRIX likes when Partition 11 covers the entire disk.\n"));
453 if ((sgi_get_start_sector(Index[0]) != 0) && verbose)
454 printf(_("The entire disk partition should start "
456 "not at diskblock %d.\n"),
457 sgi_get_start_sector(Index[0]));
458 if (debug) /* I do not understand how some disks fulfil it */
459 if ((sgi_get_num_sectors(Index[0]) != lastblock) && verbose)
460 printf(_("The entire disk partition is only %d diskblock large,\n"
461 "but the disk is %d diskblocks long.\n"),
462 sgi_get_num_sectors(Index[0]), lastblock);
463 lastblock = sgi_get_num_sectors(Index[0]);
466 printf(_("One Partition (#11) should cover the entire disk.\n"));
468 printf("sysid=%d\tpartition=%d\n",
469 sgi_get_sysid(Index[0]), Index[0]+1);
471 for (i=1, start=0; i<sortcount; i++) {
472 int cylsize = sgi_get_nsect() * sgi_get_ntrks();
473 if ((sgi_get_start_sector(Index[i]) % cylsize) != 0) {
474 if (debug) /* I do not understand how some disks fulfil it */
476 printf(_("Partition %d does not start on cylinder boundary.\n"),
479 if (sgi_get_num_sectors(Index[i]) % cylsize != 0) {
480 if (debug) /* I do not understand how some disks fulfil it */
482 printf(_("Partition %d does not end on cylinder boundary.\n"),
485 /* We cannot handle several "entire disk" entries. */
486 if (sgi_get_sysid(Index[i]) == ENTIRE_DISK) continue;
487 if (start > sgi_get_start_sector(Index[i])) {
489 printf(_("The Partition %d and %d overlap by %d sectors.\n"),
490 Index[i-1]+1, Index[i]+1,
491 start - sgi_get_start_sector(Index[i]));
492 if (gap > 0) gap = -gap;
493 if (gap == 0) gap = -1;
495 if (start < sgi_get_start_sector(Index[i])) {
497 printf(_("Unused gap of %8u sectors - sectors %8u-%u\n"),
498 sgi_get_start_sector(Index[i]) - start,
499 start, sgi_get_start_sector(Index[i])-1);
500 gap += sgi_get_start_sector(Index[i]) - start;
501 add2freelist(start, sgi_get_start_sector(Index[i]));
503 start = sgi_get_start_sector(Index[i])
504 + sgi_get_num_sectors(Index[i]);
507 printf("%2d:%12d\t%12d\t%12d\n", Index[i],
508 sgi_get_start_sector(Index[i]),
509 sgi_get_num_sectors(Index[i]),
510 sgi_get_sysid(Index[i]));
513 if (start < lastblock) {
515 printf(_("Unused gap of %8u sectors - sectors %8u-%u\n"),
516 lastblock - start, start, lastblock-1);
517 gap += lastblock - start;
518 add2freelist(start, lastblock);
521 * Done with arithmetics
525 if (!sgi_get_num_sectors(sgi_get_bootpartition())) {
526 printf(_("\nThe boot partition does not exist.\n"));
528 if (!sgi_get_num_sectors(sgi_get_swappartition())) {
529 printf(_("\nThe swap partition does not exist.\n"));
531 if ((sgi_get_sysid(sgi_get_swappartition()) != SGI_SWAP)
532 && (sgi_get_sysid(sgi_get_swappartition()) != LINUX_SWAP))
533 printf(_("\nThe swap partition has no swap type.\n"));
535 if (sgi_check_bootfile("/unix"))
536 printf(_("\tYou have chosen an unusual boot file name.\n"));
538 return (gap > 0) ? 1 : (gap == 0) ? 0 : -1;
542 sgi_change_sysid(int i, int sys)
544 if (sgi_get_num_sectors(i) == 0) /* caught already before, ... */ {
545 printf(_("Sorry You may change the Tag of non-empty partitions.\n"));
548 if (((sys != ENTIRE_DISK) && (sys != SGI_VOLHDR))
549 && (sgi_get_start_sector(i)<1)) {
551 _("It is highly recommended that the partition at offset 0\n"
552 "is of type \"SGI volhdr\", the IRIX system will rely on it to\n"
553 "retrieve from its directory standalone tools like sash and fx.\n"
554 "Only the \"SGI volume\" entire disk section may violate this.\n"
555 "Type YES if you are sure about tagging this partition differently.\n"));
556 if (strcmp (line_ptr, _("YES\n")))
559 sgilabel->partitions[i].id = SSWAP32(sys);
563 /* returns partition index of first entry marked as entire disk */
569 if (sgi_get_sysid(i) == SGI_VOLUME)
575 sgi_set_partition(int i, unsigned int start, unsigned int length, int sys) {
576 sgilabel->partitions[i].id = SSWAP32(sys);
577 sgilabel->partitions[i].num_sectors = SSWAP32(length);
578 sgilabel->partitions[i].start_sector = SSWAP32(start);
580 if (sgi_gaps() < 0) /* rebuild freelist */
581 printf(_("Do You know, You got a partition overlap on the disk?\n"));
585 sgi_set_entire(void) {
588 for (n=10; n<partitions; n++) {
589 if (!sgi_get_num_sectors(n)) {
590 sgi_set_partition(n, 0, sgi_get_lastblock(), SGI_VOLUME);
602 for (n=8; n<partitions; n++) {
603 if (!sgi_get_num_sectors(n)) {
605 * Choose same default volume header size
608 if (4096 < sgi_get_lastblock())
609 sgi_set_partition(n, 0, 4096, SGI_VOLHDR);
616 sgi_delete_partition(int i)
618 sgi_set_partition(i, 0, 0, 0);
622 sgi_add_partition(int n, int sys)
625 unsigned int first=0, last=0;
632 if (sgi_get_num_sectors(n)) {
633 printf(_("Partition %d is already defined. Delete "
634 "it before re-adding it.\n"), n + 1);
637 if ((sgi_entire() == -1)
638 && (sys != SGI_VOLUME)) {
639 printf(_("Attempting to generate entire disk entry automatically.\n"));
643 if ((sgi_gaps() == 0) && (sys != SGI_VOLUME)) {
644 printf(_("The entire disk is already covered with partitions.\n"));
647 if (sgi_gaps() < 0) {
648 printf(_("You got a partition overlap on the disk. Fix it first!\n"));
651 snprintf(mesg, sizeof(mesg), _("First %s"), str_units(SINGULAR));
653 if (sys == SGI_VOLUME) {
654 last = sgi_get_lastblock();
655 first = read_int(0, 0, last-1, 0, mesg);
657 printf(_("It is highly recommended that eleventh partition\n"
658 "covers the entire disk and is of type `SGI volume'\n"));
661 first = freelist[0].first;
662 last = freelist[0].last;
663 first = read_int(scround(first), scround(first), scround(last)-1,
666 if (display_in_cyl_units)
667 first *= units_per_sector;
669 first = first; /* align to cylinder if you know how ... */
671 last = isinfreelist(first);
673 printf(_("You will get a partition overlap on the disk. "
678 snprintf(mesg, sizeof(mesg), _(" Last %s"), str_units(SINGULAR));
679 last = read_int(scround(first), scround(last)-1, scround(last)-1,
680 scround(first), mesg)+1;
681 if (display_in_cyl_units)
682 last *= units_per_sector;
684 last = last; /* align to cylinder if You know how ... */
685 if ((sys == SGI_VOLUME) && (first != 0 || last != sgi_get_lastblock()))
686 printf(_("It is highly recommended that eleventh partition\n"
687 "covers the entire disk and is of type `SGI volume'\n"));
688 sgi_set_partition(n, first, last-first, sys);
692 create_sgilabel(void)
694 struct hd_geometry geometry;
701 unsigned long long llsectors;
702 int res; /* the result from the ioctl */
703 int sec_fac; /* the sector factor */
705 sec_fac = sector_size / 512; /* determine the sector factor */
708 _("Building a new SGI disklabel. Changes will remain in memory only,\n"
709 "until you decide to write them. After that, of course, the previous\n"
710 "content will be unrecoverably lost.\n\n"));
712 other_endian = (BYTE_ORDER == LITTLE_ENDIAN);
714 res = blkdev_get_sectors(fd, &llsectors);
717 if (!ioctl(fd, HDIO_GETGEO, &geometry)) {
718 heads = geometry.heads;
719 sectors = geometry.sectors;
721 /* the get device size ioctl was successful */
722 unsigned long long llcyls;
723 llcyls = llsectors / (heads * sectors * sec_fac);
725 if (cylinders != llcyls) /* truncated? */
728 /* otherwise print error and use truncated version */
729 cylinders = geometry.cylinders;
731 _("Warning: BLKGETSIZE ioctl failed on %s. "
732 "Using geometry cylinder value of %d.\n"
733 "This value may be truncated for devices"
734 " > 33.8 GB.\n"), disk_device, cylinders);
738 for (i = 0; i < 4; i++) {
740 if (valid_part_table_flag(MBRbuffer)) {
741 if (get_part_table(i)->sys_ind) {
742 old[i].sysid = get_part_table(i)->sys_ind;
743 old[i].start = get_start_sect(get_part_table(i));
744 old[i].nsect = get_nr_sects(get_part_table(i));
745 printf(_("Trying to keep parameters of partition %d.\n"), i);
747 printf(_("ID=%02x\tSTART=%d\tLENGTH=%d\n"),
748 old[i].sysid, old[i].start, old[i].nsect);
753 memset(MBRbuffer, 0, sizeof(MBRbuffer));
754 sgilabel->magic = SSWAP32(SGI_LABEL_MAGIC);
755 sgilabel->boot_part = SSWAP16(0);
756 sgilabel->swap_part = SSWAP16(1);
758 /* sizeof(sgilabel->boot_file) = 16 > 6 */
759 memset(sgilabel->boot_file, 0, 16);
760 strcpy((char *) sgilabel->boot_file, "/unix");
762 sgilabel->devparam.skew = (0);
763 sgilabel->devparam.gap1 = (0);
764 sgilabel->devparam.gap2 = (0);
765 sgilabel->devparam.sparecyl = (0);
766 sgilabel->devparam.pcylcount = SSWAP16(geometry.cylinders);
767 sgilabel->devparam.head_vol0 = SSWAP16(0);
768 sgilabel->devparam.ntrks = SSWAP16(geometry.heads);
769 /* tracks/cylinder (heads) */
770 sgilabel->devparam.cmd_tag_queue_depth = (0);
771 sgilabel->devparam.unused0 = (0);
772 sgilabel->devparam.unused1 = SSWAP16(0);
773 sgilabel->devparam.nsect = SSWAP16(geometry.sectors);
775 sgilabel->devparam.bytes = SSWAP16(sector_size);
776 sgilabel->devparam.ilfact = SSWAP16(1);
777 sgilabel->devparam.flags = SSWAP32(TRACK_FWD|\
778 IGNORE_ERRORS|RESEEK);
779 sgilabel->devparam.datarate = SSWAP32(0);
780 sgilabel->devparam.retries_on_error = SSWAP32(1);
781 sgilabel->devparam.ms_per_word = SSWAP32(0);
782 sgilabel->devparam.xylogics_gap1 = SSWAP16(0);
783 sgilabel->devparam.xylogics_syncdelay = SSWAP16(0);
784 sgilabel->devparam.xylogics_readdelay = SSWAP16(0);
785 sgilabel->devparam.xylogics_gap2 = SSWAP16(0);
786 sgilabel->devparam.xylogics_readgate = SSWAP16(0);
787 sgilabel->devparam.xylogics_writecont = SSWAP16(0);
788 memset(&(sgilabel->directory), 0, sizeof(struct volume_directory)*15);
789 memset(&(sgilabel->partitions), 0, sizeof(struct sgi_partition)*16);
795 for (i = 0; i < 4; i++) {
797 sgi_set_partition(i, old[i].start, old[i].nsect, old[i].sysid);
805 /* do nothing in the beginning */
811 /* do nothing in the beginning */
815 sgi_set_pcylcount(void)
817 /* do nothing in the beginning */
823 /* do nothing in the beginning */
829 /* do nothing in the beginning */
832 /* _____________________________________________________________
838 sgiinfo*info=calloc(1, sizeof(sgiinfo));
839 info->magic=SSWAP32(SGI_INFO_MAGIC);
840 info->b1=SSWAP32(-1);
841 info->b2=SSWAP16(-1);
843 /* You may want to replace this string !!!!!!! */
844 strcpy((char *) info->scsi_string, "IBM OEM 0662S12 3 30");
845 strcpy((char *) info->serial, "0000");
846 info->check1816 = SSWAP16(18*256 +16);
847 strcpy((char *) info->installer, "Sfx version 5.3, Oct 18, 1994");