/* Direct.c */

/*
 *      This is part of ODS2 written by Paul Nankervis,
 *      email address:  Paulnank@au1.ibm.com
 *
 *      ODS2 is distributed freely for all members of the
 *      VMS community to use. However all derived works
 *      must maintain comments in their source to acknowledge
 *      the contributions of the original author and
 *      subsequent contributors.   This is free software; no
 *      warranty is offered,  and while we believe it to be useful,
 *      you use it at your own risk.
 */

/*  This module does all directory file handling - mostly
 *  lookups of filenames in directory files...
 */

#if !defined( DEBUG ) && defined( DEBUG_DIRECT )
#define DEBUG DEBUG_DIRECT
#else
#ifndef DEBUG
#define DEBUG 0
#endif
#endif

#define DEBUGx on

#include <ctype.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "access.h"
#include "descrip.h"
#include "direct.h"
#include "fibdef.h"
#include "ods2.h"
#include "ssdef.h"
#include "stsdef.h"
#include "sysmsg.h"

#define BLOCKSIZE 512
#define MAXREC (BLOCKSIZE - 2)

/* Some statistical counters... */

static int direct_lookups = 0;
static int direct_searches = 0;
static int direct_deletes = 0;
static int direct_inserts = 0;
static int direct_splits = 0;
static int direct_checks = 0;
static int direct_matches = 0;

/************************************************************** direct_show() */

/* direct_show - to print directory statistics */

void direct_show( void ) {
    printf( "DIRECT_SHOW Lookups: %d ", direct_lookups );
    printf( "Searches: %d ", direct_searches );
    printf( "Deletes: %d ", direct_deletes );
    printf( "Inserts: %d ", direct_inserts );
    printf( "Splits: %d\n", direct_splits );
}

/*************************************************************** name_check() */

/* name_check() - take a name specification and return name length without
           the version number, an integer version number, and a wildcard flag */

static vmscond_t name_check( char *str, int len, int *retlen,
                             int *retver, int *wildflag ) {
    int wildcard = FALSE;
    char *name_start = str;
    register int dots = 0;
    register char *name = name_start;
    register char *name_end = name + len;
    direct_checks++;

    /* Go through the specification checking for illegal characters */

    while (name < name_end) {
        register char ch = *name++;
        if (ch == '.') {
            if ((name - name_start) > 40)
                return SS$_BADFILENAME;
            name_start = name;
            if (dots++ > 1)
                break;
        } else {
            if (ch == ';') {
                break;
            } else {
                if (ch == '*' || ch == '%') {
                    wildcard = TRUE;
                } else {
                    if (ch == '[' || ch == ']' || ch == ':' ||
                        !isprint(ch))
                        return SS$_BADFILENAME;
                }
            }
        }
    }
    if ((name - name_start) > 40)
        return SS$_BADFILENAME;

    /* Return the name length and start checking the version */

    *retlen = (int)(name - str - 1);
    dots = 0;
    if (name < name_end) {
        register char ch = *name;
        if (ch == '*') {
            if (++name < name_end)
                return SS$_BADFILENAME;
            dots = 32768;       /* Wildcard representation of version! */
            wildcard = TRUE;
        } else {
            register int sign = 1;
            if (ch == '-') {
                name++;
                sign = -1;
            }
            while (name < name_end) {
                ch = *name++;
                if (!isdigit(ch))
                    return SS$_BADFILENAME;
                dots = dots * 10 + (ch - '0');
            }
            dots *= sign;
        }
    }
    *retver = dots;
    *wildflag = wildcard;
    return SS$_NORMAL;
}

/*************************************************************** name_match() */

#define MAT_LT 0
#define MAT_EQ 1
#define MAT_GT 2
#define MAT_NE 3

/* name_match() - compare a name specification with a directory entry
               and determine if there is a match, too big, too small... */

