|
|
1.1 root 1: /*++
2:
3: Copyright (c) 1993 - Colorado Memory Systems, Inc.
4: All Rights Reserved
5:
6: Module Name:
7:
8: mapbad.c
9:
10: Abstract:
11:
12: These routines map out bad blocks.
13:
14: Revision History:
15:
16:
17:
18:
19: --*/
20:
21: //
22: // include files
23: //
24:
25: #include <ntddk.h>
26: #include <ntddtape.h>
27: #include "common.h"
28: #include "q117.h"
29: #include "protos.h"
30:
31: #define SECTOR_POINTER(i,x) (((UBYTE *)ioArray[i].Data)+(x)*BYTES_PER_SECTOR)
32:
33: void
34: q117RotateData(
35: IN OUT PQ117_CONTEXT Context,
36: IN PIO_REQUEST IoArray,
37: IN int IoLast,
38: IN int ShiftAmount
39: );
40:
41:
42: STATUS
43: q117MapBadBlock (
44: IN PIO_REQUEST IoRequest,
45: OUT PVOID *DataPointer,
46: IN OUT USHORT *BytesLeft,
47: IN OUT SEGMENT *CurrentSegment,
48: IN OUT USHORT *Remainder,
49: IN OUT PQ117_CONTEXT Context
50: )
51:
52: /*++
53:
54: Routine Description:
55:
56: Moves existing blocks out of bad sector area and into other
57: segments (that already have data in them).
58:
59: Arguments:
60:
61: IoRequest -
62:
63: DataPointer -
64:
65: BytesLeft -
66:
67: CurrentSegment -
68:
69: Remainder -
70:
71: Context -
72:
73: Return Value:
74:
75:
76:
77: --*/
78:
79: {
80: IO_REQUEST ioArray[UNIX_MAXBFS];
81: LONG queueItemsUsed;
82: LONG i;
83: UCHAR newBadSectors;
84: UCHAR overflowSectors;
85: LONG cur_bad;
86: ULONG oldBadSectorMap;
87: ULONG currentIndex;
88: LONG ret;
89: SEGMENT currentSegment;
90: USHORT bytesBad;
91:
92: //
93: // get current queue position
94: //
95: currentIndex = q117GetQueueIndex(Context);
96:
97: //
98: // get the current queue list (and clear queue)
99: //
100: queueItemsUsed = 0;
101: ioArray[queueItemsUsed++] = *IoRequest;
102: CheckedDump(QIC117SHOWTD,("Re-mapping: %x ", ioArray[0].Block));
103:
104: while (!q117QueueEmpty(Context)) {
105:
106: ioArray[queueItemsUsed++] = *q117Dequeue(FlushItem, Context);
107: CheckedDump(QIC117SHOWTD,("%x " ,
108: ioArray[queueItemsUsed-1].Block));
109:
110: }
111: CheckedDump(QIC117SHOWTD,("\n"));
112:
113: q117ClearQueue(Context);
114:
115: //
116: // write out current buffer
117: //
118: Context->CurrentOperation.UpdateBadMap = TRUE;
119: bytesBad = overflowSectors = 0;
120: currentSegment = BLOCK_TO_SEGMENT(IoRequest->Block);
121: cur_bad = q117CountBits(Context, currentSegment, 0l);
122:
123: while (newBadSectors = q117CountBits(NULL, 0,
124: (~(oldBadSectorMap =
125: q117ReadBadSectorList(Context, currentSegment))) &
126: ioArray[0].BadList)) {
127:
128:
129: CheckedDump(QIC117SHOWTD | QIC117SHOWBAD,("Current: %x New: %x\n" ,
130: oldBadSectorMap,
131: ioArray[0].BadList
132: ));
133:
134: //
135: // add bits to bad sector map and set global flag to update bad
136: // sector map
137: //
138: ret = q117UpdateBadMap(Context, currentSegment, ioArray[0].BadList);
139: if (ret != NoErr) {
140: return(ret);
141: }
142:
143: //
144: // update total number of mapped out sectors
145: //
146: cur_bad += newBadSectors;
147: bytesBad += newBadSectors * BYTES_PER_SECTOR;
148: overflowSectors += newBadSectors;
149:
150: if (cur_bad+ECC_BLOCKS_PER_SEGMENT >= BLOCKS_PER_SEGMENT) {
151: //
152: // fake a write (because all data blocks are bad)
153: //
154: ioArray[0].BadList = 0;
155:
156: } else {
157:
158: //
159: // move extra data out of the way of correction sectors
160: //
161: CheckedDump(QIC117SHOWTD,("moving %x to %x (%x bytes)\n" ,
162: DATA_BLOCKS_PER_SEGMENT-cur_bad,
163: BLOCKS_PER_SEGMENT-overflowSectors,
164: bytesBad));
165:
166: RtlMoveMemory(
167: SECTOR_POINTER(0,BLOCKS_PER_SEGMENT-overflowSectors),
168: SECTOR_POINTER(0,DATA_BLOCKS_PER_SEGMENT-cur_bad),
169: bytesBad);
170:
171: //
172: // compute error correction and write out smaller chunk of data
173: //
174: q117IssIOReq(
175: ioArray[0].Data,
176: DWrite,
177: ioArray[0].Block,
178: ioArray[0].BufferInfo,
179: Context);
180:
181: IoRequest = q117Dequeue(WaitForItem, Context);
182:
183: if (IoRequest->Status && IoRequest->Status != BadBlk) {
184: return(IoRequest->Status);
185: }
186:
187:
188: //
189: // Get new bad sectors (if any)
190: //
191: ioArray[0].BadList = IoRequest->BadList;
192:
193: //
194: // move data back to contiguous form
195: //
196: CheckedDump(QIC117SHOWTD,("moving %x to %x (%x bytes)\n" ,
197: BLOCKS_PER_SEGMENT-overflowSectors,
198: DATA_BLOCKS_PER_SEGMENT-cur_bad,
199: bytesBad));
200:
201: RtlMoveMemory(
202: SECTOR_POINTER(0,DATA_BLOCKS_PER_SEGMENT-cur_bad),
203: SECTOR_POINTER(0,BLOCKS_PER_SEGMENT-overflowSectors),
204: bytesBad);
205: }
206:
207: } // end of while
208:
209: //
210: // move bytes mapped out into start of current segment
211: //
212: if (cur_bad+ECC_BLOCKS_PER_SEGMENT < BLOCKS_PER_SEGMENT) {
213:
214: CheckedDump(QIC117SHOWTD,("moving %x to %x (%x bytes)\n" ,
215: DATA_BLOCKS_PER_SEGMENT-cur_bad,
216: 0,
217: bytesBad));
218:
219: RtlMoveMemory(
220: SECTOR_POINTER(0,0),
221: SECTOR_POINTER(0,DATA_BLOCKS_PER_SEGMENT-cur_bad),
222: bytesBad);
223:
224: }
225:
226: //
227: // We need to skip segments with no data area (less than 4 good segments)
228: //
229: while ((*BytesLeft = q117GoodDataBytes(*CurrentSegment,Context)) <= 0) {
230: ++(*CurrentSegment);
231: }
232:
233: //
234: // if we hit the end of the tape or we are in the directory
235: //
236: if (*CurrentSegment == Context->CurrentOperation.LastSegment) {
237: //
238: // if fill data >= number of new bad sectors then continue
239: //
240: if (Context->CurrentOperation.BytesZeroFilled < bytesBad) {
241: //
242: // update the bad sector map and return BadBlk detected
243: //
244: if (ret = q117DoUpdateBad(Context))
245: return(ret);
246: else
247: return(BadBlk);
248: }
249: }
250:
251: //
252: // if insufficient space in next good segment then error out
253: //
254: if (bytesBad > *BytesLeft) {
255: //
256: // update the bad sector map and return BadBlk detected
257: //
258: if (ret = q117DoUpdateBad(Context))
259: return(ret);
260: else
261: return(BadBlk);
262: }
263:
264: //
265: // if more than one item was queued, move all previous data
266: // out of queue 0 and put overflow into queue 0.
267: //
268:
269: if (queueItemsUsed > 1) {
270:
271: //
272: // Rotate the memory through the
273: // buffers leaving the most recent data in the item 0
274: //
275: q117RotateData(
276: Context,
277: ioArray,
278: queueItemsUsed-1,
279: overflowSectors
280: );
281: }
282:
283: //
284: // set current queue position back (and re-queue data)
285: //
286: q117SetQueueIndex(currentIndex, Context);
287:
288: for (i=1;i<queueItemsUsed;++i)
289: q117IssIOReq((PVOID)NULL,DWrite,ioArray[i].Block,NULL,Context);
290:
291: //
292: // set the current data pointer and bytes left in buffer
293: //
294:
295: if (Context->CurrentOperation.BytesZeroFilled) {
296:
297: if (Context->CurrentOperation.BytesZeroFilled >= bytesBad) {
298: Context->CurrentOperation.BytesZeroFilled -= bytesBad;
299: *Remainder = 0;
300: } else {
301: *Remainder = bytesBad - Context->CurrentOperation.BytesZeroFilled;
302: }
303:
304: } else {
305:
306: *Remainder = bytesBad;
307:
308: }
309:
310: *DataPointer = (UCHAR *)ioArray[0].Data + *Remainder;
311: *BytesLeft -= *Remainder;
312:
313: return(NoErr);
314: }
315:
316: STATUS
317: q117UpdateBadMap(
318: IN OUT PQ117_CONTEXT Context,
319: IN SEGMENT Segment,
320: IN ULONG BadSectors
321: )
322:
323: /*++
324:
325: Routine Description:
326:
327: if bitmap bad sector map, or in bad sectors
328: else
329: make badsectors bitmap into a list of sectors
330: and merge them into the existing bad sector map.
331:
332: Arguments:
333:
334: IoRequest -
335:
336: DataPointer -
337:
338: BytesLeft -
339:
340: CurrentSegment -
341:
342: Remainder -
343:
344: Context -
345:
346: Return Value:
347:
348:
349:
350: --*/
351:
352: {
353: BAD_LIST badList[BLOCKS_PER_SEGMENT];
354: ULONG curSector;
355: ULONG newSector;
356: USHORT insertionPoint = 0;
357: USHORT listEnd = 0;
358: USHORT currentSectorIndex = 0;
359: UCHAR newCount;
360:
361: #if DBG
362:
363: USHORT i,j;
364: ULONG tmpSector;
365:
366: #endif
367:
368: if (Context->CurrentTape.TapeFormatCode == QIC_FORMAT) {
369:
370: Context->CurrentTape.BadMapPtr->BadSectors[Segment] |=
371: BadSectors;
372:
373: } else {
374:
375: newCount = q117CountBits(NULL, 0,
376: (~(q117ReadBadSectorList(Context, Segment))) &
377: BadSectors);
378:
379: CheckedDump(QIC117SHOWBAD,( "UpdateBadMap newCount = %d\n", newCount));
380:
381: // Convert bad sector map (bit field) into a list of sectors
382: // that need to be inserted into the list
383: q117BadMapToBadList(Segment,
384: ((~(q117ReadBadSectorList(Context, Segment))) &
385: BadSectors), badList);
386:
387: // Get first entry to add
388: newSector = q117BadListEntryToSector(badList[currentSectorIndex].ListEntry);
389:
390: // Get first entry in table
391: curSector = q117BadListEntryToSector(Context->CurrentTape.BadMapPtr->BadList[listEnd++].ListEntry);
392:
393: // While entry in table valid, and not at end of the list
394: while (curSector && (listEnd < MAX_BAD_LIST) ) {
395:
396: // If entry less than new item, then increment insertionPoint
397: if (newSector > curSector) {
398:
399: insertionPoint++;
400:
401: }
402:
403: // Get next sector
404: curSector = q117BadListEntryToSector(Context->CurrentTape.BadMapPtr->BadList[listEnd++].ListEntry);
405: }
406:
407: //
408: // Now, list end will be the real end of list
409: // and insertionPoint will be the point at which the first
410: // sector int the new sector list needs to be added.
411: //
412: listEnd--;
413:
414: #if DBG
415:
416: CheckedDump(QIC117SHOWBAD,( "\n\nQ117 BSL: BSL to add ---\n"));
417: for (i=0; i < newCount; i++) {
418: tmpSector = q117BadListEntryToSector(badList[i].ListEntry);
419: CheckedDump(QIC117SHOWBAD,( " %08lx",tmpSector));
420: if (!((i+1) % 5)) {
421: CheckedDump(QIC117SHOWBAD,( "\n"));
422: }
423: }
424: CheckedDump(QIC117SHOWBAD,( "\n"));
425:
426: #endif
427:
428: // make sure there is enough room for the new items
429: if ((listEnd + newCount) >= MAX_BAD_LIST) {
430:
431: return(UnusTape);
432:
433: }
434:
435: //
436: // While more sectors to merge
437: //
438: while (currentSectorIndex < newCount) {
439:
440: newSector = q117BadListEntryToSector(
441: badList[currentSectorIndex].ListEntry
442: );
443:
444: //
445: // get new insertion point (skip any lessor sectors). First
446: // time through, this will already be done.
447: //
448:
449: while (insertionPoint <= listEnd) {
450:
451: //
452: // Get sector at insertionPoint
453: //
454: curSector = q117BadListEntryToSector(
455: Context->CurrentTape.BadMapPtr->
456: BadList[insertionPoint].ListEntry
457: );
458:
459: //
460: // if new sector is bigger than the current sector
461: // get the next one
462: //
463: if (newSector > curSector) {
464:
465: ++insertionPoint;
466:
467: } else {
468:
469: // We found our next insertion point
470: break;
471:
472: }
473:
474: }
475:
476: //
477: // If the sector we need to add is not the same as the current
478: // sector at the insertionPoint or we are past the end of the
479: // current list, then we need to add the sector
480: // to the list.
481: // NOTE: curSector will not be valid at end of list, but
482: // we don't care.
483: //
484:
485: if (curSector != newSector || insertionPoint > listEnd) {
486:
487: //
488: // make space for the new entry (if inserting)
489: //
490: if (listEnd >= insertionPoint) {
491: RtlMoveMemory(
492: Context->CurrentTape.BadMapPtr->
493: BadList[insertionPoint+1].ListEntry,
494: Context->CurrentTape.BadMapPtr->
495: BadList[insertionPoint].ListEntry,
496: ((ULONG)(listEnd - insertionPoint + 1)
497: * LIST_ENTRY_SIZE)
498: );
499:
500: }
501:
502: //
503: // add the new entry
504: //
505: RtlMoveMemory(
506: Context->CurrentTape.BadMapPtr->
507: BadList[insertionPoint].ListEntry,
508: badList[currentSectorIndex].ListEntry,
509: LIST_ENTRY_SIZE);
510:
511: ++insertionPoint;
512:
513: //
514: // we just made our list bigger
515: //
516: ++listEnd;
517:
518: }
519:
520: //
521: // Now get next sector to add
522: //
523: ++currentSectorIndex;
524:
525: }
526:
527: #if DBG
528:
529: CheckedDump(QIC117SHOWBAD,( "\n\nQ117 BSL: New BSL ------\n"));
530: for (i=0; i < listEnd; i++) {
531: tmpSector = q117BadListEntryToSector(Context->CurrentTape.BadMapPtr->BadList[i].ListEntry);
532: CheckedDump(QIC117SHOWBAD,( " %08lx",tmpSector));
533: if (!((i+1) % 5)) {
534: CheckedDump(QIC117SHOWBAD,( "\n"));
535: }
536: }
537: CheckedDump(QIC117SHOWBAD,( "\n"));
538:
539: #endif
540:
541: }
542:
543: return(NoErr);
544: }
545:
546: VOID
547: q117BadMapToBadList(
548: IN SEGMENT Segment,
549: IN ULONG BadSectors,
550: IN BAD_LIST_PTR BadListPtr
551: )
552:
553: /*++
554:
555: Routine Description:
556:
557: Arguments:
558:
559: IoRequest -
560:
561: DataPointer -
562:
563: BytesLeft -
564:
565: CurrentSegment -
566:
567: Remainder -
568:
569: Context -
570:
571: Return Value:
572:
573:
574:
575: --*/
576:
577: {
578: USHORT listIndex = 0;
579: ULONG sector;
580:
581: CheckedDump(QIC117SHOWBAD,( "BadMapToList Segment -> %08lx\n",Segment));
582: sector = (ULONG)((Segment * BLOCKS_PER_SEGMENT) + 1);
583:
584: while (BadSectors &&
585: (listIndex < BLOCKS_PER_SEGMENT)) {
586:
587: if (BadSectors & 1l) {
588:
589: BadListPtr[listIndex].ListEntry[0] = (UBYTE)(sector & 0xff);
590: BadListPtr[listIndex].ListEntry[1] = (UBYTE)((sector >> 8) & 0xff);
591: BadListPtr[listIndex].ListEntry[2] = (UBYTE)((sector >> 16) & 0xff);
592: CheckedDump(QIC117SHOWBAD,( "BadMapToList -> %08lx\n",sector));
593: listIndex++;
594:
595: }
596:
597: sector++;
598: BadSectors >>= 1;
599: }
600:
601: if (listIndex < BLOCKS_PER_SEGMENT) {
602:
603: BadListPtr[listIndex].ListEntry[0] = (UBYTE)0;
604: BadListPtr[listIndex].ListEntry[1] = (UBYTE)0;
605: BadListPtr[listIndex].ListEntry[2] = (UBYTE)0;
606:
607: }
608:
609: return;
610: }
611:
612: ULONG
613: q117BadListEntryToSector(
614: IN UCHAR *ListEntry
615: )
616:
617: /*++
618:
619: Routine Description:
620:
621: Arguments:
622:
623: IoRequest -
624:
625: DataPointer -
626:
627: BytesLeft -
628:
629: CurrentSegment -
630:
631: Remainder -
632:
633: Context -
634:
635: Return Value:
636:
637:
638:
639: --*/
640:
641: {
642: ULONG sector = 0l;
643:
644: #ifndef i386
645:
646: sector = (UBYTE)ListEntry[2];
647: sector <<= 8;
648: sector |= (UBYTE)ListEntry[1];
649: sector <<= 8;
650: sector |= (UBYTE)ListEntry[0];
651:
652: return(sector);
653:
654: #else
655:
656: return((0x00FFFFFF & *(ULONG *)ListEntry));
657:
658: #endif
659:
660: }
661:
662: void
663: q117RotateData(
664: IN OUT PQ117_CONTEXT Context,
665: IN PIO_REQUEST IoArray,
666: IN int IoLast,
667: IN int ShiftAmount
668: )
669: /*++
670:
671: Routine Description:
672:
673: This routine shifts the data around _ShiftAmount_ times, by one
674: sector.
675:
676: Sample:
677:
678: IoArray[ 0 1 2 3
679: data sector 0 1 2 3 4 5 6 7 8
680:
681: Step 1: data 0 shifted up
682:
683: IoArray[ 0 1 2 3
684: data sector 0 0 1 2 3 4 5 6 7 8
685: s d
686:
687: Step 2: source copied to dest
688:
689: IoArray[ 0 1 2 3
690: data sector 8 0 1 2 3 4 5 6 7 8
691: d s
692:
693: Step 3: pointers decremented (source wraps) and source copied to dest
694:
695: IoArray[ 0 1 2 3
696: data sector 8 0 1 2 3 4 5 6 7 7
697: d s
698:
699: Step 4: pointers decremented and source copied to dest
700:
701: IoArray[ 0 1 2 3
702: data sector 8 0 1 2 3 4 5 6 6 7
703: d s
704:
705:
706: Step 5: pointers decremented and source copied to dest
707:
708: IoArray[ 0 1 2 3
709: data sector 8 0 1 2 3 4 5 5 6 7
710: d s
711:
712:
713: Step 6: pointers decremented and source copied to dest
714:
715: IoArray[ 0 1 2 3
716: data sector 8 0 1 2 3 4 4 5 6 7
717: d s
718:
719:
720: Step 7: pointers decremented and source copied to dest
721:
722: IoArray[ 0 1 2 3
723: data sector 8 0 1 2 3 3 4 5 6 7
724: d s
725:
726:
727: Step 8: pointers decremented and source copied to dest
728:
729: IoArray[ 0 1 2 3
730: data sector 8 0 1 2 2 3 4 5 6 7
731: d s
732:
733: Step 9: pointers decremented and source copied to dest
734:
735: IoArray[ 0 1 2 3
736: data sector 8 0 1 1 2 3 4 5 6 7
737: d s
738:
739: Step 10: pointers decremented and source copied to dest
740:
741: IoArray[ 0 1 2 3
742: data sector 8 0 0 1 2 3 4 5 6 7
743: d s
744:
745: Step 10: pointers decremented and source copied to dest
746:
747: IoArray[ 0 1 2 3
748: data sector 8 8 0 1 2 3 4 5 6 7
749: s d
750:
751: Step 11: process starts over at step 2
752:
753: Step 12: data in buffer 0 shifted back down.
754:
755:
756:
757: Arguments:
758:
759: Return Value:
760:
761:
762:
763: --*/
764: {
765: UCHAR *finish;
766: UCHAR *source;
767: int sourceSize;
768: int sourceIndex;
769: UCHAR *destination;
770: int destinationSize;
771: int destinationIndex;
772: int loop;
773:
774: //
775: // Shift over by one (possibly into ECC area to allow for a non
776: // destructive rotate. This will be shifted back after operation
777: //
778: RtlMoveMemory((UCHAR *)IoArray[0].Data + BYTES_PER_SECTOR, IoArray[0].Data,
779: ShiftAmount * BYTES_PER_SECTOR);
780:
781: for (loop = 0; loop < ShiftAmount; ++loop) {
782:
783: //
784: // Begin by overwriting area we just moved out of (see above move)
785: //
786:
787: destination = IoArray[0].Data;
788: destinationSize = BYTES_PER_SECTOR;
789: destinationIndex = 0;
790:
791: //
792: // source points to last sector in queue
793: //
794: sourceIndex = IoLast;
795: sourceSize =
796: q117GoodDataBytes(
797: BLOCK_TO_SEGMENT(IoArray[sourceIndex].Block),
798: Context
799: );
800: source = (UCHAR *)IoArray[sourceIndex].Data + sourceSize
801: - BYTES_PER_SECTOR;
802:
803: finish = source;
804:
805: do {
806:
807: //
808: // shift the data
809: //
810: CheckedDump(QIC117SHOWTD,("shifting %x-%x[%x]{%x} to %x-%x[%x]\n" ,
811: sourceIndex, IoArray[sourceIndex].Block,
812: sourceSize - BYTES_PER_SECTOR,
813: (ULONG *)source,
814: destinationIndex, IoArray[destinationIndex].Block,
815: destinationSize - BYTES_PER_SECTOR));
816:
817: RtlMoveMemory(destination, source, BYTES_PER_SECTOR);
818:
819: //
820: // Decrement the pointer
821: //
822: destinationSize -= BYTES_PER_SECTOR;
823: destination -= BYTES_PER_SECTOR;
824:
825: if (destinationSize == 0) {
826:
827: if (destinationIndex == 0) {
828:
829: destinationIndex = IoLast;
830:
831: } else {
832:
833: --destinationIndex;
834:
835: }
836:
837: if (destinationIndex == 0) {
838:
839: destinationSize = (ShiftAmount+1) * BYTES_PER_SECTOR;
840:
841: } else {
842:
843: destinationSize =
844: q117GoodDataBytes(
845: BLOCK_TO_SEGMENT(IoArray[destinationIndex].Block),
846: Context
847: );
848:
849: }
850: destination = (UCHAR *)IoArray[destinationIndex].Data
851: + destinationSize
852: - BYTES_PER_SECTOR;
853: }
854:
855: //
856: // Decrement the pointer
857: //
858: sourceSize -= BYTES_PER_SECTOR;
859: source -= BYTES_PER_SECTOR;
860:
861: //
862: // If we hit the begining of the buffer,
863: // go to the previous buffer.
864: //
865: if (sourceSize == 0) {
866:
867: if (sourceIndex == 0) {
868:
869: sourceIndex = IoLast;
870:
871: } else {
872:
873: --sourceIndex;
874:
875: }
876:
877: if (sourceIndex == 0) {
878:
879: sourceSize = (ShiftAmount+1) * BYTES_PER_SECTOR;
880:
881: } else {
882:
883: sourceSize =
884: q117GoodDataBytes(
885: BLOCK_TO_SEGMENT(IoArray[sourceIndex].Block),
886: Context
887: );
888:
889: }
890: source = (UCHAR *)IoArray[sourceIndex].Data + sourceSize
891: - BYTES_PER_SECTOR;
892: }
893:
894: } while (source != finish);
895:
896: }
897:
898: //
899: // move the data back to the start of the buffer
900: //
901: RtlMoveMemory(
902: (UCHAR *)IoArray[0].Data,
903: (UCHAR *)IoArray[0].Data + BYTES_PER_SECTOR,
904: ShiftAmount * BYTES_PER_SECTOR
905: );
906: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.