Annotation of 43BSDReno/bin/cp/cp.c, revision 1.1.1.1

1.1       root        1: /*
                      2:  * Copyright (c) 1988 The Regents of the University of California.
                      3:  * All rights reserved.
                      4:  *
                      5:  * This code is derived from software contributed to Berkeley by
                      6:  * David Hitz of Auspex Systems Inc.
                      7:  *
                      8:  * Redistribution and use in source and binary forms are permitted provided
                      9:  * that: (1) source distributions retain this entire copyright notice and
                     10:  * comment, and (2) distributions including binaries display the following
                     11:  * acknowledgement:  ``This product includes software developed by the
                     12:  * University of California, Berkeley and its contributors'' in the
                     13:  * documentation or other materials provided with the distribution and in
                     14:  * all advertising materials mentioning features or use of this software.
                     15:  * Neither the name of the University nor the names of its contributors may
                     16:  * be used to endorse or promote products derived from this software without
                     17:  * specific prior written permission.
                     18:  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
                     19:  * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
                     20:  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
                     21:  */
                     22: 
                     23: #ifndef lint
                     24: char copyright[] =
                     25: "@(#) Copyright (c) 1988 The Regents of the University of California.\n\
                     26:  All rights reserved.\n";
                     27: #endif /* not lint */
                     28: 
                     29: #ifndef lint
                     30: static char sccsid[] = "@(#)cp.c       5.20 (Berkeley) 6/29/90";
                     31: #endif /* not lint */
                     32: 
                     33: /*
                     34:  * cp copies source files to target files.
                     35:  * 
                     36:  * The global PATH_T structures "to" and "from" always contain paths to the
                     37:  * current source and target files, respectively.  Since cp does not change
                     38:  * directories, these paths can be either absolute or dot-realative.
                     39:  * 
                     40:  * The basic algorithm is to initialize "to" and "from", and then call the
                     41:  * recursive copy() function to do the actual work.  If "from" is a file,
                     42:  * copy copies the data.  If "from" is a directory, copy creates the
                     43:  * corresponding "to" directory, and calls itself recursively on all of
                     44:  * the entries in the "from" directory.
                     45:  */
                     46: 
                     47: #include <sys/param.h>
                     48: #include <sys/stat.h>
                     49: #include <sys/file.h>
                     50: #include <sys/dir.h>
                     51: #include <sys/time.h>
                     52: #include <stdio.h>
                     53: #include <errno.h>
                     54: #include <stdlib.h>
                     55: #include <string.h>
                     56: 
                     57: #define        type(st)        ((st).st_mode & S_IFMT)
                     58: 
                     59: typedef struct {
                     60:        char    p_path[MAXPATHLEN + 1]; /* pointer to the start of a path. */
                     61:        char    *p_end;                 /* pointer to NULL at end of path. */
                     62: } PATH_T;
                     63: 
                     64: PATH_T from = { "", from.p_path };
                     65: PATH_T to = { "", to.p_path };
                     66: 
                     67: uid_t myuid;
                     68: int exit_val, myumask;
                     69: int iflag, pflag, orflag, rflag;
                     70: int (*statfcn)();
                     71: char *buf, *pname;
                     72: char *path_append(), *path_basename();
                     73: 
                     74: main(argc, argv)
                     75:        int argc;
                     76:        char **argv;
                     77: {
                     78:        extern int optind;
                     79:        struct stat to_stat;
                     80:        register int c, r;
                     81:        int symfollow, lstat(), stat();
                     82:        char *old_to, *p;
                     83: 
                     84:        /*
                     85:         * cp is used by mv(1) -- except for usage statements, print
                     86:         * the "called as" program name.
                     87:         */
                     88:        pname = (p = rindex(*argv,'/')) ? ++p : *argv;
                     89: 
                     90:        symfollow = 0;
                     91:        while ((c = getopt(argc, argv, "Rfhipr")) != EOF) {
                     92:        switch ((char)c) {
                     93:                case 'f':
                     94:                        iflag = 0;
                     95:                        break;
                     96:                case 'h':
                     97:                        symfollow = 1;
                     98:                        break;
                     99:                case 'i':
                    100:                        iflag = isatty(fileno(stdin));
                    101:                        break;
                    102:                case 'p':
                    103:                        pflag = 1;
                    104:                        break;
                    105:                case 'R':
                    106:                        rflag = 1;
                    107:                        break;
                    108:                case 'r':
                    109:                        orflag = 1;
                    110:                        break;
                    111:                case '?':
                    112:                default:
                    113:                        usage();
                    114:                        break;
                    115:                }
                    116:        }
                    117:        argc -= optind;
                    118:        argv += optind;
                    119: 
                    120:        if (argc < 2)
                    121:                usage();
                    122: 
                    123:        if (rflag && orflag) {
                    124:                (void)fprintf(stderr,
                    125:                    "cp: the -R and -r options are mutually exclusive.\n");
                    126:                exit(1);
                    127:        }
                    128: 
                    129:        buf = (char *)malloc(MAXBSIZE);
                    130:        if (!buf) {
                    131:                (void)fprintf(stderr, "%s: out of space.\n", pname);
                    132:                exit(1);
                    133:        }
                    134: 
                    135:        myuid = getuid();
                    136: 
                    137:        /* copy the umask for explicit mode setting */
                    138:        myumask = umask(0);
                    139:        (void)umask(myumask);
                    140: 
                    141:        /* consume last argument first. */
                    142:        if (!path_set(&to, argv[--argc]))
                    143:                exit(exit_val);
                    144: 
                    145:        statfcn = symfollow || !rflag ? stat : lstat;
                    146: 
                    147:        /*
                    148:         * Cp has two distinct cases:
                    149:         *
                    150:         * Case (1)       $ cp [-rip] source target
                    151:         *
                    152:         * Case (2)       $ cp [-rip] source1 ... directory
                    153:         *
                    154:         * In both cases, source can be either a file or a directory.
                    155:         *
                    156:         * In (1), the target becomes a copy of the source. That is, if the
                    157:         * source is a file, the target will be a file, and likewise for
                    158:         * directories.
                    159:         *
                    160:         * In (2), the real target is not directory, but "directory/source".
                    161:         */
                    162: 
                    163:        r = stat(to.p_path, &to_stat);
                    164:        if (r == -1 && errno != ENOENT) {
                    165:                error(to.p_path);
                    166:                exit(1);
                    167:        }
                    168:        if (r == -1 || type(to_stat) != S_IFDIR) {
                    169:                /*
                    170:                 * Case (1).  Target is not a directory.
                    171:                 */
                    172:                if (argc > 1) {
                    173:                        usage();
                    174:                        exit(1);
                    175:                }
                    176:                if (!path_set(&from, *argv))
                    177:                        exit(exit_val);
                    178:                copy();
                    179:        }
                    180:        else {
                    181:                /*
                    182:                 * Case (2).  Target is a directory.
                    183:                 */
                    184:                for (;; ++argv) {
                    185:                        if (!path_set(&from, *argv))
                    186:                                continue;
                    187:                        old_to = path_append(&to, path_basename(&from), -1);
                    188:                        if (!old_to)
                    189:                                continue;
                    190:                        copy();
                    191:                        if (!--argc)
                    192:                                break;
                    193:                        path_restore(&to, old_to);
                    194:                }
                    195:        }
                    196:        exit(exit_val);
                    197: }
                    198: 
                    199: /* copy file or directory at "from" to "to". */
                    200: copy()
                    201: {
                    202:        struct stat from_stat, to_stat;
                    203:        int dne, statval;
                    204: 
                    205:        statval = statfcn(from.p_path, &from_stat);
                    206:        if (statval == -1) {
                    207:                error(from.p_path);
                    208:                return;
                    209:        }
                    210: 
                    211:        /* not an error, but need to remember it happened */
                    212:        if (stat(to.p_path, &to_stat) == -1)
                    213:                dne = 1;
                    214:        else {
                    215:                if (to_stat.st_dev == from_stat.st_dev &&
                    216:                    to_stat.st_ino == from_stat.st_ino) {
                    217:                        (void)fprintf(stderr,
                    218:                            "%s: %s and %s are identical (not copied).\n",
                    219:                            pname, to.p_path, from.p_path);
                    220:                        exit_val = 1;
                    221:                        return;
                    222:                }
                    223:                dne = 0;
                    224:        }
                    225: 
                    226:        switch(type(from_stat)) {
                    227:        case S_IFLNK:
                    228:                copy_link(!dne);
                    229:                return;
                    230:        case S_IFDIR:
                    231:                if (!rflag && !orflag) {
                    232:                        (void)fprintf(stderr,
                    233:                            "%s: %s is a directory (not copied).\n",
                    234:                            pname, from.p_path);
                    235:                        exit_val = 1;
                    236:                        return;
                    237:                }
                    238:                if (dne) {
                    239:                        /*
                    240:                         * If the directory doesn't exist, create the new
                    241:                         * one with the from file mode plus owner RWX bits,
                    242:                         * modified by the umask.  Trade-off between being
                    243:                         * able to write the directory (if from directory is
                    244:                         * 555) and not causing a permissions race.  If the
                    245:                         * umask blocks owner writes cp fails.
                    246:                         */
                    247:                        if (mkdir(to.p_path, from_stat.st_mode|S_IRWXU) < 0) {
                    248:                                error(to.p_path);
                    249:                                return;
                    250:                        }
                    251:                }
                    252:                else if (type(to_stat) != S_IFDIR) {
                    253:                        (void)fprintf(stderr, "%s: %s: not a directory.\n",
                    254:                            pname, to.p_path);
                    255:                        return;
                    256:                }
                    257:                copy_dir();
                    258:                /*
                    259:                 * If not -p and directory didn't exist, set it to be the
                    260:                 * same as the from directory, umodified by the umask;
                    261:                 * arguably wrong, but it's been that way forever.
                    262:                 */
                    263:                if (pflag)
                    264:                        setfile(&from_stat, 0);
                    265:                else if (dne)
                    266:                        (void)chmod(to.p_path, from_stat.st_mode);
                    267:                return;
                    268:        case S_IFCHR:
                    269:        case S_IFBLK:
                    270:                if (rflag) {
                    271:                        copy_special(&from_stat, !dne);
                    272:                        return;
                    273:                }
                    274:                break;
                    275:        case S_IFIFO:
                    276:                if (rflag) {
                    277:                        copy_fifo(&from_stat, !dne);
                    278:                        return;
                    279:                }
                    280:                break;
                    281:        }
                    282:        copy_file(&from_stat, dne);
                    283: }
                    284: 
                    285: copy_file(fs, dne)
                    286:        struct stat *fs;
                    287:        int dne;
                    288: {
                    289:        register int from_fd, to_fd, rcount, wcount;
                    290:        struct stat to_stat;
                    291: 
                    292:        if ((from_fd = open(from.p_path, O_RDONLY, 0)) == -1) {
                    293:                error(from.p_path);
                    294:                return;
                    295:        }
                    296: 
                    297:        /*
                    298:         * If the file exists and we're interactive, verify with the user.
                    299:         * If the file DNE, set the mode to be the from file, minus setuid
                    300:         * bits, modified by the umask; arguably wrong, but it makes copying
                    301:         * executables work right and it's been that way forever.  (The
                    302:         * other choice is 666 or'ed with the execute bits on the from file
                    303:         * modified by the umask.)
                    304:         */
                    305:        if (!dne) {
                    306:                if (iflag) {
                    307:                        int checkch, ch;
                    308: 
                    309:                        (void)fprintf(stderr, "overwrite %s? ", to.p_path);
                    310:                        checkch = ch = getchar();
                    311:                        while (ch != '\n' && ch != EOF)
                    312:                                ch = getchar();
                    313:                        if (checkch != 'y') {
                    314:                                (void)close(from_fd);
                    315:                                return;
                    316:                        }
                    317:                }
                    318:                to_fd = open(to.p_path, O_WRONLY|O_TRUNC, 0);
                    319:        } else
                    320:                to_fd = open(to.p_path, O_WRONLY|O_CREAT|O_TRUNC,
                    321:                    fs->st_mode & ~(S_ISUID|S_ISGID));
                    322: 
                    323:        if (to_fd == -1) {
                    324:                error(to.p_path);
                    325:                (void)close(from_fd);
                    326:                return;
                    327:        }
                    328: 
                    329:        while ((rcount = read(from_fd, buf, MAXBSIZE)) > 0) {
                    330:                wcount = write(to_fd, buf, rcount);
                    331:                if (rcount != wcount || wcount == -1) {
                    332:                        error(to.p_path);
                    333:                        break;
                    334:                }
                    335:        }
                    336:        if (rcount < 0)
                    337:                error(from.p_path);
                    338:        if (pflag)
                    339:                setfile(fs, to_fd);
                    340:        /*
                    341:         * If the source was setuid or setgid, lose the bits unless the
                    342:         * copy is owned by the same user and group.
                    343:         */
                    344:        else if (fs->st_mode & (S_ISUID|S_ISGID) && fs->st_uid == myuid)
                    345:                if (fstat(to_fd, &to_stat))
                    346:                        error(to.p_path);
                    347: #define        RETAINBITS      (S_ISUID|S_ISGID|S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO)
                    348:                else if (fs->st_gid == to_stat.st_gid && fchmod(to_fd,
                    349:                    fs->st_mode & RETAINBITS & ~myumask))
                    350:                        error(to.p_path);
                    351:        (void)close(from_fd);
                    352:        (void)close(to_fd);
                    353: }
                    354: 
                    355: copy_dir()
                    356: {
                    357:        struct stat from_stat;
                    358:        struct direct *dp, **dir_list;
                    359:        register int dir_cnt, i;
                    360:        char *old_from, *old_to;
                    361: 
                    362:        dir_cnt = scandir(from.p_path, &dir_list, NULL, NULL);
                    363:        if (dir_cnt == -1) {
                    364:                (void)fprintf(stderr, "%s: can't read directory %s.\n",
                    365:                    pname, from.p_path);
                    366:                exit_val = 1;
                    367:        }
                    368: 
                    369:        /*
                    370:         * Instead of handling directory entries in the order they appear
                    371:         * on disk, do non-directory files before directory files.
                    372:         * There are two reasons to do directories last.  The first is
                    373:         * efficiency.  Files tend to be in the same cylinder group as
                    374:         * their parent, whereas directories tend not to be.  Copying files
                    375:         * all at once reduces seeking.  Second, deeply nested tree's
                    376:         * could use up all the file descriptors if we didn't close one
                    377:         * directory before recursivly starting on the next.
                    378:         */
                    379:        /* copy files */
                    380:        for (i = 0; i < dir_cnt; ++i) {
                    381:                dp = dir_list[i];
                    382:                if (dp->d_namlen <= 2 && dp->d_name[0] == '.'
                    383:                    && (dp->d_name[1] == NULL || dp->d_name[1] == '.'))
                    384:                        goto done;
                    385:                old_from = path_append(&from, dp->d_name, (int)dp->d_namlen);
                    386:                if (!old_from)
                    387:                        goto done;
                    388: 
                    389:                if (statfcn(from.p_path, &from_stat) < 0) {
                    390:                        error(dp->d_name);
                    391:                        path_restore(&from, old_from);
                    392:                        goto done;
                    393:                }
                    394:                if (type(from_stat) == S_IFDIR) {
                    395:                        path_restore(&from, old_from);
                    396:                        continue;
                    397:                }
                    398:                old_to = path_append(&to, dp->d_name, (int)dp->d_namlen);
                    399:                if (old_to) {
                    400:                        copy();
                    401:                        path_restore(&to, old_to);
                    402:                }
                    403:                path_restore(&from, old_from);
                    404: done:          dir_list[i] = NULL;
                    405:                (void)free((void *)dp);
                    406:        }
                    407: 
                    408:        /* copy directories */
                    409:        for (i = 0; i < dir_cnt; ++i) {
                    410:                dp = dir_list[i];
                    411:                if (!dp)
                    412:                        continue;
                    413:                old_from = path_append(&from, dp->d_name, (int) dp->d_namlen);
                    414:                if (!old_from) {
                    415:                        (void)free((void *)dp);
                    416:                        continue;
                    417:                }
                    418:                old_to = path_append(&to, dp->d_name, (int) dp->d_namlen);
                    419:                if (!old_to) {
                    420:                        (void)free((void *)dp);
                    421:                        path_restore(&from, old_from);
                    422:                        continue;
                    423:                }
                    424:                copy();
                    425:                free((void *)dp);
                    426:                path_restore(&from, old_from);
                    427:                path_restore(&to, old_to);
                    428:        }
                    429:        free((void *)dir_list);
                    430: }
                    431: 
                    432: copy_link(exists)
                    433:        int exists;
                    434: {
                    435:        int len;
                    436:        char link[MAXPATHLEN];
                    437: 
                    438:        if ((len = readlink(from.p_path, link, sizeof(link))) == -1) {
                    439:                error(from.p_path);
                    440:                return;
                    441:        }
                    442:        link[len] = '\0';
                    443:        if (exists && unlink(to.p_path)) {
                    444:                error(to.p_path);
                    445:                return;
                    446:        }
                    447:        if (symlink(link, to.p_path)) {
                    448:                error(link);
                    449:                return;
                    450:        }
                    451: }
                    452: 
                    453: copy_fifo(from_stat, exists)
                    454:        struct stat *from_stat;
                    455:        int exists;
                    456: {
                    457:        if (exists && unlink(to.p_path)) {
                    458:                error(to.p_path);
                    459:                return;
                    460:        }
                    461:        if (mkfifo(to.p_path, from_stat->st_mode)) {
                    462:                error(to.p_path);
                    463:                return;
                    464:        }
                    465:        if (pflag)
                    466:                setfile(from_stat, 0);
                    467: }
                    468: 
                    469: copy_special(from_stat, exists)
                    470:        struct stat *from_stat;
                    471:        int exists;
                    472: {
                    473:        if (exists && unlink(to.p_path)) {
                    474:                error(to.p_path);
                    475:                return;
                    476:        }
                    477:        if (mknod(to.p_path, from_stat->st_mode,  from_stat->st_rdev)) {
                    478:                error(to.p_path);
                    479:                return;
                    480:        }
                    481:        if (pflag)
                    482:                setfile(from_stat, 0);
                    483: }
                    484: 
                    485: setfile(fs, fd)
                    486:        register struct stat *fs;
                    487:        int fd;
                    488: {
                    489:        static struct timeval tv[2];
                    490: 
                    491:        fs->st_mode &= S_ISUID|S_ISGID|S_IRWXU|S_IRWXG|S_IRWXO;
                    492: 
                    493:        tv[0].tv_sec = fs->st_atime;
                    494:        tv[1].tv_sec = fs->st_mtime;
                    495:        if (utimes(to.p_path, tv))
                    496:                error(to.p_path);
                    497:        /*
                    498:         * Changing the ownership probably won't succeed, unless we're root
                    499:         * or POSIX_CHOWN_RESTRICTED is not set.  Set uid/gid before setting
                    500:         * the mode; current BSD behavior is to remove all setuid bits on
                    501:         * chown.  If chown fails, lose setuid/setgid bits.
                    502:         */
                    503:        if (fd ? fchown(fd, fs->st_uid, fs->st_gid) :
                    504:            chown(to.p_path, fs->st_uid, fs->st_gid)) {
                    505:                if (errno != EPERM)
                    506:                        error(to.p_path);
                    507:                fs->st_mode &= ~(S_ISUID|S_ISGID);
                    508:        }
                    509:        if (fd ? fchmod(fd, fs->st_mode) : chmod(to.p_path, fs->st_mode))
                    510:                error(to.p_path);
                    511: }
                    512: 
                    513: ismember(gid)
                    514:        gid_t gid;
                    515: {
                    516:        register int cnt;
                    517:        static int ngroups, groups[NGROUPS];
                    518: 
                    519:        if (!ngroups) {
                    520:                ngroups = getgroups(NGROUPS, groups);
                    521:                if (ngroups == -1) {
                    522:                        ngroups = 0;
                    523:                        exit_val = 1;
                    524:                        (void)fprintf(stderr, "%s: %s\n",
                    525:                            pname, strerror(errno));
                    526:                        return(0);
                    527:                }
                    528:        }
                    529:        for (cnt = ngroups; cnt--;)
                    530:                if (gid == groups[cnt])
                    531:                        return(1);
                    532:        return(0);
                    533: }
                    534: 
                    535: error(s)
                    536:        char *s;
                    537: {
                    538:        exit_val = 1;
                    539:        (void)fprintf(stderr, "%s: %s: %s\n", pname, s, strerror(errno));
                    540: }
                    541: 
                    542: /********************************************************************
                    543:  * Path Manipulation Routines.
                    544:  ********************************************************************/
                    545: 
                    546: /*
                    547:  * These functions manipulate paths in PATH_T structures.
                    548:  * 
                    549:  * They eliminate multiple slashes in paths when they notice them, and keep
                    550:  * the path non-slash terminated.
                    551:  *
                    552:  * Both path_set() and path_append() return 0 if the requested name
                    553:  * would be too long.
                    554:  */
                    555: 
                    556: #define        STRIP_TRAILING_SLASH(p) { \
                    557:        while ((p)->p_end > (p)->p_path && (p)->p_end[-1] == '/') \
                    558:                *--(p)->p_end = 0; \
                    559:        }
                    560: 
                    561: /*
                    562:  * Move specified string into path.  Convert "" to "." to handle BSD
                    563:  * semantics for a null path.  Strip trailing slashes.
                    564:  */
                    565: path_set(p, string)
                    566:        register PATH_T *p;
                    567:        char *string;
                    568: {
                    569:        if (strlen(string) > MAXPATHLEN) {
                    570:                (void)fprintf(stderr, "%s: %s: name too long.\n",
                    571:                    pname, string);
                    572:                exit_val = 1;
                    573:                return(0);
                    574:        }
                    575: 
                    576:        (void)strcpy(p->p_path, string);
                    577:        p->p_end = p->p_path + strlen(p->p_path);
                    578: 
                    579:        if (p->p_path == p->p_end) {
                    580:                *p->p_end++ = '.';
                    581:                *p->p_end = 0;
                    582:        }
                    583: 
                    584:        STRIP_TRAILING_SLASH(p);
                    585:        return(1);
                    586: }
                    587: 
                    588: /*
                    589:  * Append specified string to path, inserting '/' if necessary.  Return a
                    590:  * pointer to the old end of path for restoration.
                    591:  */
                    592: char *
                    593: path_append(p, name, len)
                    594:        register PATH_T *p;
                    595:        char *name;
                    596:        int len;
                    597: {
                    598:        char *old;
                    599: 
                    600:        old = p->p_end;
                    601:        if (len == -1)
                    602:                len = strlen(name);
                    603: 
                    604:        /*
                    605:         * The final "+ 1" accounts for the '/' between old path and name.
                    606:         */
                    607:        if ((len + p->p_end - p->p_path + 1) > MAXPATHLEN) {
                    608:                (void)fprintf(stderr,
                    609:                    "%s: %s/%s: name too long.\n", pname, p->p_path, name);
                    610:                exit_val = 1;
                    611:                return(0);
                    612:        }
                    613: 
                    614:        /*
                    615:         * This code should always be executed, since paths shouldn't
                    616:         * end in '/'.
                    617:         */
                    618:        if (p->p_end[-1] != '/') {
                    619:                *p->p_end++ = '/';
                    620:                *p->p_end = 0;
                    621:        }
                    622: 
                    623:        (void)strncat(p->p_end, name, len);
                    624:        p->p_end += len;
                    625:        *p->p_end = 0;
                    626: 
                    627:        STRIP_TRAILING_SLASH(p);
                    628:        return(old);
                    629: }
                    630: 
                    631: /*
                    632:  * Restore path to previous value.  (As returned by path_append.)
                    633:  */
                    634: path_restore(p, old)
                    635:        PATH_T *p;
                    636:        char *old;
                    637: {
                    638:        p->p_end = old;
                    639:        *p->p_end = 0;
                    640: }
                    641: 
                    642: /*
                    643:  * Return basename of path.  (Like basename(1).)
                    644:  */
                    645: char *
                    646: path_basename(p)
                    647:        PATH_T *p;
                    648: {
                    649:        char *basename;
                    650: 
                    651:        basename = rindex(p->p_path, '/');
                    652:        return(basename ? ++basename : p->p_path);
                    653: }
                    654: 
                    655: usage()
                    656: {
                    657:        (void)fprintf(stderr,
                    658: "usage: cp [-Rfhip] src target;\n   or: cp [-Rfhip] src1 ... srcN directory\n");
                    659:        exit(1);
                    660: }

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.