int name_match(char *spec, int spec_len, char *dirent, int dirent_len) {
    int percent = MAT_GT;
    register char *name = spec, *entry = dirent;
    register char *name_end = name + spec_len, *entry_end = entry + dirent_len;
    direct_matches++;

    /* See how much name matches without wildcards... */

    while (name < name_end && entry < entry_end) {
        register char sch = *name;
        if (sch != '*') {
            register char ech = *entry;
            if (sch != ech) {
              if (toupper(sch) != toupper(ech)) {
                if (sch == '%') {
                  percent = MAT_NE;
                } else {
                  break;
                }
              }
            }
        } else {
            break;
        }
        name++;
        entry++;
    }

    /* Mismatch - return result unless wildcard... */

    if (name >= name_end) {
        if (entry >= entry_end) {
            return MAT_EQ;
        } else {
            return percent;
        }
    } else {
        /* See if we can find a match with wildcards */

        if (*name != '*') {
            if (percent == MAT_NE)
                return MAT_NE;
            if (entry < entry_end)
                if (toupper(*entry) > toupper(*name))
                    return MAT_GT;
            return MAT_LT;
        }
        /* Strip out wildcard(s) - if end then we match! */

        do {
            name++;
        } while (name < name_end && *name == '*');
        if (name >= name_end)
            return MAT_EQ;

        /* Proceed to examine the specification past the wildcard... */

        while (name < name_end) {
            register int offset = 1;
            register char fch = toupper(*name++);

            /* See if can can find a match for the first character... */

            if (fch != '%') {
                while (entry < entry_end) {
                    if (toupper(*entry) != fch) {
                        entry++;
                    } else {
                        break;
                    }
                }
            }
            /* Give up if we can't find that one lousy character... */

            if (entry >= entry_end)
                return MAT_NE;
            entry++;

            /* See how much of the rest we can match... */

            while (name < name_end && entry < entry_end) {
                register char sch = *name, ech;
                if (sch == '*')
                    break;
                if (sch != (ech = *entry))
                    if (toupper(sch) != toupper(ech))
                        if (sch != '%')
                            break;
                name++;
                entry++;
                offset++;
            }

            /* If matching died because of a wildcard we are OK... */

            if (name < name_end && *name == '*') {
                do {
                    name++;
                } while (name < name_end && *name == '*');
                if (name >= name_end)
                    return MAT_EQ;

                /* Otherwise we finished OK or we need to try again... */

            } else {
                if (name >= name_end && entry >= entry_end)
                    return MAT_EQ;
                name -= offset;
                entry -= (size_t)offset - 1;
            }
        }
    }

    /* No more specification - match depends on remainder of entry... */

    if (entry < entry_end)
        return MAT_NE;
    return MAT_EQ;
}

/*************************************************************** insert_ent() */

/* insert_ent() - procedure to add a directory entry at record dr entry de */

