Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "sql/mojo/enable_mojo_fs.h" | |
| 6 | |
| 7 #include "base/logging.h" | |
| 8 #include "base/rand_util.h" | |
| 9 #include "components/filesystem/public/interfaces/file.mojom.h" | |
| 10 #include "components/filesystem/public/interfaces/file_system.mojom.h" | |
| 11 #include "components/filesystem/public/interfaces/types.mojom.h" | |
| 12 #include "mojo/public/cpp/bindings/lib/template_util.h" | |
| 13 #include "mojo/util/capture_util.h" | |
| 14 #include "third_party/sqlite/sqlite3.h" | |
| 15 | |
| 16 using mojo::Capture; | |
| 17 | |
| 18 namespace sql { | |
| 19 namespace { | |
| 20 | |
| 21 // Implementation of the sqlite3 Mojo proxying vfs. | |
| 22 // | |
| 23 // This is a bunch of C callback objects which transparently proxy sqlite3's | |
| 24 // filesystem reads/writes over the mojo:filesystem service. The main | |
| 25 // entrypoint is sqlite3_mojovfs(), which proxies all the file open/delete/etc | |
| 26 // operations. mojo:filesystem has support for passing a raw file descriptor | |
| 27 // over the IPC barrier, and most of the implementation of sqlite3_io_methods | |
| 28 // is derived from the default sqlite3 unix VFS and operates on the raw file | |
| 29 // descriptors. | |
| 30 | |
| 31 const int kMaxPathName = 512; | |
| 32 | |
| 33 // Additional data that we store in the sqlite3_vfs's |pAppData| member. | |
|
Scott Hess - ex-Googler
2015/06/17 19:32:58
A weird thought just occurred to me. Could pAppDa
Elliot Glaysher
2015/06/17 21:59:34
Done.
| |
| 34 struct MojoVFSData { | |
| 35 // The default vfs at the time MojoVFS was installed. We use the to pass | |
| 36 // through things like randomness requests and per-platform sleep calls. | |
| 37 sqlite3_vfs* parent; | |
| 38 | |
| 39 // When we initialize the subsystem, we are given a filesystem::Directory | |
| 40 // object, which is the root directory of a mojo:filesystem. All access to | |
| 41 // various files are specified from this root directory. | |
| 42 filesystem::DirectoryPtr root_directory; | |
| 43 }; | |
| 44 | |
| 45 sqlite3_vfs* GetParentVFS(sqlite3_vfs* mojo_vfs) { | |
| 46 return reinterpret_cast<MojoVFSData*>(mojo_vfs->pAppData)->parent; | |
| 47 } | |
| 48 | |
| 49 filesystem::DirectoryPtr& GetRootDirectory(sqlite3_vfs* mojo_vfs) { | |
| 50 return reinterpret_cast<MojoVFSData*>(mojo_vfs->pAppData)->root_directory; | |
| 51 } | |
|
Scott Hess - ex-Googler
2015/06/17 19:32:58
Why is this reinterpret_cast<> and GetDirectory()
Elliot Glaysher
2015/06/17 21:59:35
You can static_cast a void pointer to anything. Yo
| |
| 52 | |
| 53 // A struct which extends the base sqlite3_file to also hold on to a file | |
| 54 // pipe. We reinterpret_cast our sqlite3_file structs to this struct | |
| 55 // instead. This is "safe" because this struct is really just a slab of | |
| 56 // malloced memory, of which we tell sqlite how large we want with szOsFile. | |
| 57 struct MojoVFSFile { | |
| 58 // The "vtable" of our sqlite3_file "subclass". | |
| 59 sqlite3_file base; | |
| 60 | |
| 61 // We keep an open pipe to the File object to keep it from cleaning itself | |
| 62 // up. | |
| 63 filesystem::FilePtr file_ptr; | |
| 64 }; | |
| 65 | |
| 66 filesystem::FilePtr& GetFSFile(sqlite3_file* vfs_file) { | |
| 67 return reinterpret_cast<MojoVFSFile*>(vfs_file)->file_ptr; | |
| 68 } | |
| 69 | |
| 70 int MojoVFSClose(sqlite3_file* file) { | |
| 71 DVLOG(1) << "MojoVFSClose(*)"; | |
| 72 using mojo::InterfacePtr; | |
| 73 GetFSFile(file).~InterfacePtr<filesystem::File>(); | |
| 74 return SQLITE_OK; | |
| 75 } | |
| 76 | |
| 77 int MojoVFSRead(sqlite3_file* sql_file, | |
| 78 void* buffer, | |
| 79 int size, | |
| 80 sqlite3_int64 offset) { | |
| 81 DVLOG(1) << "MojoVFSRead (" << size << " @ " << offset << ")"; | |
| 82 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 83 mojo::Array<uint8_t> mojo_data; | |
| 84 GetFSFile(sql_file)->Read(size, offset, filesystem::WHENCE_FROM_BEGIN, | |
| 85 Capture(&error, &mojo_data)); | |
| 86 GetFSFile(sql_file).WaitForIncomingResponse(); | |
| 87 | |
| 88 if (error != filesystem::FILE_ERROR_OK) { | |
| 89 // TODO(erg): Better implementation here. | |
| 90 NOTIMPLEMENTED(); | |
| 91 return SQLITE_IOERR_READ; | |
| 92 } | |
| 93 | |
| 94 if (mojo_data.size() == static_cast<size_t>(size)) { | |
| 95 memcpy(buffer, &mojo_data.front(), mojo_data.size()); | |
| 96 return SQLITE_OK; | |
| 97 } | |
| 98 | |
| 99 // We didn't read the entire buffer. We need to copy the data AND zero fill | |
| 100 // the rest of the buffer. | |
| 101 if (mojo_data.size()) | |
| 102 memcpy(buffer, &mojo_data.front(), mojo_data.size()); | |
|
Scott Hess - ex-Googler
2015/06/17 19:32:58
Pull this out for sharing before the full-read tes
Elliot Glaysher
2015/06/17 21:59:34
Done, but we still have to check mojo_data.size()
Scott Hess - ex-Googler
2015/06/19 20:48:01
Acknowledged.
| |
| 103 memset(reinterpret_cast<char*>(buffer) + mojo_data.size(), 0, | |
| 104 size - mojo_data.size()); | |
| 105 | |
| 106 return SQLITE_IOERR_SHORT_READ; | |
| 107 } | |
| 108 | |
| 109 int MojoVFSWrite(sqlite3_file* sql_file, | |
| 110 const void* buffer, | |
| 111 int size, | |
| 112 sqlite_int64 offset) { | |
| 113 DVLOG(1) << "MojoVFSWrite(*, " << size << ", " << offset << ")"; | |
| 114 mojo::Array<uint8_t> mojo_data(size); | |
| 115 memcpy(&mojo_data.front(), buffer, size); | |
| 116 | |
| 117 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 118 uint32_t num_bytes_written = 0; | |
| 119 GetFSFile(sql_file)->Write(mojo_data.Pass(), offset, | |
| 120 filesystem::WHENCE_FROM_BEGIN, | |
| 121 Capture(&error, &num_bytes_written)); | |
| 122 GetFSFile(sql_file).WaitForIncomingResponse(); | |
| 123 if (error != filesystem::FILE_ERROR_OK) { | |
| 124 // TODO(erg): Better implementation here. | |
| 125 NOTIMPLEMENTED(); | |
| 126 return SQLITE_IOERR_WRITE; | |
| 127 } | |
| 128 if (num_bytes_written != static_cast<uint32_t>(size)) { | |
| 129 NOTIMPLEMENTED(); | |
| 130 return SQLITE_IOERR_WRITE; | |
| 131 } | |
| 132 | |
| 133 return SQLITE_OK; | |
| 134 } | |
| 135 | |
| 136 int MojoVFSTruncate(sqlite3_file* sql_file, sqlite_int64 size) { | |
| 137 DVLOG(1) << "MojoVFSTruncate(*, " << size << ")"; | |
| 138 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 139 GetFSFile(sql_file)->Truncate(size, Capture(&error)); | |
| 140 GetFSFile(sql_file).WaitForIncomingResponse(); | |
| 141 if (error != filesystem::FILE_ERROR_OK) { | |
| 142 // TODO(erg): Better implementation here. | |
| 143 NOTIMPLEMENTED(); | |
| 144 return SQLITE_IOERR_TRUNCATE; | |
| 145 } | |
| 146 | |
| 147 return SQLITE_OK; | |
| 148 } | |
| 149 | |
| 150 int MojoVFSSync(sqlite3_file* sql_file, int flags) { | |
| 151 DVLOG(1) << "MojoVFSSync(*, " << flags << ")"; | |
| 152 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 153 GetFSFile(sql_file)->Flush(Capture(&error)); | |
| 154 GetFSFile(sql_file).WaitForIncomingResponse(); | |
| 155 if (error != filesystem::FILE_ERROR_OK) { | |
| 156 // TODO(erg): Better implementation here. | |
| 157 NOTIMPLEMENTED(); | |
| 158 return SQLITE_IOERR_FSYNC; | |
| 159 } | |
| 160 | |
| 161 return SQLITE_OK; | |
| 162 } | |
| 163 | |
| 164 int MojoVFSFileSize(sqlite3_file* sql_file, sqlite_int64* size) { | |
| 165 DVLOG(1) << "MojoVFSFileSize(*)"; | |
| 166 | |
| 167 filesystem::FileError err = filesystem::FILE_ERROR_FAILED; | |
| 168 filesystem::FileInformationPtr file_info; | |
| 169 GetFSFile(sql_file)->Stat(Capture(&err, &file_info)); | |
| 170 GetFSFile(sql_file).WaitForIncomingResponse(); | |
| 171 | |
| 172 if (err != filesystem::FILE_ERROR_OK) { | |
| 173 // TODO(erg): Better implementation here. | |
| 174 NOTIMPLEMENTED(); | |
| 175 return SQLITE_IOERR_FSTAT; | |
| 176 } | |
| 177 | |
| 178 *size = file_info->size; | |
| 179 return SQLITE_OK; | |
| 180 } | |
| 181 | |
| 182 // TODO(erg): The current base::File interface isn't sufficient to handle | |
| 183 // sqlite's locking primitives, which are done on byte ranges in the file. (See | |
| 184 // "File Locking Notes" in sqlite3.c.) | |
| 185 int MojoVFSLock(sqlite3_file* pFile, int eLock) { | |
| 186 DVLOG(1) << "MojoVFSLock(*, " << eLock << ")"; | |
| 187 return SQLITE_OK; | |
| 188 } | |
| 189 int MojoVFSUnlock(sqlite3_file* pFile, int eLock) { | |
| 190 DVLOG(1) << "MojoVFSUnlock(*, " << eLock << ")"; | |
| 191 return SQLITE_OK; | |
| 192 } | |
| 193 int MojoVFSCheckReservedLock(sqlite3_file* pFile, int* pResOut) { | |
| 194 DVLOG(1) << "MojoVFSCheckReservedLock(*)"; | |
| 195 *pResOut = 0; | |
| 196 return SQLITE_OK; | |
| 197 } | |
| 198 | |
| 199 // TODO(erg): This is the minimal implementation to get a few tests passing; | |
| 200 // lots more needs to be done here. | |
| 201 int MojoVFSFileControl(sqlite3_file* pFile, int op, void* pArg) { | |
| 202 DVLOG(1) << "MojoVFSFileControl(*, " << op << ", *)"; | |
| 203 if (op == SQLITE_FCNTL_PRAGMA) { | |
| 204 // Returning NOTFOUND tells sqlite that we aren't doing any processing. | |
| 205 return SQLITE_NOTFOUND; | |
| 206 } | |
| 207 | |
| 208 return SQLITE_OK; | |
| 209 } | |
| 210 | |
| 211 int MojoVFSSectorSize(sqlite3_file* pFile) { | |
| 212 DVLOG(1) << "MojoVFSSectorSize(*)"; | |
| 213 // Use the default sector size. | |
| 214 return 0; | |
| 215 } | |
| 216 | |
| 217 int MojoVFSDeviceCharacteristics(sqlite3_file* pFile) { | |
| 218 DVLOG(1) << "MojoVFSDeviceCharacteristics(*)"; | |
| 219 NOTIMPLEMENTED(); | |
| 220 return 0; | |
| 221 } | |
| 222 | |
| 223 static sqlite3_io_methods mojo_vfs_io_methods = { | |
| 224 1, /* iVersion */ | |
| 225 MojoVFSClose, /* xClose */ | |
| 226 MojoVFSRead, /* xRead */ | |
| 227 MojoVFSWrite, /* xWrite */ | |
| 228 MojoVFSTruncate, /* xTruncate */ | |
| 229 MojoVFSSync, /* xSync */ | |
| 230 MojoVFSFileSize, /* xFileSize */ | |
| 231 MojoVFSLock, /* xLock */ | |
| 232 MojoVFSUnlock, /* xUnlock */ | |
| 233 MojoVFSCheckReservedLock, /* xCheckReservedLock */ | |
| 234 MojoVFSFileControl, /* xFileControl */ | |
| 235 MojoVFSSectorSize, /* xSectorSize */ | |
| 236 MojoVFSDeviceCharacteristics, /* xDeviceCharacteristics */ | |
| 237 }; | |
| 238 | |
| 239 int MojoVFSOpen(sqlite3_vfs* mojo_vfs, | |
| 240 const char* name, | |
| 241 sqlite3_file* file, | |
| 242 int flags, | |
| 243 int* pOutFlags) { | |
| 244 DVLOG(1) << "MojoVFSOpen(*, " << name << ", *, " << flags << ")"; | |
| 245 int open_flags = 0; | |
| 246 if (flags & SQLITE_OPEN_EXCLUSIVE) { | |
| 247 DCHECK(flags & SQLITE_OPEN_CREATE); | |
| 248 open_flags = filesystem::kFlagCreate; | |
| 249 } else if (flags & SQLITE_OPEN_CREATE) { | |
| 250 DCHECK(flags & SQLITE_OPEN_READWRITE); | |
| 251 open_flags = filesystem::kFlagOpenAlways; | |
| 252 } else { | |
| 253 open_flags = filesystem::kFlagOpen; | |
| 254 } | |
| 255 open_flags |= filesystem::kFlagRead; | |
| 256 if (flags & SQLITE_OPEN_READWRITE) | |
| 257 open_flags |= filesystem::kFlagWrite; | |
| 258 if (flags & SQLITE_OPEN_DELETEONCLOSE) | |
| 259 open_flags |= filesystem::kDeleteOnClose; | |
| 260 | |
| 261 // Grab the incoming file | |
| 262 filesystem::FilePtr file_ptr; | |
| 263 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 264 GetRootDirectory(mojo_vfs)->OpenFile(mojo::String(name), GetProxy(&file_ptr), | |
| 265 open_flags, Capture(&error)); | |
| 266 GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); | |
| 267 if (error != filesystem::FILE_ERROR_OK) { | |
| 268 // TODO(erg): Translate more of the mojo error codes into sqlite error | |
| 269 // codes. | |
| 270 return SQLITE_CANTOPEN; | |
| 271 } | |
| 272 | |
| 273 // Set the method table so we can be closed (and run the manual dtor call to | |
| 274 // match the following placement news). | |
| 275 file->pMethods = &mojo_vfs_io_methods; | |
| 276 | |
| 277 // |file| is actually a malloced buffer of size szOsFile. This means that we | |
| 278 // need to manually use placement new to construct the C++ object which owns | |
| 279 // the pipe to our file. | |
| 280 new (&GetFSFile(file)) filesystem::FilePtr(file_ptr.Pass()); | |
| 281 | |
| 282 return SQLITE_OK; | |
| 283 } | |
| 284 | |
| 285 int MojoVFSDelete(sqlite3_vfs* mojo_vfs, const char* filename, int sync_dir) { | |
| 286 DVLOG(1) << "MojoVFSDelete(*, " << filename << ", " << sync_dir << ")"; | |
| 287 // TODO(erg): The default windows sqlite VFS has retry code to work around | |
| 288 // antivirus software keeping files open. We'll probably have to do something | |
| 289 // like that in the far future if we ever support Windows. | |
|
Scott Hess - ex-Googler
2015/06/17 19:32:58
Yeah, this sucks. It would probably make more sen
| |
| 290 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 291 GetRootDirectory(mojo_vfs)->Delete(filename, 0, Capture(&error)); | |
| 292 GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); | |
| 293 | |
| 294 if (error == filesystem::FILE_ERROR_OK && sync_dir) { | |
| 295 GetRootDirectory(mojo_vfs)->Flush(Capture(&error)); | |
| 296 GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); | |
| 297 } | |
| 298 | |
| 299 return error == filesystem::FILE_ERROR_OK ? SQLITE_OK : SQLITE_IOERR_DELETE; | |
| 300 } | |
| 301 | |
| 302 int MojoVFSAccess(sqlite3_vfs* mojo_vfs, | |
| 303 const char* filename, | |
| 304 int flags, | |
| 305 int* result) { | |
| 306 DVLOG(1) << "MojoVFSAccess(*, " << filename << ", " << flags << ", *)"; | |
| 307 filesystem::FileError error = filesystem::FILE_ERROR_FAILED; | |
| 308 | |
| 309 if (flags == SQLITE_ACCESS_READWRITE || flags == SQLITE_ACCESS_READ) { | |
| 310 bool is_writable = false; | |
| 311 GetRootDirectory(mojo_vfs) | |
| 312 ->IsWritable(filename, Capture(&error, &is_writable)); | |
| 313 GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); | |
| 314 *result = is_writable; | |
| 315 return SQLITE_OK; | |
| 316 } | |
| 317 | |
| 318 if (flags == SQLITE_ACCESS_EXISTS) { | |
| 319 bool exists = false; | |
| 320 GetRootDirectory(mojo_vfs)->Exists(filename, Capture(&error, &exists)); | |
| 321 GetRootDirectory(mojo_vfs).WaitForIncomingResponse(); | |
| 322 *result = exists; | |
| 323 return SQLITE_OK; | |
| 324 } | |
| 325 | |
| 326 return SQLITE_IOERR; | |
| 327 } | |
| 328 | |
| 329 int MojoVFSFullPathname(sqlite3_vfs* mojo_vfs, | |
| 330 const char* relative_path, | |
| 331 int absolute_path_size, | |
| 332 char* absolute_path) { | |
| 333 // The sandboxed process doesn't need to know the absolute path of the file. | |
| 334 sqlite3_snprintf(absolute_path_size, absolute_path, "%s", relative_path); | |
| 335 return SQLITE_OK; | |
| 336 } | |
| 337 | |
| 338 // Don't let SQLite dynamically load things. (If we are using the | |
| 339 // mojo:filesystem proxying VFS, then it's highly likely that we are sandboxed | |
| 340 // and that any attempt to dlopen() a shared object is folly.) | |
| 341 void* MojoVFSDlOpen(sqlite3_vfs*, const char*) { | |
| 342 return 0; | |
| 343 } | |
| 344 | |
| 345 void MojoVFSDlError(sqlite3_vfs*, int buf_size, char* error_msg) { | |
| 346 sqlite3_snprintf(buf_size, error_msg, "Dynamic loading not supported"); | |
| 347 } | |
| 348 | |
| 349 void (*MojoVFSDlSym(sqlite3_vfs*, void*, const char*))(void) { | |
| 350 return 0; | |
| 351 } | |
| 352 | |
| 353 void MojoVFSDlClose(sqlite3_vfs*, void*) { | |
| 354 return; | |
| 355 } | |
| 356 | |
| 357 int MojoVFSRandomness(sqlite3_vfs* mojo_vfs, int size, char* out) { | |
| 358 base::RandBytes(out, size); | |
| 359 return size; | |
| 360 } | |
| 361 | |
| 362 // Proxy the rest of the calls down to the OS specific handler. | |
| 363 int MojoVFSSleep(sqlite3_vfs* mojo_vfs, int micro) { | |
| 364 return GetParentVFS(mojo_vfs)->xSleep(GetParentVFS(mojo_vfs), micro); | |
| 365 } | |
| 366 | |
| 367 int MojoVFSCurrentTime(sqlite3_vfs* mojo_vfs, double* time) { | |
| 368 return GetParentVFS(mojo_vfs)->xCurrentTime(GetParentVFS(mojo_vfs), time); | |
| 369 } | |
| 370 | |
| 371 int MojoVFSGetLastError(sqlite3_vfs* mojo_vfs, int a, char* b) { | |
| 372 return 0; | |
| 373 } | |
| 374 | |
| 375 int MojoVFSCurrentTimeInt64(sqlite3_vfs* mojo_vfs, sqlite3_int64* out) { | |
| 376 return GetParentVFS(mojo_vfs)->xCurrentTimeInt64(GetParentVFS(mojo_vfs), out); | |
| 377 } | |
| 378 | |
| 379 static sqlite3_vfs mojo_vfs = { | |
| 380 1, /* iVersion */ | |
| 381 sizeof(MojoVFSFile), /* szOsFile */ | |
| 382 kMaxPathName, /* mxPathname */ | |
| 383 0, /* pNext */ | |
| 384 "mojo", /* zName */ | |
| 385 0, /* pAppData */ | |
| 386 MojoVFSOpen, /* xOpen */ | |
| 387 MojoVFSDelete, /* xDelete */ | |
| 388 MojoVFSAccess, /* xAccess */ | |
| 389 MojoVFSFullPathname, /* xFullPathname */ | |
| 390 MojoVFSDlOpen, /* xDlOpen */ | |
| 391 MojoVFSDlError, /* xDlError */ | |
| 392 MojoVFSDlSym, /* xDlSym */ | |
| 393 MojoVFSDlClose, /* xDlClose */ | |
| 394 MojoVFSRandomness, /* xRandomness */ | |
| 395 MojoVFSSleep, /* xSleep */ | |
| 396 MojoVFSCurrentTime, /* xCurrentTime */ | |
| 397 MojoVFSGetLastError, /* xGetLastError */ | |
| 398 MojoVFSCurrentTimeInt64 /* xCurrentTimeInt64 */ | |
| 399 }; | |
| 400 | |
| 401 } // namespace | |
| 402 | |
| 403 ScopedMojoFilesystemVFS::ScopedMojoFilesystemVFS( | |
| 404 filesystem::DirectoryPtr root_directory) { | |
| 405 CHECK(!mojo_vfs.pAppData); | |
| 406 | |
| 407 MojoVFSData* data = new MojoVFSData; | |
| 408 data->parent = sqlite3_vfs_find(NULL); | |
| 409 data->root_directory = root_directory.Pass(); | |
| 410 mojo_vfs.pAppData = data; | |
| 411 | |
| 412 mojo_vfs.mxPathname = data->parent->mxPathname; | |
| 413 | |
| 414 CHECK(sqlite3_vfs_register(&mojo_vfs, 1) == SQLITE_OK); | |
| 415 } | |
| 416 | |
| 417 ScopedMojoFilesystemVFS::~ScopedMojoFilesystemVFS() { | |
| 418 CHECK(mojo_vfs.pAppData); | |
| 419 | |
| 420 MojoVFSData* data = static_cast<MojoVFSData*>(mojo_vfs.pAppData); | |
| 421 sqlite3_vfs* previous_fs = data->parent; | |
| 422 delete data; | |
| 423 mojo_vfs.pAppData = nullptr; | |
| 424 | |
| 425 CHECK(sqlite3_vfs_unregister(&mojo_vfs) == SQLITE_OK); | |
| 426 CHECK(sqlite3_vfs_register(previous_fs, 1) == SQLITE_OK); | |
|
Scott Hess - ex-Googler
2015/06/17 19:32:58
Suggest you restore the registration setup before
Elliot Glaysher
2015/06/17 21:59:35
Done.
| |
| 427 } | |
| 428 | |
| 429 filesystem::DirectoryPtr& ScopedMojoFilesystemVFS::GetDirectory() { | |
| 430 return static_cast<MojoVFSData*>(mojo_vfs.pAppData)->root_directory; | |
|
Scott Hess - ex-Googler
2015/06/17 19:32:58
Maybe this should be:
return GetRootDirectory(&m
| |
| 431 } | |
| 432 | |
| 433 } // namespace sql | |
| OLD | NEW |