plan9fox/sys/src/cmd/acme/ecmd.c
cinap_lenrek afa5800b5b acme: fix suicide *and* resource leak in ecmd.c (thanks igor)
To reproduce the suicide try running the following in acme:

• 'Edit B <ls lib'

by select and middle clicking in a window that is in your $home.

There is a very high chance acme will commit suicide like this:

<snip>
cpu% broke
echo kill>/proc/333310/ctl # acme
cpu% acid 333310
/proc/333310/text:amd64 plan 9 executable
/sys/lib/acid/port
/sys/lib/acid/amd64
acid: lstk()
edittext(nr=0x31,q=0x0,r=0x45aa10)+0x8 /sys/src/cmd/acme/ecmd.c:135
xfidwrite(x=0x461230)+0x28a /sys/src/cmd/acme/xfid.c:479
        w=0x0
        qid=0x5
        fc=0x461390
        t=0x1
        nr=0x100000031
        r=0x45aa10
        eval=0x3100000000
        a=0x405621
        nb=0x500000001
        err=0x419310
        q0=0x100000000
        tq0=0x80
        tq1=0x8000000000
        buf=0x41e8d800000000
xfidctl(arg=0x461230)+0x35 /sys/src/cmd/acme/xfid.c:52
        x=0x461230
launcheramd64(arg=0x461230,f=0x22357e)+0x10 /sys/src/libthread/amd64.c:11
0xfefefefefefefefe ?file?:0
</snap>

The suicide issue is caused by the following chain of events:

• /sys/src/cmd/acme/ecmd.c:/^edittext is called at
/sys/src/cmd/acme/xfid.c:479 passing nil as its first parameter:

<snip>
...
        case QWeditout:
                r = fullrunewrite(x, &nr);
                if(w)
                        err = edittext(w, w->wrselrange.q1, r, nr);
                else
                        err = edittext(nil, 0, r, nr);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
</snap>

...and /sys/src/cmd/acme/ecmd.c:/^edittext dereferences the
first parameter that is *nil* at the first statement:

<snip>
char*
edittext(Window *w, int q, Rune *r, int nr)
{
        File *f;

        f = w->body.file;
^^^^^^^^^^^^^^^^^^^^^
This will crash if 'w' is *nil*

        switch(editing){
...
</snap>

Moving the the derefernce of 'w' into the case where it is
needed (see above patch) fixes the suicude.

The memory leak is fixed in /sys/src/cmd/acme/ecmd.c:/^filelist.  The
current implementation of filelist(...) breaks its contract with its
caller, thereby leading to a memory leak in /sys/src/cmd/acme/ecmd.c:/^B_cmd
and /sys/src/cmd/acme/ecmd.c:/^D_cmd.

The contract /sys/src/cmd/acme/ecmd.c:/^filelist seems to have with
its callers is that in case of success it fills up a 'collection' that
callers can then clear with a call to clearcollection(...).

The fix above honours this contract and thereby removes the leak.

After you apply the patch the following two tests should succeed:

• Execute by select and middle click in a Tag:
        'Edit B lib/profile'

• Execute by select and middle click in a Tag:
        'Edit B <ls lib'

The former lead to a resource leak that is now fixed.

The latter lead to a suicide that is now fixed by moving the statement
that dereferences the parameter to the location where it is needed,
which is not the path used in the case of 'Edit B <ls'.

Cheers,
Igor
2021-04-02 15:51:15 +02:00

1367 lines
25 KiB
C

#include <u.h>
#include <libc.h>
#include <draw.h>
#include <thread.h>
#include <cursor.h>
#include <mouse.h>
#include <keyboard.h>
#include <frame.h>
#include <fcall.h>
#include <plumb.h>
#include "dat.h"
#include "edit.h"
#include "fns.h"
int Glooping;
int nest;
char Enoname[] = "no file name given";
Address addr;
File *menu;
Rangeset sel;
extern Text* curtext;
Rune *collection;
int ncollection;
int append(File*, Cmd*, long);
int pdisplay(File*);
void pfilename(File*);
void looper(File*, Cmd*, int);
void filelooper(Text*, Cmd*, int);
void linelooper(File*, Cmd*);
Address lineaddr(long, Address, int);
int filematch(File*, String*);
File *tofile(String*);
Rune* cmdname(File *f, String *s, int);
void runpipe(Text*, int, Rune*, int, int);
void
clearcollection(void)
{
free(collection);
collection = nil;
ncollection = 0;
}
void
resetxec(void)
{
Glooping = nest = 0;
clearcollection();
}
void
mkaddr(Address *a, File *f)
{
a->r.q0 = f->curtext->q0;
a->r.q1 = f->curtext->q1;
a->f = f;
}
int
cmdexec(Text *t, Cmd *cp)
{
int i;
Addr *ap;
File *f;
Window *w;
Address dot;
if(t == nil)
w = nil;
else
w = t->w;
if(w==nil && (cp->addr==0 || cp->addr->type!='"') &&
!utfrune("bBnqUXY!", cp->cmdc) &&
!(cp->cmdc=='D' && cp->text))
editerror("no current window");
i = cmdlookup(cp->cmdc); /* will be -1 for '{' */
f = nil;
if(t && t->w){
t = &t->w->body;
f = t->file;
f->curtext = t;
}
if(i>=0 && cmdtab[i].defaddr != aNo){
if((ap=cp->addr)==0 && cp->cmdc!='\n'){
cp->addr = ap = newaddr();
ap->type = '.';
if(cmdtab[i].defaddr == aAll)
ap->type = '*';
}else if(ap && ap->type=='"' && ap->next==0 && cp->cmdc!='\n'){
ap->next = newaddr();
ap->next->type = '.';
if(cmdtab[i].defaddr == aAll)
ap->next->type = '*';
}
if(cp->addr){ /* may be false for '\n' (only) */
static Address none = {0,0,nil};
if(f){
mkaddr(&dot, f);
addr = cmdaddress(ap, dot, 0);
}else /* a " */
addr = cmdaddress(ap, none, 0);
f = addr.f;
t = f->curtext;
}
}
switch(cp->cmdc){
case '{':
mkaddr(&dot, f);
if(cp->addr != nil)
dot = cmdaddress(cp->addr, dot, 0);
for(cp = cp->cmd; cp; cp = cp->next){
if(dot.r.q1 > t->file->nc)
editerror("dot extends past end of buffer during { command");
t->q0 = dot.r.q0;
t->q1 = dot.r.q1;
cmdexec(t, cp);
}
break;
default:
if(i < 0)
editerror("unknown command %c in cmdexec", cp->cmdc);
i = (*cmdtab[i].fn)(t, cp);
return i;
}
return 1;
}
char*
edittext(Window *w, int q, Rune *r, int nr)
{
File *f;
switch(editing){
case Inactive:
return "permission denied";
case Inserting:
f = w->body.file;
eloginsert(f, q, r, nr);
return nil;
case Collecting:
collection = runerealloc(collection, ncollection+nr+1);
runemove(collection+ncollection, r, nr);
ncollection += nr;
collection[ncollection] = '\0';
return nil;
default:
return "unknown state in edittext";
}
}
/* string is known to be NUL-terminated */
Rune*
filelist(Text *t, Rune *r, int nr)
{
if(nr == 0)
return nil;
r = skipbl(r, nr, &nr);
clearcollection();
if(r[0] != '<'){
if((collection = runestrdup(r)) != nil)
ncollection += runestrlen(r);
}else
/* use < command to collect text */
runpipe(t, '<', r+1, nr-1, Collecting);
return collection;
}
int
a_cmd(Text *t, Cmd *cp)
{
return append(t->file, cp, addr.r.q1);
}
int
b_cmd(Text*, Cmd *cp)
{
File *f;
f = tofile(cp->text);
if(nest == 0)
pfilename(f);
curtext = f->curtext;
return TRUE;
}
int
B_cmd(Text *t, Cmd *cp)
{
Rune *list, *r, *s;
int nr;
list = filelist(t, cp->text->r, cp->text->n);
if(list == nil)
editerror(Enoname);
r = list;
nr = runestrlen(r);
r = skipbl(r, nr, &nr);
if(nr == 0)
new(t, t, nil, 0, 0, r, 0);
else while(nr > 0){
s = findbl(r, nr, &nr);
*s = '\0';
new(t, t, nil, 0, 0, r, runestrlen(r));
if(nr > 0)
r = skipbl(s+1, nr-1, &nr);
}
clearcollection();
return TRUE;
}
int
c_cmd(Text *t, Cmd *cp)
{
elogreplace(t->file, addr.r.q0, addr.r.q1, cp->text->r, cp->text->n);
t->q0 = addr.r.q0;
t->q1 = addr.r.q0;
return TRUE;
}
int
d_cmd(Text *t, Cmd*)
{
if(addr.r.q1 > addr.r.q0)
elogdelete(t->file, addr.r.q0, addr.r.q1);
t->q0 = addr.r.q0;
t->q1 = addr.r.q0;
return TRUE;
}
void
D1(Text *t)
{
if(t->w->body.file->ntext>1 || winclean(t->w, FALSE))
colclose(t->col, t->w, TRUE);
}
int
D_cmd(Text *t, Cmd *cp)
{
Rune *list, *r, *s, *n;
int nr, nn;
Window *w;
Runestr dir, rs;
char buf[128];
list = filelist(t, cp->text->r, cp->text->n);
if(list == nil){
D1(t);
return TRUE;
}
dir = dirname(t, nil, 0);
r = list;
nr = runestrlen(r);
r = skipbl(r, nr, &nr);
do{
s = findbl(r, nr, &nr);
*s = '\0';
/* first time through, could be empty string, meaning delete file empty name */
nn = runestrlen(r);
if(r[0]=='/' || nn==0 || dir.nr==0){
rs.r = runestrdup(r);
rs.nr = nn;
}else{
n = runemalloc(dir.nr+1+nn);
runemove(n, dir.r, dir.nr);
n[dir.nr] = '/';
runemove(n+dir.nr+1, r, nn);
rs = cleanrname((Runestr){n, dir.nr+1+nn});
}
w = lookfile(rs.r, rs.nr);
if(w == nil){
snprint(buf, sizeof buf, "no such file %.*S", rs.nr, rs.r);
free(rs.r);
editerror(buf);
}
free(rs.r);
D1(&w->body);
if(nr > 0)
r = skipbl(s+1, nr-1, &nr);
}while(nr > 0);
clearcollection();
free(dir.r);
return TRUE;
}
static int
readloader(void *v, uint q0, Rune *r, int nr)
{
if(nr > 0)
eloginsert(v, q0, r, nr);
return 0;
}
int
e_cmd(Text *t, Cmd *cp)
{
Rune *name;
File *f;
int i, isdir, q0, q1, fd, nulls, samename, allreplaced;
char *s, tmp[128];
Dir *d;
f = t->file;
q0 = addr.r.q0;
q1 = addr.r.q1;
if(cp->cmdc == 'e'){
if(winclean(t->w, TRUE)==FALSE)
editerror(""); /* winclean generated message already */
q0 = 0;
q1 = f->nc;
}
allreplaced = (q0==0 && q1==f->nc);
name = cmdname(f, cp->text, cp->cmdc=='e');
if(name == nil)
editerror(Enoname);
i = runestrlen(name);
samename = runeeq(name, i, t->file->name, t->file->nname);
s = runetobyte(name, i);
free(name);
fd = open(s, OREAD);
if(fd < 0){
snprint(tmp, sizeof tmp, "can't open %s: %r", s);
free(s);
editerror(tmp);
}
d = dirfstat(fd);
isdir = (d!=nil && (d->qid.type&QTDIR));
free(d);
if(isdir){
close(fd);
snprint(tmp, sizeof tmp, "%s is a directory", s);
free(s);
editerror(tmp);
}
elogdelete(f, q0, q1);
nulls = 0;
loadfile(fd, q1, &nulls, readloader, f);
free(s);
close(fd);
if(nulls)
warning(nil, "%s: NUL bytes elided\n", s);
else if(allreplaced && samename)
f->editclean = TRUE;
return TRUE;
}
int
f_cmd(Text *t, Cmd *cp)
{
Rune *name;
String *str;
String empty;
if(cp->text == nil){
empty.n = 0;
empty.r = L"";
str = &empty;
}else
str = cp->text;
name = cmdname(t->file, str, TRUE);
free(name);
pfilename(t->file);
return TRUE;
}
int
g_cmd(Text *t, Cmd *cp)
{
if(t->file != addr.f){
warning(nil, "internal error: g_cmd f!=addr.f\n");
return FALSE;
}
if(rxcompile(cp->re->r) == FALSE)
editerror("bad regexp in g command");
if(rxexecute(t, nil, addr.r.q0, addr.r.q1, &sel) ^ cp->cmdc=='v'){
t->q0 = addr.r.q0;
t->q1 = addr.r.q1;
return cmdexec(t, cp->cmd);
}
return TRUE;
}
int
i_cmd(Text *t, Cmd *cp)
{
return append(t->file, cp, addr.r.q0);
}
void
copy(File *f, Address addr2)
{
long p;
int ni;
Rune *buf;
buf = fbufalloc();
for(p=addr.r.q0; p<addr.r.q1; p+=ni){
ni = addr.r.q1-p;
if(ni > RBUFSIZE)
ni = RBUFSIZE;
bufread(f, p, buf, ni);
eloginsert(addr2.f, addr2.r.q1, buf, ni);
}
fbuffree(buf);
}
void
move(File *f, Address addr2)
{
if(addr.f!=addr2.f || addr.r.q1<=addr2.r.q0){
elogdelete(f, addr.r.q0, addr.r.q1);
copy(f, addr2);
}else if(addr.r.q0 >= addr2.r.q1){
copy(f, addr2);
elogdelete(f, addr.r.q0, addr.r.q1);
}else if(addr.r.q0==addr2.r.q0 && addr.r.q1==addr2.r.q1){
; /* move to self; no-op */
}else
editerror("move overlaps itself");
}
int
m_cmd(Text *t, Cmd *cp)
{
Address dot, addr2;
mkaddr(&dot, t->file);
addr2 = cmdaddress(cp->mtaddr, dot, 0);
if(cp->cmdc == 'm')
move(t->file, addr2);
else
copy(t->file, addr2);
return TRUE;
}
int
p_cmd(Text *t, Cmd*)
{
return pdisplay(t->file);
}
int
s_cmd(Text *t, Cmd *cp)
{
int i, j, k, c, m, n, nrp, didsub;
long p1, op, delta;
String *buf;
Rangeset *rp;
char *err;
Rune *rbuf;
n = cp->num;
op= -1;
if(rxcompile(cp->re->r) == FALSE)
editerror("bad regexp in s command");
nrp = 0;
rp = nil;
delta = 0;
didsub = FALSE;
for(p1 = addr.r.q0; p1<=addr.r.q1 && rxexecute(t, nil, p1, addr.r.q1, &sel); ){
if(sel.r[0].q0 == sel.r[0].q1){ /* empty match? */
if(sel.r[0].q0 == op){
p1++;
continue;
}
p1 = sel.r[0].q1+1;
}else
p1 = sel.r[0].q1;
op = sel.r[0].q1;
if(--n>0)
continue;
nrp++;
rp = erealloc(rp, nrp*sizeof(Rangeset));
rp[nrp-1] = sel;
}
rbuf = fbufalloc();
buf = allocstring(0);
for(m=0; m<nrp; m++){
buf->n = 0;
buf->r[0] = L'\0';
sel = rp[m];
for(i = 0; i<cp->text->n; i++)
if((c = cp->text->r[i])=='\\' && i<cp->text->n-1){
c = cp->text->r[++i];
if('1'<=c && c<='9') {
j = c-'0';
if(sel.r[j].q1-sel.r[j].q0>RBUFSIZE){
err = "replacement string too long";
goto Err;
}
bufread(t->file, sel.r[j].q0, rbuf, sel.r[j].q1-sel.r[j].q0);
for(k=0; k<sel.r[j].q1-sel.r[j].q0; k++)
Straddc(buf, rbuf[k]);
}else
Straddc(buf, c);
}else if(c!='&')
Straddc(buf, c);
else{
if(sel.r[0].q1-sel.r[0].q0>RBUFSIZE){
err = "right hand side too long in substitution";
goto Err;
}
bufread(t->file, sel.r[0].q0, rbuf, sel.r[0].q1-sel.r[0].q0);
for(k=0; k<sel.r[0].q1-sel.r[0].q0; k++)
Straddc(buf, rbuf[k]);
}
elogreplace(t->file, sel.r[0].q0, sel.r[0].q1, buf->r, buf->n);
delta -= sel.r[0].q1-sel.r[0].q0;
delta += buf->n;
didsub = 1;
if(!cp->flag)
break;
}
free(rp);
freestring(buf);
fbuffree(rbuf);
if(!didsub && nest==0)
editerror("no substitution");
t->q0 = addr.r.q0;
t->q1 = addr.r.q1;
return TRUE;
Err:
free(rp);
freestring(buf);
fbuffree(rbuf);
editerror(err);
return FALSE;
}
int
u_cmd(Text *t, Cmd *cp)
{
int n, oseq, flag;
n = cp->num;
flag = TRUE;
if(n < 0){
n = -n;
flag = FALSE;
}
oseq = -1;
while(n-->0 && t->file->seq!=0 && t->file->seq!=oseq){
oseq = t->file->seq;
undo(t, nil, nil, flag, 0, nil, 0);
}
return TRUE;
}
int
w_cmd(Text *t, Cmd *cp)
{
Rune *r;
File *f;
f = t->file;
if(f->seq == seq)
editerror("can't write file with pending modifications");
r = cmdname(f, cp->text, FALSE);
if(r == nil)
editerror("no name specified for 'w' command");
putfile(f, addr.r.q0, addr.r.q1, r, runestrlen(r));
/* r is freed by putfile */
return TRUE;
}
int
x_cmd(Text *t, Cmd *cp)
{
if(cp->re)
looper(t->file, cp, cp->cmdc=='x');
else
linelooper(t->file, cp);
return TRUE;
}
int
X_cmd(Text *t, Cmd *cp)
{
filelooper(t, cp, cp->cmdc=='X');
return TRUE;
}
void
runpipe(Text *t, int cmd, Rune *cr, int ncr, int state)
{
Rune *r, *s;
int n;
Runestr dir;
Window *w;
r = skipbl(cr, ncr, &n);
if(n == 0)
editerror("no command specified for %c", cmd);
w = nil;
if(state == Inserting){
w = t->w;
t->q0 = addr.r.q0;
t->q1 = addr.r.q1;
if(cmd == '<' || cmd=='|')
elogdelete(t->file, t->q0, t->q1);
}
s = runemalloc(n+2);
s[0] = cmd;
runemove(s+1, r, n);
n++;
dir.r = nil;
dir.nr = 0;
if(t != nil)
dir = dirname(t, nil, 0);
if(dir.nr==1 && dir.r[0]=='.'){ /* sigh */
free(dir.r);
dir.r = nil;
dir.nr = 0;
}
editing = state;
if(t!=nil && t->w!=nil)
incref(t->w); /* run will decref */
run(w, runetobyte(s, n), dir.r, dir.nr, TRUE, nil, nil, TRUE);
free(s);
if(t!=nil && t->w!=nil)
winunlock(t->w);
qunlock(&row);
recvul(cedit);
qlock(&row);
editing = Inactive;
if(t!=nil && t->w!=nil)
winlock(t->w, 'M');
}
int
pipe_cmd(Text *t, Cmd *cp)
{
runpipe(t, cp->cmdc, cp->text->r, cp->text->n, Inserting);
return TRUE;
}
long
nlcount(Text *t, long q0, long q1, long *pnr)
{
long nl, start;
Rune *buf;
int i, nbuf;
buf = fbufalloc();
nbuf = 0;
i = nl = 0;
start = q0;
while(q0 < q1){
if(i == nbuf){
nbuf = q1-q0;
if(nbuf > RBUFSIZE)
nbuf = RBUFSIZE;
bufread(t->file, q0, buf, nbuf);
i = 0;
}
if(buf[i++] == '\n'){
start = q0+1;
nl++;
}
q0++;
}
fbuffree(buf);
if(pnr != nil)
*pnr = q0 - start;
return nl;
}
enum {
PosnLine = 0,
PosnChars = 1,
PosnLineChars = 2,
};
void
printposn(Text *t, int mode)
{
long l1, l2, r1, r2;
if (t != nil && t->file != nil && t->file->name != nil)
warning(nil, "%.*S:", t->file->nname, t->file->name);
switch(mode) {
case PosnChars:
warning(nil, "#%d", addr.r.q0);
if(addr.r.q1 != addr.r.q0)
warning(nil, ",#%d", addr.r.q1);
warning(nil, "\n");
return;
default:
case PosnLine:
l1 = 1+nlcount(t, 0, addr.r.q0, nil);
l2 = l1+nlcount(t, addr.r.q0, addr.r.q1, nil);
/* check if addr ends with '\n' */
if(addr.r.q1>0 && addr.r.q1>addr.r.q0 && textreadc(t, addr.r.q1-1)=='\n')
--l2;
warning(nil, "%lud", l1);
if(l2 != l1)
warning(nil, ",%lud", l2);
warning(nil, "\n");
return;
case PosnLineChars:
l1 = 1+nlcount(t, 0, addr.r.q0, &r1);
l2 = l1+nlcount(t, addr.r.q0, addr.r.q1, &r2);
if(l2 == l1)
r2 += r1;
warning(nil, "%lud+#%lud", l1, r1);
if(l2 != l1)
warning(nil, ",%lud+#%lud", l2, r2);
warning(nil, "\n");
return;
}
}
int
eq_cmd(Text *t, Cmd *cp)
{
int mode;
switch(cp->text->n){
case 0:
mode = PosnLine;
break;
case 1:
if(cp->text->r[0] == '#'){
mode = PosnChars;
break;
}
if(cp->text->r[0] == '+'){
mode = PosnLineChars;
break;
}
default:
SET(mode);
editerror("newline expected");
}
printposn(t, mode);
return TRUE;
}
int
nl_cmd(Text *t, Cmd *cp)
{
Address a;
File *f;
f = t->file;
if(cp->addr == 0){
/* First put it on newline boundaries */
mkaddr(&a, f);
addr = lineaddr(0, a, -1);
a = lineaddr(0, a, 1);
addr.r.q1 = a.r.q1;
if(addr.r.q0==t->q0 && addr.r.q1==t->q1){
mkaddr(&a, f);
addr = lineaddr(1, a, 1);
}
}
textshow(t, addr.r.q0, addr.r.q1, 1);
return TRUE;
}
int
append(File *f, Cmd *cp, long p)
{
if(cp->text->n > 0)
eloginsert(f, p, cp->text->r, cp->text->n);
f->curtext->q0 = p;
f->curtext->q1 = p;
return TRUE;
}
int
pdisplay(File *f)
{
long p1, p2;
int np;
Rune *buf;
p1 = addr.r.q0;
p2 = addr.r.q1;
if(p2 > f->nc)
p2 = f->nc;
buf = fbufalloc();
while(p1 < p2){
np = p2-p1;
if(np>RBUFSIZE-1)
np = RBUFSIZE-1;
bufread(f, p1, buf, np);
buf[np] = L'\0';
warning(nil, "%S", buf);
p1 += np;
}
fbuffree(buf);
f->curtext->q0 = addr.r.q0;
f->curtext->q1 = addr.r.q1;
return TRUE;
}
void
pfilename(File *f)
{
int dirty;
Window *w;
w = f->curtext->w;
/* same check for dirty as in settag, but we know ncache==0 */
dirty = !w->isdir && !w->isscratch && f->mod;
warning(nil, "%c%c%c %.*S\n", " '"[dirty],
'+', " ."[curtext!=nil && curtext->file==f], f->nname, f->name);
}
void
loopcmd(File *f, Cmd *cp, Range *rp, long nrp)
{
long i;
for(i=0; i<nrp; i++){
f->curtext->q0 = rp[i].q0;
f->curtext->q1 = rp[i].q1;
cmdexec(f->curtext, cp);
}
}
void
looper(File *f, Cmd *cp, int xy)
{
long p, op, nrp;
Range r, tr;
Range *rp;
r = addr.r;
op= xy? -1 : r.q0;
nest++;
if(rxcompile(cp->re->r) == FALSE)
editerror("bad regexp in %c command", cp->cmdc);
nrp = 0;
rp = nil;
for(p = r.q0; p<=r.q1; ){
if(!rxexecute(f->curtext, nil, p, r.q1, &sel)){ /* no match, but y should still run */
if(xy || op>r.q1)
break;
tr.q0 = op, tr.q1 = r.q1;
p = r.q1+1; /* exit next loop */
}else{
if(sel.r[0].q0==sel.r[0].q1){ /* empty match? */
if(sel.r[0].q0==op){
p++;
continue;
}
p = sel.r[0].q1+1;
}else
p = sel.r[0].q1;
if(xy)
tr = sel.r[0];
else
tr.q0 = op, tr.q1 = sel.r[0].q0;
}
op = sel.r[0].q1;
nrp++;
rp = erealloc(rp, nrp*sizeof(Range));
rp[nrp-1] = tr;
}
loopcmd(f, cp->cmd, rp, nrp);
free(rp);
--nest;
}
void
linelooper(File *f, Cmd *cp)
{
long nrp, p;
Range r, linesel;
Address a, a3;
Range *rp;
nest++;
nrp = 0;
rp = nil;
r = addr.r;
a3.f = f;
a3.r.q0 = a3.r.q1 = r.q0;
a = lineaddr(0, a3, 1);
linesel = a.r;
for(p = r.q0; p<r.q1; p = a3.r.q1){
a3.r.q0 = a3.r.q1;
if(p!=r.q0 || linesel.q1==p){
a = lineaddr(1, a3, 1);
linesel = a.r;
}
if(linesel.q0 >= r.q1)
break;
if(linesel.q1 >= r.q1)
linesel.q1 = r.q1;
if(linesel.q1 > linesel.q0)
if(linesel.q0>=a3.r.q1 && linesel.q1>a3.r.q1){
a3.r = linesel;
nrp++;
rp = erealloc(rp, nrp*sizeof(Range));
rp[nrp-1] = linesel;
continue;
}
break;
}
loopcmd(f, cp->cmd, rp, nrp);
free(rp);
--nest;
}
struct Looper
{
Cmd *cp;
int XY;
Window **w;
int nw;
} loopstruct; /* only one; X and Y can't nest */
void
alllooper(Window *w, void *v)
{
Text *t;
struct Looper *lp;
Cmd *cp;
lp = v;
cp = lp->cp;
// if(w->isscratch || w->isdir)
// return;
t = &w->body;
/* only use this window if it's the current window for the file */
if(t->file->curtext != t)
return;
// if(w->nopen[QWevent] > 0)
// return;
/* no auto-execute on files without names */
if(cp->re==nil && t->file->nname==0)
return;
if(cp->re==nil || filematch(t->file, cp->re)==lp->XY){
lp->w = erealloc(lp->w, (lp->nw+1)*sizeof(Window*));
lp->w[lp->nw++] = w;
}
}
void
alllocker(Window *w, void *v)
{
if(v)
incref(w);
else
winclose(w);
}
void
filelooper(Text *t, Cmd *cp, int XY)
{
int i;
Text *targ;
if(Glooping++)
editerror("can't nest %c command", "YX"[XY]);
nest++;
loopstruct.cp = cp;
loopstruct.XY = XY;
if(loopstruct.w) /* error'ed out last time */
free(loopstruct.w);
loopstruct.w = nil;
loopstruct.nw = 0;
allwindows(alllooper, &loopstruct);
/*
* add a ref to all windows to keep safe windows accessed by X
* that would not otherwise have a ref to hold them up during
* the shenanigans. note this with globalincref so that any
* newly created windows start with an extra reference.
*/
allwindows(alllocker, (void*)1);
globalincref = 1;
/*
* Unlock the window running the X command.
* We'll need to lock and unlock each target window in turn.
*/
if(t && t->w)
winunlock(t->w);
for(i=0; i<loopstruct.nw; i++){
targ = &loopstruct.w[i]->body;
if(targ && targ->w)
winlock(targ->w, cp->cmdc);
cmdexec(targ, cp->cmd);
if(targ && targ->w)
winunlock(targ->w);
}
if(t && t->w)
winlock(t->w, cp->cmdc);
allwindows(alllocker, (void*)0);
globalincref = 0;
free(loopstruct.w);
loopstruct.w = nil;
--Glooping;
--nest;
}
void
nextmatch(File *f, String *r, long p, int sign)
{
if(rxcompile(r->r) == FALSE)
editerror("bad regexp in command address");
if(sign >= 0){
if(!rxexecute(f->curtext, nil, p, 0x7FFFFFFFL, &sel))
editerror("no match for regexp");
if(sel.r[0].q0==sel.r[0].q1 && sel.r[0].q0==p){
if(++p>f->nc)
p = 0;
if(!rxexecute(f->curtext, nil, p, 0x7FFFFFFFL, &sel))
editerror("address");
}
}else{
if(!rxbexecute(f->curtext, p, &sel))
editerror("no match for regexp");
if(sel.r[0].q0==sel.r[0].q1 && sel.r[0].q1==p){
if(--p<0)
p = f->nc;
if(!rxbexecute(f->curtext, p, &sel))
editerror("address");
}
}
}
File *matchfile(String*);
Address charaddr(long, Address, int);
Address lineaddr(long, Address, int);
Address
cmdaddress(Addr *ap, Address a, int sign)
{
File *f = a.f;
Address a1, a2;
do{
switch(ap->type){
case 'l':
case '#':
a = (*(ap->type=='#'?charaddr:lineaddr))(ap->num, a, sign);
break;
case '.':
mkaddr(&a, f);
break;
case '$':
a.r.q0 = a.r.q1 = f->nc;
break;
case '\'':
editerror("can't handle '");
// a.r = f->mark;
break;
case '?':
sign = -sign;
if(sign == 0)
sign = -1;
/* fall through */
case '/':
nextmatch(f, ap->re, sign>=0? a.r.q1 : a.r.q0, sign);
a.r = sel.r[0];
break;
case '"':
f = matchfile(ap->re);
mkaddr(&a, f);
break;
case '*':
a.r.q0 = 0, a.r.q1 = f->nc;
return a;
case ',':
case ';':
if(ap->left)
a1 = cmdaddress(ap->left, a, 0);
else
a1.f = a.f, a1.r.q0 = a1.r.q1 = 0;
if(ap->type == ';'){
f = a1.f;
a = a1;
f->curtext->q0 = a1.r.q0;
f->curtext->q1 = a1.r.q1;
}
if(ap->next)
a2 = cmdaddress(ap->next, a, 0);
else
a2.f = a.f, a2.r.q0 = a2.r.q1 = f->nc;
if(a1.f != a2.f)
editerror("addresses in different files");
a.f = a1.f, a.r.q0 = a1.r.q0, a.r.q1 = a2.r.q1;
if(a.r.q1 < a.r.q0)
editerror("addresses out of order");
return a;
case '+':
case '-':
sign = 1;
if(ap->type == '-')
sign = -1;
if(ap->next==0 || ap->next->type=='+' || ap->next->type=='-')
a = lineaddr(1L, a, sign);
break;
default:
error("cmdaddress");
return a;
}
}while(ap = ap->next); /* assign = */
return a;
}
struct Tofile{
File *f;
String *r;
};
void
alltofile(Window *w, void *v)
{
Text *t;
struct Tofile *tp;
tp = v;
if(tp->f != nil)
return;
if(w->isscratch || w->isdir)
return;
t = &w->body;
/* only use this window if it's the current window for the file */
if(t->file->curtext != t)
return;
// if(w->nopen[QWevent] > 0)
// return;
if(runeeq(tp->r->r, tp->r->n, t->file->name, t->file->nname))
tp->f = t->file;
}
File*
tofile(String *r)
{
struct Tofile t;
String rr;
rr.r = skipbl(r->r, r->n, &rr.n);
t.f = nil;
t.r = &rr;
allwindows(alltofile, &t);
if(t.f == nil)
editerror("no such file\"%S\"", rr.r);
return t.f;
}
void
allmatchfile(Window *w, void *v)
{
struct Tofile *tp;
Text *t;
tp = v;
if(w->isscratch || w->isdir)
return;
t = &w->body;
/* only use this window if it's the current window for the file */
if(t->file->curtext != t)
return;
// if(w->nopen[QWevent] > 0)
// return;
if(filematch(w->body.file, tp->r)){
if(tp->f != nil)
editerror("too many files match \"%S\"", tp->r->r);
tp->f = w->body.file;
}
}
File*
matchfile(String *r)
{
struct Tofile tf;
tf.f = nil;
tf.r = r;
allwindows(allmatchfile, &tf);
if(tf.f == nil)
editerror("no file matches \"%S\"", r->r);
return tf.f;
}
int
filematch(File *f, String *r)
{
char *buf;
Rune *rbuf;
Window *w;
int match, i, dirty;
Rangeset s;
/* compile expr first so if we get an error, we haven't allocated anything */
if(rxcompile(r->r) == FALSE)
editerror("bad regexp in file match");
buf = fbufalloc();
w = f->curtext->w;
/* same check for dirty as in settag, but we know ncache==0 */
dirty = !w->isdir && !w->isscratch && f->mod;
snprint(buf, BUFSIZE, "%c%c%c %.*S\n", " '"[dirty],
'+', " ."[curtext!=nil && curtext->file==f], f->nname, f->name);
rbuf = bytetorune(buf, &i);
fbuffree(buf);
match = rxexecute(nil, rbuf, 0, i, &s);
free(rbuf);
return match;
}
Address
charaddr(long l, Address addr, int sign)
{
if(sign == 0)
addr.r.q0 = addr.r.q1 = l;
else if(sign < 0)
addr.r.q1 = addr.r.q0 -= l;
else if(sign > 0)
addr.r.q0 = addr.r.q1 += l;
if(addr.r.q0<0 || addr.r.q1>addr.f->nc)
editerror("address out of range");
return addr;
}
Address
lineaddr(long l, Address addr, int sign)
{
int n;
int c;
File *f = addr.f;
Address a;
long p;
a.f = f;
if(sign >= 0){
if(l == 0){
if(sign==0 || addr.r.q1==0){
a.r.q0 = a.r.q1 = 0;
return a;
}
a.r.q0 = addr.r.q1;
p = addr.r.q1-1;
}else{
if(sign==0 || addr.r.q1==0){
p = 0;
n = 1;
}else{
p = addr.r.q1-1;
n = textreadc(f->curtext, p++)=='\n';
}
while(n < l){
if(p >= f->nc)
editerror("address out of range");
if(textreadc(f->curtext, p++) == '\n')
n++;
}
a.r.q0 = p;
}
while(p < f->nc && textreadc(f->curtext, p++)!='\n')
;
a.r.q1 = p;
}else{
p = addr.r.q0;
if(l == 0)
a.r.q1 = addr.r.q0;
else{
for(n = 0; n<l; ){ /* always runs once */
if(p == 0){
if(++n != l)
editerror("address out of range");
}else{
c = textreadc(f->curtext, p-1);
if(c != '\n' || ++n != l)
p--;
}
}
a.r.q1 = p;
if(p > 0)
p--;
}
while(p > 0 && textreadc(f->curtext, p-1)!='\n') /* lines start after a newline */
p--;
a.r.q0 = p;
}
return a;
}
struct Filecheck
{
File *f;
Rune *r;
int nr;
};
void
allfilecheck(Window *w, void *v)
{
struct Filecheck *fp;
File *f;
fp = v;
f = w->body.file;
if(w->body.file == fp->f)
return;
if(runeeq(fp->r, fp->nr, f->name, f->nname))
warning(nil, "warning: duplicate file name \"%.*S\"\n", fp->nr, fp->r);
}
Rune*
cmdname(File *f, String *str, int set)
{
Rune *r, *s;
int n;
struct Filecheck fc;
Runestr newname;
r = nil;
n = str->n;
s = str->r;
if(n == 0){
/* no name; use existing */
if(f->nname == 0)
return nil;
r = runemalloc(f->nname+1);
runemove(r, f->name, f->nname);
return r;
}
s = skipbl(s, n, &n);
if(n == 0)
goto Return;
if(s[0] == '/'){
r = runemalloc(n+1);
runemove(r, s, n);
}else{
newname = dirname(f->curtext, runestrdup(s), n);
n = newname.nr;
r = runemalloc(n+1); /* NUL terminate */
runemove(r, newname.r, n);
free(newname.r);
}
fc.f = f;
fc.r = r;
fc.nr = n;
allwindows(allfilecheck, &fc);
if(f->nname == 0)
set = TRUE;
Return:
if(set && !runeeq(r, n, f->name, f->nname)){
filemark(f);
f->mod = TRUE;
f->curtext->w->dirty = TRUE;
winsetname(f->curtext->w, r, n);
}
return r;
}