vmscond_t insert_ent( struct FCB * fcb, uint32_t eofblk, uint32_t curblk,
                      struct VIOC * vioc, char *buffer,
                      struct dir$r_rec * dr, struct dir$r_ent * de,
                      char *filename, uint32_t filelen,
                      uint32_t version, struct fiddef * fid,
                      uint16_t *reslen, struct dsc$descriptor *resdsc ) {
    register vmscond_t sts = SS$_NORMAL;
    register size_t inuse = 0;
    register uint16_t addlen;
    uint16_t verlimit;
    size_t avail = 0, len;
    char *p, vbuf[sizeof(";65535")];

    verlimit = version >> 16;
    version &= 0xffff;

    /* Return result */

    if( resdsc != NULL && resdsc->dsc$w_length != 0 ) {
        avail = resdsc->dsc$w_length;
        p = resdsc->dsc$a_pointer;
        len = filelen;
        if( len > avail )
            len = avail;
        memcpy( p, filename, len );
        avail -= len;
        p += len;
        len = snprintf( vbuf, sizeof( vbuf ), ";%u", version );
        if( len > avail )
            len = avail;
        memcpy( p, vbuf, len );
        p += len;
        *reslen = (unsigned short)(p - resdsc->dsc$a_pointer);
    }

    /* Compute space required... */

    addlen = sizeof(struct dir$r_ent);
    direct_inserts++;
    if (de == NULL)
        addlen += (((size_t)filelen + 1) & ~1) + offsetof(struct dir$r_rec, dir$t_name);

    /* Compute block space in use ... */

    {
        int invalid_dir = TRUE;
        while (TRUE) {
            register size_t reclen;
            register struct dir$r_rec *nr;

            nr = (struct dir$r_rec *) (buffer + inuse);

            if( dr == nr )
                invalid_dir = FALSE;
            reclen = F11WORD(nr->dir$w_size);
            if (reclen == 0xffff) /* End of data marker */
                break;
            reclen += 2;
            inuse += reclen;
            reclen -= (((size_t)nr->dir$b_namecount + 1) & ~1) + offsetof(struct dir$r_rec, dir$t_name);
            if (inuse > MAXREC || (inuse & 1) || reclen <= 0 ||
                reclen % sizeof(struct dir$r_ent) != 0) {
                deaccesschunk( vioc, 0, 0, FALSE );
                return SS$_BADIRECTORY;
            }
        }

        if (invalid_dir) {
            printf("BUGCHECK invalid dir\n");
            exit(EXIT_FAILURE);
        }
        if (de != NULL) {
            if (F11WORD(dr->dir$w_size) > MAXREC ||
                (char *) de < dr->dir$t_name + dr->dir$b_namecount ||
                (char *) de > (char *) dr + F11WORD(dr->dir$w_size) + 2) {
                printf("BUGCHECK invalid de\n");
                exit(EXIT_FAILURE);
            }
        }
    }

    /* If not enough space free extend the directory... */

    if (addlen > MAXREC - inuse) {
        register struct dir$r_rec *nr;
        unsigned keep_new = 0;
        char *newbuf;
        struct VIOC *newvioc;
        uint32_t newblk = eofblk + 1;

        direct_splits++;
#if DEBUG
        printf( "Splitting directory record... %s", dr->dir$t_name );
        if( de != NULL )
            printf( " %u,%u,%u,%u\n", de->dir$w_fid.fid$w_num,
                de->dir$w_fid.fid$w_seq, de->dir$w_fid.fid$b_rvn, de->dir$w_fid.fid$b_nmx );
        else
            putchar( '\n' );
#endif
        if( newblk > fcb->hiblock ) {
#if DEBUG
            printf("Extending directory during split\n");
#endif
            if( $FAILS(sts = update_extend( fcb, newblk - fcb->hiblock, 0 )) ) {
#if DEBUG
                printf( "Failed to extend a directory during a split\n" );
#endif
                return $SETLEVEL(SS$_DIRALLOC, SEVERE);
            }
        }

        if( fcb->highwater != 0 && newblk > fcb->highwater -1 ) {
            fcb->highwater = newblk + 1;
            fcb->head->fh2$l_highwater = F11LONG( newblk + 1 );
        }

        if(  $SUCCESSFUL(sts = accesschunk( fcb, newblk, &newvioc, &newbuf, NULL, 1 )) ) {
            while( newblk > curblk + 1 ) {
                char *frombuf;
                struct VIOC *fromvioc;

                if( $FAILED(sts = accesschunk( fcb, newblk - 1, &fromvioc, &frombuf, NULL, 1 )) )
                    break;
                memcpy( newbuf, frombuf, BLOCKSIZE );
                sts = deaccesschunk( newvioc, newblk, 1, TRUE);
                newvioc = fromvioc;
                newbuf = frombuf;
                newblk--;
                if( $FAILED(sts) )
                    break;
            }
        } else {
            newvioc = NULL;
        }
        if( $FAILED(sts) ) {
            if( newvioc != NULL )
                deaccesschunk( newvioc, 0, 0, FALSE);
            deaccesschunk( vioc, 0, 0, FALSE );
            return sts;
        }
        memset( newbuf, 0, BLOCKSIZE );
        eofblk++;
        fcb->head->fh2$w_recattr.fat$l_efblk = F11SWAP( eofblk + 1 );

        /* First find where the next record is... */

        nr = dr;
        if (F11WORD(nr->dir$w_size) <= MAXREC)
            nr = (struct dir$r_rec *) ((char *) nr + F11WORD(nr->dir$w_size) + 2);

        /* Can we split between records? */

        if (de == NULL || (char *) dr != buffer ||
            F11WORD(nr->dir$w_size) <= MAXREC) {
            register struct dir$r_rec *sp;

            sp = dr;
            if ((char *) dr == buffer && de != NULL)
                sp = nr;
            memcpy(newbuf, sp, ((buffer + BLOCKSIZE) - (char *) sp));
            memset(sp, 0, ((buffer + BLOCKSIZE) - (char *) sp));
            sp->dir$w_size = F11WORD(0xffff);
            if (sp == dr &&
                (de != NULL || (char *) sp >= buffer + MAXREC - addlen)) {
                if (de != NULL)
                    de = (struct dir$r_ent *)
                        (newbuf + ((char *) de - (char *) sp));
                dr = (struct dir$r_rec *) (newbuf + ((char *) dr - (char *) sp));
                keep_new = 1;
            }
            /* OK, we have to split the record then.. */

        } else {
            register uint32_t reclen;
            register struct dir$r_rec *nbr;

            reclen = ( (((size_t)dr->dir$b_namecount + 1) & ~1) +
                       offsetof(struct dir$r_rec, dir$t_name) );
            nbr = (struct dir$r_rec *) newbuf;
#if DEBUG
            printf("Super split %s %u,%u,%u,%u\n", dr->dir$t_name, de->dir$w_fid.fid$w_num,
               de->dir$w_fid.fid$w_seq, de->dir$w_fid.fid$b_rvn, de->dir$w_fid.fid$b_nmx);
#endif
            memcpy(newbuf, buffer, reclen);
            memcpy(newbuf + reclen, de, ((char *) nr - (char *) de) + 2);
            nbr->dir$w_size = F11WORD(reclen + ((char *) nr - (char *) de) - 2);

            memset((char *) de + 2, 0, ((char *) nr - (char *) de));
            ((struct dir$r_rec *) de)->dir$w_size = F11WORD(0xffff);
            dr->dir$w_size = F11WORD(((char *) de - (char *) dr) - 2);
            if ((char *) de >= (char *) nr) {
                dr = (struct dir$r_rec *) newbuf;
                de = (struct dir$r_ent *) (newbuf + reclen);
                keep_new = 1;
            }
        }

        /* Need to decide which buffer we are going to keep (to write to) */

        if (keep_new) {
            sts = deaccesschunk( vioc, curblk, 1, TRUE);
            curblk = newblk;
            vioc = newvioc;
            buffer = newbuf;
        } else {
            sts = deaccesschunk( newvioc, newblk, 1, TRUE );
        }
        if( $FAILED(sts) )
            printf("I/O error splitting directory record: %s\n", getmsg(sts, 0));
    }
    /* After that we can just add the record or entry as appropriate... */

    if (de == NULL) {
        memmove((char *) dr + addlen, dr,
                BLOCKSIZE - (((char *) dr + addlen) - buffer));
        dr->dir$w_size = F11WORD(addlen - 2);
        dr->dir$w_verlimit = verlimit;
        dr->dir$b_flags = 0;
        dr->dir$b_namecount = filelen;
        memcpy(dr->dir$t_name, filename, filelen);
        de = (struct dir$r_ent *)
                 ((char *) dr + (addlen - sizeof(struct dir$r_ent)));
    } else {
        dr->dir$w_size = F11WORD(F11WORD(dr->dir$w_size) + addlen);
        memmove((char *) de + addlen, de,
                BLOCKSIZE - (((char *) de + addlen) - buffer));
    }

    /* Write the entry values are we are done! */

    de->dir$w_version = F11WORD(version);
    fid_copy( &de->dir$w_fid, fid, 0);
    return deaccesschunk( vioc, curblk, 1, TRUE );
}

