|
|
1.1 root 1: /*
2: * Written by Julian Elischer ([email protected])
3: * for TRW Financial Systems for use under the MACH(2.5) operating system.
1.1.1.2 ! root 4: * Hacked by Theo de Raadt <[email protected]>
1.1 root 5: *
6: * TRW Financial Systems, in accordance with their agreement with Carnegie
7: * Mellon University, makes this software available to CMU to distribute
8: * or use in any manner that they see fit as long as this message is kept with
9: * the software. For this reason TFS also grants any other persons or
10: * organisations permission to use or modify this software.
11: *
12: * TFS supplies this software to be publicly redistributed
13: * on the understanding that TFS is not responsible for the correct
14: * functioning of this software in any circumstances.
15: *
1.1.1.2 ! root 16: * scsiconf.c,v 1.9 1993/05/27 10:14:02 deraadt Exp
1.1 root 17: */
18:
1.1.1.2 ! root 19: #include "sys/types.h"
! 20: #include "sys/param.h"
! 21: #include "sys/systm.h"
! 22: #include "sys/errno.h"
! 23: #include "sys/ioctl.h"
! 24: #include "sys/buf.h"
! 25: #include "sys/proc.h"
! 26: #include "sys/user.h"
! 27: #include "sys/dkbad.h"
! 28: #include "sys/disklabel.h"
! 29: #include "scsi/scsi_all.h"
! 30: #include "scsi/scsiconf.h"
1.1 root 31:
32: #include "st.h"
33: #include "sd.h"
34: #include "ch.h"
35: #include "cd.h"
36: #define NBLL 0
37: #define NCALS 0
38: #define NKIL 0
39:
40: #if NSD > 0
1.1.1.2 ! root 41: extern int sdattach();
1.1 root 42: #endif NSD
43: #if NST > 0
1.1.1.2 ! root 44: extern int stattach();
1.1 root 45: #endif NST
46: #if NCH > 0
1.1.1.2 ! root 47: extern int chattach();
1.1 root 48: #endif NCH
49: #if NCD > 0
1.1.1.2 ! root 50: extern int cdattach();
1.1 root 51: #endif NCD
52: #if NBLL > 0
1.1.1.2 ! root 53: extern int bllattach();
1.1 root 54: #endif NBLL
55: #if NCALS > 0
1.1.1.2 ! root 56: extern int calsattach();
1.1 root 57: #endif NCALS
58: #if NKIL > 0
1.1.1.2 ! root 59: extern int kil_attach();
1.1 root 60: #endif NKIL
61:
1.1.1.2 ! root 62: struct scsidevs knowndevs[] = {
1.1 root 63: #if NSD > 0
1.1.1.2 ! root 64: {
! 65: SC_TSD, T_DIRECT, T_FIXED, "standard", "any" ,"any",
! 66: sdattach, "sd" ,SC_ONE_LU
! 67: }, {
! 68: SC_TSD, T_DIRECT, T_FIXED, "MAXTOR ", "XT-4170S ", "B5A ",
! 69: sdattach, "mx1", SC_ONE_LU
! 70: },
1.1 root 71: #endif NSD
72: #if NST > 0
1.1.1.2 ! root 73: {
! 74: SC_TST, T_SEQUENTIAL, T_REMOV, "standard", "any", "any",
! 75: stattach, "st" ,SC_ONE_LU
! 76: },
1.1 root 77: #endif NST
1.1.1.2 ! root 78: #if NCD > 0
! 79: {
! 80: SC_TCD, T_READONLY, T_REMOV, "SONY ", "CD-ROM CDU-8012 ", "3.1a",
! 81: cdattach, "cd", SC_ONE_LU
! 82: }, {
! 83: SC_TCD, T_READONLY, T_REMOV, "PIONEER ", "CD-ROM DRM-600 ", "any",
! 84: cdattach, "cd", SC_MORE_LUS
! 85: },
! 86: #endif NCD
1.1 root 87: #if NCALS > 0
1.1.1.2 ! root 88: {
! 89: -1, T_PROCESSOR, T_FIXED, "standard" , "any" ,"any",
! 90: calsattach, "cals", SC_MORE_LUS
! 91: }
1.1 root 92: #endif NCALS
93: #if NCH > 0
1.1.1.2 ! root 94: {
! 95: -1, T_CHANGER, T_REMOV, "standard", "any", "any",
! 96: chattach, "ch", SC_ONE_LU
! 97: },
1.1 root 98: #endif NCH
99: #if NBLL > 0
1.1.1.2 ! root 100: {
! 101: -1, T_PROCESSOR, T_FIXED, "AEG ", "READER ", "V1.0",
! 102: bllattach, "bll", SC_MORE_LUS
! 103: },
1.1 root 104: #endif NBLL
105: #if NKIL > 0
1.1.1.2 ! root 106: {
! 107: -1, T_SCANNER, T_FIXED, "KODAK ", "IL Scanner 900 ", "any",
! 108: kil_attach, "kil", SC_ONE_LU
! 109: },
1.1 root 110: #endif NKIL
111: };
112:
1.1.1.2 ! root 113: /* controls debug level within the scsi subsystem: see scsiconf.h */
! 114: int scsi_debug = 0;
1.1 root 115:
1.1.1.2 ! root 116: struct scsidevs *
! 117: scsi_probe(int masunit, struct scsi_switch *sw, int physid, int type, int want)
1.1 root 118: {
1.1.1.2 ! root 119: static struct scsi_inquiry_data inqbuf;
! 120: struct scsidevs *ret = (struct scsidevs *)0;
! 121: int targ = physid >> 3;
! 122: int lun = physid & 7;
! 123: char *qtype=NULL, *dtype=NULL, *desc;
! 124: char manu[9], model[17], revision[5];
! 125: int len;
! 126:
! 127: bzero(&inqbuf, sizeof inqbuf);
! 128:
! 129: /*printf("probe: %s%d targ %d lun %d\n",
! 130: sw->name, masunit, targ, lun);*/
! 131:
! 132: if( scsi_ready(masunit, targ, lun, sw,
! 133: SCSI_NOSLEEP | SCSI_NOMASK) != COMPLETE)
! 134: return (struct scsidevs *)-1;
1.1 root 135:
1.1.1.2 ! root 136: if( scsi_inquire(masunit, targ, lun, sw, (u_char *)&inqbuf,
! 137: SCSI_NOSLEEP | SCSI_NOMASK) != COMPLETE)
1.1 root 138: return (struct scsidevs *)0;
139:
1.1.1.2 ! root 140: if( inqbuf.device_qualifier==3 && inqbuf.device_type==T_NODEVICE)
! 141: return (struct scsidevs *)0;
1.1 root 142:
1.1.1.2 ! root 143: switch(inqbuf.device_qualifier) {
! 144: case 0:
! 145: qtype = "";
1.1 root 146: break;
1.1.1.2 ! root 147: case 1:
! 148: qtype = "Unit not Connected!";
1.1 root 149: break;
1.1.1.2 ! root 150: case 2:
! 151: qtype =", Reserved Peripheral Qualifier!";
1.1 root 152: break;
1.1.1.2 ! root 153: case 3:
! 154: qtype = ", The Target can't support this Unit!";
1.1 root 155: break;
1.1.1.2 ! root 156: default:
! 157: dtype = "vendor specific";
! 158: qtype = "";
1.1 root 159: break;
160: }
161:
1.1.1.2 ! root 162: if (dtype == NULL) {
! 163: switch(inqbuf.device_type) {
! 164: case T_DIRECT:
! 165: dtype = "direct";
! 166: break;
! 167: case T_SEQUENTIAL:
! 168: dtype = "seq";
! 169: break;
! 170: case T_PRINTER:
! 171: dtype = "pr";
! 172: break;
! 173: case T_PROCESSOR:
! 174: dtype = "cpu";
! 175: break;
! 176: case T_READONLY:
! 177: dtype = "ro";
! 178: break;
! 179: case T_WORM:
! 180: dtype = "worm";
! 181: break;
! 182: case T_SCANNER:
! 183: dtype = "scan";
! 184: break;
! 185: case T_OPTICAL:
! 186: dtype = "optic";
! 187: break;
! 188: case T_CHANGER:
! 189: dtype = "changer";
! 190: break;
! 191: case T_COMM:
! 192: dtype = "comm";
! 193: break;
! 194: default:
! 195: dtype = "???";
! 196: break;
1.1 root 197: }
1.1.1.2 ! root 198: }
1.1 root 199:
1.1.1.2 ! root 200: if(inqbuf.ansii_version > 0) {
! 201: len = inqbuf.additional_length +
! 202: ((char *)inqbuf.unused - (char *)&inqbuf);
! 203: if( len > sizeof(struct scsi_inquiry_data) - 1)
1.1 root 204: len = sizeof(struct scsi_inquiry_data) - 1;
1.1.1.2 ! root 205: desc = inqbuf.vendor;
! 206: desc[len-(desc-(char *)&inqbuf)] = 0;
! 207: strncpy(manu, inqbuf.vendor, sizeof inqbuf.vendor);
! 208: manu[sizeof inqbuf.vendor] = '\0';
! 209: strncpy(model, inqbuf.product, sizeof inqbuf.product);
! 210: model[sizeof inqbuf.product] = '\0';
! 211: strncpy(revision, inqbuf.revision, sizeof inqbuf.revision);
! 212: revision[sizeof inqbuf.revision] = '\0';
! 213: } else {
! 214: desc = "early protocol device";
! 215: strcpy(manu, "????");
! 216: strcpy(model, "");
! 217: strcpy(revision, "");
! 218: }
! 219:
! 220: if(want)
! 221: goto print;
! 222:
! 223: ret = selectdev(masunit, targ, lun, sw, inqbuf.device_qualifier,
! 224: inqbuf.device_type, inqbuf.removable, manu, model, revision, type);
! 225: if(sw->printed[targ] & (1<<lun))
! 226: return ret;
! 227:
! 228: print:
! 229: printf("%s%d targ %d lun %d: type %d(%s) %s <%s%s%s> SCSI%d\n",
! 230: sw->name, masunit, targ, lun,
! 231: inqbuf.device_type, dtype,
! 232: inqbuf.removable ? "removable" : "fixed",
! 233: manu, model, revision, inqbuf.ansii_version);
! 234: if(qtype[0])
! 235: printf("%s%d targ %d lun %d: qualifier %d(%s)\n",
! 236: sw->name, masunit, targ, lun,
! 237: inqbuf.device_qualifier, qtype);
! 238: sw->printed[targ] |= (1<<lun);
! 239: return ret;
1.1 root 240: }
241:
1.1.1.2 ! root 242: void
! 243: scsi_warn(int masunit, int mytarg, struct scsi_switch *sw)
1.1 root 244: {
1.1.1.2 ! root 245: struct scsidevs *match = (struct scsidevs *)0;
! 246: int physid;
! 247: int targ, lun;
1.1 root 248:
1.1.1.2 ! root 249: for(targ=0; targ<8; targ++) {
! 250: if(targ==mytarg)
1.1 root 251: continue;
1.1.1.2 ! root 252: for(lun=0; lun<8; lun++) {
! 253: /* check if device already used, or empty */
! 254: if( sw->empty[targ] & (1<<lun) )
! 255: continue;
! 256: if( sw->used[targ] & (1<<lun) )
! 257: continue;
! 258:
! 259: physid = targ*8 + lun;
! 260: match = scsi_probe(masunit, sw, physid, 0, 0);
! 261:
! 262: if(match == (struct scsidevs *)-1) {
! 263: if(lun==0)
! 264: sw->empty[targ] = 0xff;
! 265: else
! 266: sw->empty[targ] = 0xff;
! 267: continue;
! 268: }
! 269: if(match) {
! 270: targ = physid >> 3;
! 271: lun = physid & 7;
! 272: if(match->flags & SC_MORE_LUS)
! 273: sw->empty[targ] |= (1<<lun);
! 274: else
! 275: sw->empty[targ] = 0xff;
! 276: }
1.1 root 277: }
1.1.1.2 ! root 278: }
! 279: }
! 280:
! 281: /*
! 282: * not quite perfect. If we have two "drive ?" entries, this will
! 283: * probe through all the devices twice. It should have realized that
! 284: * any device that is not found the first time won't exist later on.
! 285: */
! 286: int
! 287: scsi_attach(int masunit, int mytarg, struct scsi_switch *sw,
! 288: int *physid, int *unit, int type)
! 289: {
! 290: struct scsidevs *match = (struct scsidevs *)0;
! 291: int targ, lun;
! 292: int ret=0;
! 293:
! 294: /*printf("%s%d probing at targ %d lun %d..\n",
! 295: sw->name, masunit, *physid >> 3, *physid & 7);*/
! 296:
! 297: if( *physid!=-1 ) {
! 298: targ = *physid >> 3;
! 299: lun = *physid & 7;
! 300:
! 301: if( (sw->empty[targ] & (1<<lun)) || (sw->used[targ] & (1<<lun)) )
! 302: return 0;
! 303:
! 304: match = scsi_probe(masunit, sw, *physid, type, 0);
! 305: if(match == (struct scsidevs *)-1) {
! 306: match = (struct scsidevs *)0;
! 307: if(lun==0)
! 308: sw->empty[targ] = 0xff;
! 309: else
! 310: sw->empty[targ] |= (1<<lun);
! 311: return 0;
1.1 root 312: }
1.1.1.2 ! root 313:
! 314: if(!match)
! 315: return 0;
! 316:
! 317: ret = (*(match->attach_rtn))(masunit, sw, *physid, unit);
! 318: goto success;
! 319: }
! 320:
! 321: for(targ=0; targ<8; targ++) {
! 322: if(targ==mytarg)
1.1 root 323: continue;
1.1.1.2 ! root 324: for(lun=0; lun<8; lun++) {
! 325: if( (sw->empty[targ] & (1<<lun)) || (sw->used[targ] & (1<<lun)) )
! 326: continue;
! 327:
! 328: *physid = targ*8 + lun;
! 329: match = scsi_probe(masunit, sw, *physid, type, 0);
! 330: if( match==(struct scsidevs *)-1) {
! 331: if(lun==0)
! 332: sw->empty[targ] = 0xff;
! 333: else
! 334: sw->empty[targ] |= (1<<lun);
! 335: match = (struct scsidevs *)0;
! 336: continue;
! 337: }
! 338: if(!match)
! 339: break;
! 340: ret = (*(match->attach_rtn))(masunit, sw, *physid, unit);
! 341: if(ret)
! 342: goto success;
! 343: return 0;
! 344: }
! 345: }
! 346: *physid = -1; /* failed... */
! 347: return 0;
! 348:
! 349: success:
! 350: targ = *physid >> 3;
! 351: lun = *physid & 7;
! 352: if(match->flags & SC_MORE_LUS)
! 353: sw->used[targ] |= (1<<lun);
! 354: else
! 355: sw->used[targ] = 0xff;
! 356: return ret;
! 357: }
! 358:
! 359: /*
! 360: * Try make as good a match as possible with
! 361: * available sub drivers
! 362: */
! 363: struct scsidevs *
! 364: selectdev(int unit, int target, int lu, struct scsi_switch *sw, int qual,
! 365: int dtype, int remov, char *manu, char *model, char *rev, int type)
! 366: {
! 367: struct scsidevs *sdbest = (struct scsidevs *)0;
! 368: struct scsidevs *sdent = knowndevs;
! 369: int numents = sizeof(knowndevs)/sizeof(struct scsidevs);
! 370: int count = 0, sdbestes = 0;
! 371:
! 372: dtype |= (qual << 5);
! 373:
! 374: sdent--;
! 375: while( count++ < numents) {
! 376: sdent++;
! 377: if(dtype != sdent->dtype)
1.1 root 378: continue;
1.1.1.2 ! root 379: if(type != sdent->type)
1.1 root 380: continue;
1.1.1.2 ! root 381: if(sdbestes < 1) {
! 382: sdbestes = 1;
! 383: sdbest = sdent;
1.1 root 384: }
1.1.1.2 ! root 385: if(remov != sdent->removable)
1.1 root 386: continue;
1.1.1.2 ! root 387: if(sdbestes < 2) {
! 388: sdbestes = 2;
! 389: sdbest = sdent;
! 390: }
! 391: if(sdent->flags & SC_SHOWME)
! 392: printf("\n%s-\n%s-", sdent->manufacturer, manu);
! 393: if(strcmp(sdent->manufacturer, manu))
! 394: continue;
! 395: if(sdbestes < 3) {
! 396: sdbestes = 3;
! 397: sdbest = sdent;
! 398: }
! 399: if(sdent->flags & SC_SHOWME)
! 400: printf("\n%s-\n%s-",sdent->model, model);
! 401: if(strcmp(sdent->model, model))
! 402: continue;
! 403: if(sdbestes < 4) {
! 404: sdbestes = 4;
! 405: sdbest = sdent;
! 406: }
! 407: if(sdent->flags & SC_SHOWME)
! 408: printf("\n%s-\n%s-",sdent->version, rev);
! 409: if(strcmp(sdent->version, rev))
! 410: continue;
! 411: if(sdbestes < 5) {
! 412: sdbestes = 5;
! 413: sdbest = sdent;
1.1 root 414: break;
415: }
416: }
1.1.1.2 ! root 417: return sdbest;
1.1 root 418: }
419:
1.1.1.2 ! root 420: /*
! 421: * Do a scsi operation asking a device if it is
! 422: * ready. Use the scsi_cmd routine in the switch
! 423: * table.
! 424: */
! 425: int
! 426: scsi_ready(int unit, int target, int lu,
! 427: struct scsi_switch *sw, int flags)
1.1 root 428: {
1.1.1.2 ! root 429: struct scsi_test_unit_ready scsi_cmd;
! 430: struct scsi_xfer scsi_xfer;
1.1 root 431: volatile int rval;
1.1.1.2 ! root 432: int key;
1.1 root 433:
434: bzero(&scsi_cmd, sizeof(scsi_cmd));
435: bzero(&scsi_xfer, sizeof(scsi_xfer));
436: scsi_cmd.op_code = TEST_UNIT_READY;
437:
1.1.1.2 ! root 438: scsi_xfer.flags = flags | INUSE;
! 439: scsi_xfer.adapter = unit;
! 440: scsi_xfer.targ = target;
! 441: scsi_xfer.lu = lu;
! 442: scsi_xfer.cmd = (struct scsi_generic *)&scsi_cmd;
! 443: scsi_xfer.retries = 8;
! 444: scsi_xfer.timeout = 10000;
! 445: scsi_xfer.cmdlen = sizeof(scsi_cmd);
! 446: scsi_xfer.data = 0;
! 447: scsi_xfer.datalen = 0;
! 448: scsi_xfer.resid = 0;
! 449: scsi_xfer.when_done = 0;
! 450: scsi_xfer.done_arg = 0;
! 451: retry: scsi_xfer.error = 0;
! 452:
! 453: /* don't use interrupts! */
! 454:
! 455: rval = (*(sw->scsi_cmd))(&scsi_xfer);
! 456: if (rval != COMPLETE) {
! 457: if(scsi_debug) {
! 458: printf("scsi error, rval = 0x%x\n", rval);
! 459: printf("code from driver: 0x%x\n", scsi_xfer.error);
! 460: }
! 461: switch(scsi_xfer.error) {
! 462: case XS_SENSE:
! 463: /*
! 464: * Any sense value is illegal except UNIT ATTENTION
! 465: * In which case we need to check again to get the
! 466: * correct response. (especially exabytes)
! 467: */
! 468: if(scsi_xfer.sense.error_class == 7 ) {
1.1 root 469: key = scsi_xfer.sense.ext.extended.sense_key ;
1.1.1.2 ! root 470: switch(key) {
! 471: case 2: /* not ready BUT PRESENT! */
! 472: return(COMPLETE);
! 473: case 6:
1.1 root 474: spinwait(1000);
1.1.1.2 ! root 475: if(scsi_xfer.retries--) {
1.1 root 476: scsi_xfer.flags &= ~ITSDONE;
477: goto retry;
478: }
479: return(COMPLETE);
480: default:
481: if(scsi_debug)
1.1.1.2 ! root 482: printf("%d:%d,key=%x.", target,
! 483: lu, key);
1.1 root 484: }
485: }
486: return(HAD_ERROR);
1.1.1.2 ! root 487: case XS_BUSY:
1.1 root 488: spinwait(1000);
1.1.1.2 ! root 489: if(scsi_xfer.retries--) {
1.1 root 490: scsi_xfer.flags &= ~ITSDONE;
491: goto retry;
492: }
1.1.1.2 ! root 493: return COMPLETE; /* it's busy so it's there */
! 494: case XS_TIMEOUT:
1.1 root 495: default:
1.1.1.2 ! root 496: return HAD_ERROR;
1.1 root 497: }
498: }
1.1.1.2 ! root 499: return COMPLETE;
1.1 root 500: }
1.1.1.2 ! root 501:
! 502: /*
! 503: * Do a scsi operation asking a device what it is
! 504: * Use the scsi_cmd routine in the switch table.
! 505: */
! 506: int
! 507: scsi_inquire(int unit, int target, int lu, struct scsi_switch *sw,
! 508: u_char *inqbuf, int flags)
1.1 root 509: {
1.1.1.2 ! root 510: struct scsi_inquiry scsi_cmd;
! 511: struct scsi_xfer scsi_xfer;
1.1 root 512:
513: bzero(&scsi_cmd, sizeof(scsi_cmd));
514: bzero(&scsi_xfer, sizeof(scsi_xfer));
515: scsi_cmd.op_code = INQUIRY;
516: scsi_cmd.length = sizeof(struct scsi_inquiry_data);
517:
1.1.1.2 ! root 518: scsi_xfer.flags = flags | SCSI_DATA_IN | INUSE;
! 519: scsi_xfer.adapter = unit;
! 520: scsi_xfer.targ = target;
! 521: scsi_xfer.lu = lu;
! 522: scsi_xfer.retries = 8;
! 523: scsi_xfer.timeout = 10000;
! 524: scsi_xfer.cmd = (struct scsi_generic *)&scsi_cmd;
! 525: scsi_xfer.cmdlen = sizeof(struct scsi_inquiry);
! 526: scsi_xfer.data = inqbuf;
! 527: scsi_xfer.datalen = sizeof(struct scsi_inquiry_data);
! 528: scsi_xfer.resid = sizeof(struct scsi_inquiry_data);
! 529: scsi_xfer.when_done = 0;
! 530: scsi_xfer.done_arg = 0;
! 531:
! 532: retry:
! 533: scsi_xfer.error=0;
! 534: /* don't use interrupts! */
! 535:
! 536: if ((*(sw->scsi_cmd))(&scsi_xfer) != COMPLETE) {
! 537: if(scsi_debug)
! 538: printf("inquiry had error(0x%x) ",scsi_xfer.error);
! 539: switch(scsi_xfer.error) {
! 540: case XS_NOERROR:
! 541: break;
! 542: case XS_SENSE:
! 543: /*
! 544: * Any sense value is illegal except UNIT ATTENTION
! 545: * In which case we need to check again to get the
! 546: * correct response. (especially exabytes)
! 547: */
! 548: if( scsi_xfer.sense.error_class==7 &&
! 549: scsi_xfer.sense.ext.extended.sense_key==6) {
! 550: /* it's changed so it's there */
1.1 root 551: spinwait(1000);
1.1.1.2 ! root 552: if(scsi_xfer.retries--) {
! 553: scsi_xfer.flags &= ~ITSDONE;
! 554: goto retry;
1.1 root 555: }
1.1.1.2 ! root 556: return COMPLETE;
1.1 root 557: }
1.1.1.2 ! root 558: return HAD_ERROR;
! 559: case XS_BUSY:
1.1 root 560: spinwait(1000);
1.1.1.2 ! root 561: if(scsi_xfer.retries--) {
1.1 root 562: scsi_xfer.flags &= ~ITSDONE;
563: goto retry;
564: }
1.1.1.2 ! root 565: case XS_TIMEOUT:
1.1 root 566: default:
567: return(HAD_ERROR);
568: }
569: }
1.1.1.2 ! root 570: return COMPLETE;
1.1 root 571: }
572:
1.1.1.2 ! root 573: /*
! 574: * convert a physical address to 3 bytes,
! 575: * MSB at the lowest address,
! 576: * LSB at the highest.
! 577: */
! 578: void
! 579: lto3b(u_long val, u_char *bytes)
1.1 root 580: {
581: *bytes++ = (val&0xff0000)>>16;
582: *bytes++ = (val&0xff00)>>8;
583: *bytes = val&0xff;
584: }
585:
1.1.1.2 ! root 586: /*
! 587: * The reverse of lto3b
! 588: */
! 589: u_long
! 590: _3btol(u_char *bytes)
1.1 root 591: {
1.1.1.2 ! root 592: u_long rc;
! 593:
1.1 root 594: rc = (*bytes++ << 16);
595: rc += (*bytes++ << 8);
596: rc += *bytes;
1.1.1.2 ! root 597: return rc;
1.1 root 598: }
599:
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.