From: stephan Date: Tue, 25 Nov 2025 18:52:08 +0000 (+0000) Subject: Improve kvvfs file name validation. Add sqlite3.kvvfs.unlink(). X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f0ca498c6120282f0881108288b24faaf6b44085;p=thirdparty%2Fsqlite.git Improve kvvfs file name validation. Add sqlite3.kvvfs.unlink(). FossilOrigin-Name: 0dfdf4681cf63541de971a20be21b33d0d3b38e8281f302d20aca9492df3da42 --- diff --git a/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js index 6bf7371cb1..e0146deda4 100644 --- a/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js +++ b/ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js @@ -100,10 +100,17 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ toss3 = util.toss3, hop = (o,k)=>Object.prototype.hasOwnProperty.call(o,k); + const kvvfsMethods = new sqlite3_kvvfs_methods( + /* Wraps the static sqlite3_api_methods singleton */ + wasm.exports.sqlite3__wasm_kvvfs_methods() + ); + util.assert( 32<=kvvfsMethods.$nKeySize, "unexpected kvvfsMethods.$nKeySize: "+kvvfsMethods.$nKeySize); + const cache = Object.assign(Object.create(null),{ - rxJournalSuffix: /-journal$/, // TOOD: lazily init once we figure out where + rxJournalSuffix: /-journal$/, zKeyJrnl: wasm.allocCString("jrnl"), - zKeySz: wasm.allocCString("sz") + zKeySz: wasm.allocCString("sz"), + keySize: kvvfsMethods.$nKeySize }); const debug = sqlite3.__isUnderTest @@ -185,6 +192,33 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ */ const kvvfsKeyPrefix = (v)=>kvvfsIsPersistentName(v) ? 'kvvfs-'+v+'-' : ''; + /** + Throws if storage name n is not valid for use as a storage name. + This is intended for the high-level APIs, not the low-level ones. + */ + const validateStorageName = function(n,mayBeJournal=false){ + let maxLen = cache.keySize - 1; + if( cache.rxJournalSuffix.test(n) ){ + if( !mayBeJournal ){ + toss3(capi.SQLITE_MISUSE, "Storage names may not have a '-journal' suffix."); + } + }else{ + maxLen -= 8 /* "-journal" */; + } + const len = n.length; + if( len > maxLen ){ + toss3(capi.SQLITE_RANGE, "Storage name is too long."); + } + let i; + for( i = 0; i < len; ++i ){ + const ch = n.codePointAt(i); + if( ch<45 || (ch & 0x80) ){ + toss3(capi.SQLITE_RANGE, + "Illegal character ("+ch+"d) in storage name:",n); + } + } + }; + /** Create a new instance of the objects which go into cache.storagePool. @@ -203,7 +237,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ /** deleteAtRefc0 objects will be removed by xClose() when refc reaches 0. The others will persist, to give the illusion of - real back-end storage. Managed by xOpen(). + real back-end storage. Managed by xOpen(). By default this is + false but the delete-on-close=1 flag can be used to set this to + true. */ deleteAtRefc0: false, /** @@ -227,9 +263,38 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ xOpen() and xClose(). */ files: [], + /** + A list of objects with various event callbacks. See + sqlite3_js_kvvfs_listen(). + */ listeners: [] }); + /** + Deletes the cache.storagePool entries for store and its + db/journal counterpart. + */ + const deleteStorage = function(store){ + const other = cache.rxJournalSuffix.test(store.jzClass) + ? store.jzClass.replace(cache.rxJournalSuffix,'') + : store.jzClass+'-journal'; + debug("cleaning up storage handles [", store.jzClass, other,"]",store); + delete cache.storagePool[store.jzClass]; + delete cache.storagePool[other]; + if( !sqlite3.__isUnderTest ){ + /* In test runs, leave these for inspection. If we delete them here, + any prior dumps of them emitted via the console get cleared out + because the console shows live objects instead of call-time + static dumps. */ + delete store.storage; + delete store.refc; + } + }; + + /** + Add both store.jzClass and store.jzClass+"-journal" + to cache,storagePool. + */ const installStorageAndJournal = (store)=> cache.storagePool[store.jzClass] = cache.storagePool[store.jzClass+'-journal'] = store; @@ -248,6 +313,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }); if( globalThis.Storage ){ + /* If available, install local/session storage. */ if( globalThis.localStorage instanceof globalThis.Storage ){ cache.storagePool.local = newStorageObj('local', globalThis.localStorage); } @@ -256,6 +322,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } } + /* Add "-journal" twins for each cache.storagePool entry... */ for(const k of Object.keys(cache.storagePool)){ /* Journals in kvvfs are are stored as individual records within their Storage-ish object, named "{storage.keyPrefix}jrnl". We @@ -293,7 +360,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 'delete': key */ - const notifyListners = async function(eventName,store,...args){ + const notifyListeners = async function(eventName,store,...args){ store.listeners.forEach((v)=>{ const f = v?.[eventName]; if( !f ) return; @@ -410,10 +477,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ const originalIoMethods = (kvvfsFile)=> originalMethods[kvvfsFile.$isJournal ? 'ioJrnl' : 'ioDb']; - const kvvfsMethods = new sqlite3_kvvfs_methods( - /* Wraps the static sqlite3_api_methods singleton */ - wasm.exports.sqlite3__wasm_kvvfs_methods() - ); const pVfs = new capi.sqlite3_vfs(kvvfsMethods.$pVfs); const pIoDb = new capi.sqlite3_io_methods(kvvfsMethods.$pIoDb); const pIoJrnl = new capi.sqlite3_io_methods(kvvfsMethods.$pIoJrnl); @@ -519,7 +582,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ const jxKey = wasm.cstrToJs(zXKey); const jData = wasm.cstrToJs(zData); store.storage.setItem(jxKey, jData); - notifyListners('write', store, jxKey, jData); + notifyListeners('write', store, jxKey, jData); return 0; }catch(e){ error("kvrecordWrite()",e); @@ -534,7 +597,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ //if(!zXKey) return capi.SQLITE_NOMEM; const jxKey = wasm.cstrToJs(zXKey); store.storage.removeItem(jxKey); - notifyListners('delete', store, jxKey); + notifyListeners('delete', store, jxKey); return 0; }catch(e){ error("kvrecordDelete()",e); @@ -557,24 +620,10 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ if( !zName ){ zName = (cache.zEmpty ??= wasm.allocCString("")); } - const n = wasm.cstrlen(zName); - if( !n ){ - toss3(capi.SQLITE_RANGE, - "Storage name may not be empty (backwards", - "compatibilty constraint)"); - }else if( n > kvvfsMethods.$nKeySize - 8 /*"-journal"*/ - 1 ){ - toss3(capi.SQLITE_RANGE, - "Storage name is too long:", wasm.cstrToJs(zName)); - } - let i = 0; - for( ; i < n; ++i ){ - const ch = wasm.peek8(wasm.ptr.add(zName, i)); - if( ch < 45 || (ch & 0x80) ){ - toss3(capi.SQLITE_RANGE, - "Illegal character ("+ch+"d) in storage name."); - } - } const jzClass = wasm.cstrToJs(zName); + validateStorageName(jzClass, true); + util.assert( jzClass.length===wasm.cstrlen(zName), + "ASCII-only validation failed" ); if( (flags & (capi.SQLITE_OPEN_MAIN_DB | capi.SQLITE_OPEN_TEMP_DB | capi.SQLITE_OPEN_TRANSIENT_DB)) @@ -586,13 +635,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ flags, pOutFlags); if( rc ) return rc; let deleteAt0 = false; - if(n && wasm.isPtr(zName)){ + if(wasm.isPtr(arguments[1]/*original zName*/)){ if(capi.sqlite3_uri_boolean(zName, "delete-on-close", 0)){ deleteAt0 = true; - //warn("transient=",deleteAt0); - } - if(capi.sqlite3_uri_boolean(zName, "wipe-before-open", 0)){ - // TODO? } } const f = new KVVfsFile(pProtoFile); @@ -619,7 +664,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ debug("xOpen installed storage handle [",nm, nm+"-journal","]", s); } pFileHandles.set(pProtoFile, {storage: s, file: f, jzClass}); - notifyListners('open', s, s.files.length); + notifyListeners('open', s, s.files.length); return 0; }catch(e){ warn("xOpen:",e); @@ -753,20 +798,11 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ const s = storageForZClass(h.jzClass); s.files = s.files.filter((v)=>v!==h.file); if( --s.refc<=0 && s.deleteAtRefc0 ){ - const other = h.file.$isJournal - ? h.jzClass.replace(cache.rxJournalSuffix,'') - : h.jzClass+'-journal'; - debug("cleaning up storage handles [", h.jzClass, other,"]",s); - delete cache.storagePool[h.jzClass]; - delete cache.storagePool[other]; - if( !sqlite3.__isUnderTest ){ - delete s.storage; - delete s.refc; - } + deleteStorage(s); } originalIoMethods(h.file).xClose(pFile); h.file.dispose(); - notifyListners('close', s, s.files.length); + notifyListeners('close', s, s.files.length); }else{ /* Can happen if xOpen fails */ } @@ -901,7 +937,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ - It accepts an arbitrary storage name. In v1 this was a silent no-op for any names other than ('local','session',''). - - The second argument was added. + - The second argument was added. Its default value reflects the + legacy behavior. - It throws if a db currently has the storage opened. That version 1 did not throw for this case was due to an architectural @@ -933,7 +970,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }; /** - This routine guesses the approximate amount of + This routine estimates the approximate amount of storage used by the given kvvfs back-end. Its arguments are as documented for sqlite3_js_kvvfs_clear(), @@ -967,20 +1004,6 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ return sz * 2 /* because JS uses 2-byte char encoding */; }; - /** - Throws if storage name n is not valid for use as a storage name. - This is intended for the high-level APIs, not the low-level ones. - */ - const validateStorageName = function(n){ - if( cache.rxJournalSuffix.test(n) ){ - toss3(capi.SQLITE_MISUSE, "Storage names may not have a '-journal' suffix."); - } - if( n.length>23 ){ - toss3(capi.SQLITE_RANGE, "Storage name is too long."); - } - // TODO: check all of kvvfs's name constraints - }; - /** Copies the entire contents of the given transient storage object into a JSON-friendly form. The returned object is structured as @@ -1066,7 +1089,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }/* sqlite3_js_kvvfs_export */; /** - INCOMPLETE. This interface is subject to change. + EXPERIMENTAL. This interface is subject to change. The counterpart of sqlite3_js_kvvfs_export(). Its argument must be the result of that function() or @@ -1143,6 +1166,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }; /** + EXPERIMENTAL. This interface is subject to change. + If no kvvfs storage exists with the given name, one is installed. If one exists, its reference count is increased so that it won't be freed by the closing of a database or journal @@ -1161,6 +1186,42 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }; /** + Conditionally "unlinks" a kvvfs storage object, reducing its + reference count by 1. + + This is a no-op if name ends in "-journal" or refers to a + built-in storage object ('local', 'session', or 'localThread'). + + It will not lower the refcount below the number of + currently-opened db/journal files for the storage (so that it + cannot delete it out from under them). + + If the refcount reaches 0 then the storage object is + removed. + + Returns true if it reduces the refcount, else false. A result of + true does not necessarily mean that the storage unit was removed, + just that its refcount was lowered. + */ + const sqlite3_js_kvvfs_unlink = function(name){ + const store = storageForZClass(name); + if( !store + || kvvfsIsPersistentName(store.jzClass) + || 'localThread'===store.jzClass + || cache.rxJournalSuffix.test(name) ) return false; + if( store.refc > store.files.length || 0===store.files.length ){ + if( --store.refc<=0 ){ + /* Ignoring deleteAtRefc0 for an explicit unlink */ + deleteStorage(store); + } + return true; + } + return false; + }; + + /** + EXPERIMENTAL. This interface is subject to change. + Adds an event listener to a kvvfs storage object. The idea is that this can be used to asynchronously back up one kvvfs storage object to another or another channel entirely. (The caveat in the @@ -1172,7 +1233,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ - storage: the name of the kvvfs storage object. - reserve [=false]: if true, sqlite3_js_kvvfs_reserve() is used - to ensure that the storage exists. + to ensure that the storage exists if it does not already. + If this is false and the storage does not exist then an + exception is thrown. - events: an object which may have any of the following callback function properties: open, close, write, delete. @@ -1198,6 +1261,27 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ were written (both strings). - 'delete' gets the string-type key of the deleted record. + + Passing the same object ot sqlite3_js_kvvfs_unlisten() will + remove the listener. + + The arguments to 'write' and 'delete' are in one of the following + forms: + + - 'sz' = the unencoded db size as a string + + - 'jrnl' = the current db journal as a string + + - '[1-9][0-9]*' (a db page number) = an encoded db page + + For 'local' and 'session' storage, all of those keys have a + prefix of 'kvvfs-local-' resp. 'kvvfs-session-'. This is required + both for backwards compatibility and to enable dbs in those + storage objects to coexit with client data. Other storage objects + do not have a prefix. + + Design note: JS has StorageEvents but only in the main thread, + which is why the listeners are not based on that. */ const sqlite3_js_kvvfs_listen = function(opt){ if( !opt || 'object'!==typeof opt ){ @@ -1221,7 +1305,9 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }; /** - Removes all kvvfs event listeners for the given options + EXPERIMENTAL. This interface is subject to change. + + Removes the kvvfs event listeners for the given options object. It must be passed the same object instance which was passed to sqlite3_js_kvvfs_listen(). @@ -1235,7 +1321,26 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ } }; - if(sqlite3?.oo1?.DB){ + /** + Public interface for kvvfs v2. The capi.sqlite3_js_kvvfs_...() + routines remain in place for v1. Some members of this class proxy + to those functions but use different default argument values in + some cases. + */ + sqlite3.kvvfs = Object.assign(Object.create(null),{ + reserve: sqlite3_js_kvvfs_reserve, + import: sqlite3_js_kvvfs_import, + export: sqlite3_js_kvvfs_export, + unlink: sqlite3_js_kvvfs_unlink, + listen: sqlite3_js_kvvfs_listen, + unlisten: sqlite3_js_kvvfs_unlisten, + exists: (name)=>!!storageForZClass(name), + // DIFFERENT DEFAULTS for the arguments: + size: (which,emptyIsAName=true)=>capi.sqlite3_js_kvvfs_size(which,emptyIsAName), + clear: (which,emptyIsAName=true)=>capi.sqlite3_js_kvvfs_clear(which,emptyIsAName), + }); + + if(sqlite3.oo1?.DB){ /** Functionally equivalent to DB(storageName,'c','kvvfs') except that it throws if the given storage name is not one of 'local' @@ -1265,6 +1370,8 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ case ":sessionStorage:": opt.filename = 'session'; break; case ":localStorage:": opt.filename = 'local'; break; } + const m = /(file:(\/\/)?)([^?]+)/.exec(opt.filename); + validateStorageName( m ? m[3] : opt.filename); DB.dbCtorHelper.call(this, opt); }; sqlite3.oo1.JsStorageDb.defaultStorageName = 'session'; @@ -1297,22 +1404,5 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ }/* __isUnderTest */ }/*sqlite3.oo1.JsStorageDb*/ - /** - Public interface for kvvfs v2. The capi.sqlite3_js_kvvfs_...() - routines remain in place for v1. Some members of this class proxy - to those functions but use different default argument values in - some cases. - */ - sqlite3.kvvfs = Object.assign(Object.create(null),{ - reserve: sqlite3_js_kvvfs_reserve, - import: sqlite3_js_kvvfs_import, - export: sqlite3_js_kvvfs_export, - listen: sqlite3_js_kvvfs_listen, - unlisten: sqlite3_js_kvvfs_unlisten, - // DIFFERENT DEFAULTS for the second arguments: - size: (which,emptyIsAName=true)=>capi.sqlite3_js_kvvfs_size(which,emptyIsAName), - clear: (which,emptyIsAName=true)=>capi.sqlite3_js_kvvfs_clear(which,emptyIsAName), - }); - })/*globalThis.sqlite3ApiBootstrap.initializers*/; //#endif not omit-kvvfs diff --git a/ext/wasm/tester1.c-pp.js b/ext/wasm/tester1.c-pp.js index 9f9ed7e2e3..d9d7430ed5 100644 --- a/ext/wasm/tester1.c-pp.js +++ b/ext/wasm/tester1.c-pp.js @@ -3018,6 +3018,14 @@ globalThis.sqlite3InitModule = sqlite3InitModule; db and SQLite is not calling xGetLastError() to fetch the error string. */ }, capi.SQLITE_RANGE); + T.mustThrowMatching(()=>{ + new JDb("012345678901234567890123"/*too long*/); + }, capi.SQLITE_RANGE); + { + const name = "01234567890123456789012" /* max name length */; + (new JDb(name)).close(); + T.assert( sqlite3.kvvfs.unlink(name) ); + } try { const exportDb = sqlite3.kvvfs.export; diff --git a/manifest b/manifest index 7103ec9a3e..642f1872bf 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Add\sthe\ssqlite3.kvvfs\snamespace\sfor\sthe\snew\skvvfs\sAPIs\sinstead\sof\sadding\smore\ssqlite3_js_kvvfs_...()\smethods.\sReinstate\sthat\sclearing\skvvfs\sstorage\sis\sillegal\swhen\sdb\shandles\sare\sopened,\ssolely\sfor\ssanity's\ssake\s(they\scan\sactually\srecover\sfrom\sthat\sbut\ssupporting\ssuch\suse\sfeels\sill-advised). -D 2025-11-25T16:41:56.975 +C Improve\skvvfs\sfile\sname\svalidation.\sAdd\ssqlite3.kvvfs.unlink(). +D 2025-11-25T18:52:08.177 F .fossil-settings/binary-glob 61195414528fb3ea9693577e1980230d78a1f8b0a54c78cf1b9b24d0a409ed6a x F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea @@ -600,7 +600,7 @@ F ext/wasm/api/sqlite3-api-worker1.c-pp.js 1041dd645e8e821c082b628cd8d9acf70c667 F ext/wasm/api/sqlite3-license-version-header.js 0c807a421f0187e778dc1078f10d2994b915123c1223fe752b60afdcd1263f89 F ext/wasm/api/sqlite3-opfs-async-proxy.js 9654b565b346dc609b75d15337f20acfa7af7d9d558da1afeb9b6d8eaa404966 F ext/wasm/api/sqlite3-vfs-helper.c-pp.js 3f828cc66758acb40e9c5b4dcfd87fd478a14c8fb7f0630264e6c7fa0e57515d -F ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js c9dcbfb0aca85c11491427c9980174294022a0982f6aaf8c6a8374fa5ca76c83 +F ext/wasm/api/sqlite3-vfs-kvvfs.c-pp.js bd901a57c958a31bbbcaecdb7993467161f38e72b8b5e8c8eaaa898a078b85ea F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js a2eea6442556867b589e04107796c6e1d04a472219529eeb45b7cd221d7d048b F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 88ce2078267a2d1af57525a32d896295f4a8db7664de0e17e82dc9ff006ed8d3 F ext/wasm/api/sqlite3-vtab-helper.c-pp.js 9097074724172e31e56ce20ccd7482259cf72a76124213cbc9469d757676da86 @@ -647,7 +647,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555 F ext/wasm/test-opfs-vfs.js 1618670e466f424aa289859fe0ec8ded223e42e9e69b5c851f809baaaca1a00c F ext/wasm/tester1-worker.c-pp.html 0e432ec2c0d99cd470484337066e8d27e7aee4641d97115338f7d962bf7b081a F ext/wasm/tester1.c-pp.html 52d88fe2c6f21a046030a36410b4839b632f4424028197a45a3d5669ea724ddb -F ext/wasm/tester1.c-pp.js 5030080f96b0f85ce78e3937fffd51e2a54b32d1fd7e6d3bce6c6bf7dcc5646a +F ext/wasm/tester1.c-pp.js 87a6276e7f5970d5c6bd51a00972ee22230ada535793518b90f3524969592a56 F ext/wasm/tests/opfs/concurrency/index.html 657578a6e9ce1e9b8be951549ed93a6a471f4520a99e5b545928668f4285fb5e F ext/wasm/tests/opfs/concurrency/test.js d08889a5bb6e61937d0b8cbb78c9efbefbf65ad09f510589c779b7cc6a803a88 F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2 @@ -2178,8 +2178,8 @@ F tool/version-info.c 33d0390ef484b3b1cb685d59362be891ea162123cea181cb8e6d2cf6dd F tool/warnings-clang.sh bbf6a1e685e534c92ec2bfba5b1745f34fb6f0bc2a362850723a9ee87c1b31a7 F tool/warnings.sh d924598cf2f55a4ecbc2aeb055c10bd5f48114793e7ba25f9585435da29e7e98 F tool/win/sqlite.vsix deb315d026cc8400325c5863eef847784a219a2f -P 2bf31ef8027a3e15887d4dcd26fe09463b5f8852c5ce443f7d07c23d29c37311 -R 30d74c08232d6eedb21ecc8d5c58c72f +P 02793c5905e6b99379cd5ad6bfe1eb6cccf839da081fc174dce7b06245e212fb +R ed6be124fc5c288abe328108bd2f7a96 U stephan -Z 2911ac5a013b42fb37b269f1bdfc6178 +Z 630b67750a1abe72dfc01e3136fc3599 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index c62c523ce7..0c5819bac0 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -02793c5905e6b99379cd5ad6bfe1eb6cccf839da081fc174dce7b06245e212fb +0dfdf4681cf63541de971a20be21b33d0d3b38e8281f302d20aca9492df3da42