/*************************************************************** delete_ent() */

/* delete_ent() - delete a directory entry */

vmscond_t delete_ent( struct FCB * fcb, struct VIOC * vioc, uint32_t curblk,
                      struct dir$r_rec * dr, struct dir$r_ent * de,
                      char *buffer, uint32_t eofblk ) {
    vmscond_t sts = SS$_NORMAL;
    uint32_t ent;

    direct_deletes++;
    ent = ((size_t)F11WORD(dr->dir$w_size) + 2 - offsetof(struct dir$r_rec, dir$t_name)
           - (((size_t)dr->dir$b_namecount + 1) & ~1)) / sizeof(struct dir$r_ent);
    if (ent > 1) {
        char *ne;

        ne = (char *) de + sizeof(struct dir$r_ent);
        memmove( de, ne, BLOCKSIZE - (ne - buffer) );
        dr->dir$w_size = F11WORD(F11WORD(dr->dir$w_size) - sizeof(struct dir$r_ent));
    } else {
        char *nr;

        nr = (char *) dr + F11WORD(dr->dir$w_size) + 2;
        if( eofblk == 1 || (char *) dr > buffer ||
            (nr <= buffer + MAXREC && (unsigned short) *nr < BLOCKSIZE) ) {
            memmove(dr, nr, BLOCKSIZE - (nr - buffer));
        } else {
            while (curblk < eofblk) {
                char *nxtbuffer;
                struct VIOC *nxtvioc;

                sts = accesschunk(fcb, curblk + 1, &nxtvioc, &nxtbuffer, NULL, 1);
                if( $FAILED(sts) )
                    break;
                memcpy(buffer, nxtbuffer, BLOCKSIZE);
                if( $FAILS(sts = deaccesschunk(vioc, curblk++, 1, TRUE)) )
                    break;
                buffer = nxtbuffer;
                vioc = nxtvioc;
            }
            if(  $SUCCESSFUL(sts) ) {
                fcb->head->fh2$w_recattr.fat$l_efblk = F11SWAP(eofblk);
                eofblk--;
            }
        }
    }
    {
        vmscond_t retsts;

        retsts = deaccesschunk(vioc, curblk, 1, TRUE);
        if(  $SUCCESSFUL(sts) )
            sts = retsts;
        return sts;
    }
}

/*************************************************************** return_ent() */

/* return_ent() - return information about a directory entry */

vmscond_t return_ent(struct FCB * fcb, struct VIOC * vioc, uint32_t curblk,
                     struct dir$r_rec * dr, struct dir$r_ent * de, struct fibdef * fib,
                     uint16_t *reslen, struct dsc_descriptor * resdsc,
                     int wildcard) {
    if( resdsc != NULL ) {
        register uint32_t scale = 10;
        register uint16_t version;
        register size_t length;
        register char *ptr;
        register size_t outlen;

        version = F11WORD(de->dir$w_version);
        length = dr->dir$b_namecount;
        ptr = resdsc->dsc_a_pointer;
        outlen = resdsc->dsc_w_length;

        if (length > outlen)
            length = outlen;
        memcpy(ptr, dr->dir$t_name, length);
        while( version >= scale )
            scale *= 10;
        ptr += length;
        if( length < outlen ) {
            *ptr++ = ';';
            length++;
            do {
                if( length >= outlen )
                    break;
                scale /= 10;
                *ptr++ = version / scale + '0';
                version %= scale;
                length++;
            } while( scale > 1 );
        }
        *reslen = (uint16_t)length;
    }
    fid_copy( (struct fiddef *)&fib->fib$w_fid_num, &de->dir$w_fid, 0 );

    if( fib->fib$b_fid_rvn == 0 )
        fib->fib$b_fid_rvn = fcb->rvn;

    fib->fib$w_verlimit = dr->dir$w_verlimit;

    if( wildcard || (fib->fib$w_nmctl & FIB$M_WILD) ) {
        fib->fib$l_wcc = curblk;
    } else {
        fib->fib$l_wcc = 0;
    }
    return deaccesschunk(vioc, 0, 0, TRUE);
}

/*************************************************************** search_ent() */

/* search_ent() - search for a directory entry */

