# include "../ingres.h" # include "../aux.h" # include "../unix.h" # include "../catalog.h" # include "../symbol.h" # include "../tree.h" # include "qrymod.h" /* ** PROTECT -- protection algorithm ** ** This module performs the INGRES protection algorithm, as ** presented in Stonebraker & Rubinstein, "The INGRES Protection ** System", with a few modifications. ** ** The basic algorithm is as follows: ** ** The algorithm is applied once to each variable used in the ** query. Each variable has an initial check performed to ** determine applicability -- if the current user owns the ** relation, or if the relation is specially marked as being ** "all access to everyone", then the algorithm is skipped, ** thereby having effectively no restriction. ** ** The set of all such variables is computed in 'protect', ** and then 'dopro' is called to process each of those. This ** is so the protection algorithm does not get applied recursively ** to constraints which define more than one variable. Notice ** that this could in itself be a protection violation, if it ** were acceptable to reference a relation you do not own in a ** PERMIT statement. ** ** The effective query mode for this variable is then computed. ** This is the same as the query mode of the whole query if ** the variable in question is the result variable, otherwise ** it is "retrieve" mode. ** ** The next step is to scan the query tree and create sets of ** domains referenced. Four sets are created: ** uset -- the set of domains updated (actually, ** referenced in the target list -- on a ** retrieve, this will be the set of domains ** retrieved to the user). ** rset -- the set of domains retrieved in some ** context other than the left hand side of ** an equal sign. ** aset -- the set of domains aggregated. This only ** includes domains aggregated with a simple ** aggregate (not an aggregate function) with ** no qualification, since it may be possible ** to come up with too much information other- ** wise. ** qset -- the set of domains retrieved for use in ** a qualification, but never stored. This ** includes domains in a qualification of an ** aggregate or aggregate function. ** For more details of domains in each of these sets, look at ** the routine 'makedset'. ** ** If we had a retrieve operation in the first place, we will ** then merge 'uset' into 'rset' and clear 'uset', so that ** now 'uset' only contains domains which are actually updated, ** and 'rset' contains all domains which are retrieved. ** ** Now that we know what is referenced, we can scan the protection ** catalog. We scan the entire catalog once for each variable ** mentioned in the query (except as already taken care of as ** described above). ** ** We must create a set of all operations on this variable which ** are not yet resolved, that is, for which no PERMIT statements ** which qualify have been issued. We store this set in the ** variable "noperm". As PERMIT statements are found, bits will ** be cleared. If the variable is not zero by the end of the ** scan of the protection catalog, then we reject the query, ** saying that we don't have permission -- giving us default ** to deny. ** ** For each tuple in the protection catalog for this relation, ** we call "proappl" to see if it applies to this query. This ** routine checks the user, terminal, time of day, and so forth ** (in fact, everything which is query-independent) and tells ** whether this tuple might apply. ** ** If the tuple passes this initial check, we then do the query- ** dependent checking. This amounts to calling "prochk" once ** for each operation (and domain set) in the query. What we ** get back is a set of operations which this tuple applies to. ** If zero, the tuple doesn't apply at all; otherwise, it ** applies to at least one operation. If it applies to some- ** thing, we call it "interesting". ** ** For "interesting" tuples, we now get the corresponding ** qualification (if one exists), and disjoin it to a set of ** protection constraints held in "pqual". Also, we mark ** any operations we found as having been done, by clearing ** bits in "noperm". ** ** When we have completed scanning the protection catalog, ** we check "noperm". If it is non-zero, then we have some ** operation for which a PERMIT statement has not been issued, ** and we issue an error message. If this variable is ok, ** then we go on and try the next variable. ** ** When all variables have been accounted for, we check to ** see if we have any qualifications collected from the ** protection algorithm. If so, we conjoin them to the ** query tree. ** ** Finally, we return the root of the modified tree. This ** tree is guaranteed to have no authorization violations, ** and may be run as a regular query. ** ** Parameters: ** root -- the root of the tree. ** ** Returns: ** The root of the modified and authorized tree. ** ** Side Effects: ** A possible non-local return on access violation. ** ** Defined Constants: ** none ** ** Defines: ** Proopmap -- a mapping from query modes to operation ** set bits in the protection catalog. ** protect -- the driver for the protection algorithm. ** dopro -- the guts of the algorithm. ** makedset -- the routine to make the domain sets. ** proappl -- to check if a protection tuple applies. ** prochk -- to check what operations it handles. ** pr_set -- debugging set print. ** ** Requires: ** Prodes -- an open descriptor for the protection ** catalog. ** access methods. ** gettree() -- to get the qualification for a particular ** PERMIT statement. ** ferror -- to signal an error on a protection ** violation. ** appqual() -- to append the protection qualification ** (conjunctively) to the query tree. ** Usercode -- set to the current user -- used to ** determine applicability of a particular ** protection tuple. ** Terminal -- set to the terminal id for the current ** terminal, blank if background. This is used ** similarly. ** localtime() -- system routine to convert a system ** date to a vector of day, month, day-of-week, ** etc. ** tree() -- to create an OR node. ** norml() -- to renormalize the tree. ** ** Called By: ** qrymod() ** ** Files: ** 'protect' relation -- the relation holding the ** protection information. ** 'violation' relation -- the relation into which ** protection violations are logged. ** Not supported in this version. ** 'tree' relation -- holds the query trees for the ** PERMIT statements. ** ** Trace Flags: ** 50 - 59 ** ** Diagnostics: ** 3500 -- Protection violation. ** ** Syserrs: ** protect: null tree ** The tree fetched from the 'tree' catalog ** was effectively null. ** protect: get ** A bad return from the 'get' on the protect ** catalog. ** ** History: ** 2/14/79 -- version 6.2 release. */ int Proopmap[MAXPROQM + 1] = { PRO_RETR, /* 0 -- mdRETTERM */ PRO_RETR, /* 1 -- mdRETR */ PRO_APP, /* 2 -- mdAPP */ PRO_REPL, /* 3 -- mdREPL */ PRO_DEL, /* 4 -- mdDEL */ }; extern struct descriptor Prodes; # ifndef xV7_UNIX extern char Terminal; # endif # ifdef xV7_UNIX extern char Terminal[3]; # endif extern QTREE *gettree(); QTREE *protect(root) QTREE *root; { QTREE *r; register int i; register int vn; int qmode; int varset; r = root; # ifdef xQTR1 tTfp(50, -1, "\n->PROTECT\n\n"); # endif varset = 0; /* ** Scan the range table and create a set of all variables ** which are 'interesting', that is, on which the protectin ** algorithm should be performed. */ for (vn = 0; vn < MAXVAR + 1; vn++) { if (!Rangev[vn].rused) continue; /* if owner, accept any query */ if (bequal(Rangev[vn].rowner, Usercode, 2)) continue; /* check for "no restriction" bit asserted (= clear) */ if ((Rangev[vn].rstat & S_PROTALL) == 0) continue; if ((Rangev[vn].rstat & S_PROTRET) == 0 && (Qmode == mdRETR || Qmode == mdRET_UNI)) continue; varset |= 1 << vn; } /* ** For each variable specified in varset (that is, for each ** variable in the initial query), do the real algorithm. */ for (vn = 0; vn < MAXVAR + 1; vn++) { if ((varset & (1 << vn)) == 0) continue; # ifdef xQTR1 if (tTf(50, 1)) printf("\nvn=%d: %.12s\n", vn, Rangev[vn].relid); # endif /* ** Determine the query mode for this variable. This ** is not the query mode of the original query, ** unless the variable is the result variable. */ qmode = Qmode; if (vn != Resultvar || qmode == mdRET_UNI) qmode = mdRETTERM; # ifdef xQTR3 if (qmode == 1 || qmode > 4 || qmode < 0) syserr("protect: bad qmode %d", qmode); # endif /* do the interesting part of the algorithm */ dopro(vn, r, qmode, NULL); } /* return the (authorized) tree */ # ifdef xQTR1 if (tTf(50, 15)) treepr(r, "PROTECT->"); # endif return (r); } /* ** DOPRO -- actually do the protection algorithm ** ** This is the guts of it, broken off because it must be called ** recursively on aggregates. The algorithm is as discussed ** in the module header. ** ** Parameters: ** varno -- the variable number of interest. ** root -- the root of the tree to modify. ** qmode -- the effective query mode for this relation. ** byset -- if non-NULL, a set of domains passed back ** which gets bound out of the aggregate func, ** in other words, the by list. ** ** Returns: ** none ** ** Side Effects: ** The tree pointed at by 'root' gets modified. ** Quite possibly 'Rangev' and 'Remap' get clobbered. ** ** Requires: ** makedset -- to create domain usage sets, also ** detects aggregates and calls 'dopro' ** recursively. ** proappl -- to do query-independent checking for ** protection tuple applicability. ** prochk -- to do query-dependent checking for protection ** tuple applicability. ** appqual -- to add the protection qualification. ** norml -- to renormalize the tree, what with all those ** messy OR nodes and all. ** trimqlend -- to adjust the tree for norml. ** ** Called By: ** protect ** makedset -- on aggregates and aggregate functions. ** ** Trace Flags: ** 51 */ dopro(varno, root, qmode, byset) int varno; QTREE *root; int qmode; int byset[8]; { int qset[8]; int uset[8]; int aset[8]; int rset[8]; int zeros[8]; QTREE *p; QTREE *pqual; register int i; register int vn; register QTREE *t; int noperm; int noqual; struct protect prokey, protup; struct tup_id lotid, hitid; struct qvect { QTREE *q_qual; int q_mode; }; struct qvect quals[4]; int j; QTREE *trimqlend(), *tree(), *norml(); t = root; vn = varno; /* create domain usage sets */ for (i = 0; i < 8; i++) { zeros[i] = uset[i] = rset[i] = qset[i] = aset[i] = 0; if (byset != NULL) byset[i] = 0; } /* ** Create domain usage set for target list side. There are ** two general cases: this is the root of the tree, or this ** is the head of an aggregate. */ switch (t->sym.type) { case AGHEAD: /* ** An aggregate head falls into two classes: simple ** aggregate and aggregate function. In an aggregate ** function, care must be taken to bind the variables ** in the by-list outside of the aggregate. We use ** 'rset' as a temporary here. */ if (t->left->sym.type == BYHEAD) { /* make by-list set */ makedset(vn, t->left->left, NULL, rset, aset, qset); /* merge by-list set into qualification set */ for (i = 0; i < 8; i++) { if (byset != NULL) byset[i] |= rset[i]; qset[i] |= rset[i]; rset[i] = 0; } /* make aggregate list set */ makedset(vn, t->left->right->right, NULL, rset, aset, qset); } else { /* simple aggregate */ # ifdef xQTR3 if (t->left->sym.type != AOP) syserr("dopro: AGHEAD->left %d", t->left->sym.type); # endif /* check for qualification */ if (t->right->sym.type == QLEND) { /* simple, unqualified aggregate */ makedset(vn, t->left->right, NULL, aset, aset, qset); } else { # ifdef xQTR3 if (t->right->sym.type != AND) syserr("dopro: AND=%d", t->right->sym.type); # endif makedset(vn, t->left->right, NULL, rset, aset, qset); } } break; case ROOT: makedset(vn, t->left, uset, rset, aset, qset); break; } /* scan qualifcation */ makedset(vn, t->right, NULL, &qset, &aset, &qset); /* if retrieval, drop the 'update' set */ if (qmode == mdRETTERM) { for (i = 0; i < 8; i++) { uset[i] = 0; } } # ifdef xQTR1 if (tTf(51, 2)) { printf("qmode %d\n", qmode); pr_set(uset, "uset"); pr_set(rset, "rset"); pr_set(aset, "aset"); pr_set(qset, "qset"); } # endif /* create a bit map of all referenced operations */ noperm = 0; if (!bequal(uset, zeros, sizeof zeros)) noperm |= Proopmap[qmode]; if (!bequal(&rset, &zeros, sizeof zeros)) noperm |= PRO_RETR; if (!bequal(&aset, &zeros, sizeof zeros)) noperm |= PRO_AGGR; if (!bequal(&qset, &zeros, sizeof zeros)) noperm |= PRO_TEST; /* if no oper, then the query was probably just aggr's */ if (noperm == 0) return; /* initialize qualification portion */ for (i = 0; i < 4; ) quals[i++].q_qual = NULL; noqual = FALSE; /* check the protection catalog */ opencatalog("protect", 0); setkey(&Prodes, &prokey, Rangev[vn].relid, PRORELID); setkey(&Prodes, &prokey, Rangev[vn].rowner, PRORELOWN); find(&Prodes, EXACTKEY, &lotid, &hitid, &prokey); while ((i = get(&Prodes, &lotid, &hitid, &protup, TRUE)) == 0) { if (kcompare(&Prodes, &prokey, &protup) != 0) continue; # ifdef xQTR2 if (tTf(51, 4)) { printf("PROTECT: "); printup(&Prodes, &protup); } # endif /* check if this is the correct user, terminal, etc */ if (!proappl(&protup)) continue; /* alright, let's check the operation */ i = 0; if (qmode != mdRETTERM) i = quals[0].q_mode = prochk(Proopmap[qmode], &uset, &protup); i |= quals[1].q_mode = prochk(PRO_RETR, &rset, &protup); i |= quals[2].q_mode = prochk(PRO_AGGR, &aset, &protup); i |= quals[3].q_mode = prochk(PRO_TEST, &qset, &protup); # ifdef xQTR2 if (tTf(51, 5)) printf("Satisfies operations %o\n", i); # endif /* see if this tuple is "interesting" */ if (i == 0) continue; /* it is! get the qualification (if any) */ if (protup.protree >= 0) { p = gettree(Rangev[vn].relid, Rangev[vn].rowner, mdPROT, protup.protree, FALSE); # ifdef xQTR2 if (tTf(51, 6)) treepr(p, "Protection Clause"); # endif p = trimqlend(p->right); # ifdef xQTR3 /* check for a non-null qualification */ if (p == NULL) syserr("protect: null tree"); # endif /* translate to the interesting variable */ j = protup.proresvar; if (Remap[j] >= 0) j = Remap[j]; mergevar(j, varno, p); /* disjoin the protection qual to real qual */ for (j = 0; j < 4; j++) { if (quals[j].q_mode == 0) continue; if (quals[j].q_qual == NULL) quals[j].q_qual = p; else quals[j].q_qual = tree(quals[j].q_qual, p, OR, 0); } } else noqual = TRUE; /* mark this operation as having been handled */ noperm &= ~i; } /* test 'get' return code */ if (i < 0) syserr("protect: get"); # ifdef xQTR1 if (tTf(51, 12)) printf("No perm on %o\n", noperm); # endif /* see if no tuples applied for some operation */ if (noperm != 0) ferror(3500, Qmode, vn, 0); /* see if we want to modify the query at all */ if (!noqual) { /* conjoin the qualification */ pqual = NULL; for (i = 0; i < 4; i++) if (quals[i].q_qual != NULL) if (pqual == NULL) pqual = quals[i].q_qual; else pqual = tree(pqual, quals[i].q_qual, AND, 0); pqual = tree(pqual, tree(NULL, NULL, QLEND, 0), AND, 0); appqual(pqual, t); /* normalize the tree */ t->right = norml(trimqlend(t->right)); } } /* ** MAKEDSET -- make domain reference sets ** ** This routine creates some sets which reflect the usage of ** domains for a particular variable. ** ** The interesting nodes are 'case' labels in the large ** switch statement which comprises most of the code. To ** describe briefly: ** ** VAR nodes are easy: if they are for the current variable, ** set the bit corresponding to the domain in the ** 'retrieval' set. They can have no descendents, ** so just return. ** RESDOM nodes are also easy: they can be handled the same, ** but the bit is set in the 'update' set instead. ** AGHEAD nodes signal the beginning of an aggregate or ** aggregate function. In this case, we scan the ** qualification first (noting that RESDOM and VAR ** nodes are processed as 'qualification' sets ** instead of 'retrieval' or 'update' sets). Then, ** if the aggregate has a WHERE clause or a BY list, ** we treat it as a retrieve; otherwise, we call our- ** selves recursively treating VAR nodes as 'aggregate' ** types rather than 'retrieve' types. ** BYHEAD nodes signal the beginning of a BY list. The left ** subtree (the actual BY-list) is processed with ** RESDOM nodes ignored (since they are pseudo-domains ** anyhow) and VAR nodes mapped into the 'qualification' ** set. Then we check the right subtree (which better ** begin with an AOP node!) and continue processing. ** AOP nodes must have a null left subtree, so we just drop ** to the right subtree and iterate. Notice that we ** do NOT map VAR nodes into the 'aggregate' set for ** this node, since this has already been done by the ** AGHEAD node; also, this aggregate might be counted ** as a retrieve operation instead of an aggregate ** operation (as far as the protection system is con- ** cerned) -- this has been handled by the AGHEAD ** node. ** All other nodes are processed recursively along both edges. ** ** Parameters: ** vn -- the variable number that we are currently ** interested in. ** tree -- the root of the tree to scan. Notice that this ** will in general be only one half of the tree -- ** makedset will be called once for the target ** list and once for the qualification, with ** different sets for the following parameters. ** uset -- adjusted to be the set of all domains ** updated. ** rset -- adjusted to be the set of all domains ** retrieved implicitly, that is, on the right- ** hand-side of an assignment operator. ** aset -- adjusted to be the set of all domains ** aggregated. Notice that this set is not ** adjusted explicitly, but rather is passed ** to recursive incarnations of this routine ** as 'rset'. ** qset -- adjusted to be the set of domains retrieved ** implicitly in a qualification. Like 'aset', ** this is passed as 'rset' to recursive ** incarnations. ** ** Returns: ** none ** ** Side Effects: ** none ** ** Requires: ** lsetbit() ** ** Called By: ** protect() -- in two places. ** ** Trace Flags: ** 53 ** ** Diagnostics: ** none ** ** Syserrs: ** makedset: AOP->left ** A truly obscure error message, this means ** that the left subtree of an AOP node was ** not null. ** makedset: AOP %d ** Another goodie, this one means that the ** right branch of a BYHEAD node was not an ** AOP node as required; the %d is the type ** it actually was. */ makedset(vn, tree, uset, rset, aset, qset) int vn; QTREE *tree; int uset[8]; int rset[8]; int aset[8]; int qset[8]; { register QTREE *t; register int i; int byset[8]; t = tree; # ifdef xQTR1 if (tTf(53, 0)) { printf("->makedset\n"); pr_set(uset, "uset"); pr_set(rset, "rset"); pr_set(aset, "aset"); pr_set(qset, "qset"); } # endif while (t != NULL) { switch (t->sym.type) { case VAR: if (((struct qt_var *)t)->varno == vn) lsetbit(((struct qt_var *)t)->attno, rset); break; case AGHEAD: /* do protection on qualification */ dopro(vn, t, -1, byset); /* merge by-list set into qualification set */ for (i = 0; i < 8; i++) qset[i] |= byset[i]; break; case BYHEAD: case AOP: syserr("makedset: node %d", t->sym.type); case RESDOM: if (((struct qt_res *)t)->resno == 0) { /* tid -- ignore right subtree (and this node) */ t = t->left; continue; } if (uset != NULL) lsetbit(((struct qt_res *)t)->resno, uset); /* explicit fall-through to "default" case */ default: /* handle left subtree (recursively) */ makedset(vn, t->left, uset, rset, aset, qset); /* handle right subtree (iteratively) */ t = t->right; continue; } break; } # ifdef xQTR1 if (tTf(53, 15)) { printf("makedset->\n"); pr_set(uset, "uset"); pr_set(rset, "rset"); pr_set(aset, "aset"); pr_set(qset, "qset"); } # endif return; } /* ** PROAPPL -- check for protection tuple applicable ** ** A given protection catalog tuple is checked in a query- ** independent way for applicability. ** ** This routine checks such environmental constraints as the ** user, the terminal, and the time of day. The code is ** fairly straightforward, just take a look. ** ** One note: the user and terminal codes contained in the ** protection catalog are blank to mean 'any value' of the ** corresponding field. ** ** Parameters: ** protup -- the protection tuple to compare against. ** ** Returns: ** TRUE -- this tuple applies to the current environment. ** FALSE -- this tuple does not apply. ** ** Side Effects: ** none (unless you include trashing the static vector ** returned by localtime). ** ** Requires: ** localtime() ** Usercode -- the code for the current user. ** Terminal -- the result of a 'ttyn()' call, that is, ** the current terminal id. This should be ** blank if running in background. ** ** Called By: ** protect() ** ** Trace Flags: ** 54 ** ** Diagnostics: ** none ** ** Syserrs: ** none ** ** History: ** 8/21/78 (eric) -- written. */ proappl(protup) struct protect *protup; { register struct protect *p; int tvect[2]; register int *tt; extern int *localtime(); register int mtime; p = protup; /* check for correct user [insert clique code here] */ if (!bequal(" ", p->prouser, 2)) { if (!bequal(p->prouser, Usercode, 2)) return (FALSE); } /* check for correct terminal */ # ifndef xV7_UNIX if (p->proterm != ' ') { if (p->proterm != Terminal) return (FALSE); } # endif # ifdef xV7_UNIX if (p->proterm[0] != ' ') { if (!bequal(p->proterm, Terminal, 2)) return (FALSE); } # endif /* check for correct time of day & week */ time(tvect); tt = localtime(tvect); mtime = tt[2] * 60 + tt[1]; if (p->protodbgn > mtime || p->protodend < mtime) return (FALSE); if (p->prodowbgn > tt[6] || p->prodowend < tt[6]) return (FALSE); /* hasn't failed yet -- I guess it's ok */ return (TRUE); } /* ** PROCHK -- query-dependent protection tuple check ** ** This routine does the query-dependent part of checking ** the validity of a protection tuple. Unlike proappl, ** which looked at aspects of the environment but not the ** query being run, this routine assumes that the environ- ** ment is ok, and checks that if it applies to this tuple. ** ** Two things are checked. The first is if this tuple applies ** to the operation in question (passed as 'inbit'). The ** second is if the set of domains in the tuple contains the ** set of domains in the query. If either of these fail, ** the return is zero. Otherwise the return is the operation ** bit. In otherwise, the return is the operation to which ** this tuple applies (if any). ** ** As a special check, the domain set is checked for all ** zero. If so, no domains have been referenced for this ** operation at all, and we return zero. In other words, this ** tuple might apply to this operation, but since we don't ** use the operation anyhow we will ignore it. It is important ** to handle things in this way so that the qualification for ** this tuple doesn't get appended if the variable is not ** used in a particular context. ** ** Parameters: ** inbit -- the bit describing the operation to be ** checked. Note that only one bit should ** be set in this word, although this is ** not checked. ** domset -- the set of domains actually referenced ** in this query for the operation described ** by 'inbit'. ** protup -- the tuple in question. ** ** Returns: ** The operation (if any) to which this tuple applies. ** ** Side Effects: ** none ** ** Called By: ** protect() -- in four places. ** ** Trace Flags: ** 55 ** ** Diagnostics: ** none ** ** Syserrs: ** none ** ** History: ** 8/21/78 (eric) -- written. */ prochk(inbit, domset, protup) int inbit; int domset[8]; struct protect *protup; { register struct protect *p; register int *d; register int i; p = protup; d = domset; # ifdef xQTR1 if (tTf(55, 0)) { printf("->prochk, inbit=%o, proopset=%o\n", inbit, p->proopset); pr_set(d, "domset"); pr_set(p->prodomset, "prodomset"); } # endif /* check for null domain set, if so return zero */ for (i = 0; i < 8; i++) if (d[i] != 0) break; if (i >= 8) { # ifdef xQTR2 tTfp(55, 15, "prochk-> null set\n"); # endif return (0); } /* see if this tuple applies to this operation */ if ((inbit & p->proopset) == 0) { # ifdef xQTR2 tTfp(55, 15, "prochk-> no op\n"); # endif return (0); } /* check if domains are a subset */ for (i = 0; i < 8; i++) { if ((d[i] & ~p->prodomset[i]) != 0) { /* failure */ # ifdef xQTR2 tTfp(55, 15, "prochk-> not subset\n"); # endif return (0); } } /* this is hereby an "interesting" tuple */ # ifdef xQTR2 if (tTf(55, 15)) printf("prochk-> %d\n", inbit); # endif return (inbit); } # ifdef xQTR1 /* ** PR_SET -- print set for debugging ** ** This routine prints a 128-bit set for debugging. ** ** Parameters: ** xset -- the set to convert. ** labl -- a label to print before the set. ** ** Returns: ** a pointer to the converted string. ** ** Side Effects: ** none ** ** History: ** 8/22/78 (eric) -- written. */ pr_set(xset, labl) int xset[8]; char *labl; { register int *x; register int i; register long *y; printf("\t%s: ", labl); y = (long *) (x = xset); if (x == NULL) { printf("\n"); return; } for (i = 7; i >= 0; i--) printf("%o/", x[i]); printf(" <> "); for (i = 0; i < 4; i++) printf("/%s", locv(y[i])); printf("\n"); } # endif