Index: src/test_quota.c ================================================================== --- src/test_quota.c +++ src/test_quota.c @@ -25,11 +25,11 @@ ** This callback has the opportunity to enlarge the quota. If the ** callback does enlarge the quota such that the total size of all ** files within the group is less than the new quota, then the write ** continues as if nothing had happened. */ -#include "sqlite3.h" +#include "test_quota.h" #include #include /* ** For an build without mutexes, no-op the mutex calls. @@ -108,10 +108,22 @@ struct quotaConn { sqlite3_file base; /* Base class - must be first */ quotaFile *pFile; /* The underlying file */ /* The underlying VFS sqlite3_file is appended to this object */ }; + +/* +** An instance of the following object records the state of an +** open file. This object is opaque to all users - the internal +** structure is only visible to the functions below. +*/ +struct quota_FILE { + FILE *f; /* Open stdio file pointer */ + sqlite3_int64 iOfst; /* Current offset into the file */ + quotaFile *pFile; /* The file record in the quota system */ +}; + /************************* Global Variables **********************************/ /* ** All global variables used by this file are containing within the following ** gQuota structure. @@ -223,13 +235,15 @@ ** [...] Matches one character from the enclosed list of ** characters. ** ** [^...] Matches one character not in the enclosed list. ** +** / Matches "/" or "\\" +** */ static int quotaStrglob(const char *zGlob, const char *z){ - int c, c2; + int c, c2, cx; int invert; int seen; while( (c = (*(zGlob++)))!=0 ){ if( c=='*' ){ @@ -242,12 +256,13 @@ while( *z && quotaStrglob(zGlob-1,z)==0 ){ z++; } return (*z)!=0; } + cx = (c=='/') ? '\\' : c; while( (c2 = (*(z++)))!=0 ){ - while( c2!=c ){ + while( c2!=c && c2!=cx ){ c2 = *(z++); if( c2==0 ) return 0; } if( quotaStrglob(zGlob,z) ) return 1; } @@ -281,10 +296,13 @@ prior_c = c2; } c2 = *(zGlob++); } if( c2==0 || (seen ^ invert)==0 ) return 0; + }else if( c=='/' ){ + if( z[0]!='/' && z[0]!='\\' ) return 0; + z++; }else{ if( c!=(*(z++)) ) return 0; } } return *z==0; @@ -311,17 +329,134 @@ } /* Find a file in a quota group and return a pointer to that file. ** Return NULL if the file is not in the group. */ -static quotaFile *quotaFindFile(quotaGroup *pGroup, const char *zName){ +static quotaFile *quotaFindFile( + quotaGroup *pGroup, /* Group in which to look for the file */ + const char *zName, /* Full pathname of the file */ + int createFlag /* Try to create the file if not found */ +){ quotaFile *pFile = pGroup->pFiles; while( pFile && strcmp(pFile->zFilename, zName)!=0 ){ pFile = pFile->pNext; + } + if( pFile==0 && createFlag ){ + int nName = strlen(zName); + pFile = (quotaFile *)sqlite3_malloc( sizeof(*pFile) + nName + 1 ); + if( pFile ){ + memset(pFile, 0, sizeof(*pFile)); + pFile->zFilename = (char*)&pFile[1]; + memcpy(pFile->zFilename, zName, nName+1); + pFile->pNext = pGroup->pFiles; + if( pGroup->pFiles ) pGroup->pFiles->ppPrev = &pFile->pNext; + pFile->ppPrev = &pGroup->pFiles; + pGroup->pFiles = pFile; + pFile->pGroup = pGroup; + } } return pFile; } + +/* +** Figure out if we are dealing with Unix, Windows, or some other +** operating system. After the following block of preprocess macros, +** all of SQLITE_OS_UNIX, SQLITE_OS_WIN, SQLITE_OS_OS2, and SQLITE_OS_OTHER +** will defined to either 1 or 0. One of the four will be 1. The other +** three will be 0. +*/ +#if defined(SQLITE_OS_OTHER) +# if SQLITE_OS_OTHER==1 +# undef SQLITE_OS_UNIX +# define SQLITE_OS_UNIX 0 +# undef SQLITE_OS_WIN +# define SQLITE_OS_WIN 0 +# undef SQLITE_OS_OS2 +# define SQLITE_OS_OS2 0 +# else +# undef SQLITE_OS_OTHER +# endif +#endif +#if !defined(SQLITE_OS_UNIX) && !defined(SQLITE_OS_OTHER) +# define SQLITE_OS_OTHER 0 +# ifndef SQLITE_OS_WIN +# if defined(_WIN32) || defined(WIN32) || defined(__CYGWIN__) \ + || defined(__MINGW32__) || defined(__BORLANDC__) +# define SQLITE_OS_WIN 1 +# define SQLITE_OS_UNIX 0 +# define SQLITE_OS_OS2 0 +# elif defined(__EMX__) || defined(_OS2) || defined(OS2) \ + || defined(_OS2_) || defined(__OS2__) +# define SQLITE_OS_WIN 0 +# define SQLITE_OS_UNIX 0 +# define SQLITE_OS_OS2 1 +# else +# define SQLITE_OS_WIN 0 +# define SQLITE_OS_UNIX 1 +# define SQLITE_OS_OS2 0 +# endif +# else +# define SQLITE_OS_UNIX 0 +# define SQLITE_OS_OS2 0 +# endif +#else +# ifndef SQLITE_OS_WIN +# define SQLITE_OS_WIN 0 +# endif +#endif + +#if SQLITE_OS_UNIX +# include +#endif +#if SQLITE_OS_WIN +# include +# include +#endif + +/* +** Translate UTF8 to MBCS for use in fopen() calls. Return a pointer to the +** translated text.. Call quota_mbcs_free() to deallocate any memory +** used to store the returned pointer when done. +*/ +static char *quota_utf8_to_mbcs(const char *zUtf8){ +#if SQLITE_OS_WIN + int n; /* Bytes in zUtf8 */ + int nWide; /* number of UTF-16 characters */ + int nMbcs; /* Bytes of MBCS */ + LPWSTR zTmpWide; /* The UTF16 text */ + char *zMbcs; /* The MBCS text */ + int codepage; /* Code page used by fopen() */ + + n = strlen(zUtf8); + nWide = MultiByteToWideChar(CP_UTF8, 0, zUtf8, -1, NULL, 0); + if( nWide==0 ) return 0; + zTmpWide = (LPWSTR)sqlite3_malloc( (nWide+1)*sizeof(zTmpWide[0]) ); + if( zTmpWide==0 ) return 0; + MultiByteToWideChar(CP_UTF8, 0, zUtf8, -1, zTmpWide, nWide); + codepage = AreFileApisANSI() ? CP_ACP : CP_OEMCP; + nMbcs = WideCharToMultiByte(codepage, 0, zTmpWide, nWide, 0, 0, 0, 0); + zMbcs = nMbcs ? (char*)sqlite3_malloc( nMbcs+1 ) : 0; + if( zMbcs ){ + WideCharToMultiByte(codepage, 0, zTmpWide, nWide, zMbcs, nMbcs, 0, 0); + } + sqlite3_free(zTmpWide); + return zMbcs; +#else + return (char*)zUtf8; /* No-op on unix */ +#endif +} + +/* +** Deallocate any memory allocated by quota_utf8_to_mbcs(). +*/ +static void quota_mbcs_free(char *zOld){ +#if SQLITE_OS_WIN + sqlite3_free(zOld); +#else + /* No-op on unix */ +#endif +} /************************* VFS Method Wrappers *****************************/ /* ** This is the xOpen method used for the "quota" VFS. ** @@ -362,29 +497,17 @@ */ pQuotaOpen = (quotaConn*)pConn; pSubOpen = quotaSubOpen(pConn); rc = pOrigVfs->xOpen(pOrigVfs, zName, pSubOpen, flags, pOutFlags); if( rc==SQLITE_OK ){ - pFile = quotaFindFile(pGroup, zName); - if( pFile==0 ){ - int nName = strlen(zName); - pFile = (quotaFile *)sqlite3_malloc( sizeof(*pFile) + nName + 1 ); - if( pFile==0 ){ - quotaLeave(); - pSubOpen->pMethods->xClose(pSubOpen); - return SQLITE_NOMEM; - } - memset(pFile, 0, sizeof(*pFile)); - pFile->zFilename = (char*)&pFile[1]; - memcpy(pFile->zFilename, zName, nName+1); - pFile->pNext = pGroup->pFiles; - if( pGroup->pFiles ) pGroup->pFiles->ppPrev = &pFile->pNext; - pFile->ppPrev = &pGroup->pFiles; - pGroup->pFiles = pFile; - pFile->pGroup = pGroup; - pFile->deleteOnClose = (flags & SQLITE_OPEN_DELETEONCLOSE)!=0; - } + pFile = quotaFindFile(pGroup, zName, 1); + if( pFile==0 ){ + quotaLeave(); + pSubOpen->pMethods->xClose(pSubOpen); + return SQLITE_NOMEM; + } + pFile->deleteOnClose = (flags & SQLITE_OPEN_DELETEONCLOSE)!=0; pFile->nRef++; pQuotaOpen->pFile = pFile; if( pSubOpen->pMethods->iVersion==1 ){ pQuotaOpen->base.pMethods = &gQuota.sIoMethodsV1; }else{ @@ -421,11 +544,11 @@ */ if( rc==SQLITE_OK ){ quotaEnter(); pGroup = quotaGroupFind(zName); if( pGroup ){ - pFile = quotaFindFile(pGroup, zName); + pFile = quotaFindFile(pGroup, zName, 0); if( pFile ){ if( pFile->nRef ){ pFile->deleteOnClose = 1; }else{ quotaRemoveFile(pFile); @@ -453,11 +576,14 @@ rc = pSubOpen->pMethods->xClose(pSubOpen); quotaEnter(); pFile->nRef--; if( pFile->nRef==0 ){ quotaGroup *pGroup = pFile->pGroup; - if( pFile->deleteOnClose ) quotaRemoveFile(pFile); + if( pFile->deleteOnClose ){ + gQuota.pOrigVfs->xDelete(gQuota.pOrigVfs, pFile->zFilename, 0); + quotaRemoveFile(pFile); + } quotaGroupDeref(pGroup); } quotaLeave(); return rc; } @@ -587,11 +713,17 @@ /* Pass xFileControl requests through to the original VFS unchanged. */ static int quotaFileControl(sqlite3_file *pConn, int op, void *pArg){ sqlite3_file *pSubOpen = quotaSubOpen(pConn); - return pSubOpen->pMethods->xFileControl(pSubOpen, op, pArg); + int rc = pSubOpen->pMethods->xFileControl(pSubOpen, op, pArg); +#if defined(SQLITE_FCNTL_VFSNAME) + if( op==SQLITE_FCNTL_VFSNAME && rc==SQLITE_OK ){ + *(char**)pArg = sqlite3_mprintf("quota/%z", *(char**)pArg); + } +#endif + return rc; } /* Pass xSectorSize requests through to the original VFS unchanged. */ static int quotaSectorSize(sqlite3_file *pConn){ @@ -803,11 +935,12 @@ char *zFull; sqlite3_file *fd; int rc; int outFlags = 0; sqlite3_int64 iSize; - fd = sqlite3_malloc(gQuota.sThisVfs.szOsFile + gQuota.sThisVfs.mxPathname+1); + fd = (sqlite3_file*)sqlite3_malloc(gQuota.sThisVfs.szOsFile + + gQuota.sThisVfs.mxPathname+1); if( fd==0 ) return SQLITE_NOMEM; zFull = gQuota.sThisVfs.szOsFile + (char*)fd; rc = gQuota.pOrigVfs->xFullPathname(gQuota.pOrigVfs, zFilename, gQuota.sThisVfs.mxPathname+1, zFull); if( rc==SQLITE_OK ){ @@ -821,19 +954,233 @@ quotaGroup *pGroup; quotaFile *pFile; quotaEnter(); pGroup = quotaGroupFind(zFull); if( pGroup ){ - pFile = quotaFindFile(pGroup, zFull); + pFile = quotaFindFile(pGroup, zFull, 0); if( pFile ) quotaRemoveFile(pFile); } quotaLeave(); } sqlite3_free(fd); return rc; } +/* +** Open a potentially quotaed file for I/O. +*/ +quota_FILE *sqlite3_quota_fopen(const char *zFilename, const char *zMode){ + quota_FILE *p = 0; + char *zFull = 0; + char *zFullTranslated; + int rc; + quotaGroup *pGroup; + quotaFile *pFile; + + zFull = (char*)sqlite3_malloc(gQuota.sThisVfs.mxPathname + 1); + if( zFull==0 ) return 0; + rc = gQuota.pOrigVfs->xFullPathname(gQuota.pOrigVfs, zFilename, + gQuota.sThisVfs.mxPathname+1, zFull); + if( rc ) goto quota_fopen_error; + p = (quota_FILE*)sqlite3_malloc(sizeof(*p)); + if( p==0 ) goto quota_fopen_error; + memset(p, 0, sizeof(*p)); + zFullTranslated = quota_utf8_to_mbcs(zFull); + if( zFullTranslated==0 ) goto quota_fopen_error; + p->f = fopen(zFullTranslated, zMode); + quota_mbcs_free(zFullTranslated); + if( p->f==0 ) goto quota_fopen_error; + quotaEnter(); + pGroup = quotaGroupFind(zFull); + if( pGroup ){ + pFile = quotaFindFile(pGroup, zFull, 1); + if( pFile==0 ){ + quotaLeave(); + goto quota_fopen_error; + } + pFile->nRef++; + p->pFile = pFile; + } + quotaLeave(); + sqlite3_free(zFull); + return p; + +quota_fopen_error: + sqlite3_free(zFull); + if( p && p->f ) fclose(p->f); + sqlite3_free(p); + return 0; +} + +/* +** Read content from a quota_FILE +*/ +size_t sqlite3_quota_fread( + void *pBuf, /* Store the content here */ + size_t size, /* Size of each element */ + size_t nmemb, /* Number of elements to read */ + quota_FILE *p /* Read from this quota_FILE object */ +){ + return fread(pBuf, size, nmemb, p->f); +} + +/* +** Write content into a quota_FILE. Invoke the quota callback and block +** the write if we exceed quota. +*/ +size_t sqlite3_quota_fwrite( + void *pBuf, /* Take content to write from here */ + size_t size, /* Size of each element */ + size_t nmemb, /* Number of elements */ + quota_FILE *p /* Write to this quota_FILE objecct */ +){ + sqlite3_int64 iOfst; + sqlite3_int64 iEnd; + sqlite3_int64 szNew; + quotaFile *pFile; + + iOfst = ftell(p->f); + iEnd = iOfst + size*nmemb; + pFile = p->pFile; + if( pFile && pFile->iSizepGroup; + quotaEnter(); + szNew = pGroup->iSize - pFile->iSize + iEnd; + if( szNew>pGroup->iLimit && pGroup->iLimit>0 ){ + if( pGroup->xCallback ){ + pGroup->xCallback(pFile->zFilename, &pGroup->iLimit, szNew, + pGroup->pArg); + } + if( szNew>pGroup->iLimit && pGroup->iLimit>0 ){ + iEnd = pGroup->iLimit - pGroup->iSize + pFile->iSize; + nmemb = (iEnd - iOfst)/size; + iEnd = iOfst + size*nmemb; + szNew = pGroup->iSize - pFile->iSize + iEnd; + } + } + pGroup->iSize = szNew; + pFile->iSize = iEnd; + quotaLeave(); + } + return fwrite(pBuf, size, nmemb, p->f); +} + +/* +** Close an open quota_FILE stream. +*/ +int sqlite3_quota_fclose(quota_FILE *p){ + int rc; + quotaFile *pFile; + rc = fclose(p->f); + pFile = p->pFile; + if( pFile ){ + quotaEnter(); + pFile->nRef--; + if( pFile->nRef==0 ){ + quotaGroup *pGroup = pFile->pGroup; + if( pFile->deleteOnClose ){ + gQuota.pOrigVfs->xDelete(gQuota.pOrigVfs, pFile->zFilename, 0); + quotaRemoveFile(pFile); + } + quotaGroupDeref(pGroup); + } + quotaLeave(); + } + sqlite3_free(p); + return rc; +} + +/* +** Flush memory buffers for a quota_FILE to disk. +*/ +int sqlite3_quota_fflush(quota_FILE *p, int doFsync){ + int rc; + rc = fflush(p->f); + if( rc==0 && doFsync ){ +#if SQLITE_OS_UNIX + rc = fsync(fileno(p->f)); +#endif +#if SQLITE_OS_WIN + rc = _commit(_fileno(p->f)); +#endif + } + return rc!=0; +} + +/* +** Seek on a quota_FILE stream. +*/ +int sqlite3_quota_fseek(quota_FILE *p, long offset, int whence){ + return fseek(p->f, offset, whence); +} + +/* +** rewind a quota_FILE stream. +*/ +void sqlite3_quota_rewind(quota_FILE *p){ + rewind(p->f); +} + +/* +** Tell the current location of a quota_FILE stream. +*/ +long sqlite3_quota_ftell(quota_FILE *p){ + return ftell(p->f); +} + +/* +** Remove a managed file. Update quotas accordingly. +*/ +int sqlite3_quota_remove(const char *zFilename){ + char *zFull; /* Full pathname for zFilename */ + int nFull; /* Number of bytes in zFilename */ + int rc; /* Result code */ + quotaGroup *pGroup; /* Group containing zFilename */ + quotaFile *pFile; /* A file in the group */ + quotaFile *pNextFile; /* next file in the group */ + int diff; /* Difference between filenames */ + char c; /* First character past end of pattern */ + + zFull = (char*)sqlite3_malloc(gQuota.sThisVfs.mxPathname + 1); + if( zFull==0 ) return SQLITE_NOMEM; + rc = gQuota.pOrigVfs->xFullPathname(gQuota.pOrigVfs, zFilename, + gQuota.sThisVfs.mxPathname+1, zFull); + if( rc ){ + sqlite3_free(zFull); + return rc; + } + + /* Figure out the length of the full pathname. If the name ends with + ** / (or \ on windows) then remove the trailing /. + */ + nFull = strlen(zFull); + if( nFull>0 && (zFull[nFull-1]=='/' || zFull[nFull-1]=='\\') ){ + nFull--; + zFull[nFull] = 0; + } + + quotaEnter(); + pGroup = quotaGroupFind(zFull); + if( pGroup ){ + for(pFile=pGroup->pFiles; pFile && rc==SQLITE_OK; pFile=pNextFile){ + pNextFile = pFile->pNext; + diff = memcmp(zFull, pFile->zFilename, nFull); + if( diff==0 && ((c = pFile->zFilename[nFull])==0 || c=='/' || c=='\\') ){ + if( pFile->nRef ){ + pFile->deleteOnClose = 1; + }else{ + rc = gQuota.pOrigVfs->xDelete(gQuota.pOrigVfs, pFile->zFilename, 0); + quotaRemoveFile(pFile); + quotaGroupDeref(pGroup); + } + } + } + } + quotaLeave(); + sqlite3_free(zFull); + return rc; +} /***************************** Test Code ***********************************/ #ifdef SQLITE_TEST #include @@ -1058,13 +1405,17 @@ Tcl_ListObjAppendElement(interp, pGroupTerm, Tcl_NewWideIntObj(pGroup->iLimit)); Tcl_ListObjAppendElement(interp, pGroupTerm, Tcl_NewWideIntObj(pGroup->iSize)); for(pFile=pGroup->pFiles; pFile; pFile=pFile->pNext){ + int i; + char zTemp[1000]; pFileTerm = Tcl_NewObj(); + sqlite3_snprintf(sizeof(zTemp), zTemp, "%s", pFile->zFilename); + for(i=0; zTemp[i]; i++){ if( zTemp[i]=='\\' ) zTemp[i] = '/'; } Tcl_ListObjAppendElement(interp, pFileTerm, - Tcl_NewStringObj(pFile->zFilename, -1)); + Tcl_NewStringObj(zTemp, -1)); Tcl_ListObjAppendElement(interp, pFileTerm, Tcl_NewWideIntObj(pFile->iSize)); Tcl_ListObjAppendElement(interp, pFileTerm, Tcl_NewWideIntObj(pFile->nRef)); Tcl_ListObjAppendElement(interp, pFileTerm, @@ -1075,10 +1426,276 @@ } quotaLeave(); Tcl_SetObjResult(interp, pResult); return TCL_OK; } + +/* +** tclcmd: sqlite3_quota_fopen FILENAME MODE +*/ +static int test_quota_fopen( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + const char *zFilename; /* File pattern to configure */ + const char *zMode; /* Mode string */ + quota_FILE *p; /* Open string object */ + char zReturn[50]; /* Name of pointer to return */ + + /* Process arguments */ + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "FILENAME MODE"); + return TCL_ERROR; + } + zFilename = Tcl_GetString(objv[1]); + zMode = Tcl_GetString(objv[2]); + p = sqlite3_quota_fopen(zFilename, zMode); + sqlite3_snprintf(sizeof(zReturn), zReturn, "%p", p); + Tcl_SetResult(interp, zReturn, TCL_VOLATILE); + return TCL_OK; +} + +/* Defined in test1.c */ +extern void *sqlite3TestTextToPtr(const char*); + +/* +** tclcmd: sqlite3_quota_fread HANDLE SIZE NELEM +*/ +static int test_quota_fread( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + char *zBuf; + int sz; + int nElem; + int got; + + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE SIZE NELEM"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + if( Tcl_GetIntFromObj(interp, objv[2], &sz) ) return TCL_ERROR; + if( Tcl_GetIntFromObj(interp, objv[3], &nElem) ) return TCL_ERROR; + zBuf = (char*)sqlite3_malloc( sz*nElem + 1 ); + if( zBuf==0 ){ + Tcl_SetResult(interp, "out of memory", TCL_STATIC); + return TCL_ERROR; + } + got = sqlite3_quota_fread(zBuf, sz, nElem, p); + if( got<0 ) got = 0; + zBuf[got*sz] = 0; + Tcl_SetResult(interp, zBuf, TCL_VOLATILE); + sqlite3_free(zBuf); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_fwrite HANDLE SIZE NELEM CONTENT +*/ +static int test_quota_fwrite( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + char *zBuf; + int sz; + int nElem; + int got; + + if( objc!=5 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE SIZE NELEM CONTENT"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + if( Tcl_GetIntFromObj(interp, objv[2], &sz) ) return TCL_ERROR; + if( Tcl_GetIntFromObj(interp, objv[3], &nElem) ) return TCL_ERROR; + zBuf = Tcl_GetString(objv[4]); + got = sqlite3_quota_fwrite(zBuf, sz, nElem, p); + Tcl_SetObjResult(interp, Tcl_NewIntObj(got)); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_fclose HANDLE +*/ +static int test_quota_fclose( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + int rc; + + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + rc = sqlite3_quota_fclose(p); + Tcl_SetObjResult(interp, Tcl_NewIntObj(rc)); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_fflush HANDLE ?HARDSYNC? +*/ +static int test_quota_fflush( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + int rc; + int doSync = 0; + + if( objc!=2 && objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE ?HARDSYNC?"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + if( objc==3 ){ + if( Tcl_GetBooleanFromObj(interp, objv[2], &doSync) ) return TCL_ERROR; + } + rc = sqlite3_quota_fflush(p, doSync); + Tcl_SetObjResult(interp, Tcl_NewIntObj(rc)); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_fseek HANDLE OFFSET WHENCE +*/ +static int test_quota_fseek( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + int ofst; + const char *zWhence; + int whence; + int rc; + + if( objc!=4 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE OFFSET WHENCE"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + if( Tcl_GetIntFromObj(interp, objv[2], &ofst) ) return TCL_ERROR; + zWhence = Tcl_GetString(objv[3]); + if( strcmp(zWhence, "SEEK_SET")==0 ){ + whence = SEEK_SET; + }else if( strcmp(zWhence, "SEEK_CUR")==0 ){ + whence = SEEK_CUR; + }else if( strcmp(zWhence, "SEEK_END")==0 ){ + whence = SEEK_END; + }else{ + Tcl_AppendResult(interp, + "WHENCE should be SEEK_SET, SEEK_CUR, or SEEK_END", (char*)0); + return TCL_ERROR; + } + rc = sqlite3_quota_fseek(p, ofst, whence); + Tcl_SetObjResult(interp, Tcl_NewIntObj(rc)); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_rewind HANDLE +*/ +static int test_quota_rewind( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + sqlite3_quota_rewind(p); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_ftell HANDLE +*/ +static int test_quota_ftell( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + quota_FILE *p; + sqlite3_int64 x; + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "HANDLE"); + return TCL_ERROR; + } + p = sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + x = sqlite3_quota_ftell(p); + Tcl_SetObjResult(interp, Tcl_NewWideIntObj(x)); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_remove FILENAME +*/ +static int test_quota_remove( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + const char *zFilename; /* File pattern to configure */ + int rc; + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "FILENAME"); + return TCL_ERROR; + } + zFilename = Tcl_GetString(objv[1]); + rc = sqlite3_quota_remove(zFilename); + Tcl_SetObjResult(interp, Tcl_NewIntObj(rc)); + return TCL_OK; +} + +/* +** tclcmd: sqlite3_quota_glob PATTERN TEXT +** +** Test the glob pattern matching. Return 1 if TEXT matches PATTERN +** and return 0 if it does not. +*/ +static int test_quota_glob( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + const char *zPattern; /* The glob pattern */ + const char *zText; /* Text to compare agains the pattern */ + int rc; + if( objc!=3 ){ + Tcl_WrongNumArgs(interp, 1, objv, "PATTERN TEXT"); + return TCL_ERROR; + } + zPattern = Tcl_GetString(objv[1]); + zText = Tcl_GetString(objv[2]); + rc = quotaStrglob(zPattern, zText); + Tcl_SetObjResult(interp, Tcl_NewIntObj(rc)); + return TCL_OK; +} /* ** This routine registers the custom TCL commands defined in this ** module. This should be the only procedure visible from outside ** of this module. @@ -1087,14 +1704,24 @@ static struct { char *zName; Tcl_ObjCmdProc *xProc; } aCmd[] = { { "sqlite3_quota_initialize", test_quota_initialize }, - { "sqlite3_quota_shutdown", test_quota_shutdown }, - { "sqlite3_quota_set", test_quota_set }, - { "sqlite3_quota_file", test_quota_file }, - { "sqlite3_quota_dump", test_quota_dump }, + { "sqlite3_quota_shutdown", test_quota_shutdown }, + { "sqlite3_quota_set", test_quota_set }, + { "sqlite3_quota_file", test_quota_file }, + { "sqlite3_quota_dump", test_quota_dump }, + { "sqlite3_quota_fopen", test_quota_fopen }, + { "sqlite3_quota_fread", test_quota_fread }, + { "sqlite3_quota_fwrite", test_quota_fwrite }, + { "sqlite3_quota_fclose", test_quota_fclose }, + { "sqlite3_quota_fflush", test_quota_fflush }, + { "sqlite3_quota_fseek", test_quota_fseek }, + { "sqlite3_quota_rewind", test_quota_rewind }, + { "sqlite3_quota_ftell", test_quota_ftell }, + { "sqlite3_quota_remove", test_quota_remove }, + { "sqlite3_quota_glob", test_quota_glob }, }; int i; for(i=0; i + +/* Make this callable from C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +/* +** Initialize the quota VFS shim. Use the VFS named zOrigVfsName +** as the VFS that does the actual work. Use the default if +** zOrigVfsName==NULL. +** +** The quota VFS shim is named "quota". It will become the default +** VFS if makeDefault is non-zero. +** +** THIS ROUTINE IS NOT THREADSAFE. Call this routine exactly once +** during start-up. +*/ +int sqlite3_quota_initialize(const char *zOrigVfsName, int makeDefault); + +/* +** Shutdown the quota system. +** +** All SQLite database connections must be closed before calling this +** routine. +** +** THIS ROUTINE IS NOT THREADSAFE. Call this routine exactly once while +** shutting down in order to free all remaining quota groups. +*/ +int sqlite3_quota_shutdown(void); + +/* +** Create or destroy a quota group. +** +** The quota group is defined by the zPattern. When calling this routine +** with a zPattern for a quota group that already exists, this routine +** merely updates the iLimit, xCallback, and pArg values for that quota +** group. If zPattern is new, then a new quota group is created. +** +** The zPattern is always compared against the full pathname of the file. +** Even if APIs are called with relative pathnames, SQLite converts the +** name to a full pathname before comparing it against zPattern. zPattern +** is a glob pattern with the following matching rules: +** +** '*' Matches any sequence of zero or more characters. +** +** '?' Matches exactly one character. +** +** [...] Matches one character from the enclosed list of +** characters. "]" can be part of the list if it is +** the first character. Within the list "X-Y" matches +** characters X or Y or any character in between the +** two. Ex: "[0-9]" matches any digit. +** +** [^...] Matches one character not in the enclosed list. +** +** / Matches either / or \. This allows glob patterns +** containing / to work on both unix and windows. +** +** Note that, unlike unix shell globbing, the directory separator "/" +** can match a wildcard. So, for example, the pattern "/abc/xyz/" "*" +** matches any files anywhere in the directory hierarchy beneath +** /abc/xyz. +** +** The glob algorithm works on bytes. Multi-byte UTF8 characters are +** matched as if each byte were a separate character. +** +** If the iLimit for a quota group is set to zero, then the quota group +** is disabled and will be deleted when the last database connection using +** the quota group is closed. +** +** Calling this routine on a zPattern that does not exist and with a +** zero iLimit is a no-op. +** +** A quota group must exist with a non-zero iLimit prior to opening +** database connections if those connections are to participate in the +** quota group. Creating a quota group does not affect database connections +** that are already open. +** +** The patterns that define the various quota groups should be distinct. +** If the same filename matches more than one quota group pattern, then +** the behavior of this package is undefined. +*/ +int sqlite3_quota_set( + const char *zPattern, /* The filename pattern */ + sqlite3_int64 iLimit, /* New quota to set for this quota group */ + void (*xCallback)( /* Callback invoked when going over quota */ + const char *zFilename, /* Name of file whose size increases */ + sqlite3_int64 *piLimit, /* IN/OUT: The current limit */ + sqlite3_int64 iSize, /* Total size of all files in the group */ + void *pArg /* Client data */ + ), + void *pArg, /* client data passed thru to callback */ + void (*xDestroy)(void*) /* Optional destructor for pArg */ +); + +/* +** Bring the named file under quota management, assuming its name matches +** the glob pattern of some quota group. Or if it is already under +** management, update its size. If zFilename does not match the glob +** pattern of any quota group, this routine is a no-op. +*/ +int sqlite3_quota_file(const char *zFilename); + +/* +** The following object serves the same role as FILE in the standard C +** library. It represents an open connection to a file on disk for I/O. +** +** A single quota_FILE should not be used by two or more threads at the +** same time. Multiple threads can be using different quota_FILE objects +** simultaneously, but not the same quota_FILE object. +*/ +typedef struct quota_FILE quota_FILE; + +/* +** Create a new quota_FILE object used to read and/or write to the +** file zFilename. The zMode parameter is as with standard library zMode. +*/ +quota_FILE *sqlite3_quota_fopen(const char *zFilename, const char *zMode); + +/* +** Perform I/O against a quota_FILE object. When doing writes, the +** quota mechanism may result in a short write, in order to prevent +** the sum of sizes of all files from going over quota. +*/ +size_t sqlite3_quota_fread(void*, size_t, size_t, quota_FILE*); +size_t sqlite3_quota_fwrite(void*, size_t, size_t, quota_FILE*); + +/* +** Flush all written content held in memory buffers out to disk. +** This is the equivalent of fflush() in the standard library. +** +** If the hardSync parameter is true (non-zero) then this routine +** also forces OS buffers to disk - the equivalent of fsync(). +** +** This routine return zero on success and non-zero if something goes +** wrong. +*/ +int sqlite3_quota_fflush(quota_FILE*, int hardSync); + +/* +** Close a quota_FILE object and free all associated resources. The +** file remains under quota management. +*/ +int sqlite3_quota_fclose(quota_FILE*); + +/* +** Move the read/write pointer for a quota_FILE object. Or tell the +** current location of the read/write pointer. +*/ +int sqlite3_quota_fseek(quota_FILE*, long, int); +void sqlite3_quota_rewind(quota_FILE*); +long sqlite3_quota_ftell(quota_FILE*); + +/* +** Delete a file from the disk, if that file is under quota management. +** Adjust quotas accordingly. +** +** If zFilename is the name of a directory that matches one of the +** quota glob patterns, then all files under quota management that +** are contained within that directory are deleted. +** +** A standard SQLite result code is returned (SQLITE_OK, SQLITE_NOMEM, etc.) +** When deleting a directory of files, if the deletion of any one +** file fails (for example due to an I/O error), then this routine +** returns immediately, with the error code, and does not try to +** delete any of the other files in the specified directory. +** +** All files are removed from quota management and deleted from disk. +** However, no attempt is made to remove empty directories. +** +** This routine is a no-op for files that are not under quota management. +*/ +int sqlite3_quota_remove(const char *zFilename); + +#ifdef __cplusplus +} /* end of the 'extern "C"' block */ +#endif +#endif /* _QUOTA_H_ */ ADDED test/quota-glob.test Index: test/quota-glob.test ================================================================== --- /dev/null +++ test/quota-glob.test @@ -0,0 +1,87 @@ +# 2011 December 1 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# +# Tests for the glob-style string compare operator embedded in the +# quota shim. +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +catch { unset testnum } +catch { unset pattern } +catch { unset text } +catch { unset ans } + +foreach {testnum pattern text ans} { + 1 abcdefg abcdefg 1 + 2 abcdefG abcdefg 0 + 3 abcdef abcdefg 0 + 4 abcdefgh abcdefg 0 + 5 abcdef? abcdefg 1 + 6 abcdef? abcdef 0 + 7 abcdef? abcdefgh 0 + 8 abcdefg abcdef? 0 + 9 abcdef? abcdef? 1 + 10 abc/def abc/def 1 + 11 abc//def abc/def 0 + 12 */abc/* x/abc/y 1 + 13 */abc/* /abc/ 1 + 16 */abc/* x///a/ab/abc 0 + 17 */abc/* x//a/ab/abc/ 1 + 16 */abc/* x///a/ab/abc 0 + 17 */abc/* x//a/ab/abc/ 1 + 18 **/abc/** x//a/ab/abc/ 1 + 19 *?/abc/*? x//a/ab/abc/y 1 + 20 ?*/abc/?* x//a/ab/abc/y 1 + 21 {abc[cde]efg} abcbefg 0 + 22 {abc[cde]efg} abccefg 1 + 23 {abc[cde]efg} abcdefg 1 + 24 {abc[cde]efg} abceefg 1 + 25 {abc[cde]efg} abcfefg 0 + 26 {abc[^cde]efg} abcbefg 1 + 27 {abc[^cde]efg} abccefg 0 + 28 {abc[^cde]efg} abcdefg 0 + 29 {abc[^cde]efg} abceefg 0 + 30 {abc[^cde]efg} abcfefg 1 + 31 {abc[c-e]efg} abcbefg 0 + 32 {abc[c-e]efg} abccefg 1 + 33 {abc[c-e]efg} abcdefg 1 + 34 {abc[c-e]efg} abceefg 1 + 35 {abc[c-e]efg} abcfefg 0 + 36 {abc[^c-e]efg} abcbefg 1 + 37 {abc[^c-e]efg} abccefg 0 + 38 {abc[^c-e]efg} abcdefg 0 + 39 {abc[^c-e]efg} abceefg 0 + 40 {abc[^c-e]efg} abcfefg 1 + 41 {abc[c-e]efg} abc-efg 0 + 42 {abc[-ce]efg} abc-efg 1 + 43 {abc[ce-]efg} abc-efg 1 + 44 {abc[][*?]efg} {abc]efg} 1 + 45 {abc[][*?]efg} {abc*efg} 1 + 46 {abc[][*?]efg} {abc?efg} 1 + 47 {abc[][*?]efg} {abc[efg} 1 + 48 {abc[^][*?]efg} {abc]efg} 0 + 49 {abc[^][*?]efg} {abc*efg} 0 + 50 {abc[^][*?]efg} {abc?efg} 0 + 51 {abc[^][*?]efg} {abc[efg} 0 + 52 {abc[^][*?]efg} {abcdefg} 1 + 53 {*[xyz]efg} {abcxefg} 1 + 54 {*[xyz]efg} {abcwefg} 0 +} { + do_test quota-glob-$testnum.1 { + sqlite3_quota_glob $::pattern $::text + } $::ans + do_test quota-glob-$testnum.2 { + sqlite3_quota_glob $::pattern [string map {/ \\} $::text] + } $::ans +} +finish_test Index: test/quota.test ================================================================== --- test/quota.test +++ test/quota.test @@ -46,10 +46,11 @@ # afterwards. Then close the database and successfully shut # down the quota system. # sqlite3_quota_initialize "" 1 +unset -nocomplain quota_request_ok proc quota_check {filename limitvar size} { upvar $limitvar limit lappend ::quota [set limit] $size if {[info exists ::quota_request_ok]} { set limit $size } ADDED test/quota2.test Index: test/quota2.test ================================================================== --- /dev/null +++ test/quota2.test @@ -0,0 +1,236 @@ +# 2011 December 1 +# +# The author disclaims copyright to this source code. In place of +# a legal notice, here is a blessing: +# +# May you do good and not evil. +# May you find forgiveness for yourself and forgive others. +# May you share freely, never taking more than you give. +# +#*********************************************************************** +# + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +source $testdir/malloc_common.tcl + +db close +sqlite3_quota_initialize "" 1 + +foreach dir {quota2a/x1 quota2a/x2 quota2a quota2b quota2c} { + file delete -force $dir +} +foreach dir {quota2a quota2a/x1 quota2a/x2 quota2b quota2c} { + file mkdir $dir +} + +# The standard_path procedure converts a pathname into a standard format +# that is the same across platforms. +# +unset -nocomplain ::quota_pwd ::quota_mapping +set ::quota_pwd [string map {\\ /} [pwd]] +set ::quota_mapping [list $::quota_pwd PWD] +proc standard_path {x} { + set x [string map {\\ /} $x] + return [string map $::quota_mapping $x] +} + +# The quota_check procedure is a callback from the quota handler. +# It has three arguments which are (1) the full pathname of the file +# that has gone over quota, (2) the quota limit, (3) the requested +# new quota size to cover the last write. These three values are +# appended to the global variable $::quota. The filename is processed +# to convert every \ character into / and to change the name of the +# working directory to PWD. +# +# The quota is increased to the request if the ::quota_request_ok +# global variable is true. +# +set ::quota {} +set ::quota_request_ok 0 + +proc quota_check {filename limitvar size} { + upvar $limitvar limit + lappend ::quota [standard_path $filename] [set limit] $size + if {$::quota_request_ok} {set limit $size} +} + +sqlite3_quota_set */quota2a/* 4000 quota_check +sqlite3_quota_set */quota2b/* 5000 quota_check + +unset -nocomplain bigtext +for {set i 1} {$i<=1000} {incr i} { + if {$i%10==0} { + append bigtext [format "%06d\n" $i] + } else { + append bigtext [format "%06d " $i] + } +} + +catch { unset h1 } +catch { unset x } +do_test quota2-1.1 { + set ::h1 [sqlite3_quota_fopen quota2a/xyz.txt w+b] + sqlite3_quota_fwrite $::h1 1 7000 $bigtext +} {4000} +do_test quota2-1.2 { + set ::quota +} {PWD/quota2a/xyz.txt 4000 7000} +do_test quota2-1.3 { + sqlite3_quota_rewind $::h1 + set ::x [sqlite3_quota_fread $::h1 1001 7] + string length $::x +} {3003} +do_test quota2-1.4 { + string match $::x [string range $::bigtext 0 3002] +} {1} +do_test quota2-1.5 { + sqlite3_quota_fseek $::h1 0 SEEK_END + sqlite3_quota_ftell $::h1 +} {4000} +do_test quota2-1.6 { + sqlite3_quota_fseek $::h1 -100 SEEK_END + sqlite3_quota_ftell $::h1 +} {3900} +do_test quota2-1.7 { + sqlite3_quota_fseek $::h1 -100 SEEK_CUR + sqlite3_quota_ftell $::h1 +} {3800} +do_test quota2-1.8 { + sqlite3_quota_fseek $::h1 50 SEEK_CUR + sqlite3_quota_ftell $::h1 +} {3850} +do_test quota2-1.9 { + sqlite3_quota_fseek $::h1 50 SEEK_SET + sqlite3_quota_ftell $::h1 +} {50} +do_test quota2-1.10 { + sqlite3_quota_rewind $::h1 + sqlite3_quota_ftell $::h1 +} {0} +do_test quota2-1.11 { + standard_path [sqlite3_quota_dump] +} {{*/quota2b/* 5000 0} {*/quota2a/* 4000 4000 {PWD/quota2a/xyz.txt 4000 1 0}}} +do_test quota2-1.12 { + sqlite3_quota_fclose $::h1 + standard_path [sqlite3_quota_dump] +} {{*/quota2b/* 5000 0} {*/quota2a/* 4000 4000 {PWD/quota2a/xyz.txt 4000 0 0}}} +do_test quota2-1.13 { + sqlite3_quota_remove quota2a/xyz.txt + standard_path [sqlite3_quota_dump] +} {{*/quota2b/* 5000 0} {*/quota2a/* 4000 0}} + + +set quota {} +do_test quota2-2.1 { + set ::h1 [sqlite3_quota_fopen quota2c/xyz.txt w+b] + sqlite3_quota_fwrite $::h1 1 7000 $bigtext +} {7000} +do_test quota2-2.2 { + set ::quota +} {} +do_test quota2-2.3 { + sqlite3_quota_rewind $::h1 + set ::x [sqlite3_quota_fread $::h1 1001 7] + string length $::x +} {6006} +do_test quota2-2.4 { + string match $::x [string range $::bigtext 0 6005] +} {1} +do_test quota2-2.5 { + sqlite3_quota_fseek $::h1 0 SEEK_END + sqlite3_quota_ftell $::h1 +} {7000} +do_test quota2-2.6 { + sqlite3_quota_fseek $::h1 -100 SEEK_END + sqlite3_quota_ftell $::h1 +} {6900} +do_test quota2-2.7 { + sqlite3_quota_fseek $::h1 -100 SEEK_CUR + sqlite3_quota_ftell $::h1 +} {6800} +do_test quota2-2.8 { + sqlite3_quota_fseek $::h1 50 SEEK_CUR + sqlite3_quota_ftell $::h1 +} {6850} +do_test quota2-2.9 { + sqlite3_quota_fseek $::h1 50 SEEK_SET + sqlite3_quota_ftell $::h1 +} {50} +do_test quota2-2.10 { + sqlite3_quota_rewind $::h1 + sqlite3_quota_ftell $::h1 +} {0} +do_test quota2-2.11 { + standard_path [sqlite3_quota_dump] +} {{*/quota2b/* 5000 0} {*/quota2a/* 4000 0}} +do_test quota2-2.12 { + sqlite3_quota_fclose $::h1 + standard_path [sqlite3_quota_dump] +} {{*/quota2b/* 5000 0} {*/quota2a/* 4000 0}} + +do_test quota2-3.1 { + sqlite3_quota_set */quota2b/* 0 quota_check + set ::h1 [sqlite3_quota_fopen quota2a/x1/a.txt a] + sqlite3_quota_fwrite $::h1 10 10 $bigtext +} {10} +do_test quota2-3.2 { + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 100 {PWD/quota2a/x1/a.txt 100 1 0}}} +do_test quota2-3.3a { + sqlite3_quota_fflush $::h1 0 + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 100 {PWD/quota2a/x1/a.txt 100 1 0}}} +do_test quota2-3.3b { + sqlite3_quota_fflush $::h1 1 + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 100 {PWD/quota2a/x1/a.txt 100 1 0}}} +do_test quota2-3.3c { + sqlite3_quota_fflush $::h1 + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 100 {PWD/quota2a/x1/a.txt 100 1 0}}} +do_test quota2-3.4 { + sqlite3_quota_fclose $::h1 + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 100 {PWD/quota2a/x1/a.txt 100 0 0}}} +do_test quota2-3.5 { + set ::h2 [sqlite3_quota_fopen quota2a/x2/b.txt a] + sqlite3_quota_fwrite $::h2 10 20 $bigtext + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 300 {PWD/quota2a/x2/b.txt 200 1 0} {PWD/quota2a/x1/a.txt 100 0 0}}} +do_test quota2-3.6 { + set ::h3 [sqlite3_quota_fopen quota2a/x1/c.txt a] + sqlite3_quota_fwrite $::h3 10 50 $bigtext + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 800 {PWD/quota2a/x1/c.txt 500 1 0} {PWD/quota2a/x2/b.txt 200 1 0} {PWD/quota2a/x1/a.txt 100 0 0}}} +do_test quota2-3.7 { + file exists quota2a/x1/a.txt +} {1} +do_test quota2-3.8 { + file exists quota2a/x2/b.txt +} {1} +do_test quota2-3.9 { + file exists quota2a/x1/c.txt +} {1} +do_test quota2-3.10 { + sqlite3_quota_remove quota2a/x1 + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 700 {PWD/quota2a/x1/c.txt 500 1 1} {PWD/quota2a/x2/b.txt 200 1 0}}} +do_test quota2-3.11 { + sqlite3_quota_fclose $::h2 + sqlite3_quota_fclose $::h3 + standard_path [sqlite3_quota_dump] +} {{*/quota2a/* 4000 200 {PWD/quota2a/x2/b.txt 200 0 0}}} +do_test quota2-3.12 { + file exists quota2a/x1/a.txt +} {0} +do_test quota2-3.13 { + file exists quota2a/x2/b.txt +} {1} +do_test quota2-3.14 { + file exists quota2a/x1/c.txt +} {0} + +catch { sqlite3_quota_shutdown } +catch { unset quota_request_ok } +finish_test