vmscond_t search_ent( struct FCB *fcb,
                      struct dsc_descriptor *fibdsc,
                      struct dsc_descriptor *filedsc, uint16_t *reslen,
                      struct dsc_descriptor *resdsc, uint32_t eofblk,
                      unsigned action ) {
    register vmscond_t sts;
    uint32_t curblk;
    struct VIOC *vioc = NULL;
    char *searchspec, *buffer;
    int searchlen, version, wildcard, wcc_flag;
    struct fibdef *fib;

    fib = (struct fibdef *) fibdsc->dsc_a_pointer;
    direct_lookups++;

    /* 1) Generate start block (wcc gives start point)
     * 2) Search for start
     * 3) Scan until found or too big or end.
     */

    curblk = fib->fib$l_wcc;
    if (curblk != 0) {
        searchspec = resdsc->dsc_a_pointer;
        sts = name_check( searchspec, *reslen, &searchlen, &version, &wildcard );
        if( MODIFIES(action) || wildcard )
            sts = RMS$_WLD;
        wcc_flag = TRUE;
    } else {
        searchspec = filedsc->dsc_a_pointer;
        sts = name_check( searchspec, filedsc->dsc_w_length, &searchlen, &version,
                          &wildcard);
        if( (MODIFIES(action) && wildcard) || (action == DIRECT_CREATE && version < 0) ) {
            sts = RMS$_WLD;
        }
        wcc_flag = FALSE;
    }
    if( $FAILED(sts) )
        return sts;

    /* Identify starting block...*/

    if (*searchspec == '*' || *searchspec == '%') {
        curblk = 1;
    } else {
        register uint32_t loblk = 1, hiblk = eofblk;
        if( curblk < 1 || curblk > eofblk )
            curblk = (eofblk + 1) / 2;

        while (loblk < hiblk) {
            register int cmp;
            register uint32_t newblk;
            register struct dir$r_rec *dr;
            direct_searches++;

            if( $FAILS(sts = accesschunk( fcb, curblk, &vioc, &buffer, NULL, MODIFIES(action) )) )
                return sts;
            dr = (struct dir$r_rec *) buffer;
            if( F11WORD(dr->dir$w_size) > MAXREC ) {
                cmp = MAT_GT;
            } else {
                cmp = name_match( searchspec, searchlen, dr->dir$t_name,
                                  dr->dir$b_namecount );
                if (cmp == MAT_EQ) {
                    if (wildcard || version < 1 || version > 32767) {
                        cmp = MAT_NE;   /* no match - want to find start */
                    } else {
                        register struct dir$r_ent *de =
                            (struct dir$r_ent *)
                                (dr->dir$t_name + ((dr->dir$b_namecount + 1) & ~1));
                        if (F11WORD(de->dir$w_version) < version) {
                            cmp = MAT_GT;       /* too far... */
                        } else {
                            if (F11WORD(de->dir$w_version) > version) {
                                cmp = MAT_LT;   /* further ahead... */
                            }
                        }
                    }
                }
            }
            switch (cmp) {
                case MAT_LT:
                    if (curblk == fib->fib$l_wcc) {
                        newblk = hiblk = loblk = curblk;
                    } else {
                        loblk = curblk;
                        newblk = (loblk + hiblk + 1) / 2;
                    }
                    break;
                case MAT_GT:
                case MAT_NE:
                    newblk = (loblk + curblk) / 2;
                    hiblk = curblk - 1;
                    break;
                default:
                    newblk = hiblk = loblk = curblk;
            }
            if (newblk != curblk) {
                if( $FAILS(sts = deaccesschunk( vioc, 0, 0, TRUE )) )
                    return sts;
                vioc = NULL;
                curblk = newblk;
            }
        }
    }

    /* Now to read sequentially to find entry... */

    {
        char last_name[80];
        unsigned last_len = 0;
        register int relver = 0;
        while( $SUCCESSFUL(sts) && curblk <= eofblk ) {
            register struct dir$r_rec *dr;
            register int cmp = MAT_LT;

            /* Access a directory block. Reset relative version if it starts
               with a record we haven't seen before... */

            if( vioc == NULL ) {
                if( $FAILS(sts = accesschunk( fcb, curblk, &vioc,
                                              &buffer, NULL, MODIFIES(action) )) )
                    return sts;
            }
            dr = (struct dir$r_rec *) buffer;
            if (last_len != dr->dir$b_namecount) {
                relver = 0;
            } else {
                if( name_match(last_name, last_len, dr->dir$t_name, last_len) !=
                    MAT_EQ ) {
                    relver = 0;
                }
            }

            /* Now loop through the records seeing which match our spec... */

            while (TRUE) {        /* dr records within block */
                register char *nr = (char *) dr + F11WORD(dr->dir$w_size) + 2;
                if (nr >= buffer + BLOCKSIZE)
                    break;
                if (dr->dir$t_name + dr->dir$b_namecount >= nr)
                    break;
                cmp = name_match(searchspec, searchlen, dr->dir$t_name,
                                 dr->dir$b_namecount);
                if (cmp == MAT_GT && wcc_flag) {
                    wcc_flag = FALSE;
                    searchspec = filedsc->dsc_a_pointer;
                    if( $FAILS(sts = name_check(searchspec, filedsc->dsc_w_length,
                                     &searchlen, &version, &wildcard)) )
                        break;
                } else {
                    if (cmp == MAT_EQ) {
                        register struct dir$r_ent *de;
                        de = (struct dir$r_ent *)
                                 (dr->dir$t_name +
                                  ((dr->dir$b_namecount + 1) & ~1));
                        if (version == 0 && action == DIRECT_CREATE) {
                            version = F11WORD(de->dir$w_version) + 1;
                            if (version > 32767) {
                                sts = RMS$_VER;
                                break;
                            }
                        }
                        /* Look at each directory entry to see
                           if it is what we want...    */

                        if ((char *) dr != buffer) relver = 0;
                        cmp = MAT_LT;
                        while ((char *) de < nr) {
                            if ((version < 1) ?
                                    (relver > version) :
                                    (version < F11WORD(de->dir$w_version))) {
                                relver--;
                                de++;
                            } else {
                                if (version > 32767 || version == relver ||
                                    version == F11WORD(de->dir$w_version)) {
                                    cmp = MAT_EQ;
                                } else {
                                    cmp = MAT_GT;
                                }
                                if (!wcc_flag) {
                                    break;
                                } else {
                                    wcc_flag = FALSE;
                                    searchspec = filedsc->dsc_a_pointer;
                                    if( $FAILS(sts = name_check( searchspec,
                                                                 filedsc->dsc_w_length,
                                                                 &searchlen, &version,
                                                                 &wildcard )) )
                                        break;
                                    if (name_match(searchspec, searchlen,
                                                   dr->dir$t_name,
                                                   dr->dir$b_namecount) !=
                                        MAT_EQ) {
                                        cmp = MAT_NE;
                                        break;
                                    }
                                    if (version < 0) {
                                        relver = -32768;
                                        cmp = MAT_GT;
                                        break;
                                    }
                                    if (cmp == MAT_EQ) {
                                        relver--;
                                        de++;
                                    }
                                    cmp = MAT_LT;
                                }
                            }
                        }
                        if( $FAILED(sts) )
                            break;

                        /* Decide what to do with the entry we have found... */

                        if (cmp == MAT_EQ) {
                            switch (action) {
                            case DIRECT_FIND:
                                return return_ent( fcb, vioc, curblk, dr, de, fib,
                                                   reslen, resdsc, wildcard );
                            case DIRECT_DELETE:
                                return delete_ent( fcb, vioc, curblk, dr, de,
                                                   buffer, eofblk );
                            case DIRECT_CREATE:
                                sts = SS$_DUPFILENAME;
                                break;
                            default:
                                abort();
                            }
                        } else {
                            if( cmp != MAT_NE && action == DIRECT_CREATE ) {
                                 return insert_ent( fcb, eofblk, curblk, vioc, buffer,
                                                   dr, de, searchspec, searchlen,
                                                    (fib->fib$w_verlimit << 16) | version,
                                                    (struct fiddef *)&fib->fib$w_fid_num, reslen, resdsc );

                            }
                        }
                    }
                    /*  Finish unless we expect more... */

                    if (cmp == MAT_GT && !wildcard)
                        break;

                    /* If this is the last record in the block store the name
                       so that if it continues into the next block we can get
                       the relative version right! Sigh! */

                    if (F11WORD(((struct dir$r_rec *) nr)->dir$w_size) > MAXREC) {
                        last_len = dr->dir$b_namecount;
                        if (last_len > sizeof(last_name)) {
                            last_len = sizeof(last_name);
                        }
                        memcpy(last_name, dr->dir$t_name, last_len);
                        dr = (struct dir$r_rec *) nr;
                        break;
                    }
                    dr = (struct dir$r_rec *) nr;
                }
            }

            /* Release the buffer ready to get the next one - unless it is the
               last one in which case we can't defer an insert any longer!! */

            if( $FAILED(sts) || action != DIRECT_CREATE ||
                (cmp != MAT_GT && curblk < eofblk)) {
                register vmscond_t dests;

                if( $FAILS(dests = deaccesschunk( vioc, 0, 0, TRUE )) ) {
                    sts = dests;
                    break;
                }
                vioc = NULL;
                curblk++;
            } else {
                if( version == 0 )
                    version = 1;
                return insert_ent( fcb, eofblk, curblk, vioc, buffer, dr, NULL,
                                  searchspec, searchlen, (fib->fib$w_verlimit << 16) | version,
                                   (struct fiddef *)&fib->fib$w_fid_num,
                                   reslen, resdsc );
            }
        }                       /* curblk blocks within file */
    }

    /* We achieved nothing! Report the failure... */

    if( $SUCCESSFUL(sts) ) {
        fib->fib$l_wcc = 0;
        if (wcc_flag || wildcard) {
            sts = SS$_NOMOREFILES;
        } else {
            sts = SS$_NOSUCHFILE;
        }
    }
    return sts;
}

