|
|
1.1 ! root 1: /* ! 2: * Copyright (c) 1999 Apple Computer, Inc. All rights reserved. ! 3: * ! 4: * @APPLE_LICENSE_HEADER_START@ ! 5: * ! 6: * Portions Copyright (c) 1999 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 1.1 (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: /* ! 26: * Copyright (c) 1993 NeXT Computer, Inc. ! 27: * ! 28: * Protected mode emulation. ! 29: * ! 30: * HISTORY ! 31: * ! 32: * 8 Apr 1993 ? at NeXT ! 33: * Created. ! 34: */ ! 35: ! 36: #import <mach/mach_types.h> ! 37: ! 38: #import <architecture/i386/sel.h> ! 39: #import <architecture/i386/table.h> ! 40: ! 41: #import <machdep/i386/trap.h> ! 42: #import <machdep/i386/desc_inline.h> ! 43: #import <machdep/i386/err_inline.h> ! 44: ! 45: #import "PCprivate.h" ! 46: #import "PCmiscInline.h" ! 47: #import "PCtaskInline.h" ! 48: ! 49: static __inline__ ! 50: boolean_t ! 51: fetch_LDT_entry( ! 52: thread_t thread, ! 53: unsigned int index, ! 54: ldt_entry_t *desc ! 55: ) ! 56: { ! 57: PCshared_t shared = threadPCShared(thread); ! 58: unsigned int byte_index = index * sizeof (ldt_entry_t); ! 59: ! 60: if (shared->LDT.length <= byte_index) ! 61: return (FALSE); ! 62: ! 63: return (fetch8Byte(thread, shared->LDT.address + byte_index, desc)); ! 64: } ! 65: ! 66: static __inline__ ! 67: boolean_t ! 68: fetch_IDT_entry( ! 69: thread_t thread, ! 70: unsigned int index, ! 71: idt_entry_t *desc ! 72: ) ! 73: { ! 74: PCshared_t shared = threadPCShared(thread); ! 75: unsigned int byte_index = index * sizeof (ldt_entry_t); ! 76: ! 77: if (shared->IDT.length <= byte_index) ! 78: return (FALSE); ! 79: ! 80: return (fetch8Byte(thread, shared->IDT.address + byte_index, desc)); ! 81: } ! 82: ! 83: static ! 84: boolean_t ! 85: fetch_desc( ! 86: thread_t thread, ! 87: sel_t sel, ! 88: dt_entry_t *desc ! 89: ) ! 90: { ! 91: if (sel.ti != SEL_LDT) ! 92: return (FALSE); ! 93: ! 94: return (fetch_LDT_entry(thread, sel.index, (ldt_entry_t *)desc)); ! 95: } ! 96: ! 97: static __inline__ ! 98: vm_offset_t ! 99: segment_base( ! 100: data_desc_t *desc ! 101: ) ! 102: { ! 103: conv_base_t _base = { 0 }; ! 104: ! 105: _base.fields.base00 = desc->base00; ! 106: _base.fields.base16 = desc->base16; ! 107: _base.fields.base24 = desc->base24; ! 108: ! 109: return (_base.word); ! 110: } ! 111: ! 112: static __inline__ ! 113: boolean_t ! 114: fetch_segment_4byte( ! 115: thread_t thread, ! 116: data_desc_t *desc, ! 117: vm_offset_t offset, ! 118: boolean_t offsz, ! 119: unsigned int *data ! 120: ) ! 121: { ! 122: vm_offset_t base = segment_base(desc); ! 123: ! 124: if (!offsz) ! 125: offset &= 0xffff; ! 126: ! 127: return (fetch4Byte(thread, base + offset, data)); ! 128: } ! 129: ! 130: static ! 131: boolean_t __inline__ ! 132: fetch_segment( ! 133: thread_t thread, ! 134: data_desc_t *desc, ! 135: vm_offset_t offset, ! 136: boolean_t offsz, ! 137: void *data, ! 138: unsigned int length ! 139: ) ! 140: { ! 141: #define sdata ((unsigned short *)data) ! 142: vm_offset_t base = segment_base(desc); ! 143: unsigned int offmsk; ! 144: ! 145: if (offsz) ! 146: offmsk = 0xffffffff; ! 147: else ! 148: offmsk = 0xffff; ! 149: ! 150: thread->recover = (vm_offset_t)&&fault; ! 151: ! 152: while (length > 0) { ! 153: offset &= offmsk; ! 154: ! 155: asm volatile( ! 156: "movw %%fs:%1,%0" ! 157: : "=q" (*sdata) ! 158: : "m" (*(unsigned short *)(base + offset))); ! 159: ! 160: sdata++; offset += sizeof (*sdata); length -= sizeof (*sdata); ! 161: } ! 162: ! 163: thread->recover = 0; ! 164: return (TRUE); ! 165: ! 166: fault: ! 167: thread->recover = 0; ! 168: return (FALSE); ! 169: ! 170: #undef sdata ! 171: } ! 172: ! 173: static __inline__ ! 174: boolean_t ! 175: store_segment( ! 176: thread_t thread, ! 177: data_desc_t *desc, ! 178: vm_offset_t offset, ! 179: boolean_t offsz, ! 180: void *data, ! 181: unsigned int length ! 182: ) ! 183: { ! 184: #define sdata ((unsigned short *)data) ! 185: vm_offset_t base = segment_base(desc); ! 186: unsigned int offmsk; ! 187: ! 188: if (offsz) ! 189: offmsk = 0xffffffff; ! 190: else ! 191: offmsk = 0xffff; ! 192: ! 193: thread->recover = (vm_offset_t)&&fault; ! 194: ! 195: while (length > 0) { ! 196: offset &= offmsk; ! 197: ! 198: asm volatile( ! 199: "movw %0,%%fs:%1" ! 200: : ! 201: : "r" (*sdata), "m" (*(unsigned short *)(base + offset))); ! 202: ! 203: sdata++; offset += sizeof (*sdata); length -= sizeof (*sdata); ! 204: } ! 205: ! 206: thread->recover = 0; ! 207: return (TRUE); ! 208: ! 209: fault: ! 210: thread->recover = 0; ! 211: return (FALSE); ! 212: ! 213: #undef sdata ! 214: } ! 215: ! 216: static ! 217: boolean_t ! 218: handle_bop( ! 219: thread_t thread, ! 220: thread_saved_state_t *state ! 221: ) ! 222: { ! 223: PCcontext_t context = threadPCContext(thread); ! 224: code_desc_t code; ! 225: data_desc_t stack; ! 226: unsigned int inst; ! 227: unsigned char bbyte; ! 228: ! 229: if (!fetch_desc(thread, state->frame.cs, (dt_entry_t *)&code)) ! 230: return (FALSE); ! 231: ! 232: if (!fetch_segment_4byte( ! 233: thread, ! 234: (data_desc_t *)&code, state->frame.eip, code.opsz, ! 235: &inst)) ! 236: return (FALSE); ! 237: ! 238: if (*(unsigned short *)&inst != 0xC4C4) ! 239: return (FALSE); ! 240: ! 241: bbyte = *((unsigned char *)&inst + 2); ! 242: ! 243: if (bbyte == 0xFE) { ! 244: PCcancelTimers(context); ! 245: ! 246: context->callHandler = PC_HANDLE_EXIT; ! 247: ! 248: PCcallMonitor(thread, state); ! 249: /* NOTREACHED */ ! 250: } ! 251: else if (bbyte == 0xFA) { ! 252: PCbopFA_t args; ! 253: ! 254: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 255: return (FALSE); ! 256: ! 257: if (!fetch_segment( ! 258: thread, ! 259: &stack, state->frame.esp, stack.stksz, ! 260: &args, sizeof (args))) ! 261: return (FALSE); ! 262: ! 263: return (PCbopFA(thread, state, &args)); ! 264: } ! 265: else if (bbyte == 0xFC) { ! 266: PCbopFC_t args; ! 267: ! 268: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 269: return (FALSE); ! 270: ! 271: if (!fetch_segment( ! 272: thread, ! 273: &stack, state->frame.esp, stack.stksz, ! 274: &args, sizeof (args))) ! 275: return (FALSE); ! 276: ! 277: return (PCbopFC(thread, state, &args)); ! 278: /* NOTREACHED */ ! 279: } ! 280: else if (bbyte == 0xFD) { ! 281: PCbopFD_t args; ! 282: ! 283: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 284: return (FALSE); ! 285: ! 286: if (!fetch_segment( ! 287: thread, ! 288: &stack, state->frame.esp, stack.stksz, ! 289: &args, sizeof (args))) ! 290: return (FALSE); ! 291: ! 292: PCbopFD(thread, state, &args); ! 293: /* NOTREACHED */ ! 294: } ! 295: else { ! 296: context->trapNum = bbyte; ! 297: context->callHandler = PC_HANDLE_BOP; ! 298: ! 299: PCcallMonitor(thread, state); ! 300: /* NOTREACHED */ ! 301: } ! 302: } ! 303: ! 304: static ! 305: boolean_t ! 306: emulate_CLI( ! 307: thread_t thread, ! 308: thread_saved_state_t *state, ! 309: code_desc_t *desc ! 310: ) ! 311: { ! 312: PCcontext_t context = threadPCContext(thread); ! 313: ! 314: if (desc->opsz) ! 315: state->frame.eip += 1; ! 316: else ! 317: (unsigned short)state->frame.eip += 1; ! 318: ! 319: context->IF_flag = FALSE; ! 320: ! 321: return (TRUE); ! 322: } ! 323: ! 324: static ! 325: boolean_t ! 326: emulate_STI( ! 327: thread_t thread, ! 328: thread_saved_state_t *state, ! 329: code_desc_t *desc ! 330: ) ! 331: { ! 332: PCcontext_t context = threadPCContext(thread); ! 333: ! 334: if (desc->opsz) ! 335: state->frame.eip += 1; ! 336: else ! 337: (unsigned short)state->frame.eip += 1; ! 338: ! 339: if (!context->IF_flag) { ! 340: context->IF_flag = TRUE; ! 341: ! 342: context->pendingCallbacks |= (context->runOptions & PC_RUN_IF_SET); ! 343: } ! 344: ! 345: return (TRUE); ! 346: } ! 347: ! 348: static ! 349: boolean_t ! 350: push_frame16_err_code( ! 351: thread_t thread, ! 352: thread_saved_state_t *state, ! 353: boolean_t IF_flag, ! 354: err_code_t err ! 355: ) ! 356: { ! 357: struct { ! 358: unsigned short err; ! 359: unsigned short ip; ! 360: sel_t cs; ! 361: unsigned short flags; ! 362: } frame; ! 363: data_desc_t stack; ! 364: ! 365: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 366: return (FALSE); /* not possible */ ! 367: ! 368: frame.err = err_to_error_code(err); ! 369: ! 370: frame.ip = state->frame.eip; ! 371: frame.cs = state->frame.cs; ! 372: ! 373: frame.flags = state->frame.eflags; ! 374: if (IF_flag) ! 375: frame.flags |= EFL_IF; ! 376: else ! 377: frame.flags &= ~EFL_IF; ! 378: ! 379: if (!store_segment( ! 380: thread, ! 381: &stack, ! 382: state->frame.esp - sizeof (frame), stack.stksz, ! 383: &frame, sizeof (frame))) ! 384: return (FALSE); ! 385: ! 386: if (stack.stksz) ! 387: state->frame.esp -= sizeof (frame); ! 388: else ! 389: (unsigned short)state->frame.esp -= sizeof (frame); ! 390: ! 391: return (TRUE); ! 392: } ! 393: ! 394: static ! 395: boolean_t ! 396: push_frame16( ! 397: thread_t thread, ! 398: thread_saved_state_t *state, ! 399: boolean_t IF_flag ! 400: ) ! 401: { ! 402: struct { ! 403: unsigned short ip; ! 404: sel_t cs; ! 405: unsigned short flags; ! 406: } frame; ! 407: data_desc_t stack; ! 408: ! 409: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 410: return (FALSE); /* not possible */ ! 411: ! 412: frame.ip = state->frame.eip; ! 413: frame.cs = state->frame.cs; ! 414: ! 415: frame.flags = state->frame.eflags; ! 416: if (IF_flag) ! 417: frame.flags |= EFL_IF; ! 418: else ! 419: frame.flags &= ~EFL_IF; ! 420: ! 421: if (!store_segment( ! 422: thread, ! 423: &stack, ! 424: state->frame.esp - sizeof (frame), stack.stksz, ! 425: &frame, sizeof (frame))) ! 426: return (FALSE); ! 427: ! 428: if (stack.stksz) ! 429: state->frame.esp -= sizeof (frame); ! 430: else ! 431: (unsigned short)state->frame.esp -= sizeof (frame); ! 432: ! 433: return (TRUE); ! 434: } ! 435: ! 436: static ! 437: boolean_t ! 438: push_frame32_err_code( ! 439: thread_t thread, ! 440: thread_saved_state_t *state, ! 441: boolean_t IF_flag, ! 442: err_code_t err ! 443: ) ! 444: { ! 445: struct { ! 446: err_code_t err; ! 447: unsigned int eip; ! 448: sel_t cs; ! 449: unsigned short :16; ! 450: unsigned int eflags; ! 451: } frame; ! 452: data_desc_t stack; ! 453: ! 454: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 455: return (FALSE); /* not possible */ ! 456: ! 457: frame.err = err; ! 458: ! 459: frame.eip = state->frame.eip; ! 460: frame.cs = state->frame.cs; ! 461: ! 462: frame.eflags = state->frame.eflags; ! 463: if (IF_flag) ! 464: frame.eflags |= EFL_IF; ! 465: else ! 466: frame.eflags &= ~EFL_IF; ! 467: ! 468: if (!store_segment( ! 469: thread, ! 470: &stack, ! 471: state->frame.esp - sizeof (frame), stack.stksz, ! 472: &frame, sizeof (frame))) ! 473: return (FALSE); ! 474: ! 475: if (stack.stksz) ! 476: state->frame.esp -= sizeof (frame); ! 477: else ! 478: (unsigned short)state->frame.esp -= sizeof (frame); ! 479: ! 480: return (TRUE); ! 481: } ! 482: ! 483: static ! 484: boolean_t ! 485: push_frame32( ! 486: thread_t thread, ! 487: thread_saved_state_t *state, ! 488: boolean_t IF_flag ! 489: ) ! 490: { ! 491: struct { ! 492: unsigned int eip; ! 493: sel_t cs; ! 494: unsigned short :16; ! 495: unsigned int eflags; ! 496: } frame; ! 497: data_desc_t stack; ! 498: ! 499: if (!fetch_desc(thread, state->frame.ss, (dt_entry_t *)&stack)) ! 500: return (FALSE); /* not possible */ ! 501: ! 502: frame.eip = state->frame.eip; ! 503: frame.cs = state->frame.cs; ! 504: ! 505: frame.eflags = state->frame.eflags; ! 506: if (IF_flag) ! 507: frame.eflags |= EFL_IF; ! 508: else ! 509: frame.eflags &= ~EFL_IF; ! 510: ! 511: if (!store_segment( ! 512: thread, ! 513: &stack, ! 514: state->frame.esp - sizeof (frame), stack.stksz, ! 515: &frame, sizeof (frame))) ! 516: return (FALSE); ! 517: ! 518: if (stack.stksz) ! 519: state->frame.esp -= sizeof (frame); ! 520: else ! 521: (unsigned short)state->frame.esp -= sizeof (frame); ! 522: ! 523: return (TRUE); ! 524: } ! 525: ! 526: static __inline__ ! 527: void ! 528: enter_gate( ! 529: except_frame_t *frame, ! 530: trap_gate_t *gate, ! 531: boolean_t preserve_trace ! 532: ) ! 533: { ! 534: conv_offset_t _offset = { 0 }; ! 535: ! 536: _offset.fields.offset00 = gate->offset00; ! 537: _offset.fields.offset16 = gate->offset16; ! 538: ! 539: frame->eip = _offset.word; ! 540: frame->cs = gate->seg; ! 541: ! 542: if (!preserve_trace) ! 543: frame->eflags &= ~EFL_TF; ! 544: } ! 545: ! 546: static ! 547: boolean_t ! 548: emulate_exception( ! 549: thread_t thread, ! 550: thread_saved_state_t *state, ! 551: int trapno, ! 552: err_code_t err, ! 553: boolean_t inhibit_err_code ! 554: ) ! 555: { ! 556: PCcontext_t context; ! 557: trap_gate_t gate; ! 558: boolean_t is_intr, is_32bit; ! 559: code_desc_t code; ! 560: ! 561: context = threadPCContext(thread); ! 562: ! 563: if (!fetch_IDT_entry(thread, trapno, (idt_entry_t *)&gate)) ! 564: return (FALSE); /* GP(trapno*8+2+EXT) */ ! 565: ! 566: switch (gate.type) { ! 567: ! 568: case DESC_INTR_GATE: ! 569: is_32bit = TRUE; ! 570: is_intr = TRUE; ! 571: break; ! 572: ! 573: case DESC_TRAP_GATE: ! 574: is_32bit = TRUE; ! 575: is_intr = FALSE; ! 576: break; ! 577: ! 578: case 0x06: ! 579: is_32bit = FALSE; ! 580: is_intr = TRUE; ! 581: break; ! 582: ! 583: case 0x07: ! 584: is_32bit = FALSE; ! 585: is_intr = FALSE; ! 586: break; ! 587: ! 588: default: ! 589: return (FALSE); /* GP(trapno*8+2+EXT) */ ! 590: } ! 591: ! 592: if (!gate.present) ! 593: return (FALSE); /* NP(trapno*8+2+EXT) */ ! 594: ! 595: if (!fetch_desc(thread, gate.seg, (dt_entry_t *)&code)) ! 596: return (FALSE); /* GP(selector+EXT) */ ! 597: ! 598: if ((code.type & 0x18) != 0x18) // if (!code) ! 599: return (FALSE); /* GP(selector+EXT) */ ! 600: ! 601: if (!code.present) ! 602: return (FALSE); /* NP(selector+EXT) */ ! 603: ! 604: if (inhibit_err_code || (trapno < 8) || trapno == 16 || (trapno > 17)) { ! 605: if (is_32bit) { ! 606: if (!push_frame32(thread, state, context->IF_flag)) ! 607: return (FALSE); ! 608: } ! 609: else { ! 610: if (!push_frame16(thread, state, context->IF_flag)) ! 611: return (FALSE); ! 612: } ! 613: } ! 614: else ! 615: { ! 616: if (is_32bit) { ! 617: if (!push_frame32_err_code(thread, state, context->IF_flag, err)) ! 618: return (FALSE); ! 619: } ! 620: else { ! 621: if (!push_frame16_err_code(thread, state, context->IF_flag, err)) ! 622: return (FALSE); ! 623: } ! 624: } ! 625: ! 626: #define preserveTrace ((context->debugOptions & PC_DEBUG_PRESERVE_TRACE) != 0) ! 627: enter_gate(&state->frame, &gate, preserveTrace); ! 628: #undef preserveTrace ! 629: ! 630: if (is_intr) ! 631: context->IF_flag = FALSE; ! 632: ! 633: return (TRUE); ! 634: } ! 635: ! 636: static ! 637: boolean_t ! 638: handle_exception( ! 639: thread_t thread, ! 640: thread_saved_state_t *state ! 641: ) ! 642: { ! 643: PCshared_t shared; ! 644: PCcontext_t context; ! 645: ! 646: shared = threadPCShared(thread); ! 647: context = currentContext(shared); ! 648: ! 649: if (maskIsSet(&shared->faultMask, state->trapno)) { ! 650: context->trapNum = state->trapno; ! 651: context->errCode = err_to_error_code(state->frame.err); ! 652: context->callHandler = PC_HANDLE_FAULT; ! 653: ! 654: PCcallMonitor(thread, state); ! 655: /* NOTREACHED */ ! 656: } ! 657: ! 658: if (!emulate_exception( ! 659: thread, ! 660: state, state->trapno, state->frame.err, ! 661: FALSE)) ! 662: return (FALSE); ! 663: ! 664: return (TRUE); ! 665: } ! 666: ! 667: static ! 668: boolean_t ! 669: emulate_INTn( ! 670: thread_t thread, ! 671: thread_saved_state_t *state, ! 672: code_desc_t *desc, ! 673: unsigned int inst ! 674: ) ! 675: { ! 676: PCshared_t shared; ! 677: PCcontext_t context; ! 678: unsigned char trapno; ! 679: err_code_t err = { 0 }; ! 680: ! 681: shared = threadPCShared(thread); ! 682: context = currentContext(shared); ! 683: ! 684: trapno = *((unsigned char *)&inst + 1); ! 685: ! 686: if (maskIsSet(&shared->intNMask, trapno)) { ! 687: context->trapNum = trapno; ! 688: context->errCode = 0; ! 689: if (trapno <= 7) ! 690: context->callHandler = PC_HANDLE_FAULT; ! 691: else ! 692: context->callHandler = PC_HANDLE_INTN; ! 693: ! 694: PCcallMonitor(thread, state); ! 695: /* NOTREACHED */ ! 696: } ! 697: ! 698: if (desc->opsz) ! 699: state->frame.eip += 2; ! 700: else ! 701: (unsigned short)state->frame.eip += 2; ! 702: ! 703: if (!emulate_exception(thread, state, trapno, err, TRUE)) { ! 704: if (desc->opsz) ! 705: state->frame.eip -= 2; ! 706: else ! 707: (unsigned short)state->frame.eip -= 2; ! 708: return (FALSE); ! 709: } ! 710: ! 711: return (TRUE); ! 712: } ! 713: ! 714: static ! 715: boolean_t ! 716: emulate_instruction( ! 717: thread_t thread, ! 718: thread_saved_state_t *state ! 719: ) ! 720: { ! 721: code_desc_t code; ! 722: unsigned int inst; ! 723: unsigned char ibyte; ! 724: ! 725: if (!fetch_desc(thread, state->frame.cs, (dt_entry_t *)&code)) ! 726: return (FALSE); ! 727: ! 728: if (!fetch_segment_4byte( ! 729: thread, ! 730: (data_desc_t *)&code, state->frame.eip, code.opsz, ! 731: &inst)) ! 732: return (FALSE); ! 733: ! 734: ibyte = *(unsigned char *)&inst; ! 735: ! 736: if (ibyte == 0xCD) ! 737: return (emulate_INTn(thread, state, &code, inst)); ! 738: ! 739: if (ibyte == 0xFA) ! 740: return (emulate_CLI(thread, state, &code)); ! 741: ! 742: if (ibyte == 0xFB) ! 743: return (emulate_STI(thread, state, &code)); ! 744: ! 745: return (FALSE); ! 746: } ! 747: ! 748: boolean_t ! 749: PCemulatePROT( ! 750: thread_t thread, ! 751: thread_saved_state_t *state ! 752: ) ! 753: { ! 754: if (state->trapno == T_GENERAL_PROTECTION) { ! 755: if (!emulate_instruction(thread,state)) ! 756: return handle_exception(thread, state); ! 757: else ! 758: return TRUE; ! 759: } ! 760: else ! 761: if (state->trapno == T_INVALID_OPCODE) { ! 762: if (!handle_bop(thread, state)) ! 763: return handle_exception(thread, state); ! 764: else ! 765: return TRUE; ! 766: } ! 767: else ! 768: return handle_exception(thread, state); ! 769: }
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.