|
|
1.1 root 1: /* Copyright (c) 1993 NeXT Computer, Inc. All rights reserved.
2: *
3: * SCSITapeKern.m -- implementation of scsi tape driver entry point routines
4: *
5: * HISTORY
6: * 31-Mar-93 Phillip Dibner at NeXT
7: * Created. Adapted from st.c, created by Doug Mitchell at NeXT.
8: *
9: */
10:
11: /*
12: * Four different devices are implemented here:
13: *
14: * rst - generic SCSI tape, rewind on close
15: * nrst - generic SCSI tape, no rewind on close
16: * rxt - Exabyte SCSI tape, rewind on close
17: * nrxt - Exabyte SCSI tape, no rewind on close
18: *
19: * All 4 devices have the same major number. Bit 0 of the minor number
20: * selects "rewind on close" (0) or "no rewind" (1). Bit 1 of the
21: * minor number select generic (0) or Exabyte (1).
22: *
23: * The Exabyte drive currently requires these actions on open:
24: *
25: * -- enable Buffered Write mode
26: * -- Inhibit Illegal Length errors
27: * -- Disable Disconnect During Data Transfer
28: */
29:
30: #import <sys/errno.h>
31: #import <sys/types.h>
32: #import <sys/buf.h>
33: #import <sys/conf.h>
34: #import <sys/uio.h>
35: #import <sys/mtio.h>
36: #import <bsd/dev/scsireg.h>
37:
38: #import <driverkit/scsiTypes.h>
39: #import <driverkit/align.h>
40: #import <driverkit/kernelDriver.h>
41: #import <driverkit/scsiTypes.h>
42: #import <driverkit/return.h>
43: #import <driverkit/devsw.h>
44: #import <kernserv/prototypes.h>
45: #import "SCSITape.h"
46:
47:
48: #define USE_EBD 1 /* use "even byte diconnect" rather than
49: * "no disconnect during data xfer" for exabyte
50: */
51:
52: /*
53: * Unix-style entry points
54: */
55: int stopen (dev_t dev);
56: int stclose (dev_t dev);
57: int stread (dev_t dev, struct uio *uiop);
58: int stwrite (dev_t dev, struct uio *uiop);
59: int stioctl (dev_t dev, int cmd, caddr_t data, int flag);
60:
61: /*
62: * Subsidiary functions used by the kernel "glue" layer
63: */
64: static int st_rw (dev_t dev, struct uio *uiop, int rw_flag);
65: static int st_doiocsrq (id scsiTape, scsi_req_t *srp);
66:
67: /*
68: * Functions to take care of byte-ordering issues
69: */
70: extern void assign_cdb_c6s_len();
71: extern void assign_msbd_numblocks();
72: extern void assign_msbd_blocklength();
73: unsigned int read_er_info_low_24();
74:
75: extern id stIdMap[];
76:
77:
78: /*
79: * Add ourself to cdevsw. Called from SCSIGeneric layer at probe time.
80: */
81: extern int nulldev();
82: extern int nodev();
83:
84: static int stMajor = -1;
85:
86: int
87: st_devsw_init()
88: {
89: int rtn;
90:
91: /*
92: * We get called once for each IOSCSIController in the system; we
93: * only have to call IOAddToCdevsw() once.
94: */
95: if(stMajor >= 0) {
96: return stMajor;
97: }
98: rtn = IOAddToCdevsw ((IOSwitchFunc) stopen,
99: (IOSwitchFunc) stclose,
100: (IOSwitchFunc) stread,
101: (IOSwitchFunc) stwrite,
102: (IOSwitchFunc) stioctl,
103: (IOSwitchFunc) nodev,
104: (IOSwitchFunc) nulldev, // reset
105: (IOSwitchFunc) nulldev,
106: (IOSwitchFunc) nodev, // mmap
107: (IOSwitchFunc) nodev, // getc
108: (IOSwitchFunc) nodev); // putc
109: if(rtn < 0) {
110: IOLog("st: Can't find space in devsw\n");
111: }
112: else {
113: IOLog("st: major number %d\n", rtn);
114: stMajor = rtn;
115: }
116: return rtn;
117: }
118:
119:
120: int
121: stopen(dev_t dev)
122: {
123: int unit = ST_UNIT(dev);
124: id scsiTape = stIdMap[unit];
125:
126: if([scsiTape acquireDevice] == IO_R_BUSY)
127: return(EBUSY); /* already open */
128: if ((unit >= NST) || /* illegal device */
129: ([scsiTape isInitialized] == NO)) { /* hasn't been init'd */
130: [scsiTape releaseDevice];
131: return(ENXIO); /* FIXME - try to init here */
132: }
133:
134: /*
135: * We send this once, and ignore result, to clear check condition
136: * due to media change, etc.
137: */
138: [scsiTape setIgnoreCheckCondition: YES];
139: [scsiTape stTestReady];
140: [scsiTape setIgnoreCheckCondition: NO];
141:
142: if(ST_EXABYTE(dev)) {
143: struct modesel_parms *mspp;
144: struct exabyte_vudata *evudp;
145: struct mode_sel_hdr *mshp;
146:
147: mspp = IOMalloc (sizeof (struct modesel_parms));
148: evudp = (struct exabyte_vudata *) &mspp->msp_data.msd_vudata;
149:
150: /*
151: * Exabyte "custom" setup
152: */
153:
154: /* Set variable block size */
155: if([scsiTape setBlockSize: 0] != IO_R_SUCCESS) {
156: IOFree (mspp, sizeof (struct modesel_parms));
157: [scsiTape releaseDevice];
158:
159: #ifdef DEBUG
160: IOLog ("stopen: cannot set block size variable\n");
161: #endif DEBUG
162:
163: return(EIO);
164: }
165:
166: /* Suppress illegal length errors */
167: [scsiTape setSuppressIllegalLength: YES];
168:
169: /* Do a mode sense */
170: mspp->msp_bcount = sizeof(struct mode_sel_hdr) +
171: sizeof(struct mode_sel_bd) + MSP_VU_EXABYTE;
172:
173: if([scsiTape stModeSense: mspp] != SR_IOST_GOOD) {
174: IOFree (mspp, sizeof (struct modesel_parms));
175: [scsiTape releaseDevice];
176:
177: #ifdef DEBUG
178: IOLog ("stopen: Mode Sense failed\n");
179: #endif DEBUG
180:
181: return(EIO);
182: }
183:
184: /* some fields we have to zero as a matter of course */
185: mshp = &mspp->msp_data.msd_header;
186: mshp->msh_sd_length_0 = 0;
187: mshp->msh_med_type = 0;
188: mshp->msh_wp = 0;
189: mshp->msh_bd_length = sizeof(struct mode_sel_bd);
190: assign_msbd_blocklength (&mspp->msp_data.msd_blockdescript, 0);
191: assign_msbd_numblocks (&mspp->msp_data.msd_blockdescript, 0);
192:
193: /*
194: * set up buffered mode, #blocks = 0, even byte disconnect,
195: * enable parity; do mode selsect
196: */
197: mspp->msp_data.msd_header.msh_bufmode = 1;
198:
199: #ifdef USE_EBD
200: /* clear NDD and set EBD; enable parity */
201: evudp->nd = 0; /* disconnects OK */
202: evudp->ebd = 1; /* but only on word boundaries */
203: evudp->pe = 1; /* parity enabled */
204: evudp->nbe = 1; /* Busy status disabled */
205: #else USE_EBD
206: evudp->nd = 1;
207: #endif USE_EBD
208: if([scsiTape stModeSelect: mspp] != SR_IOST_GOOD) {
209: IOFree (mspp, sizeof (struct modesel_parms));
210: [scsiTape releaseDevice];
211:
212: #ifdef DEBUG
213: IOLog ("stopen: Mode Select failed\n");
214: #endif DEBUG
215:
216: return(EIO);
217: }
218: IOFree (mspp, sizeof (struct modesel_parms));
219: }
220: return(0);
221: }
222:
223:
224:
225:
226: int
227: stclose(dev_t dev)
228: {
229: int unit = ST_UNIT(dev);
230: id scsiTape = stIdMap[unit];
231: int rtn = 0;
232:
233: if ([scsiTape didWrite] == YES) {
234: /* we must write a file mark to close the file */
235: if ([scsiTape stCloseFile] != SR_IOST_GOOD) {
236: rtn = EIO;
237: }
238: }
239:
240: if(ST_RETURN(dev) == 0) { /* returning device? */
241: if ([scsiTape stRewind] != SR_IOST_GOOD) {
242: rtn = EIO;
243: }
244: }
245:
246: [scsiTape releaseDevice];
247: return(rtn);
248: }
249:
250:
251: int
252: stread(dev_t dev, struct uio *uiop)
253: {
254: return(st_rw(dev,uiop,SR_DMA_RD));
255: }
256:
257: int
258: stwrite(dev_t dev, struct uio *uiop)
259: {
260: return(st_rw(dev,uiop,SR_DMA_WR));
261: }
262:
263:
264: static int
265: st_rw(dev_t dev, struct uio *uiop, int rw_flag) {
266:
267: int unit = ST_UNIT(dev);
268: id scsiTape = stIdMap[unit];
269: IOSCSIRequest scsiReq;
270: struct cdb_6s *cdbp = &scsiReq.cdb.cdb_c6s;
271: void *freePtr;
272: int freeCnt;
273: unsigned char *alignedBuf;
274: IODMAAlignment dmaAlign;
275: int length;
276: int rtn = 0;
277:
278: sc_status_t scRet = -1;
279:
280: if (unit >= NST)
281: return(ENXIO);
282: if(uiop->uio_iovcnt != 1) /* single requests only */
283: return(EINVAL);
284: if(uiop->uio_iov->iov_len == 0)
285: return(0); /* nothing to do */
286:
287: #ifdef DEBUG
288: // if(rw_flag == SR_DMA_RD) {
289: // XCDBG(("st: READ; count = %xH\n", uiop->uio_iov->iov_len));
290: // }
291: // else {
292: // XCDBG(("st: WRITE; count = %xH\n", uiop->uio_iov->iov_len));
293: // }
294: #endif DEBUG
295:
296: /*
297: * FIXME: should wire user's memory and DMA from there, avoiding
298: * a copyin() or copyout().
299: */
300:
301: alignedBuf = [[scsiTape controller]
302: allocateBufferOfLength: uiop->uio_iov->iov_len
303: actualStart: &freePtr
304: actualLength: &freeCnt];
305:
306:
307: bzero(&scsiReq, sizeof(IOSCSIRequest));
308:
309: scsiReq.target = [scsiTape target];
310: scsiReq.lun = [scsiTape lun];
311:
312: [[scsiTape controller] getDMAAlignment:&dmaAlign];
313: if(dmaAlign.readLength > 1) {
314: scsiReq.maxTransfer = IOAlign(int, uiop->uio_iov->iov_len,
315: dmaAlign.readLength);
316:
317: } else {
318: scsiReq.maxTransfer = uiop->uio_iov->iov_len;
319: }
320:
321: scsiReq.timeoutLength = ST_IOTO_NORM;
322: scsiReq.disconnect = 1;
323: cdbp->c6s_lun = [scsiTape lun];
324:
325: if ([scsiTape isFixedBlock]) {
326: /* c6s_len is BLOCK COUNT */
327: length = howmany(uiop->uio_iov->iov_len, [scsiTape blockSize]);
328: cdbp->c6s_opt = C6OPT_FIXED;
329:
330: #ifdef DEBUG
331: IOLog ("SCSI Tape read/write: set up for fixed block transfer\n");
332: #endif DEBUG
333:
334:
335: } else {
336: length = uiop->uio_iov->iov_len;
337: if(rw_flag == SR_DMA_RD)
338: if ([scsiTape suppressIllegalLength]) {
339: cdbp->c6s_opt |= C6OPT_SIL;
340:
341: #ifdef DEBUG
342: IOLog ("SCSI Tape read: variable block read, suppress illegal len errs\n");
343: #endif DEBUG
344:
345: }
346: else {
347:
348: #ifdef DEBUG
349: IOLog ("SCSI Tape read: variable block read, allow illegal len errs\n");
350: #endif DEBUG
351:
352: }
353: }
354: assign_cdb_c6s_len (cdbp, length);
355:
356: #ifdef DEBUG
357: IOLog ("Transfer Length is %d\n", length);
358: #endif DEBUG
359:
360: if(length > C6S_MAXLEN) {
361: rtn = EINVAL;
362: goto out;
363: }
364:
365: if(rw_flag == SR_DMA_RD) {
366: cdbp->c6s_opcode = C6OP_READ;
367: scsiReq.read = YES;
368: }
369: else {
370: cdbp->c6s_opcode = C6OP_WRITE;
371: scsiReq.read = NO;
372:
373: }
374:
375: scsiReq.bytesTransferred = 0;
376:
377: /* Copy user data to kernel space if write. */
378: if(rw_flag == SR_DMA_WR)
379: if((rtn = copyin(uiop->uio_iov->iov_base, alignedBuf,
380: uiop->uio_iov->iov_len)))
381: goto out;
382:
383: if ((scRet = [scsiTape executeRequest: &scsiReq
384: buffer: alignedBuf
385: client: IOVmTaskSelf()
386: senseBuf: [scsiTape senseDataPtr]]) != SR_IOST_GOOD) {
387:
388: rtn = EIO;
389:
390: #ifdef DEBUG
391: IOLog ("st_rw: returned on failure from executeRequest\n");
392: IOLog ("st_rw: ---- returned %d\n", scRet);
393: #endif DEBUG
394:
395:
396: goto out;
397: }
398:
399: /* It worked. Copy data to user space if read. */
400: if(scsiReq.bytesTransferred && (rw_flag == SR_DMA_RD)) {
401: rtn = copyout(alignedBuf, uiop->uio_iov->iov_base,
402: scsiReq.bytesTransferred);
403:
404: #ifdef DEBUG
405: IOLog ("return value from copyout is %d\n", rtn);
406: #endif DEBUG
407:
408: }
409:
410: if(scsiReq.driverStatus != SR_IOST_GOOD) { // XXX Can this happen?
411: rtn = EIO;
412: }
413:
414: out:
415:
416: #ifdef DEBUG
417: IOLog ("SCSI st_rw transferred %d bytes out of %d\n",
418: scsiReq.bytesTransferred, uiop->uio_iov->iov_len);
419: #endif DEBUG
420:
421: uiop->uio_resid = uiop->uio_iov->iov_len - scsiReq.bytesTransferred;
422: IOFree (freePtr, freeCnt);
423: IOSetUNIXError (rtn);
424: return rtn;
425:
426: } /* st_rw() */
427:
428:
429: /*
430: * ioctl for SCSI Tape.
431: * XXX sc_return_t to errno conversions could use more review.
432: */
433: int
434: stioctl(dev_t dev,
435: int cmd, /* MTIOCTOP, etc */
436: caddr_t data, /* actually a ptr to mt_op or mtget, if used */
437: int flag) /* for historical reasons. Not used. */
438: {
439: int error = 0;
440: int unit = ST_UNIT(dev);
441: id scsiTape = stIdMap[unit];
442: struct mtget *mgp = (struct mtget *)data;
443: struct esense_reply *erp;
444: sc_status_t scsi_err;
445:
446:
447: if (unit >= NST)
448: return(ENXIO);
449: switch (cmd) {
450: case MTIOCTOP: /* do tape op */
451:
452: if ((scsi_err =
453: [scsiTape executeMTOperation: (struct mtop *) data]) !=
454: SR_IOST_GOOD) {
455:
456: if (scsi_err == SR_IOST_CMDREJ) {
457: error = EINVAL;
458: } else {
459: error = EIO;
460: }
461: }
462: break;
463:
464: case MTIOCGET: /* get status */
465:
466: erp = [scsiTape senseDataPtr];
467:
468: /*
469: * If we just did a request sense command as part of
470: * error recovery, avoid doing another one and
471: * thus blowing away possible volatile status info.
472: */
473: if([scsiTape senseDataValid] == NO) {
474: if((scsi_err = [scsiTape requestSense: erp]) != SR_IOST_GOOD) {
475: error = EIO;
476: break;
477: }
478: }
479:
480: /*
481: * [scsiTape senseDataPtr] now definitely contains valid
482: * sense data.
483: */
484: if(ST_EXABYTE(dev))
485: mgp->mt_type = MT_ISEXB;
486: else
487: mgp->mt_type = MT_ISGS;
488: mgp->mt_dsreg = ((u_char *)erp)[2];
489: mgp->mt_erreg = erp->er_addsensecode;
490: mgp->mt_ext_err0 = (((u_short)erp->er_stat_13) << 8) |
491: ((u_short)erp->er_stat_14);
492: mgp->mt_ext_err1 = (((u_short)erp->er_stat_15) << 8) |
493: ((u_short)erp->er_rsvd_16);
494:
495: #if __BIG_ENDIAN__
496: mgp->mt_resid = (u_int) erp->er_info;
497: #elif __LITTLE_ENDIAN__
498: mgp->mt_resid = read_er_info_low_24();
499: mgp->mt_resid |= (u_int) erp->er_info3;
500: #endif
501:
502: /* force actual request sense next time */
503: [scsiTape forceSenseDataInvalid];
504: break;
505:
506: case MTIOCFIXBLK: /* set fixed block mode */
507: error = [scsiTape
508: errnoFromReturn: [scsiTape setBlockSize: *(int *)data]];
509: break;
510:
511: case MTIOCVARBLK: /* set variable block mode */
512: error = [scsiTape
513: errnoFromReturn: [scsiTape setBlockSize: 0]];
514: break;
515:
516: case MTIOCINILL: /* inhibit illegal length
517: * errors on Read */
518: [scsiTape setSuppressIllegalLength: YES];
519: break;
520:
521: case MTIOCALILL: /* allow illegal length
522: * errors on Read */
523: [scsiTape setSuppressIllegalLength: NO];
524: break;
525:
526: case MTIOCMODSEL: /* mode select */
527: error = 0;
528: if ([scsiTape stModeSelect: (struct modesel_parms *)data] !=
529: SR_IOST_GOOD) {
530:
531: error = EIO;
532: break;
533: }
534:
535: case MTIOCMODSEN: /* mode sense */
536: error = 0;
537: if ([scsiTape stModeSense: (struct modesel_parms *)data] !=
538: SR_IOST_GOOD) {
539:
540: error = EIO;
541: break;
542: }
543:
544: case MTIOCSRQ: /* I/O via scsi_req */
545: error = st_doiocsrq(scsiTape, (struct scsi_req *) data);
546: break;
547:
548: default:
549: error = EINVAL; /* invalid argument */
550: break;
551: }
552: IOSetUNIXError (error); /* XXX Probably not necessary */
553: return error;
554: } /* stioctl() */
555:
556:
557:
558: /*
559: * Lifted directly from sg driver.
560: *
561: * Execute one scsi_req. Called from client's task context. Returns an errno.
562: */
563:
564: /*
565: * FIXME - DMA to non-page-aligned user memory doesn't work. There
566: * is data corruption on read operations; the corruption occurs on page
567: * boundaries.
568: */
569: #define FORCE_PAGE_ALIGN 1
570: #if FORCE_PAGE_ALIGN
571: int stForcePageAlign = 1;
572: #endif FORCE_PAGE_ALIGN
573:
574: static int st_doiocsrq(id scsiTape, scsi_req_t *srp)
575: {
576: void *alignedPtr = NULL;
577: unsigned alignedLen = 0;
578: void *freePtr;
579: unsigned freeLen;
580: BOOL didAlign = NO;
581: vm_task_t client = NULL;
582: int rtn = 0;
583: IOSCSIRequest scsiReq;
584: sc_status_t srtn;
585:
586: if(srp->sr_dma_max > [[scsiTape controller] maxTransfer]) {
587: return EINVAL;
588: }
589:
590: /* Get some well-aligned memory if necessary. By using
591: * allocateBufferOfLength we guarantee that there is enough space
592: * in the buffer we pass to the controller to handle
593: * end-of-buffer alignment, although we won't copy more
594: * than sr_dma_max to or from the caller.
595: */
596: if(srp->sr_dma_max != 0) {
597:
598: IODMAAlignment dmaAlign;
599: id controller = [scsiTape controller];
600: unsigned alignLength;
601: unsigned alignStart;
602:
603: /*
604: * Get appropriate alignment from controller.
605: */
606: [[scsiTape controller] getDMAAlignment:&dmaAlign];
607: if(srp->sr_dma_dir == SR_DMA_WR) {
608: alignLength = dmaAlign.writeLength;
609: alignStart = dmaAlign.writeStart;
610: }
611: else {
612: alignLength = dmaAlign.readLength;
613: alignStart = dmaAlign.readStart;
614: }
615: #if FORCE_PAGE_ALIGN
616: if(stForcePageAlign) {
617: alignStart = PAGE_SIZE;
618: }
619: #endif FORCE_PAGE_ALIGN
620: if( ( (alignStart > 1) &&
621: !IOIsAligned(srp->sr_addr, alignStart)
622: ) ||
623: ( (alignLength > 1) &&
624: !IOIsAligned(srp->sr_dma_max, alignLength)
625: ) ||
626: /*
627: ` * XXX Prevent DMA from user space for now, even if the
628: * buffer is well-aligned. We need to wire down the user
629: * memory if we are going to DMA from it.
630: */
631: YES
632: ) {
633:
634: /*
635: * DMA from kernel memory, we allocate and copy.
636: */
637:
638: didAlign = YES;
639: client = IOVmTaskSelf();
640:
641: if(alignLength > 1) {
642: alignedLen = IOAlign(unsigned,
643: srp->sr_dma_max,
644: alignLength);
645: }
646: else {
647: alignedLen = srp->sr_dma_max;
648: }
649: alignedPtr = [controller allocateBufferOfLength:
650: srp->sr_dma_max
651: actualStart:&freePtr
652: actualLength:&freeLen];
653: if(srp->sr_dma_dir == SR_DMA_WR) {
654: rtn = copyin(srp->sr_addr, alignedPtr,
655: srp->sr_dma_max);
656: if(rtn) {
657: rtn = EFAULT;
658: goto err_exit;
659: }
660: }
661: }
662: else {
663: /*
664: * Well-aligned buffer, DMA directly to/from user
665: * space.
666: */
667: alignedLen = srp->sr_dma_max;
668: alignedPtr = srp->sr_addr;
669: client = IOVmTaskCurrent();
670: didAlign = NO;
671: }
672: }
673:
674: /*
675: * Generate a contemporary version of scsi_req.
676: */
677: bzero(&scsiReq, sizeof(scsiReq));
678: scsiReq.target = [scsiTape target];
679: scsiReq.lun = [scsiTape lun];
680:
681: /*
682: * Careful. this assumes that the old and new cdb structs are
683: * equivalent...
684: */
685: scsiReq.cdb = srp->sr_cdb;
686: scsiReq.read = (srp->sr_dma_dir == SR_DMA_RD) ? YES : NO;
687: scsiReq.maxTransfer = alignedLen;
688: scsiReq.timeoutLength = srp->sr_ioto;
689: scsiReq.disconnect = 1;
690:
691: /*
692: * Go for it.
693: *
694: * XXX Should use the SCSITape object's sense buffer, because
695: * that's where MTIOCGET looks for valid sense data, and then
696: * copy back the sense data to the old-style scsi_req's sense
697: * buffer.
698: */
699: srtn = [scsiTape executeRequest:&scsiReq
700: buffer : alignedPtr
701: client : client
702: senseBuf : &srp->sr_esense];
703:
704: /*
705: * Copy status back to user. Note that if we got this far, we
706: * return good status from the function; errors are in
707: * srp->sr_io_status.
708: */
709: srp->sr_io_status = srtn;
710: srp->sr_scsi_status = scsiReq.scsiStatus;
711: srp->sr_dma_xfr = scsiReq.bytesTransferred;
712: if(srp->sr_dma_xfr > srp->sr_dma_max) {
713: srp->sr_dma_xfr = srp->sr_dma_max;
714: }
715: ns_time_to_timeval(scsiReq.totalTime, &srp->sr_exec_time);
716:
717: /*
718: * Copy read data back to user if appropriate.
719: */
720: if((srp->sr_dma_dir == SR_DMA_RD) &&
721: (scsiReq.bytesTransferred != 0) && didAlign) {
722:
723: rtn = copyout(alignedPtr,
724: srp->sr_addr,
725: srp->sr_dma_xfr);
726: }
727: err_exit:
728: if(didAlign) {
729: IOFree(freePtr, freeLen);
730: }
731: return rtn;
732: }
733:
734:
735: /*
736: * Supporting function for managing byte-order swapping.
737: */
738: unsigned int
739: read_er_info_low_24(struct esense_reply *erp)
740: {
741: #if __BIG_ENDIAN__
742: return ((unsigned int) erp->er_info);
743: #elif __LITTLE_ENDIAN__
744: return (unsigned int)
745: (erp->er_info2 << 16) + (erp->er_info1 << 8) + erp->er_info0;
746:
747: #endif
748:
749: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.