/******************************************************************* direct() */

/* direct() - this routine handles all directory manipulations:-
         action 0 - find directory entry
                1 - delete entry
                2 - create an entry
  */

unsigned direct( struct VCB * vcb, struct dsc_descriptor * fibdsc,
                 struct dsc_descriptor * filedsc, uint16_t *reslen,
                 struct dsc_descriptor * resdsc, unsigned action ) {
    struct FCB *fcb;
    register vmscond_t sts;
    uint32_t eofblk;
    register struct fibdef *fib;

    fib = (struct fibdef *) fibdsc->dsc_a_pointer;

    if( $SUCCESSFUL(sts = accessfile( vcb, (struct fiddef *) &fib->fib$w_did_num,
                                      &fcb, MODIFIES(action) )) ) {
        if (F11LONG(fcb->head->fh2$l_filechar) & FH2$M_DIRECTORY) {
            eofblk = F11SWAP(fcb->head->fh2$w_recattr.fat$l_efblk);
            if( F11WORD(fcb->head->fh2$w_recattr.fat$w_ffbyte) == 0 )
                --eofblk;
            if( eofblk == 0 ) {
                struct VIOC *vioc;
                char *buffer;
                uint32_t blocks;

                if( action != DIRECT_CREATE ) {
                    int searchlen, version, wildcard;

                    deaccessfile( fcb );
                    if( fib->fib$l_wcc ) {
                        if( $FAILS(sts = name_check( resdsc->dsc$a_pointer, *reslen,
                                                     &searchlen, &version, &wildcard )) )
                            return sts;
                        fib->fib$l_wcc = 0;
                        return SS$_NOMOREFILES;
                    }
                    if( $FAILS(sts = name_check( filedsc->dsc$a_pointer,
                                                 filedsc->dsc$w_length,
                                                 &searchlen, &version, &wildcard )) )
                        return sts;
                    if( wildcard )
                        return SS$_NOMOREFILES;
                    return SS$_NOSUCHFILE;
                }

                eofblk = F11SWAP(fcb->head->fh2$w_recattr.fat$l_hiblk);
                if( eofblk == 0 ) {
                    if( $FAILS(sts = update_extend( fcb, 1, 0 )) ) {
                        deaccessfile( fcb );
                        return sts;
                    }
                    eofblk = F11SWAP(fcb->head->fh2$w_recattr.fat$l_hiblk);
                    if( fcb->highwater != 0 ) {
                        fcb->highwater = 2;
                        fcb->head->fh2$l_highwater = F11SWAP( 2 );
                    }
                }
                fcb->head->fh2$w_recattr.fat$l_efblk = F11SWAP( 2 );
                fcb->head->fh2$w_recattr.fat$w_ffbyte = 0;
                eofblk = 1;
                if( $FAILS(sts = accesschunk( fcb, 1, &vioc, &buffer, &blocks, 1 )) ) {
                    deaccessfile( fcb );
                    return sts;
                }
                memset( buffer, 0, 512 );
                ((f11word *)buffer)[0] = 0xffff;
                deaccesschunk( vioc, 1, blocks, TRUE );
            }
            sts = search_ent( fcb, fibdsc, filedsc, reslen, resdsc, eofblk, action );
        } else {
            sts = SS$_BADIRECTORY;
        }
        {
            register uint32_t dests;

            dests = deaccessfile( fcb );
            if( $SUCCESSFUL(sts) )
                sts = dests;
        }
    }
    if( MODIFIES(action) ) {
        cache_flush();
        while( vcb->dircache ) /* Too conservative...but safe */
            cache_delete( (struct CACHE *)vcb->dircache );
    }

    return sts;
}

