implement Ar; # # ar - portable (ascii) format version # include "sys.m"; sys: Sys; include "draw.m"; include "bufio.m"; bufio: Bufio; Iobuf: import bufio; include "daytime.m"; daytime: Daytime; include "string.m"; str: String; Ar: module { init: fn(nil: ref Draw->Context, nil: list of string); }; ARMAG: con "!\n"; SARMAG: con len ARMAG; ARFMAG0: con byte '`'; ARFMAG1: con byte '\n'; SARNAME: con 16; # ancient limit # # printable archive header # name[SARNAME] date[12] uid[6] gid[6] mode[8] size[10] fmag[2] # Oname: con 0; Lname: con SARNAME; Odate: con Oname+Lname; Ldate: con 12; Ouid: con Odate+Ldate; Luid: con 6; Ogid: con Ouid+Luid; Lgid: con 6; Omode: con Ogid+Lgid; Lmode: con 8; Osize: con Omode+Lmode; Lsize: con 10; Ofmag: con Osize+Lsize; Lfmag: con 2; SAR_HDR: con Ofmag+Lfmag; # 60 # # The algorithm uses up to 3 temp files. The "pivot contents" is the # archive contents specified by an a, b, or i option. The temp files are # astart - contains existing contentss up to and including the pivot contents. # amiddle - contains new files moved or inserted behind the pivot. # aend - contains the existing contentss that follow the pivot contents. # When all contentss have been processed, function 'install' streams the # temp files, in order, back into the archive. # Armember: adt { # one per archive contents name: string; # trimmed length: int; date: int; uid: int; gid: int; mode: int; size: int; contents: array of byte; fd: ref Sys->FD; # if contents is nil and fd is not nil, fd has contents next: cyclic ref Armember; new: fn(name: string, fd: ref Sys->FD): ref Armember; rdhdr: fn(b: ref Iobuf): ref Armember; read: fn(m: self ref Armember, b: ref Iobuf): int; wrhdr: fn(m: self ref Armember, fd: ref Sys->FD); write: fn(m: self ref Armember, fd: ref Sys->FD); skip: fn(m: self ref Armember, b: ref Iobuf); replace: fn(m: self ref Armember, name: string, fd: ref Sys->FD); copyout: fn(m: self ref Armember, b: ref Iobuf, destfd: ref Sys->FD); }; Arfile: adt { # one per tempfile fd: ref Sys->FD; # paging file descriptor, nil if none allocated head: ref Armember; tail: ref Armember; new: fn(): ref Arfile; copy: fn(ar: self ref Arfile, b: ref Iobuf, mem: ref Armember); insert: fn(ar: self ref Arfile, mem: ref Armember); stream: fn(ar: self ref Arfile, fd: ref Sys->FD); page: fn(ar: self ref Arfile): int; }; File: adt { name: string; trimmed: string; found: int; }; man := "mrxtdpq"; opt := "uvnbailo"; aflag := 0; bflag := 0; cflag := 0; oflag := 0; uflag := 0; vflag := 0; pivotname: string; bout: ref Iobuf; stderr: ref Sys->FD; parts: array of ref Arfile; comfun: ref fn(a: string, f: array of ref File); init(nil: ref Draw->Context, args: list of string) { sys = load Sys Sys->PATH; bufio = load Bufio Bufio->PATH; daytime = load Daytime Daytime->PATH; str = load String String->PATH; stderr = sys->fildes(2); bout = bufio->fopen(sys->fildes(1), Sys->OWRITE); if(len args < 3) usage(); args = tl args; s := hd args; args = tl args; for(i := 0; i < len s; i++){ case s[i] { 'a' => aflag = 1; 'b' => bflag = 1; 'c' => cflag = 1; 'd' => setcom(dcmd); 'i' => bflag = 1; 'l' => ; # ignored 'm' => setcom(mcmd); 'o' => oflag = 1; 'p' => setcom(pcmd); 'q' => setcom(qcmd); 'r' => setcom(rcmd); 't' => setcom(tcmd); 'u' => uflag = 1; 'v' => vflag = 1; 'x' => setcom(xcmd); * => sys->fprint(stderr, "ar: bad option `%c'\n", s[i]); usage(); } } if(aflag && bflag){ sys->fprint(stderr, "ar: only one of 'a' and 'b' can be specified\n"); usage(); } if(aflag || bflag){ pivotname = trim(hd args); args = tl args; if(len args < 2) usage(); } if(comfun == nil){ if(uflag == 0){ sys->fprint(stderr, "ar: one of [%s] must be specified\n", man); usage(); } setcom(rcmd); } cp := hd args; args = tl args; files := array[len args] of ref File; for(i = 0; args != nil; args = tl args) files[i++] = ref File(hd args, trim(hd args), 0); comfun(cp, files); # do the command allfound := 1; for(i = 0; i < len files; i++) if(!files[i].found){ sys->fprint(stderr, "ar: %s not found\n", files[i].name); allfound = 0; } bout.flush(); if(!allfound) raise "fail: file not found"; } # # select a command # setcom(fun: ref fn(s: string, f: array of ref File)) { if(comfun != nil){ sys->fprint(stderr, "ar: only one of [%s] allowed\n", man); usage(); } comfun = fun; } # # perform the 'r' and 'u' commands # rcmd(arname: string, files: array of ref File) { bar := openar(arname, Sys->ORDWR, 1); parts = array[2] of {Arfile.new(), nil}; ap := parts[0]; if(bar != nil){ while((mem := Armember.rdhdr(bar)) != nil){ if(bamatch(mem.name, pivotname)) # check for pivot ap = parts[1] = Arfile.new(); f := match(files, mem.name); if(f == nil){ ap.copy(bar, mem); continue; } f.found = 1; dfd := sys->open(f.name, Sys->OREAD); if(dfd == nil){ if(len files > 0) sys->fprint(stderr, "ar: cannot open %s: %r\n", f.name); ap.copy(bar, mem); continue; } if(uflag){ (ok, d) := sys->fstat(dfd); if(ok < 0 || d.mtime <= mem.date){ if(ok < 0) sys->fprint(stderr, "ar: cannot stat %s: %r\n", f.name); ap.copy(bar, mem); continue; } } mem.skip(bar); mesg('r', f.name); mem.replace(f.name, dfd); ap.insert(mem); dfd = nil; } } # copy in remaining files named on command line for(i := 0; i < len files; i++){ f := files[i]; if(f.found) continue; f.found = 1; dfd := sys->open(f.name, Sys->OREAD); if(dfd != nil){ mesg('a', f.name); parts[0].insert(Armember.new(f.trimmed, dfd)); }else sys->fprint(stderr, "ar: cannot open %s: %r\n", f.name); } if(bar == nil && !cflag) install(arname, parts, 1); # issue 'creating' msg else install(arname, parts, 0); } dcmd(arname: string, files: array of ref File) { if(len files == 0) return; changed := 0; parts = array[] of {Arfile.new()}; bar := openar(arname, Sys->ORDWR, 0); while((mem := Armember.rdhdr(bar)) != nil){ if(match(files, mem.name) != nil){ mesg('d', mem.name); mem.skip(bar); changed = 1; }else parts[0].copy(bar, mem); mem = nil; # conserves memory } if(changed) install(arname, parts, 0); } xcmd(arname: string, files: array of ref File) { bar := openar(arname, Sys->OREAD, 0); i := 0; while((mem := Armember.rdhdr(bar)) != nil){ if((f := match(files, mem.name)) != nil){ f.found = 1; fd := sys->create(f.name, Sys->OWRITE, mem.mode & 8r777); if(fd == nil){ sys->fprint(stderr, "ar: cannot create %s: %r\n", f.name); mem.skip(bar); }else{ mesg('x', f.name); mem.copyout(bar, fd); if(oflag){ dx := sys->nulldir; dx.atime = mem.date; dx.mtime = mem.date; if(sys->fwstat(fd, dx) < 0) sys->fprint(stderr, "ar: can't set times on %s: %r", f.name); } fd = nil; mem = nil; } if(len files > 0 && ++i >= len files) break; }else mem.skip(bar); } } pcmd(arname: string, files: array of ref File) { bar := openar(arname, Sys->OREAD, 0); i := 0; while((mem := Armember.rdhdr(bar)) != nil){ if((f := match(files, mem.name)) != nil){ if(vflag) sys->print("\n<%s>\n\n", f.name); mem.copyout(bar, sys->fildes(1)); if(len files > 0 && ++i >= len files) break; }else mem.skip(bar); mem = nil; # we no longer need the contents } } mcmd(arname: string, files: array of ref File) { if(len files == 0) return; parts = array[3] of {Arfile.new(), Arfile.new(), nil}; bar := openar(arname, Sys->ORDWR, 0); ap := parts[0]; while((mem := Armember.rdhdr(bar)) != nil){ if(bamatch(mem.name, pivotname)) ap = parts[2] = Arfile.new(); if((f := match(files, mem.name)) != nil){ mesg('m', f.name); parts[1].copy(bar, mem); }else ap.copy(bar, mem); } if(pivotname != nil && parts[2] == nil) sys->fprint(stderr, "ar: %s not found - files moved to end\n", pivotname); install(arname, parts, 0); } tcmd(arname: string, files: array of ref File) { bar := openar(arname, Sys->OREAD, 0); while((mem := Armember.rdhdr(bar)) != nil){ if((f := match(files, mem.name)) != nil){ longls := ""; if(vflag) longls = longtext(mem)+" "; bout.puts(longls+f.trimmed+"\n"); } mem.skip(bar); mem = nil; } } qcmd(arname: string, files: array of ref File) { if(aflag || bflag){ sys->fprint(stderr, "ar: abi not allowed with q\n"); raise "fail:usage"; } fd := openrawar(arname, Sys->ORDWR, 1); if(fd == nil){ if(!cflag) sys->fprint(stderr, "ar: creating %s\n", arname); fd = arcreate(arname); } # leave note group behind when writing archive; i.e. sidestep interrupts sys->seek(fd, big 0, 2); # append for(i := 0; i < len files; i++){ f := files[i]; f.found = 1; dfd := sys->open(f.name, Sys->OREAD); if(dfd != nil){ mesg('q', f.name); mem := Armember.new(f.trimmed, dfd); if(mem != nil){ mem.write(fd); mem = nil; } }else sys->fprint(stderr, "ar: cannot open %s: %r\n", f.name); } } # # open an archive and validate its header # openrawar(arname: string, mode: int, errok: int): ref Sys->FD { fd := sys->open(arname, mode); if(fd == nil){ if(!errok){ sys->fprint(stderr, "ar: cannot open %s: %r\n", arname); raise "fail:error"; } return nil; } mbuf := array[SARMAG] of byte; if(sys->read(fd, mbuf, SARMAG) != SARMAG || string mbuf != ARMAG){ sys->fprint(stderr, "ar: %s not in archive format\n", arname); raise "fail:error"; } return fd; } openar(arname: string, mode: int, errok: int): ref Iobuf { fd := openrawar(arname, mode, errok); if(fd == nil) return nil; bfd := bufio->fopen(fd, mode); bfd.seek(big SARMAG, 0); return bfd; } # # create an archive and set its header # arcreate(arname: string): ref Sys->FD { fd := sys->create(arname, Sys->OWRITE, 8r666); if(fd == nil){ sys->fprint(stderr, "ar: cannot create %s: %r\n", arname); raise "fail:create"; } a := array of byte ARMAG; mustwrite(fd, a, len a); return fd; } # # error handling # wrerr() { sys->fprint(stderr, "ar: write error: %r\n"); raise "fail:write error"; } rderr() { sys->fprint(stderr, "ar: read error: %r\n"); raise "fail:read error"; } phaseerr(offset: big) { sys->fprint(stderr, "ar: phase error at offset %bd\n", offset); raise "fail:phase error"; } usage() { sys->fprint(stderr, "usage: ar [%s][%s] archive files ...\n", opt, man); raise "fail:usage"; } # # concatenate the several sequences of members into one archive # install(arname: string, seqs: array of ref Arfile, createflag: int) { # leave process group behind when copying back; i.e. sidestep interrupts sys->pctl(Sys->NEWPGRP, nil); if(createflag) sys->fprint(stderr, "ar: creating %s\n", arname); fd := arcreate(arname); for(i := 0; i < len seqs; i++) if((ap := seqs[i]) != nil) ap.stream(fd); } # # return the command line File matching a given name # match(files: array of ref File, file: string): ref File { if(len files == 0) return ref File(file, file, 0); # empty list always matches for(i := 0; i < len files; i++) if(!files[i].found && files[i].trimmed == file){ files[i].found = 1; return files[i]; } return nil; } # # is `file' the pivot member's name and is the archive positioned # at the correct point wrt after or before options? return true if so. # state := 0; bamatch(file: string, pivot: string): int { case state { 0 => # looking for position file if(aflag){ if(file == pivot) state = 1; }else if(bflag){ if(file == pivot){ state = 2; # found return 1; } } 1 => # found - after previous file state = 2; return 1; 2 => # already found position file ; } return 0; } # # output a message, if 'v' option was specified # mesg(c: int, file: string) { if(vflag) bout.puts(sys->sprint("%c - %s\n", c, file)); } # # return just the file name # trim(s: string): string { for(j := len s; j > 0 && s[j-1] == '/';) j--; k := 0; for(i := 0; i < j; i++) if(s[i] == '/') k = i+1; return s[k: j]; } longtext(mem: ref Armember): string { s := modes(mem.mode); s += sys->sprint(" %3d/%1d", mem.uid, mem.gid); s += sys->sprint(" %7ud", mem.size); t := daytime->text(daytime->local(mem.date)); return s+sys->sprint(" %-12.12s %-4.4s ", t[4:], t[24:]); } mtab := array[] of { "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" }; modes(mode: int): string { return mtab[(mode>>6)&7]+mtab[(mode>>3)&7]+mtab[mode&7]; } # # read the header for the next archive contents # Armember.rdhdr(b: ref Iobuf): ref Armember { buf := array[SAR_HDR] of byte; if((n := b.read(buf, len buf)) != len buf){ if(n == 0) return nil; if(n > 0) sys->werrstr("unexpected end-of-file"); rderr(); } mem := ref Armember; for(i := Oname+Lname; i > Oname; i--) if(buf[i-1] != byte '/' && buf[i-1] != byte ' ') break; mem.name = string buf[Oname:i]; mem.date = intof(buf[Odate: Odate+Ldate], 10); mem.uid = intof(buf[Ouid: Ouid+Luid], 10); mem.gid = intof(buf[Ogid: Ogid+Lgid], 10); mem.mode = intof(buf[Omode: Omode+Lmode], 8); mem.size = intof(buf[Osize: Osize+Lsize], 10); if(buf[Ofmag] != ARFMAG0 || buf[Ofmag+1] != ARFMAG1) phaseerr(b.offset()-big SAR_HDR); return mem; } intof(a: array of byte, base: int): int { for(i := len a; i > 0; i--) if(a[i-1] != byte ' '){ a = a[0:i]; break; } (n, s) := str->toint(string a, base); if(s != nil){ sys->fprint(stderr, "ar: invalid integer in archive member's header: %q\n", string a); raise "fail:error"; } return n; } Armember.wrhdr(mem: self ref Armember, fd: ref Sys->FD) { b := array[SAR_HDR] of {* => byte ' '}; nm := array of byte mem.name; if(len nm > Lname) nm = nm[0:Lname]; b[Oname:] = nm; b[Odate:] = sys->aprint("%-12ud", mem.date); b[Ouid:] = sys->aprint("%-6d", 0); b[Ogid:] = sys->aprint("%-6d", 0); b[Omode:] = sys->aprint("%-8uo", mem.mode); b[Osize:] = sys->aprint("%-10ud", mem.size); b[Ofmag] = ARFMAG0; b[Ofmag+1] = ARFMAG1; mustwrite(fd, b, len b); } # # make a new member from the given file, with the file's contents # Armember.new(name: string, fd: ref Sys->FD): ref Armember { mem := ref Armember; mem.replace(name, fd); return mem; } # # replace the contents of an existing member # Armember.replace(mem: self ref Armember, name: string, fd: ref Sys->FD) { (ok, d) := sys->fstat(fd); if(ok < 0){ sys->fprint(stderr, "ar: cannot stat %s: %r\n", name); raise "fail:no stat"; } mem.name = trim(name); mem.date = d.mtime; mem.uid = 0; mem.gid = 0; mem.mode = d.mode & 8r777; mem.size = int d.length; if(big mem.size != d.length){ sys->fprint(stderr, "ar: file %s too big\n", name); raise "fail:error"; } mem.fd = fd; mem.contents = nil; # will be copied across from fd when needed } # # read the contents of an archive member # Armember.read(mem: self ref Armember, b: ref Iobuf): int { if(mem.contents != nil) return len mem.contents; mem.contents = buffer(mem.size + (mem.size&1)); n := b.read(mem.contents, len mem.contents); if(n != len mem.contents){ if(n >= 0) sys->werrstr("unexpected end-of-file"); rderr(); } return n; } mustwrite(fd: ref Sys->FD, buf: array of byte, n: int) { if(sys->write(fd, buf, n) != n) wrerr(); } # # write an archive member to ofd, including header # Armember.write(mem: self ref Armember, ofd: ref Sys->FD) { mem.wrhdr(ofd); if(mem.contents != nil){ mustwrite(ofd, mem.contents, len mem.contents); return; } if(mem.fd == nil) raise "ar: write nil fd"; buf := array[Sys->ATOMICIO] of byte; # could be bigger for(nr := mem.size; nr > 0;){ n := nr; if(n > len buf) n = len buf; n = sys->read(mem.fd, buf, n); if(n <= 0){ if(n == 0) sys->werrstr("unexpected end-of-file"); rderr(); } mustwrite(ofd, buf, n); nr -= n; } if(mem.size & 1) mustwrite(ofd, array[] of {byte '\n'}, 1); } # # seek past the current member's contents in b # Armember.skip(mem: self ref Armember, b: ref Iobuf) { b.seek(big(mem.size + (mem.size&1)), 1); } # # copy a member's contents from memory or directly from an archive to another file # Armember.copyout(mem: self ref Armember, b: ref Iobuf, ofd: ref Sys->FD) { if(mem.contents != nil){ mustwrite(ofd, mem.contents, len mem.contents); return; } buf := array[Sys->ATOMICIO] of byte; # could be bigger for(nr := mem.size; nr > 0;){ n := nr; if(n > len buf) n = len buf; n = b.read(buf, n); if(n <= 0){ if(n == 0) sys->werrstr("unexpected end-of-file"); rderr(); } mustwrite(ofd, buf, n); nr -= n; } if(mem.size & 1) b.getc(); } # # Temp file I/O subsystem. We attempt to cache all three temp files in # core. When we run out of memory we spill to disk. # The I/O model assumes that temp files: # 1) are only written on the end # 2) are only read from the beginning # 3) are only read after all writing is complete. # The architecture uses one control block per temp file. Each control # block anchors a chain of buffers, each containing an archive contents. # Arfile.new(): ref Arfile { return ref Arfile; } # # copy the contents of mem at b into the temporary # Arfile.copy(ap: self ref Arfile, b: ref Iobuf, mem: ref Armember) { mem.read(b); ap.insert(mem); } # # insert a contents buffer into the contents chain # Arfile.insert(ap: self ref Arfile, mem: ref Armember) { mem.next = nil; if(ap.head == nil) ap.head = mem; else ap.tail.next = mem; ap.tail = mem; } # # stream the contents in a temp file to the file referenced by 'fd'. # Arfile.stream(ap: self ref Arfile, fd: ref Sys->FD) { if(ap.fd != nil){ # copy prefix from disk buf := array[Sys->ATOMICIO] of byte; sys->seek(ap.fd, big 0, 0); while((n := sys->read(ap.fd, buf, len buf)) > 0) mustwrite(fd, buf, n); if(n < 0) rderr(); ap.fd = nil; } # dump the in-core buffers, which always follow the contents in the temp file for(mem := ap.head; mem != nil; mem = mem.next) mem.write(fd); } # # spill a member's contents to disk # totalmem := 0; warned := 0; tn := 0; Arfile.page(ap: self ref Arfile): int { mem := ap.head; if(ap.fd == nil && !warned){ pid := sys->pctl(0, nil); for(i := 0;; i++){ name := sys->sprint("/tmp/art%d.%d.%d", pid, tn, i); ap.fd = sys->create(name, Sys->OEXCL | Sys->ORDWR | Sys->ORCLOSE, 8r600); if(ap.fd != nil) break; if(i >= 20){ warned =1; sys->fprint(stderr,"ar: warning: can't create temp file %s: %r\n", name); return 0; # we'll simply use the memory } } tn++; } mem.write(ap.fd); ap.head = mem.next; if(ap.tail == mem) ap.tail = mem.next; totalmem -= len mem.contents; return 1; } # # account for the space taken by a contents's contents, # pushing earlier contentss to disk to keep the space below a # reasonable level # buffer(n: int): array of byte { Flush: while(totalmem + n > 1024*1024){ for(i := 0; i < len parts; i++) if(parts[i] != nil && parts[i].page()) continue Flush; break; } totalmem += n; return array[n] of byte; }