plan9fox/sys/src/cmd/patch.c
Ori Bernstein d457233c70 patch: handle stripped/empty lines mid-hunk
Some programs will strip trailing spaces from hunks;
in this case we want to treat the line as though it
came with a leading space.

This fixes applying some patches, such as

	[PATCH] Permissions for child boards in /srv
2022-07-02 20:13:31 +00:00

618 lines
10 KiB
C

#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <bio.h>
typedef struct Patch Patch;
typedef struct Hunk Hunk;
typedef struct Fbuf Fbuf;
struct Patch {
char *name;
Hunk *hunk;
usize nhunk;
};
struct Hunk {
int lnum;
char *oldpath;
int oldln;
int oldcnt;
int oldlen;
int oldsz;
char *old;
char *newpath;
int newln;
int newcnt;
int newlen;
int newsz;
char *new;
};
struct Fbuf {
int *lines;
int nlines;
int lastln;
char *buf;
int len;
};
int strip;
int reverse;
void (*addnew)(Hunk*, char*);
void (*addold)(Hunk*, char*);
char*
readline(Biobuf *f, int *lnum)
{
char *ln;
if((ln = Brdstr(f, '\n', 0)) == nil)
return nil;
*lnum += 1;
return ln;
}
void *
emalloc(ulong n)
{
void *v;
v = mallocz(n, 1);
if(v == nil)
sysfatal("malloc: %r");
setmalloctag(v, getcallerpc(&n));
return v;
}
void *
erealloc(void *v, ulong n)
{
if(n == 0)
n++;
v = realloc(v, n);
if(v == nil)
sysfatal("malloc: %r");
setmalloctag(v, getcallerpc(&n));
return v;
}
int
fileheader(char *s, char *pfx, char **name)
{
int len, n, nnull;
char *e;
if((strncmp(s, pfx, strlen(pfx))) != 0)
return -1;
for(s += strlen(pfx); *s; s++)
if(!isspace(*s))
break;
for(e = s; *e; e++)
if(isspace(*e))
break;
if(s == e)
return -1;
nnull = strlen("/dev/null");
if((e - s) != nnull || strncmp(s, "/dev/null", nnull) != 0){
n = strip;
while(s != e && n > 0){
while(s != e && *s == '/')
s++;
while(s != e && *s != '/')
s++;
n--;
}
while(*s == '/')
s++;
if(*s == '\0')
sysfatal("too many components stripped");
}
len = (e - s) + 1;
*name = emalloc(len);
strecpy(*name, *name + len, s);
return 0;
}
int
hunkheader(Hunk *h, char *s, char *oldpath, char *newpath, int lnum)
{
char *e;
memset(h, 0, sizeof(*h));
h->lnum = lnum;
h->oldpath = strdup(oldpath);
h->newpath = strdup(newpath);
h->oldlen = 0;
h->oldsz = 32;
h->old = emalloc(h->oldsz);
h->newlen = 0;
h->newsz = 32;
h->new = emalloc(h->newsz);
if(strncmp(s, "@@ -", 4) != 0)
return -1;
e = s + 4;
h->oldln = strtol(e, &e, 10);
h->oldcnt = 1;
if(*e == ','){
e++;
h->oldcnt = strtol(e, &e, 10);
}
while(*e == ' ' || *e == '\t')
e++;
if(*e != '+')
return -1;
e++;
h->newln = strtol(e, &e, 10);
if(e == s)
return -1;
h->newcnt = 1;
if(*e == ','){
e++;
h->newcnt = strtol(e, &e, 10);
}
if(e == s || *e != ' ')
return -1;
if(strncmp(e, " @@", 3) != 0)
return -1;
/*
* empty files have line number 0: keep that,
* otherwise adjust down.
*/
if(h->oldln > 0)
h->oldln--;
if(h->newln > 0)
h->newln--;
if(h->oldln < 0 || h->newln < 0 || h->oldcnt < 0 || h->newcnt < 0)
sysfatal("malformed hunk %s", s);
return 0;
}
void
addnewfn(Hunk *h, char *ln)
{
int n;
ln++;
n = strlen(ln);
while(h->newlen + n >= h->newsz){
h->newsz *= 2;
h->new = erealloc(h->new, h->newsz);
}
memcpy(h->new + h->newlen, ln, n);
h->newlen += n;
}
void
addoldfn(Hunk *h, char *ln)
{
int n;
ln++;
n = strlen(ln);
while(h->oldlen + n >= h->oldsz){
h->oldsz *= 2;
h->old = erealloc(h->old, h->oldsz);
}
memcpy(h->old + h->oldlen, ln, n);
h->oldlen += n;
}
int
addmiss(Hunk *h, char *ln, int *nold, int *nnew)
{
if(ln == nil)
return 1;
else if(ln[0] != '-' && ln[0] != '+')
return 0;
if(ln[0] == '-'){
addold(h, ln);
*nold += 1;
}else{
addnew(h, ln);
*nnew += 1;
}
return 1;
}
void
addhunk(Patch *p, Hunk *h)
{
p->hunk = erealloc(p->hunk, ++p->nhunk*sizeof(Hunk));
p->hunk[p->nhunk-1] = *h;
}
int
hunkcmp(void *a, void *b)
{
int c;
c = strcmp(((Hunk*)a)->oldpath, ((Hunk*)b)->oldpath);
if(c != 0)
return c;
return ((Hunk*)a)->oldln - ((Hunk*)b)->oldln;
}
Patch*
parse(Biobuf *f, char *name)
{
char *ln, *old, *new, **oldp, **newp;
int oldcnt, newcnt, lnum;
Patch *p;
Hunk h;
ln = nil;
lnum = 0;
p = emalloc(sizeof(Patch));
if(!reverse){
oldp = &old;
newp = &new;
}else{
oldp = &new;
newp = &old;
}
comment:
free(ln);
while((ln = readline(f, &lnum)) != nil){
if(strncmp(ln, "--- ", 4) == 0)
goto patch;
free(ln);
}
if(p->nhunk == 0)
sysfatal("%s: could not find start of patch", name);
goto out;
patch:
if(fileheader(ln, "--- ", oldp) == -1)
goto comment;
free(ln);
if((ln = readline(f, &lnum)) == nil)
goto out;
if(fileheader(ln, "+++ ", newp) == -1)
goto comment;
free(ln);
if((ln = readline(f, &lnum)) == nil)
goto out;
hunk:
oldcnt = 0;
newcnt = 0;
if(hunkheader(&h, ln, old, new, lnum) == -1)
goto comment;
free(ln);
while(1){
if((ln = readline(f, &lnum)) == nil){
if(oldcnt != h.oldcnt || newcnt != h.newcnt)
sysfatal("%s:%d: malformed hunk: mismatched counts", name, lnum);
addhunk(p, &h);
break;
}
switch(ln[0]){
default:
sysfatal("%s:%d: malformed hunk: leading junk", name, lnum);
goto out;
case '-':
addold(&h, ln);
oldcnt++;
break;
case '+':
addnew(&h, ln);
newcnt++;
break;
case '\n':
addold(&h, " \n");
addnew(&h, " \n");
oldcnt++;
newcnt++;
break;
case ' ':
addold(&h, ln);
addnew(&h, ln);
oldcnt++;
newcnt++;
break;
}
free(ln);
if(oldcnt > h.oldcnt || newcnt > h.newcnt)
sysfatal("%s:%d: malformed hunk: oversized hunk", name, lnum);
if(oldcnt < h.oldcnt || newcnt < h.newcnt)
continue;
addhunk(p, &h);
if((ln = readline(f, &lnum)) == nil)
goto out;
if(strncmp(ln, "--- ", 4) == 0)
goto patch;
if(strncmp(ln, "@@ ", 3) == 0)
goto hunk;
goto comment;
}
out:
qsort(p->hunk, p->nhunk, sizeof(Hunk), hunkcmp);
free(old);
free(new);
free(ln);
return p;
}
int
rename(int fd, char *name)
{
Dir st;
char *p;
nulldir(&st);
if((p = strrchr(name, '/')) == nil)
st.name = name;
else
st.name = p + 1;
return dirfwstat(fd, &st);
}
int
mkpath(char *path)
{
char *p, buf[ERRMAX];
int f;
if(*path == '\0')
return 0;
for(p = strchr(path+1, '/'); p != nil; p = strchr(p+1, '/')){
*p = '\0';
if(access(path, AEXIST) != 0){
if((f = create(path, OREAD, DMDIR | 0777)) == -1){
rerrstr(buf, sizeof(buf));
if(strstr(buf, "exist") == nil)
return -1;
}
close(f);
}
*p = '/';
}
return 0;
}
void
blat(char *old, char *new, char *o, usize len)
{
char *tmp;
int fd;
if(strcmp(new, "/dev/null") == 0){
if(len != 0)
sysfatal("diff modifies removed file");
if(remove(old) == -1)
sysfatal("removeold %s: %r", old);
return;
}
if(mkpath(new) == -1)
sysfatal("mkpath %s: %r", new);
if((tmp = smprint("%s.tmp%d", new, getpid())) == nil)
sysfatal("smprint: %r");
if((fd = create(tmp, OWRITE, 0666)) == -1)
sysfatal("open %s: %r", tmp);
if(write(fd, o, len) != len)
sysfatal("write %s: %r", tmp);
if(strcmp(old, new) == 0 && remove(old) == -1)
sysfatal("remove %s: %r", old);
if(rename(fd, new) == -1)
sysfatal("create %s: %r", new);
if(close(fd) == -1)
sysfatal("close %s: %r", tmp);
free(tmp);
}
int
slurp(Fbuf *f, char *path)
{
int n, i, fd, sz, len, nlines, linesz;
char *buf;
int *lines;
if((fd = open(path, OREAD)) == -1)
sysfatal("open %s: %r", path);
sz = 8192;
len = 0;
buf = emalloc(sz);
while(1){
if(len == sz){
sz *= 2;
buf = erealloc(buf, sz);
}
n = read(fd, buf + len, sz - len);
if(n == 0)
break;
if(n == -1)
sysfatal("read %s: %r", path);
len += n;
}
nlines = 0;
linesz = 32;
lines = emalloc(linesz*sizeof(int));
lines[nlines++] = 0;
for(i = 0; i < len; i++){
if(buf[i] != '\n')
continue;
if(nlines+1 == linesz){
linesz *= 2;
lines = erealloc(lines, linesz*sizeof(int));
}
lines[nlines++] = i+1;
}
f->len = len;
f->buf = buf;
f->lines = lines;
f->nlines = nlines;
f->lastln = -1;
return 0;
}
char*
search(Fbuf *f, Hunk *h, char *fname)
{
int ln, len, off, fuzz, nfuzz, scanning;
scanning = 1;
len = h->oldlen;
nfuzz = (f->nlines < 250) ? f->nlines : 250;
for(fuzz = 0; scanning && fuzz <= nfuzz; fuzz++){
scanning = 0;
ln = h->oldln - fuzz;
if(ln > f->lastln){
off = f->lines[ln];
if(off + len > f->len)
continue;
scanning = 1;
if(memcmp(f->buf + off, h->old, h->oldlen) == 0){
f->lastln = ln;
return f->buf + off;
}
}
ln = h->oldln + fuzz - 1;
if(ln <= f->nlines){
off = f->lines[ln];
if(off + len >= f->len)
continue;
scanning = 1;
if(memcmp(f->buf + off, h->old, h->oldlen) == 0){
f->lastln = ln;
return f->buf + off;
}
}
}
sysfatal("%s:%d: unable to find hunk offset in %s", fname, h->lnum, h->oldpath);
return nil;
}
char*
append(char *o, int *sz, char *s, char *e)
{
int n;
n = (e - s);
o = erealloc(o, *sz + n);
memcpy(o + *sz, s, n);
*sz += n;
return o;
}
int
apply(Patch *p, char *fname)
{
char *o, *s, *e, *curfile;
int i, osz;
Hunk *h;
Fbuf f;
e = nil;
o = nil;
osz = 0;
curfile = nil;
for(i = 0; i < p->nhunk; i++){
h = &p->hunk[i];
if(curfile == nil || strcmp(curfile, h->newpath) != 0){
if(slurp(&f, h->oldpath) == -1)
sysfatal("slurp %s: %r", h->oldpath);
curfile = h->newpath;
e = f.buf;
}
s = e;
e = search(&f, h, fname);
o = append(o, &osz, s, e);
o = append(o, &osz, h->new, h->new + h->newlen);
e += h->oldlen;
if(i+1 == p->nhunk || strcmp(curfile, p->hunk[i+1].newpath) != 0){
o = append(o, &osz, e, f.buf + f.len);
blat(h->oldpath, h->newpath, o, osz);
if(strcmp(h->newpath, "/dev/null") == 0)
print("%s\n", h->oldpath);
else
print("%s\n", h->newpath);
osz = 0;
}
}
free(o);
return 0;
}
void
freepatch(Patch *p)
{
Hunk *h;
int i;
for(i = 0; i < p->nhunk; i++){
h = &p->hunk[i];
free(h->oldpath);
free(h->newpath);
free(h->old);
free(h->new);
}
free(p->hunk);
free(p->name);
free(p);
}
void
usage(void)
{
fprint(2, "usage: %s [-R] [-p nstrip] [patch...]\n", argv0);
exits("usage");
}
void
main(int argc, char **argv)
{
Biobuf *f;
Patch *p;
int i;
ARGBEGIN{
case 'p':
strip = atoi(EARGF(usage()));
break;
case 'R':
reverse++;
break;
default:
usage();
break;
}ARGEND;
if(reverse){
addnew = addoldfn;
addold = addnewfn;
}else{
addnew = addnewfn;
addold = addoldfn;
}
if(argc == 0){
if((f = Bfdopen(0, OREAD)) == nil)
sysfatal("open stdin: %r");
if((p = parse(f, "stdin")) == nil)
sysfatal("parse patch: %r");
if(apply(p, "stdin") == -1)
sysfatal("apply stdin: %r");
freepatch(p);
Bterm(f);
}else{
for(i = 0; i < argc; i++){
if((f = Bopen(argv[i], OREAD)) == nil)
sysfatal("open %s: %r", argv[i]);
if((p = parse(f, argv[i])) == nil)
sysfatal("parse patch: %r");
if(apply(p, argv[i]) == -1)
sysfatal("apply %s: %r", argv[i]);
freepatch(p);
Bterm(f);
}
}
exits(nil);
}