Chameleon

Chameleon Svn Source Tree

Root/branches/rekursor/i386/boot2/options.c

1/*
2 * Copyright (c) 1999-2004 Apple Computer, Inc. All rights reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * Portions Copyright (c) 1999-2004 Apple Computer, Inc. All Rights
7 * Reserved. This file contains Original Code and/or Modifications of
8 * Original Code as defined in and that are subject to the Apple Public
9 * Source License Version 2.0 (the "License"). You may not use this file
10 * except in compliance with the License. Please obtain a copy of the
11 * License at http://www.apple.com/publicsource and read it before using
12 * this file.
13 *
14 * The Original Code and all software distributed under the License are
15 * distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, EITHER
16 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
17 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE OR NON- INFRINGEMENT. Please see the
19 * License for the specific language governing rights and limitations
20 * under the License.
21 *
22 * @APPLE_LICENSE_HEADER_END@
23 */
24
25#include "boot.h"
26#include "bootstruct.h"
27#include "fdisk.h"
28#include "ramdisk.h"
29#include "gui.h"
30#include "embedded.h"
31#include "pci.h"
32
33static bool shouldboot = false;
34
35extern int multiboot_timeout;
36extern int multiboot_timeout_set;
37
38extern BVRef bvChain;
39//extern intmenucount;
40
41extern intgDeviceCount;
42
43intselectIndex = 0;
44MenuItem * menuItems = NULL;
45
46enum {
47 kMenuTopRow = 5,
48 kMenuMaxItems = 10,
49 kScreenLastRow = 24
50};
51
52//==========================================================================
53
54typedef struct {
55 int x;
56 int y;
57 int type;
58} CursorState;
59
60static void changeCursor( int col, int row, int type, CursorState * cs )
61{
62 if (cs) getCursorPositionAndType( &cs->x, &cs->y, &cs->type );
63 setCursorType( type );
64 setCursorPosition( col, row, 0 );
65}
66
67static void moveCursor( int col, int row )
68{
69 setCursorPosition( col, row, 0 );
70}
71
72static void restoreCursor( const CursorState * cs )
73{
74 setCursorPosition( cs->x, cs->y, 0 );
75 setCursorType( cs->type );
76}
77
78//==========================================================================
79
80/* Flush keyboard buffer; returns TRUE if any of the flushed
81 * characters was F8.
82 */
83
84static bool flushKeyboardBuffer(void)
85{
86 bool status = false;
87
88 while ( readKeyboardStatus() ) {
89 if (bgetc() == 0x4200) status = true;
90 }
91 return status;
92}
93
94//==========================================================================
95
96static int countdown( const char * msg, int row, int timeout )
97{
98 unsigned long time;
99 int ch = 0;
100 int col = strlen(msg) + 1;
101
102 flushKeyboardBuffer();
103
104if( bootArgs->Video.v_display == VGA_TEXT_MODE )
105{
106moveCursor( 0, row );
107printf(msg);
108
109} else {
110
111position_t p = pos( gui.screen.width / 2 + 1 , ( gui.devicelist.pos.y + 3 ) + ( ( gui.devicelist.height - gui.devicelist.iconspacing ) / 2 ) );
112
113char dummy[80];
114getBootVolumeDescription( gBootVolume, dummy, 80, true );
115drawDeviceIcon( gBootVolume, gui.screen.pixmap, p );
116drawStrCenteredAt( (char *) msg, &font_small, gui.screen.pixmap, gui.countdown.pos );
117
118// make this screen the new background
119memcpy( gui.backbuffer->pixels, gui.screen.pixmap->pixels, gui.backbuffer->width * gui.backbuffer->height * 4 );
120
121}
122
123int multi_buff = 18 * (timeout);
124 int multi = ++multi_buff;
125
126 int lasttime=0;
127
128 for ( time = time18(), timeout++; timeout > 0; )
129 {
130if( time18() > lasttime)
131{
132multi--;
133lasttime=time18();
134}
135
136 if (ch = readKeyboardStatus())
137 break;
138
139 // Count can be interrupted by holding down shift,
140 // control or alt key
141 if ( ( readKeyboardShiftFlags() & 0x0F ) != 0 )
142{
143 ch = 1;
144 break;
145 }
146
147 if ( time18() >= time )
148 {
149 time += 18;
150 timeout--;
151
152if( bootArgs->Video.v_display == VGA_TEXT_MODE )
153{
154moveCursor( col, row );
155printf("(%d) ", timeout);
156}
157 }
158
159if( bootArgs->Video.v_display == GRAPHICS_MODE )
160{
161drawProgressBar( gui.screen.pixmap, 100, gui.progressbar.pos , ( multi * 100 / multi_buff ) );
162gui.redraw = true;
163updateVRAM();
164}
165
166 }
167
168 flushKeyboardBuffer();
169
170 return ch;
171}
172
173//==========================================================================
174
175static char gBootArgs[BOOT_STRING_LEN];
176static char * gBootArgsPtr = gBootArgs;
177static char * gBootArgsEnd = gBootArgs + BOOT_STRING_LEN - 1;
178static char booterCommand[BOOT_STRING_LEN];
179static char booterParam[BOOT_STRING_LEN];
180
181static void clearBootArgs(void)
182{
183gBootArgsPtr = gBootArgs;
184memset(gBootArgs, '\0', BOOT_STRING_LEN);
185
186if (bootArgs->Video.v_display == GRAPHICS_MODE) {
187clearGraphicBootPrompt();
188}
189}
190
191//==========================================================================
192
193static void showBootPrompt(int row, bool visible)
194{
195extern char bootPrompt[];
196extern char bootRescanPrompt[];
197
198if( bootArgs->Video.v_display == VGA_TEXT_MODE ) {
199changeCursor( 0, row, kCursorTypeUnderline, 0 );
200clearScreenRows( row, kScreenLastRow );
201}
202
203clearBootArgs();
204
205if (visible) {
206if (bootArgs->Video.v_display == VGA_TEXT_MODE) {
207if (gEnableCDROMRescan) {
208printf( bootRescanPrompt );
209} else {
210printf( bootPrompt );
211}
212}
213} else {
214if (bootArgs->Video.v_display == GRAPHICS_MODE) {
215clearGraphicBootPrompt();
216} else {
217printf("Press Enter to start up the foreign OS. ");
218}
219}
220}
221
222//==========================================================================
223
224static void updateBootArgs( int key )
225{
226 key &= kASCIIKeyMask;
227
228 switch ( key )
229 {
230 case kBackspaceKey:
231 if ( gBootArgsPtr > gBootArgs )
232 {
233 int x, y, t;
234 getCursorPositionAndType( &x, &y, &t );
235 if ( x == 0 && y )
236 {
237 x = 80; y--;
238 }
239 if (x)
240x--;
241if( bootArgs->Video.v_display == VGA_TEXT_MODE )
242{
243setCursorPosition( x, y, 0 );
244putca(' ', 0x07, 1);
245} else
246updateGraphicBootPrompt(kBackspaceKey);
247
248*gBootArgsPtr-- = '\0';
249}
250
251break;
252
253 default:
254 if ( key >= ' ' && gBootArgsPtr < gBootArgsEnd)
255 {
256if( bootArgs->Video.v_display == VGA_TEXT_MODE )
257putchar(key); // echo to screen
258else
259updateGraphicBootPrompt(key);
260*gBootArgsPtr++ = key;
261}
262
263break;
264 }
265}
266
267//==========================================================================
268
269static const MenuItem * gMenuItems = NULL;
270
271static int gMenuItemCount;
272static int gMenuRow;
273static int gMenuHeight;
274static int gMenuTop;
275static int gMenuBottom;
276static int gMenuSelection;
277
278static int gMenuStart;
279static int gMenuEnd;
280
281static void printMenuItem( const MenuItem * item, int highlight )
282{
283 printf(" ");
284
285 if ( highlight )
286 putca(' ', 0x70, strlen(item->name) + 4);
287 else
288 putca(' ', 0x07, 40);
289
290 printf(" %40s\n", item->name);
291}
292
293//==========================================================================
294
295static void showMenu( const MenuItem * items, int count,
296 int selection, int row, int height )
297{
298 int i;
299 CursorState cursorState;
300
301 if ( items == NULL || count == 0 )
302return;
303
304 // head and tail points to the start and the end of the list.
305 // top and bottom points to the first and last visible items
306 // in the menu window.
307
308 gMenuItems= items;
309 gMenuRow= row;
310 gMenuHeight= height;
311 gMenuItemCount= count;
312 gMenuTop= 0;
313 gMenuBottom= min( count, height ) - 1;
314 gMenuSelection= selection;
315
316 gMenuStart= 0;
317 gMenuEnd = min( count, gui.maxdevices ) - 1;
318
319// If the selected item is not visible, shift the list down.
320
321 if ( gMenuSelection > gMenuBottom )
322 {
323 gMenuTop += ( gMenuSelection - gMenuBottom );
324 gMenuBottom = gMenuSelection;
325 }
326
327if ( gMenuSelection > gMenuEnd )
328 {
329gMenuStart += ( gMenuSelection - gMenuEnd );
330 gMenuEnd = gMenuSelection;
331 }
332
333// Draw the visible items.
334
335if( bootArgs->Video.v_display == GRAPHICS_MODE )
336
337drawDeviceList(gMenuStart, gMenuEnd, gMenuSelection);
338
339else {
340
341changeCursor( 0, row, kCursorTypeHidden, &cursorState );
342
343for ( i = gMenuTop; i <= gMenuBottom; i++ )
344{
345printMenuItem( &items[i], (i == gMenuSelection) );
346}
347
348restoreCursor( &cursorState );
349 }
350}
351
352//==========================================================================
353
354static int updateMenu( int key, void ** paramPtr )
355{
356 int moved = 0;
357
358 union {
359 struct {
360 unsigned int
361 selectionUp : 1,
362 selectionDown : 1,
363 scrollUp : 1,
364 scrollDown : 1;
365 } f;
366 unsigned int w;
367 } draw = {{0}};
368
369 if ( gMenuItems == NULL )
370return 0;
371
372if( bootArgs->Video.v_display == GRAPHICS_MODE )
373{
374int res;
375
376// set navigation keys for horizontal layout as defaults
377int previous= 0x4B00;// left arrow
378int subsequent= 0x4D00;// right arrow
379int menu= 0x5000;// down arrow
380
381if ( gui.layout == VerticalLayout )
382{
383// set navigation keys for vertical layout
384previous= 0x4800;// up arrow
385subsequent= 0x5000;// down arrow
386menu= 0x4B00;// right arrow
387}
388
389if ( key == previous )
390{
391if ( gMenuSelection > gMenuTop )
392draw.f.selectionUp = 1;
393else if ( gMenuTop > 0 )
394draw.f.scrollDown = 1;
395
396}
397
398else if ( key == subsequent )
399{
400if ( gMenuSelection != gMenuBottom)
401draw.f.selectionDown = 1;
402else if ( gMenuBottom < ( gMenuItemCount - 1 ) )
403draw.f.scrollUp = 1;
404}
405
406else if ( key == menu )
407{
408if ( gui.menu.draw )
409updateInfoMenu(key);
410else
411drawInfoMenu();
412}
413
414else if ( gui.menu.draw )
415{
416res = updateInfoMenu(key);
417
418if ( res == CLOSE_INFO_MENU )
419gui.menu.draw = false;
420else
421{
422shouldboot = ( res != DO_NOT_BOOT );
423
424if ( shouldboot )
425gui.menu.draw = false;
426
427switch (res)
428{
429case BOOT_NORMAL:
430gVerboseMode = false;
431gBootMode = kBootModeNormal;
432break;
433
434case BOOT_VERBOSE:
435gVerboseMode = true;
436gBootMode = kBootModeNormal;
437*gBootArgsPtr++ = '-';
438*gBootArgsPtr++ = 'v';
439break;
440
441case BOOT_IGNORECACHE:
442gVerboseMode = false;
443gBootMode = kBootModeNormal;
444*gBootArgsPtr++ = '-';
445*gBootArgsPtr++ = 'f';
446break;
447
448case BOOT_SINGLEUSER:
449gVerboseMode = true;
450gBootMode = kBootModeNormal;
451*gBootArgsPtr++ = '-';
452*gBootArgsPtr++ = 's';
453break;
454}
455
456}
457
458}
459
460} else {
461switch ( key )
462{
463 case 0x4800: // Up Arrow
464if ( gMenuSelection != gMenuTop )
465draw.f.selectionUp = 1;
466else if ( gMenuTop > 0 )
467draw.f.scrollDown = 1;
468break;
469
470case 0x5000: // Down Arrow
471if ( gMenuSelection != gMenuBottom )
472draw.f.selectionDown = 1;
473else if ( gMenuBottom < (gMenuItemCount - 1) )
474draw.f.scrollUp = 1;
475break;
476}
477}
478
479 if ( draw.w )
480 {
481 if ( draw.f.scrollUp )
482 {
483 scollPage(0, gMenuRow, 40, gMenuRow + gMenuHeight - 1, 0x07, 1, 1);
484 gMenuTop++; gMenuBottom++;
485gMenuStart++; gMenuEnd++;
486 draw.f.selectionDown = 1;
487 }
488
489 if ( draw.f.scrollDown )
490 {
491 scollPage(0, gMenuRow, 40, gMenuRow + gMenuHeight - 1, 0x07, 1, -1);
492 gMenuTop--; gMenuBottom--;
493 gMenuStart--; gMenuEnd--;
494 draw.f.selectionUp = 1;
495 }
496
497 if ( draw.f.selectionUp || draw.f.selectionDown )
498 {
499
500CursorState cursorState;
501
502// Set cursor at current position, and clear inverse video.
503
504if( bootArgs->Video.v_display == VGA_TEXT_MODE )
505{
506changeCursor( 0, gMenuRow + gMenuSelection - gMenuTop, kCursorTypeHidden, &cursorState );
507printMenuItem( &gMenuItems[gMenuSelection], 0 );
508}
509
510if ( draw.f.selectionUp )
511{
512gMenuSelection--;
513if(( gMenuSelection - gMenuStart) == -1 )
514{
515gMenuStart--;
516gMenuEnd--;
517}
518
519} else {
520gMenuSelection++;
521if(( gMenuSelection - ( gui.maxdevices - 1) - gMenuStart) > 0 )
522{
523gMenuStart++;
524gMenuEnd++;
525}
526 }
527
528if( bootArgs->Video.v_display == VGA_TEXT_MODE )
529 {
530moveCursor( 0, gMenuRow + gMenuSelection - gMenuTop );
531printMenuItem( &gMenuItems[gMenuSelection], 1 );
532restoreCursor( &cursorState );
533
534 } else
535
536drawDeviceList (gMenuStart, gMenuEnd, gMenuSelection);
537
538}
539
540 *paramPtr = gMenuItems[gMenuSelection].param;
541 moved = 1;
542 }
543
544return moved;
545}
546
547//==========================================================================
548
549static void skipblanks( const char ** cpp )
550{
551 while ( **(cpp) == ' ' || **(cpp) == '\t' ) ++(*cpp);
552}
553
554//==========================================================================
555
556static const char * extractKernelName( char ** cpp )
557{
558 char * kn = *cpp;
559 char * cp = *cpp;
560 char c;
561
562 // Convert char to lower case.
563
564 c = *cp | 0x20;
565
566 // Must start with a letter or a '/'.
567
568 if ( (c < 'a' || c > 'z') && ( c != '/' ) )
569 return 0;
570
571 // Keep consuming characters until we hit a separator.
572
573 while ( *cp && (*cp != '=') && (*cp != ' ') && (*cp != '\t') )
574 cp++;
575
576 // Only SPACE or TAB separator is accepted.
577 // Reject everything else.
578
579 if (*cp == '=')
580 return 0;
581
582 // Overwrite the separator, and move the pointer past
583 // the kernel name.
584
585 if (*cp != '\0') *cp++ = '\0';
586 *cpp = cp;
587
588 return kn;
589}
590
591//==========================================================================
592
593static void
594printMemoryInfo(void)
595{
596 int line;
597 int i;
598 MemoryRange *mp = bootInfo->memoryMap;
599
600 // Activate and clear page 1
601 setActiveDisplayPage(1);
602 clearScreenRows(0, 24);
603 setCursorPosition( 0, 0, 1 );
604
605 printf("BIOS reported memory ranges:\n");
606 line = 1;
607 for (i=0; i<bootInfo->memoryMapCount; i++) {
608 printf("Base 0x%08x%08x, ",
609 (unsigned long)(mp->base >> 32),
610 (unsigned long)(mp->base));
611 printf("length 0x%08x%08x, type %d\n",
612 (unsigned long)(mp->length >> 32),
613 (unsigned long)(mp->length),
614 mp->type);
615 if (line++ > 20) {
616 printf("(Press a key to continue...)");
617 getc();
618 line = 0;
619 }
620 mp++;
621 }
622 if (line > 0) {
623 printf("(Press a key to continue...)");
624 getc();
625 }
626
627 setActiveDisplayPage(0);
628}
629
630char *getMemoryInfoString()
631{
632 int i;
633 MemoryRange *mp = bootInfo->memoryMap;
634char *buff = malloc(sizeof(char)*1024);
635if(!buff) return 0;
636
637char info[] = "BIOS reported memory ranges:\n";
638sprintf(buff, "%s", info);
639 for (i=0; i<bootInfo->memoryMapCount; i++) {
640 sprintf( buff+strlen(buff), "Base 0x%08x%08x, ",
641 (unsigned long)(mp->base >> 32),
642 (unsigned long)(mp->base));
643 sprintf( buff+strlen(buff), "length 0x%08x%08x, type %d\n",
644 (unsigned long)(mp->length >> 32),
645 (unsigned long)(mp->length),
646 mp->type);
647 mp++;
648 }
649return buff;
650}
651
652//==========================================================================
653
654void lspci(void)
655{
656if (bootArgs->Video.v_display == VGA_TEXT_MODE) {
657setActiveDisplayPage(1);
658clearScreenRows(0, 24);
659setCursorPosition(0, 0, 1);
660}
661
662dump_pci_dt(root_pci_dev->children);
663
664printf("(Press a key to continue...)");
665getc();
666
667if (bootArgs->Video.v_display == VGA_TEXT_MODE) {
668setActiveDisplayPage(0);
669}
670}
671
672//==========================================================================
673
674int getBootOptions(bool firstRun)
675{
676int i;
677int key;
678int nextRow;
679int timeout;
680int bvCount;
681BVRef bvr;
682BVRef menuBVR;
683bool showPrompt, newShowPrompt, isCDROM;
684
685// Initialize default menu selection entry.
686gBootVolume = menuBVR = selectBootVolume(bvChain);
687
688if (biosDevIsCDROM(gBIOSDev)) {
689isCDROM = true;
690} else {
691isCDROM = false;
692}
693
694// ensure we're in graphics mode if gui is setup
695if (gui.initialised) {
696if (bootArgs->Video.v_display == VGA_TEXT_MODE) {
697setVideoMode(GRAPHICS_MODE, 0);
698}
699}
700
701// Allow user to override default timeout.
702if (multiboot_timeout_set) {
703timeout = multiboot_timeout;
704} else if (!getIntForKey(kTimeoutKey, &timeout, &bootInfo->bootConfig)) {
705/* If there is no timeout key in the file use the default timeout
706 which is different for CDs vs. hard disks. However, if not booting
707 a CD and no config file could be loaded set the timeout
708 to zero which causes the menu to display immediately.
709 This way, if no partitions can be found, that is the disk is unpartitioned
710 or simply cannot be read) then an empty menu is displayed.
711 If some partitions are found, for example a Windows partition, then
712 these will be displayed in the menu as foreign partitions.
713 */
714if (isCDROM) {
715timeout = kCDBootTimeout;
716} else {
717timeout = sysConfigValid ? kBootTimeout : 0;
718}
719}
720
721if (timeout < 0) {
722gBootMode |= kBootModeQuiet;
723}
724
725// If the user is holding down a modifier key, enter safe mode.
726if ((readKeyboardShiftFlags() & 0x0F) != 0) {
727 gBootMode |= kBootModeSafe;
728}
729
730
731/*
732Patch from 18seven & modified by me to make it even more mac like and to include couple of commands
733
734* Bootargs keyboard shortcut
735* Keys were obtaiend from ApplePS2ToADBMap.h
736* F8 abort quiet mode, and display the menu.
737* alt+f old safe mode
738* shift+f ignore boot configuration file
739* alt+s single user mode
740* alt+v verbose (in mac its command +v)
741* alt+x safe mode (aka boot args with -x , in macs its command +x)
742* alt+l legacy mode (not sure why you need this)
743* 6 +4 = 64-bit
744* 3 + 2 = 32-bit
745*/
746clearBootArgs();
747{
748bool f8 = false, altf = false, shiftf = false, alts = false,
749 altv = false, x32 = false, x64 = false, altx = false;
750int key;
751while (readKeyboardStatus()) {
752key = bgetc ();
753if (key == 0x4200) f8 = true;
754if (key == 0x2100) altf = true;
755if (key == 0x2146) shiftf = true;
756if (key == 0x1F00) alts = true;
757if (key == 0x2F00) altv = true;
758if (key == 0x2D00) altx = true;
759if (key == 0x0403) x32 = true;
760if (key == 0x0705) x64 = true;
761}
762if (f8) {
763gBootMode &= ~kBootModeQuiet;
764timeout = 0;
765}
766if ((altf) && (gBootArgsPtr + 3 < gBootArgsEnd)) {
767*(gBootArgsPtr++) = ' ';
768*(gBootArgsPtr++) = '-';
769*(gBootArgsPtr++) = 'f';
770}
771if ((shiftf) && (gBootArgsPtr + 3 < gBootArgsEnd)) {
772*(gBootArgsPtr++) = ' ';
773*(gBootArgsPtr++) = '-';
774*(gBootArgsPtr++) = 'F';
775}
776if ((alts) && (gBootArgsPtr + 3 < gBootArgsEnd)) {
777*(gBootArgsPtr++) = ' ';
778*(gBootArgsPtr++) = '-';
779*(gBootArgsPtr++) = 's';
780}
781if ((altv) && (gBootArgsPtr + 3 < gBootArgsEnd)) {
782*(gBootArgsPtr++) = ' ';
783*(gBootArgsPtr++) = '-';
784*(gBootArgsPtr++) = 'v';
785}
786if ((altx) && (gBootArgsPtr + 3 < gBootArgsEnd)) {
787*(gBootArgsPtr++) = ' ';
788*(gBootArgsPtr++) = '-';
789*(gBootArgsPtr++) = 'x';
790}
791if ((x32) && (gBootArgsPtr + 5 < gBootArgsEnd)) { // Boot into 32-bit Kernel
792*(gBootArgsPtr++) = ' ';
793*(gBootArgsPtr++) = '-';
794*(gBootArgsPtr++) = 'x';
795*(gBootArgsPtr++) = '3';
796*(gBootArgsPtr++) = '2';
797}
798
799if ((x64) && (gBootArgsPtr + 5 < gBootArgsEnd)) { // Boot into 64-bit Kernel (in case those who are using 32-bit and wanna try 64-bit)
800*(gBootArgsPtr++) = ' ';
801*(gBootArgsPtr++) = '-';
802*(gBootArgsPtr++) = 'x';
803*(gBootArgsPtr++) = '6';
804*(gBootArgsPtr++) = '4';
805}
806}
807
808if (bootArgs->Video.v_display == VGA_TEXT_MODE) {
809setCursorPosition(0, 0, 0);
810clearScreenRows(0, kScreenLastRow);
811if (!(gBootMode & kBootModeQuiet)) {
812// Display banner and show hardware info.
813printf(bootBanner, (bootInfo->convmem + bootInfo->extmem) / 1024);
814printf(getVBEInfoString());
815}
816changeCursor(0, kMenuTopRow, kCursorTypeUnderline, 0);
817verbose("Scanning device %x...", gBIOSDev);
818}
819
820// When booting from CD, default to hard drive boot when possible.
821if (isCDROM && firstRun) {
822const char *val;
823char *prompt;
824char *name;
825int cnt;
826int optionKey;
827
828if (getValueForKey(kCDROMPromptKey, &val, &cnt, &bootInfo->bootConfig)) {
829cnt += 1;
830prompt = malloc(cnt);
831strlcpy(prompt, val, cnt);
832} else {
833name = malloc(80);
834getBootVolumeDescription(gBootVolume, name, 80, false);
835prompt = malloc(256);
836sprintf(prompt, "Press any key to start up from %s, or press F8 to enter startup options.", name);
837free(name);
838cnt = 0;
839}
840
841if (getIntForKey( kCDROMOptionKey, &optionKey, &bootInfo->bootConfig )) {
842// The key specified is a special key.
843} else if (getValueForKey( kCDROMOptionKey, &val, &cnt, &bootInfo->bootConfig ) && cnt >= 1) {
844optionKey = val[0];
845} else {
846// Default to F8.
847optionKey = 0x4200;
848}
849
850// If the timeout is zero then it must have been set above due to the
851// early catch of F8 which means the user wants to set boot options
852// which we ought to interpret as meaning he wants to boot the CD.
853if (timeout != 0) {
854key = countdown(prompt, kMenuTopRow, timeout);
855} else {
856key = optionKey;
857}
858
859if (cnt) {
860free(prompt);
861}
862
863clearScreenRows( kMenuTopRow, kMenuTopRow + 2 );
864
865// Hit the option key ?
866if (key == optionKey) {
867gBootMode &= ~kBootModeQuiet;
868timeout = 0;
869} else {
870key = key & 0xFF;
871
872// Try booting hard disk if user pressed 'h'
873if (biosDevIsCDROM(gBIOSDev) && key == 'h') {
874BVRef bvr;
875
876// Look at partitions hosting OS X other than the CD-ROM
877for (bvr = bvChain; bvr; bvr=bvr->next) {
878if ((bvr->flags & kBVFlagSystemVolume) && bvr->biosdev != gBIOSDev) {
879gBootVolume = bvr;
880}
881}
882}
883goto done;
884}
885}
886
887if (gBootMode & kBootModeQuiet) {
888// No input allowed from user.
889goto done;
890}
891
892if (firstRun && timeout > 0 && countdown("Press any key to enter startup options.", kMenuTopRow, timeout) == 0) {
893// If the user is holding down a modifier key,
894// enter safe mode.
895if ((readKeyboardShiftFlags() & 0x0F) != 0) {
896gBootMode |= kBootModeSafe;
897}
898goto done;
899}
900
901if (gDeviceCount) {
902// Allocate memory for an array of menu items.
903menuItems = malloc(sizeof(MenuItem) * gDeviceCount);
904if (menuItems == NULL) {
905goto done;
906}
907
908// Associate a menu item for each BVRef.
909for (bvr=bvChain, i=gDeviceCount-1, selectIndex=0; bvr; bvr=bvr->next) {
910if (bvr->visible) {
911getBootVolumeDescription(bvr, menuItems[i].name, 80, true);
912menuItems[i].param = (void *) bvr;
913if (bvr == menuBVR) {
914selectIndex = i;
915}
916i--;
917}
918}
919}
920
921if (bootArgs->Video.v_display == GRAPHICS_MODE) {
922// redraw the background buffer
923drawBackground();
924gui.devicelist.draw = true;
925gui.redraw = true;
926if (!(gBootMode & kBootModeQuiet)) {
927bool showBootBanner = true;
928
929// Check if "Boot Banner"=N switch is present in config file.
930getBoolForKey(kBootBannerKey, &showBootBanner, &bootInfo->bootConfig);
931if (showBootBanner) {
932// Display banner and show hardware info.
933gprintf(&gui.screen, bootBanner + 1, (bootInfo->convmem + bootInfo->extmem) / 1024);
934}
935
936// redraw background
937memcpy(gui.backbuffer->pixels, gui.screen.pixmap->pixels, gui.backbuffer->width * gui.backbuffer->height * 4);
938}
939} else {
940// Clear screen and hide the blinking cursor.
941clearScreenRows(kMenuTopRow, kMenuTopRow + 2);
942changeCursor(0, kMenuTopRow, kCursorTypeHidden, 0);
943}
944
945nextRow = kMenuTopRow;
946showPrompt = true;
947
948if (gDeviceCount) {
949if( bootArgs->Video.v_display == VGA_TEXT_MODE ) {
950printf("Use \30\31 keys to select the startup volume.");
951}
952showMenu( menuItems, gDeviceCount, selectIndex, kMenuTopRow + 2, kMenuMaxItems );
953nextRow += min( gDeviceCount, kMenuMaxItems ) + 3;
954}
955
956// Show the boot prompt.
957showPrompt = (gDeviceCount == 0) || (menuBVR->flags & kBVFlagNativeBoot);
958showBootPrompt( nextRow, showPrompt );
959
960do {
961if (bootArgs->Video.v_display == GRAPHICS_MODE) {
962// redraw background
963memcpy( gui.backbuffer->pixels, gui.screen.pixmap->pixels, gui.backbuffer->width * gui.backbuffer->height * 4 );
964// reset cursor co-ords
965gui.debug.cursor = pos( gui.screen.width - 160 , 10 );
966}
967key = getc();
968updateMenu( key, (void **) &menuBVR );
969newShowPrompt = (gDeviceCount == 0) || (menuBVR->flags & kBVFlagNativeBoot);
970
971if (newShowPrompt != showPrompt) {
972showPrompt = newShowPrompt;
973showBootPrompt( nextRow, showPrompt );
974}
975
976if (showPrompt) {
977updateBootArgs(key);
978}
979
980switch (key) {
981case kReturnKey:
982if (gui.menu.draw) {
983key=0;
984break;
985}
986if (*gBootArgs == '?') {
987char * argPtr = gBootArgs;
988
989// Skip the leading "?" character.
990argPtr++;
991getNextArg(&argPtr, booterCommand);
992getNextArg(&argPtr, booterParam);
993
994/*
995* TODO: this needs to be refactored.
996*/
997if (strcmp( booterCommand, "video" ) == 0) {
998if (bootArgs->Video.v_display == GRAPHICS_MODE) {
999showInfoBox(getVBEInfoString(), getVBEModeInfoString());
1000} else {
1001printVBEModeInfo();
1002}
1003} else if ( strcmp( booterCommand, "memory" ) == 0) {
1004if (bootArgs->Video.v_display == GRAPHICS_MODE ) {
1005showInfoBox("Memory Map", getMemoryInfoString());
1006} else {
1007printMemoryInfo();
1008}
1009} else if (strcmp(booterCommand, "lspci") == 0) {
1010lspci();
1011} else if (strcmp(booterCommand, "more") == 0) {
1012showTextFile(booterParam);
1013} else if (strcmp(booterCommand, "rd") == 0) {
1014processRAMDiskCommand(&argPtr, booterParam);
1015} else if (strcmp(booterCommand, "norescan") == 0) {
1016if (gEnableCDROMRescan) {
1017gEnableCDROMRescan = false;
1018break;
1019}
1020} else {
1021showHelp();
1022}
1023key = 0;
1024showBootPrompt(nextRow, showPrompt);
1025break;
1026}
1027gBootVolume = menuBVR;
1028setRootVolume(menuBVR);
1029gBIOSDev = menuBVR->biosdev;
1030break;
1031
1032case kEscapeKey:
1033clearBootArgs();
1034break;
1035
1036case kF5Key:
1037// New behavior:
1038// Clear gBootVolume to restart the loop
1039// if the user enabled rescanning the optical drive.
1040// Otherwise boot the default boot volume.
1041if (gEnableCDROMRescan) {
1042gBootVolume = NULL;
1043clearBootArgs();
1044}
1045break;
1046
1047case kF10Key:
1048gScanSingleDrive = false;
1049scanDisks(gBIOSDev, &bvCount);
1050gBootVolume = NULL;
1051clearBootArgs();
1052break;
1053
1054case kTabKey:
1055// New behavior:
1056// Switch between text & graphic interfaces
1057// Only Permitted if started in graphics interface
1058if (useGUI) {
1059if (bootArgs->Video.v_display == GRAPHICS_MODE) {
1060setVideoMode(VGA_TEXT_MODE, 0);
1061
1062setCursorPosition(0, 0, 0);
1063clearScreenRows(0, kScreenLastRow);
1064
1065// Display banner and show hardware info.
1066printf(bootBanner, (bootInfo->convmem + bootInfo->extmem) / 1024);
1067printf(getVBEInfoString());
1068
1069clearScreenRows(kMenuTopRow, kMenuTopRow + 2);
1070changeCursor(0, kMenuTopRow, kCursorTypeHidden, 0);
1071
1072nextRow = kMenuTopRow;
1073showPrompt = true;
1074
1075if (gDeviceCount) {
1076printf("Use \30\31 keys to select the startup volume.");
1077showMenu(menuItems, gDeviceCount, selectIndex, kMenuTopRow + 2, kMenuMaxItems);
1078nextRow += min(gDeviceCount, kMenuMaxItems) + 3;
1079}
1080
1081showPrompt = (gDeviceCount == 0) || (menuBVR->flags & kBVFlagNativeBoot);
1082showBootPrompt(nextRow, showPrompt);
1083//changeCursor( 0, kMenuTopRow, kCursorTypeUnderline, 0 );
1084} else {
1085gui.redraw = true;
1086setVideoMode(GRAPHICS_MODE, 0);
1087updateVRAM();
1088}
1089}
1090key = 0;
1091break;
1092
1093default:
1094key = 0;
1095break;
1096}
1097} while (0 == key);
1098
1099done:
1100if (bootArgs->Video.v_display == VGA_TEXT_MODE) {
1101clearScreenRows(kMenuTopRow, kScreenLastRow);
1102changeCursor(0, kMenuTopRow, kCursorTypeUnderline, 0);
1103}
1104shouldboot = false;
1105gui.menu.draw = false;
1106if (menuItems) {
1107free(menuItems);
1108menuItems = NULL;
1109}
1110return 0;
1111}
1112
1113//==========================================================================
1114
1115extern unsigned char chainbootdev;
1116extern unsigned char chainbootflag;
1117
1118bool copyArgument(const char *argName, const char *val, int cnt, char **argP, int *cntRemainingP)
1119{
1120 int argLen = argName ? strlen(argName) : 0;
1121 int len = argLen + cnt + 1; // +1 to account for space
1122
1123 if (len > *cntRemainingP) {
1124 error("Warning: boot arguments too long, truncating\n");
1125 return false;
1126 }
1127
1128 if (argName) {
1129 strncpy( *argP, argName, argLen );
1130 *argP += argLen;
1131 *argP[0] = '=';
1132 (*argP)++;
1133 len++; // +1 to account for '='
1134 }
1135 strncpy( *argP, val, cnt );
1136 *argP += cnt;
1137 *argP[0] = ' ';
1138 (*argP)++;
1139
1140 *cntRemainingP -= len;
1141 return true;
1142}
1143
1144//
1145// Returns TRUE if an argument was copied, FALSE otherwise
1146bool
1147processBootArgument(
1148 const char *argName, // The argument to search for
1149 const char *userString, // Typed-in boot arguments
1150 const char *kernelFlags, // Kernel flags from config table
1151 const char *configTable,
1152 char **argP, // Output value
1153 int *cntRemainingP, // Output count
1154 char *foundVal // found value
1155 )
1156{
1157 const char *val;
1158 int cnt;
1159 bool found = false;
1160
1161 if (getValueForBootKey(userString, argName, &val, &cnt)) {
1162 // Don't copy; these values will be copied at the end of argument processing.
1163 found = true;
1164 } else if (getValueForBootKey(kernelFlags, argName, &val, &cnt)) {
1165 // Don't copy; these values will be copied at the end of argument processing.
1166 found = true;
1167 } else if (getValueForKey(argName, &val, &cnt, &bootInfo->bootConfig)) {
1168 copyArgument(argName, val, cnt, argP, cntRemainingP);
1169 found = true;
1170 }
1171 if (found && foundVal) {
1172 strlcpy(foundVal, val, cnt+1);
1173 }
1174 return found;
1175}
1176
1177// Maximum config table value size
1178#define VALUE_SIZE 2048
1179
1180int
1181processBootOptions()
1182{
1183 const char * cp = gBootArgs;
1184 const char * val = 0;
1185 const char * kernel;
1186 int cnt;
1187 int userCnt;
1188 int cntRemaining;
1189 char * argP;
1190 char uuidStr[64];
1191 bool uuidSet = false;
1192 char * configKernelFlags;
1193 char * valueBuffer;
1194
1195 valueBuffer = malloc(VALUE_SIZE);
1196
1197 skipblanks( &cp );
1198
1199 // Update the unit and partition number.
1200
1201 if ( gBootVolume )
1202 {
1203 if (!( gBootVolume->flags & kBVFlagNativeBoot ))
1204 {
1205 readBootSector( gBootVolume->biosdev, gBootVolume->part_boff,
1206 (void *) 0x7c00 );
1207
1208 //
1209 // Setup edx, and signal intention to chain load the
1210 // foreign booter.
1211 //
1212
1213 chainbootdev = gBootVolume->biosdev;
1214 chainbootflag = 1;
1215
1216 return 1;
1217 }
1218
1219 setRootVolume(gBootVolume);
1220
1221 }
1222 // If no boot volume fail immediately because we're just going to fail
1223 // trying to load the config file anyway.
1224 else
1225 return -1;
1226
1227 // Load config table specified by the user, or use the default.
1228
1229 if (!getValueForBootKey(cp, "config", &val, &cnt)) {
1230 val = 0;
1231 cnt = 0;
1232 }
1233
1234 // Load com.apple.Boot.plist from the selected volume
1235 // and use its contents to override default bootConfig.
1236 // This is not a mandatory opeartion anymore.
1237
1238 loadOverrideConfig(&bootInfo->overrideConfig);
1239
1240 // Use the kernel name specified by the user, or fetch the name
1241 // in the config table, or use the default if not specified.
1242 // Specifying a kernel name on the command line, or specifying
1243 // a non-default kernel name in the config file counts as
1244 // overriding the kernel, which causes the kernelcache not
1245 // to be used.
1246
1247 gOverrideKernel = false;
1248 if (( kernel = extractKernelName((char **)&cp) )) {
1249 strcpy( bootInfo->bootFile, kernel );
1250 gOverrideKernel = true;
1251 } else {
1252 if ( getValueForKey( kKernelNameKey, &val, &cnt, &bootInfo->bootConfig ) ) {
1253 strlcpy( bootInfo->bootFile, val, cnt+1 );
1254 if (strcmp( bootInfo->bootFile, kDefaultKernel ) != 0) {
1255 gOverrideKernel = true;
1256 }
1257 } else {
1258 strcpy( bootInfo->bootFile, kDefaultKernel );
1259 }
1260 }
1261
1262 cntRemaining = BOOT_STRING_LEN - 2; // save 1 for NULL, 1 for space
1263 argP = bootArgs->CommandLine;
1264
1265 // Get config table kernel flags, if not ignored.
1266 if (getValueForBootKey(cp, kIgnoreBootFileFlag, &val, &cnt) ||
1267 !getValueForKey( kKernelFlagsKey, &val, &cnt, &bootInfo->bootConfig )) {
1268 val = "";
1269 cnt = 0;
1270 }
1271 configKernelFlags = malloc(cnt + 1);
1272 strlcpy(configKernelFlags, val, cnt + 1);
1273
1274 if (processBootArgument(kBootUUIDKey, cp, configKernelFlags, bootInfo->config, &argP, &cntRemaining, 0)) {
1275 // boot-uuid was set either on the command-line
1276 // or in the config file.
1277 uuidSet = true;
1278 } else {
1279
1280 //
1281 // Try an alternate method for getting the root UUID on boot helper partitions.
1282 //
1283 if (gBootVolume->flags & kBVFlagBooter)
1284 {
1285 if((loadHelperConfig(&bootInfo->helperConfig) == 0)
1286 && getValueForKey(kHelperRootUUIDKey, &val, &cnt, &bootInfo->helperConfig) )
1287 {
1288 getValueForKey(kHelperRootUUIDKey, &val, &cnt, &bootInfo->helperConfig);
1289 copyArgument(kBootUUIDKey, val, cnt, &argP, &cntRemaining);
1290 uuidSet = true;
1291 }
1292 }
1293
1294 if (!uuidSet && gBootVolume->fs_getuuid && gBootVolume->fs_getuuid (gBootVolume, uuidStr) == 0) {
1295 verbose("Setting boot-uuid to: %s\n", uuidStr);
1296 copyArgument(kBootUUIDKey, uuidStr, strlen(uuidStr), &argP, &cntRemaining);
1297 uuidSet = true;
1298 }
1299 }
1300
1301 if (!processBootArgument(kRootDeviceKey, cp, configKernelFlags, bootInfo->config, &argP, &cntRemaining, gRootDevice)) {
1302 cnt = 0;
1303 if ( getValueForKey( kBootDeviceKey, &val, &cnt, &bootInfo->bootConfig)) {
1304 valueBuffer[0] = '*';
1305 cnt++;
1306 strlcpy(valueBuffer + 1, val, cnt);
1307 val = valueBuffer;
1308 } else {
1309 if (uuidSet) {
1310 val = "*uuid";
1311 cnt = 5;
1312 } else {
1313 // Don't set "rd=.." if there is no boot device key
1314 // and no UUID.
1315 val = "";
1316 cnt = 0;
1317 }
1318 }
1319 if (cnt > 0) {
1320 copyArgument( kRootDeviceKey, val, cnt, &argP, &cntRemaining);
1321 }
1322 strlcpy( gRootDevice, val, (cnt + 1));
1323 }
1324
1325 /*
1326 * Removed. We don't need this anymore.
1327 *
1328 if (!processBootArgument(kPlatformKey, cp, configKernelFlags, bootInfo->config, &argP, &cntRemaining, gPlatformName)) {
1329 getPlatformName(gPlatformName);
1330 copyArgument(kPlatformKey, gPlatformName, strlen(gPlatformName), &argP, &cntRemaining);
1331 }
1332 */
1333
1334 if (!getValueForBootKey(cp, kSafeModeFlag, &val, &cnt) &&
1335 !getValueForBootKey(configKernelFlags, kSafeModeFlag, &val, &cnt)) {
1336 if (gBootMode & kBootModeSafe) {
1337 copyArgument(0, kSafeModeFlag, strlen(kSafeModeFlag), &argP, &cntRemaining);
1338 }
1339 }
1340
1341 // Store the merged kernel flags and boot args.
1342
1343 cnt = strlen(configKernelFlags);
1344 if (cnt) {
1345 if (cnt > cntRemaining) {
1346 error("Warning: boot arguments too long, truncating\n");
1347 cnt = cntRemaining;
1348 }
1349 strncpy(argP, configKernelFlags, cnt);
1350 argP[cnt++] = ' ';
1351 cntRemaining -= cnt;
1352 }
1353 userCnt = strlen(cp);
1354 if (userCnt > cntRemaining) {
1355 error("Warning: boot arguments too long, truncating\n");
1356 userCnt = cntRemaining;
1357 }
1358 strncpy(&argP[cnt], cp, userCnt);
1359 argP[cnt+userCnt] = '\0';
1360
1361 if(!shouldboot)
1362 {
1363 gVerboseMode = getValueForKey( kVerboseModeFlag, &val, &cnt, &bootInfo->bootConfig ) ||
1364 getValueForKey( kSingleUserModeFlag, &val, &cnt, &bootInfo->bootConfig );
1365
1366 gBootMode = ( getValueForKey( kSafeModeFlag, &val, &cnt, &bootInfo->bootConfig ) ) ?
1367 kBootModeSafe : kBootModeNormal;
1368
1369 if ( getValueForKey( kOldSafeModeFlag, &val, &cnt, &bootInfo->bootConfig ) ) {
1370 gBootMode = kBootModeSafe;
1371 }
1372
1373 if ( getValueForKey( kMKextCacheKey, &val, &cnt, &bootInfo->bootConfig ) ) {
1374 strlcpy(gMKextName, val, cnt + 1);
1375 }
1376
1377 }
1378
1379 free(configKernelFlags);
1380 free(valueBuffer);
1381
1382 return 0;
1383}
1384
1385
1386//==========================================================================
1387// Load the help file and display the file contents on the screen.
1388
1389static void showTextBuffer(char *buf, int size)
1390{
1391char*bp;
1392intline;
1393intline_offset;
1394intc;
1395
1396if (bootArgs->Video.v_display == GRAPHICS_MODE) {
1397showInfoBox( "Press q to quit\n",buf );
1398return;
1399}
1400
1401 bp = buf;
1402 while (size-- > 0) {
1403if (*bp == '\n') {
1404*bp = '\0';
1405}
1406bp++;
1407 }
1408 *bp = '\1';
1409 line_offset = 0;
1410
1411 setActiveDisplayPage(1);
1412
1413 while (1) {
1414clearScreenRows(0, 24);
1415setCursorPosition(0, 0, 1);
1416bp = buf;
1417for (line = 0; *bp != '\1' && line < line_offset; line++) {
1418while (*bp != '\0') {
1419bp++;
1420}
1421bp++;
1422}
1423for (line = 0; *bp != '\1' && line < 23; line++) {
1424setCursorPosition(0, line, 1);
1425printf("%s\n", bp);
1426while (*bp != '\0') {
1427bp++;
1428}
1429bp++;
1430}
1431
1432setCursorPosition(0, 23, 1);
1433if (*bp == '\1') {
1434printf("[Type %sq or space to quit viewer]", (line_offset > 0) ? "p for previous page, " : "");
1435} else {
1436printf("[Type %s%sq to quit viewer]", (line_offset > 0) ? "p for previous page, " : "", (*bp != '\1') ? "space for next page, " : "");
1437}
1438
1439c = getc();
1440if (c == 'q' || c == 'Q') {
1441break;
1442}
1443if ((c == 'p' || c == 'P') && line_offset > 0) {
1444line_offset -= 23;
1445}
1446if (c == ' ') {
1447if (*bp == '\1') {
1448break;
1449} else {
1450line_offset += 23;
1451}
1452}
1453 }
1454 setActiveDisplayPage(0);
1455}
1456
1457void showHelp(void)
1458{
1459if (bootArgs->Video.v_display == GRAPHICS_MODE) {
1460showInfoBox("Help. Press q to quit.\n", (char *)BootHelp_txt);
1461} else {
1462showTextBuffer((char *)BootHelp_txt, BootHelp_txt_len);
1463}
1464}
1465
1466void showTextFile(const char * filename)
1467{
1468#define MAX_TEXT_FILE_SIZE 65536
1469char*buf;
1470intfd;
1471intsize;
1472
1473if ((fd = open_bvdev("bt(0,0)", filename, 0)) < 0) {
1474printf("\nFile not found: %s\n", filename);
1475sleep(2);
1476return;
1477}
1478
1479 size = file_size(fd);
1480 if (size > MAX_TEXT_FILE_SIZE) {
1481size = MAX_TEXT_FILE_SIZE;
1482}
1483 buf = malloc(size);
1484 read(fd, buf, size);
1485 close(fd);
1486showTextBuffer(buf, size);
1487free(buf);
1488}
1489
1490// This is a very simplistic prompting scheme that just grabs two hex characters
1491// Eventually we need to do something more user-friendly like display a menu
1492// based off of the Multiboot device list
1493
1494int selectAlternateBootDevice(int bootdevice)
1495{
1496int key;
1497int newbootdevice;
1498int digitsI = 0;
1499char *end;
1500char digits[3] = {0,0,0};
1501
1502// We've already printed the current boot device so user knows what it is
1503printf("Typical boot devices are 80 (First HD), 81 (Second HD)\n");
1504printf("Enter two-digit hexadecimal boot device [%02x]: ", bootdevice);
1505do {
1506key = getc();
1507switch (key & kASCIIKeyMask) {
1508case kBackspaceKey:
1509if (digitsI > 0) {
1510int x, y, t;
1511getCursorPositionAndType(&x, &y, &t);
1512// Assume x is not 0;
1513x--;
1514setCursorPosition(x,y,0); // back up one char
1515// Overwrite with space without moving cursor position
1516putca(' ', 0x07, 1);
1517digitsI--;
1518} else {
1519// TODO: Beep or something
1520}
1521break;
1522
1523case kReturnKey:
1524digits[digitsI] = '\0';
1525newbootdevice = strtol(digits, &end, 16);
1526if (end == digits && *end == '\0') {
1527// User entered empty string
1528printf("\nUsing default boot device %x\n", bootdevice);
1529key = 0;
1530} else if(end != digits && *end == '\0') {
1531bootdevice = newbootdevice;
1532printf("\n");
1533key = 0; // We gots da boot device
1534} else {
1535printf("\nCouldn't parse. try again: ");
1536digitsI = 0;
1537}
1538break;
1539
1540default:
1541if (isxdigit(key & kASCIIKeyMask) && digitsI < 2) {
1542putc(key & kASCIIKeyMask);
1543digits[digitsI++] = key & kASCIIKeyMask;
1544} else {
1545// TODO: Beep or something
1546}
1547break;
1548};
1549} while (key != 0);
1550
1551return bootdevice;
1552}
1553
1554bool promptForRescanOption(void)
1555{
1556printf("\nWould you like to enable media rescan option?\nPress ENTER to enable or any key to skip.\n");
1557if (getc() == kReturnKey) {
1558return true;
1559} else {
1560return false;
1561}
1562}
1563

Archive Download this file

Revision: 62