/******************************************************************* dircmp() */

/* Function to compare directory cache names */

static int dircmp( uint32_t keylen, void *key, void *node ) {
    register struct DIRCACHE *dirnode;
    register int cmp;
    unsigned dlen;
    const char *kp, *dp;
    int version, wild, namelen;
    vmscond_t sts;

    if( $FAILS(sts = name_check(key, keylen, &namelen, &version, &wild) ) ) {
        abort();
    }

    dirnode = (struct DIRCACHE *) node;

    if( version != F11WORD(dirnode->entry.dir$w_version) )
        return version - F11WORD(dirnode->entry.dir$w_version);

    dlen = dirnode->record.dir$b_namecount;

    kp = (const char *)key;
    dp = dirnode->record.dir$t_name;

    while( keylen > 0 && dlen > 0 ) {
        cmp = toupper(*kp++) - toupper(*dp++);
        if( cmp != 0 )
            return cmp;
        --keylen;
        --dlen;
    }
    return keylen - dlen;
}

/***************************************************************** dir_create() */
static void *dir_create( uint32_t keylen, void *key, vmscond_t *retsts ) {
    struct DIRCACHE *dir;
    vmscond_t sts;
    int version, wild, namelen;

    if( $FAILS(sts = name_check(key, keylen, &namelen, &version, &wild) ) ) {
        *retsts = RMS$_DIR;
        return NULL;
    }
    if( wild || version == 0 ) {
        *retsts = SS$_NOSUCHFILE;
        return NULL;
    }

    if( (dir = (struct DIRCACHE *)calloc( 1,
                                          offsetof( struct DIRCACHE, record.dir$t_name ) +
                                          namelen + (namelen & 1) )) == NULL ) {
        *retsts = SS$_INSFMEM;
        return NULL;
    }

    dir->cache.objtype = OBJTYPE_DIR;
    dir->record.dir$w_size = (f11word)F11WORD( offsetof( struct dir$r_rec,
                                                         dir$t_name ) + namelen - 2);
    dir->record.dir$b_namecount = (f11byte) namelen;
    memcpy( dir->record.dir$t_name, key, namelen );
    dir->entry.dir$w_version = (f11word)F11WORD((f11word)version);

    *retsts = SS$_CREATED;
    return dir;
}

/***************************************************************** direct_dirid() */

/* Find FID of directory/ies using a cache.
 * Used by sys$parse.  Can be generalized...
 * Input descriptor is a directory name string, including subdirectories.
 * Output is the fid of the lowest level directory.
 * Higher directories are searched (and cached).
 * Any wildcards/recursion stop the search (they'll be resolved when a
 * search matches them.)
 */

vmscond_t direct_dirid( struct VCB *vcb, struct dsc$descriptor *dirdsc,
                        struct fiddef *dirid, struct fiddef *fid ) {
    register struct DIRCACHE *dir = NULL;
    vmscond_t sts;
    struct fibdef fib;
    struct dsc$descriptor fibdsc;
    struct dsc$descriptor namdsc;
    struct fiddef srcdir;
    char *dirnam;
    int dirlen;
    size_t len;
    char *bp, *dp;
    char nambuf[256];

    memset( &fib, 0, sizeof( fib ) );
    memset( &fibdsc, 0, sizeof( fibdsc ) );
    memset( &namdsc, 0, sizeof( namdsc ) );
    memset( &srcdir, 0, sizeof( srcdir ) );

    dirlen = dirdsc->dsc$w_length;

    if( (size_t)dirlen > sizeof( nambuf ) -1 )
        abort();
    dirnam = dirdsc->dsc$a_pointer;

    srcdir.fid$w_num = FID$C_MFD;
    srcdir.fid$w_seq = FID$C_MFD;
    srcdir.fid$b_rvn = 0;
    srcdir.fid$b_nmx = 0;

    if( dirlen < 1 || (dirlen == 6 && !memcmp( dirnam, "000000", 6 )) ) {
        if( dirid )
            *dirid = srcdir;
        if( fid )
            *fid = srcdir;
        return SS$_NORMAL;
    }

    if( dirid != NULL )
        memset( dirid, 0, sizeof( *dirid ) );
    if( fid != NULL )
        memset( fid, 0, sizeof( *fid ) );

    fibdsc.dsc$w_length = sizeof( fibdsc );
    fibdsc.dsc$a_pointer = (char *)&fib;

    for( bp = dp = dirnam; dp <= dirnam + dirlen; dp++) {
        char ch;

        if( dp < dirnam + dirlen ) {
            ch = *dp;
            if( ch == '*' || ch == '%' ) {
                return RMS$_WLD;
            }
            if( ch != '.' )
                continue;
        }
        len = (size_t)(dp - bp);
        if( len == 0 )
            break;

        memcpy( nambuf, bp, len );
        namdsc.dsc$w_length = (uint16_t)(len + sizeof( ".DIR;1" ) -1);
        memcpy( nambuf+len, ".DIR;1", namdsc.dsc$w_length );

        namdsc.dsc$a_pointer = nambuf;

        dir = cache_find( (void *) &vcb->dircache,  namdsc.dsc$w_length, nambuf,
                          &sts, dircmp, dir_create );
        if( dir == NULL ) {
            return RMS$_DNF;
        }
        cache_untouch( &dir->cache, FALSE );

        if( $MATCHCOND( sts, SS$_CREATED ) ) {
            memset( &fib, 0, sizeof( fib ) );
            memcpy( &fib.fib$w_did_num, &srcdir, sizeof( struct fiddef ) );

            sts = direct( vcb, &fibdsc, &namdsc, NULL, NULL, DIRECT_FIND );
            if( $FAILED(sts) ) {
                cache_delete( &dir->cache );
                return RMS$_DNF;
            }
            dir->parent = srcdir;
            dir->record.dir$w_verlimit = fib.fib$w_verlimit;
            memcpy( &dir->entry.dir$w_fid, &fib.fib$w_fid_num, sizeof( struct fiddef ) );
        }
        if( (dp + 2 < dirnam + dirlen) && dp[1] == '.' && dp[2] == '.' ) {
            return RMS$_DNF;
        }
        srcdir = dir->entry.dir$w_fid;
        bp = dp +1;
    }
    if( dir != NULL ) {
        if( dirid )
            *dirid = dir->parent;
        if( fid )
            *fid = dir->entry.dir$w_fid;
        return SS$_NORMAL;
    }
    return RMS$_DNF;
}
