/src/gdal/port/cpl_vsil_sparsefile.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /****************************************************************************** |
2 | | * |
3 | | * Project: VSI Virtual File System |
4 | | * Purpose: Implementation of sparse file virtual io driver. |
5 | | * Author: Frank Warmerdam, warmerdam@pobox.com |
6 | | * |
7 | | ****************************************************************************** |
8 | | * Copyright (c) 2010, Frank Warmerdam <warmerdam@pobox.com> |
9 | | * Copyright (c) 2010-2013, Even Rouault <even dot rouault at spatialys.com> |
10 | | * |
11 | | * SPDX-License-Identifier: MIT |
12 | | ****************************************************************************/ |
13 | | |
14 | | #include "cpl_port.h" |
15 | | #include "cpl_vsi.h" |
16 | | |
17 | | #include <cerrno> |
18 | | #include <cstddef> |
19 | | #include <cstdlib> |
20 | | #include <cstring> |
21 | | |
22 | | #include <algorithm> |
23 | | #include <map> |
24 | | #include <memory> |
25 | | #include <vector> |
26 | | |
27 | | #include "cpl_conv.h" |
28 | | #include "cpl_error.h" |
29 | | #include "cpl_minixml.h" |
30 | | #include "cpl_multiproc.h" |
31 | | #include "cpl_string.h" |
32 | | #include "cpl_vsi_virtual.h" |
33 | | |
34 | | class SFRegion |
35 | | { |
36 | | public: |
37 | | CPLString osFilename{}; |
38 | | VSILFILE *fp = nullptr; |
39 | | GUIntBig nDstOffset = 0; |
40 | | GUIntBig nSrcOffset = 0; |
41 | | GUIntBig nLength = 0; |
42 | | GByte byValue = 0; |
43 | | bool bTriedOpen = false; |
44 | | }; |
45 | | |
46 | | /************************************************************************/ |
47 | | /* ==================================================================== */ |
48 | | /* VSISparseFileHandle */ |
49 | | /* ==================================================================== */ |
50 | | /************************************************************************/ |
51 | | |
52 | | class VSISparseFileFilesystemHandler; |
53 | | |
54 | | class VSISparseFileHandle final : public VSIVirtualHandle |
55 | | { |
56 | | CPL_DISALLOW_COPY_ASSIGN(VSISparseFileHandle) |
57 | | |
58 | | VSISparseFileFilesystemHandler *m_poFS = nullptr; |
59 | | bool bEOF = false; |
60 | | bool bError = false; |
61 | | |
62 | | public: |
63 | | explicit VSISparseFileHandle(VSISparseFileFilesystemHandler *poFS) |
64 | 0 | : m_poFS(poFS) |
65 | 0 | { |
66 | 0 | } |
67 | | |
68 | | ~VSISparseFileHandle() override; |
69 | | |
70 | | GUIntBig nOverallLength = 0; |
71 | | GUIntBig nCurOffset = 0; |
72 | | |
73 | | std::vector<SFRegion> aoRegions{}; |
74 | | |
75 | | int Seek(vsi_l_offset nOffset, int nWhence) override; |
76 | | vsi_l_offset Tell() override; |
77 | | size_t Read(void *pBuffer, size_t nSize, size_t nMemb) override; |
78 | | size_t Write(const void *pBuffer, size_t nSize, size_t nMemb) override; |
79 | | void ClearErr() override; |
80 | | int Eof() override; |
81 | | int Error() override; |
82 | | int Close() override; |
83 | | }; |
84 | | |
85 | | /************************************************************************/ |
86 | | /* ==================================================================== */ |
87 | | /* VSISparseFileFilesystemHandler */ |
88 | | /* ==================================================================== */ |
89 | | /************************************************************************/ |
90 | | |
91 | | class VSISparseFileFilesystemHandler : public VSIFilesystemHandler |
92 | | { |
93 | | std::map<GIntBig, int> oRecOpenCount{}; |
94 | | CPL_DISALLOW_COPY_ASSIGN(VSISparseFileFilesystemHandler) |
95 | | |
96 | | public: |
97 | 3 | VSISparseFileFilesystemHandler() = default; |
98 | 0 | ~VSISparseFileFilesystemHandler() override = default; |
99 | | |
100 | | int DecomposePath(const char *pszPath, CPLString &osFilename, |
101 | | vsi_l_offset &nSparseFileOffset, |
102 | | vsi_l_offset &nSparseFileSize); |
103 | | |
104 | | VSIVirtualHandleUniquePtr Open(const char *pszFilename, |
105 | | const char *pszAccess, bool bSetError, |
106 | | CSLConstList /* papszOptions */) override; |
107 | | int Stat(const char *pszFilename, VSIStatBufL *pStatBuf, |
108 | | int nFlags) override; |
109 | | int Unlink(const char *pszFilename) override; |
110 | | int Mkdir(const char *pszDirname, long nMode) override; |
111 | | int Rmdir(const char *pszDirname) override; |
112 | | char **ReadDirEx(const char *pszDirname, int nMaxFiles) override; |
113 | | |
114 | | int GetRecCounter() |
115 | 5.33k | { |
116 | 5.33k | return oRecOpenCount[CPLGetPID()]; |
117 | 5.33k | } |
118 | | |
119 | | void IncRecCounter() |
120 | 0 | { |
121 | 0 | oRecOpenCount[CPLGetPID()]++; |
122 | 0 | } |
123 | | |
124 | | void DecRecCounter() |
125 | 0 | { |
126 | 0 | oRecOpenCount[CPLGetPID()]--; |
127 | 0 | } |
128 | | }; |
129 | | |
130 | | /************************************************************************/ |
131 | | /* ==================================================================== */ |
132 | | /* VSISparseFileHandle */ |
133 | | /* ==================================================================== */ |
134 | | /************************************************************************/ |
135 | | |
136 | | /************************************************************************/ |
137 | | /* ~VSISparseFileHandle() */ |
138 | | /************************************************************************/ |
139 | | |
140 | | VSISparseFileHandle::~VSISparseFileHandle() |
141 | 0 | { |
142 | 0 | VSISparseFileHandle::Close(); |
143 | 0 | } |
144 | | |
145 | | /************************************************************************/ |
146 | | /* Close() */ |
147 | | /************************************************************************/ |
148 | | |
149 | | int VSISparseFileHandle::Close() |
150 | | |
151 | 0 | { |
152 | 0 | for (unsigned int i = 0; i < aoRegions.size(); i++) |
153 | 0 | { |
154 | 0 | if (aoRegions[i].fp != nullptr) |
155 | 0 | CPL_IGNORE_RET_VAL(VSIFCloseL(aoRegions[i].fp)); |
156 | 0 | } |
157 | 0 | aoRegions.clear(); |
158 | |
|
159 | 0 | return 0; |
160 | 0 | } |
161 | | |
162 | | /************************************************************************/ |
163 | | /* Seek() */ |
164 | | /************************************************************************/ |
165 | | |
166 | | int VSISparseFileHandle::Seek(vsi_l_offset nOffset, int nWhence) |
167 | | |
168 | 0 | { |
169 | 0 | bEOF = false; |
170 | 0 | if (nWhence == SEEK_SET) |
171 | 0 | nCurOffset = nOffset; |
172 | 0 | else if (nWhence == SEEK_CUR) |
173 | 0 | { |
174 | 0 | nCurOffset += nOffset; |
175 | 0 | } |
176 | 0 | else if (nWhence == SEEK_END) |
177 | 0 | { |
178 | 0 | nCurOffset = nOverallLength + nOffset; |
179 | 0 | } |
180 | 0 | else |
181 | 0 | { |
182 | 0 | errno = EINVAL; |
183 | 0 | return -1; |
184 | 0 | } |
185 | | |
186 | 0 | return 0; |
187 | 0 | } |
188 | | |
189 | | /************************************************************************/ |
190 | | /* Tell() */ |
191 | | /************************************************************************/ |
192 | | |
193 | | vsi_l_offset VSISparseFileHandle::Tell() |
194 | | |
195 | 0 | { |
196 | 0 | return nCurOffset; |
197 | 0 | } |
198 | | |
199 | | /************************************************************************/ |
200 | | /* Read() */ |
201 | | /************************************************************************/ |
202 | | |
203 | | size_t VSISparseFileHandle::Read(void *pBuffer, size_t nSize, size_t nCount) |
204 | | |
205 | 0 | { |
206 | 0 | if (nCurOffset >= nOverallLength) |
207 | 0 | { |
208 | 0 | bEOF = true; |
209 | 0 | return 0; |
210 | 0 | } |
211 | | |
212 | | /* -------------------------------------------------------------------- */ |
213 | | /* Find what region we are in, searching linearly from the */ |
214 | | /* start. */ |
215 | | /* -------------------------------------------------------------------- */ |
216 | 0 | unsigned int iRegion = 0; // Used after for. |
217 | |
|
218 | 0 | for (; iRegion < aoRegions.size(); iRegion++) |
219 | 0 | { |
220 | 0 | if (nCurOffset >= aoRegions[iRegion].nDstOffset && |
221 | 0 | nCurOffset < |
222 | 0 | aoRegions[iRegion].nDstOffset + aoRegions[iRegion].nLength) |
223 | 0 | break; |
224 | 0 | } |
225 | |
|
226 | 0 | size_t nBytesRequested = nSize * nCount; |
227 | 0 | if (nBytesRequested == 0) |
228 | 0 | { |
229 | 0 | return 0; |
230 | 0 | } |
231 | 0 | if (nCurOffset + nBytesRequested > nOverallLength) |
232 | 0 | { |
233 | 0 | nBytesRequested = static_cast<size_t>(nOverallLength - nCurOffset); |
234 | 0 | bEOF = true; |
235 | 0 | } |
236 | | |
237 | | /* -------------------------------------------------------------------- */ |
238 | | /* Default to zeroing the buffer if no corresponding region was */ |
239 | | /* found. */ |
240 | | /* -------------------------------------------------------------------- */ |
241 | 0 | if (iRegion == aoRegions.size()) |
242 | 0 | { |
243 | 0 | memset(pBuffer, 0, nBytesRequested); |
244 | 0 | nCurOffset += nBytesRequested; |
245 | 0 | return nBytesRequested / nSize; |
246 | 0 | } |
247 | | |
248 | | /* -------------------------------------------------------------------- */ |
249 | | /* If this request crosses region boundaries, split it into two */ |
250 | | /* requests. */ |
251 | | /* -------------------------------------------------------------------- */ |
252 | 0 | size_t nBytesReturnCount = 0; |
253 | 0 | const GUIntBig nEndOffsetOfRegion = |
254 | 0 | aoRegions[iRegion].nDstOffset + aoRegions[iRegion].nLength; |
255 | |
|
256 | 0 | if (nCurOffset + nBytesRequested > nEndOffsetOfRegion) |
257 | 0 | { |
258 | 0 | const size_t nExtraBytes = static_cast<size_t>( |
259 | 0 | nCurOffset + nBytesRequested - nEndOffsetOfRegion); |
260 | | // Recurse to get the rest of the request. |
261 | |
|
262 | 0 | const GUIntBig nCurOffsetSave = nCurOffset; |
263 | 0 | nCurOffset += nBytesRequested - nExtraBytes; |
264 | 0 | bool bEOFSave = bEOF; |
265 | 0 | bEOF = false; |
266 | 0 | const size_t nBytesRead = this->Read(static_cast<char *>(pBuffer) + |
267 | 0 | nBytesRequested - nExtraBytes, |
268 | 0 | 1, nExtraBytes); |
269 | 0 | nCurOffset = nCurOffsetSave; |
270 | 0 | bEOF = bEOFSave; |
271 | 0 | if (nBytesRead < nExtraBytes) |
272 | 0 | { |
273 | | // A short read in a region of a sparse file is always an error |
274 | 0 | bError = true; |
275 | 0 | } |
276 | |
|
277 | 0 | nBytesReturnCount += nBytesRead; |
278 | 0 | nBytesRequested -= nExtraBytes; |
279 | 0 | } |
280 | | |
281 | | /* -------------------------------------------------------------------- */ |
282 | | /* Handle a constant region. */ |
283 | | /* -------------------------------------------------------------------- */ |
284 | 0 | if (aoRegions[iRegion].osFilename.empty()) |
285 | 0 | { |
286 | 0 | memset(pBuffer, aoRegions[iRegion].byValue, |
287 | 0 | static_cast<size_t>(nBytesRequested)); |
288 | |
|
289 | 0 | nBytesReturnCount += nBytesRequested; |
290 | 0 | } |
291 | | |
292 | | /* -------------------------------------------------------------------- */ |
293 | | /* Otherwise handle as a file. */ |
294 | | /* -------------------------------------------------------------------- */ |
295 | 0 | else |
296 | 0 | { |
297 | 0 | if (aoRegions[iRegion].fp == nullptr) |
298 | 0 | { |
299 | 0 | if (!aoRegions[iRegion].bTriedOpen) |
300 | 0 | { |
301 | 0 | aoRegions[iRegion].fp = |
302 | 0 | VSIFOpenL(aoRegions[iRegion].osFilename, "r"); |
303 | 0 | if (aoRegions[iRegion].fp == nullptr) |
304 | 0 | { |
305 | 0 | CPLDebug("/vsisparse/", "Failed to open '%s'.", |
306 | 0 | aoRegions[iRegion].osFilename.c_str()); |
307 | 0 | } |
308 | 0 | aoRegions[iRegion].bTriedOpen = true; |
309 | 0 | } |
310 | 0 | if (aoRegions[iRegion].fp == nullptr) |
311 | 0 | { |
312 | 0 | bError = true; |
313 | 0 | return 0; |
314 | 0 | } |
315 | 0 | } |
316 | | |
317 | 0 | if (VSIFSeekL(aoRegions[iRegion].fp, |
318 | 0 | nCurOffset - aoRegions[iRegion].nDstOffset + |
319 | 0 | aoRegions[iRegion].nSrcOffset, |
320 | 0 | SEEK_SET) != 0) |
321 | 0 | { |
322 | 0 | bError = true; |
323 | 0 | return 0; |
324 | 0 | } |
325 | | |
326 | 0 | m_poFS->IncRecCounter(); |
327 | 0 | const size_t nBytesRead = |
328 | 0 | VSIFReadL(pBuffer, 1, static_cast<size_t>(nBytesRequested), |
329 | 0 | aoRegions[iRegion].fp); |
330 | 0 | m_poFS->DecRecCounter(); |
331 | 0 | if (nBytesRead < static_cast<size_t>(nBytesRequested)) |
332 | 0 | { |
333 | | // A short read in a region of a sparse file is always an error |
334 | 0 | bError = true; |
335 | 0 | } |
336 | |
|
337 | 0 | nBytesReturnCount += nBytesRead; |
338 | 0 | } |
339 | | |
340 | 0 | nCurOffset += nBytesReturnCount; |
341 | |
|
342 | 0 | return nBytesReturnCount / nSize; |
343 | 0 | } |
344 | | |
345 | | /************************************************************************/ |
346 | | /* Write() */ |
347 | | /************************************************************************/ |
348 | | |
349 | | size_t VSISparseFileHandle::Write(const void * /* pBuffer */, |
350 | | size_t /* nSize */, size_t /* nCount */) |
351 | 0 | { |
352 | 0 | errno = EBADF; |
353 | 0 | return 0; |
354 | 0 | } |
355 | | |
356 | | /************************************************************************/ |
357 | | /* Eof() */ |
358 | | /************************************************************************/ |
359 | | |
360 | | int VSISparseFileHandle::Eof() |
361 | | |
362 | 0 | { |
363 | 0 | return bEOF ? 1 : 0; |
364 | 0 | } |
365 | | |
366 | | /************************************************************************/ |
367 | | /* Error() */ |
368 | | /************************************************************************/ |
369 | | |
370 | | int VSISparseFileHandle::Error() |
371 | | |
372 | 0 | { |
373 | 0 | return bError ? 1 : 0; |
374 | 0 | } |
375 | | |
376 | | /************************************************************************/ |
377 | | /* ClearErr() */ |
378 | | /************************************************************************/ |
379 | | |
380 | | void VSISparseFileHandle::ClearErr() |
381 | | |
382 | 0 | { |
383 | 0 | for (const auto ®ion : aoRegions) |
384 | 0 | { |
385 | 0 | if (region.fp) |
386 | 0 | region.fp->ClearErr(); |
387 | 0 | } |
388 | 0 | bEOF = false; |
389 | 0 | bError = false; |
390 | 0 | } |
391 | | |
392 | | /************************************************************************/ |
393 | | /* ==================================================================== */ |
394 | | /* VSISparseFileFilesystemHandler */ |
395 | | /* ==================================================================== */ |
396 | | /************************************************************************/ |
397 | | |
398 | | /************************************************************************/ |
399 | | /* Open() */ |
400 | | /************************************************************************/ |
401 | | |
402 | | VSIVirtualHandleUniquePtr VSISparseFileFilesystemHandler::Open( |
403 | | const char *pszFilename, const char *pszAccess, bool /* bSetError */, |
404 | | CSLConstList /* papszOptions */) |
405 | | |
406 | 5.39k | { |
407 | 5.39k | if (!STARTS_WITH_CI(pszFilename, "/vsisparse/")) |
408 | 62 | return nullptr; |
409 | | |
410 | 5.33k | if (!EQUAL(pszAccess, "r") && !EQUAL(pszAccess, "rb")) |
411 | 0 | { |
412 | 0 | errno = EACCES; |
413 | 0 | return nullptr; |
414 | 0 | } |
415 | | |
416 | | // Arbitrary number. |
417 | 5.33k | if (GetRecCounter() == 32) |
418 | 0 | return nullptr; |
419 | | |
420 | 5.33k | const CPLString osSparseFilePath = pszFilename + 11; |
421 | | |
422 | | /* -------------------------------------------------------------------- */ |
423 | | /* Does this file even exist? */ |
424 | | /* -------------------------------------------------------------------- */ |
425 | 5.33k | if (VSIFilesystemHandler::OpenStatic(osSparseFilePath, "rb") == nullptr) |
426 | 4.69k | return nullptr; |
427 | | |
428 | | /* -------------------------------------------------------------------- */ |
429 | | /* Read the XML file. */ |
430 | | /* -------------------------------------------------------------------- */ |
431 | 647 | CPLXMLNode *psXMLRoot = CPLParseXMLFile(osSparseFilePath); |
432 | | |
433 | 647 | if (psXMLRoot == nullptr) |
434 | 647 | return nullptr; |
435 | | |
436 | | /* -------------------------------------------------------------------- */ |
437 | | /* Setup the file handle on this file. */ |
438 | | /* -------------------------------------------------------------------- */ |
439 | 0 | auto poHandle = std::make_unique<VSISparseFileHandle>(this); |
440 | | |
441 | | /* -------------------------------------------------------------------- */ |
442 | | /* Translate the desired fields out of the XML tree. */ |
443 | | /* -------------------------------------------------------------------- */ |
444 | 0 | for (CPLXMLNode *psRegion = psXMLRoot->psChild; psRegion != nullptr; |
445 | 0 | psRegion = psRegion->psNext) |
446 | 0 | { |
447 | 0 | if (psRegion->eType != CXT_Element) |
448 | 0 | continue; |
449 | | |
450 | 0 | if (!EQUAL(psRegion->pszValue, "SubfileRegion") && |
451 | 0 | !EQUAL(psRegion->pszValue, "ConstantRegion")) |
452 | 0 | continue; |
453 | | |
454 | 0 | SFRegion oRegion; |
455 | |
|
456 | 0 | oRegion.osFilename = CPLGetXMLValue(psRegion, "Filename", ""); |
457 | 0 | if (atoi(CPLGetXMLValue(psRegion, "Filename.relative", "0")) != 0) |
458 | 0 | { |
459 | 0 | const std::string osSFPath = CPLGetPathSafe(osSparseFilePath); |
460 | 0 | oRegion.osFilename = CPLFormFilenameSafe( |
461 | 0 | osSFPath.c_str(), oRegion.osFilename, nullptr); |
462 | 0 | } |
463 | | |
464 | | // TODO(schwehr): Symbolic constant and an explanation for 32. |
465 | 0 | oRegion.nDstOffset = CPLScanUIntBig( |
466 | 0 | CPLGetXMLValue(psRegion, "DestinationOffset", "0"), 32); |
467 | |
|
468 | 0 | oRegion.nSrcOffset = |
469 | 0 | CPLScanUIntBig(CPLGetXMLValue(psRegion, "SourceOffset", "0"), 32); |
470 | |
|
471 | 0 | oRegion.nLength = |
472 | 0 | CPLScanUIntBig(CPLGetXMLValue(psRegion, "RegionLength", "0"), 32); |
473 | |
|
474 | 0 | oRegion.byValue = |
475 | 0 | static_cast<GByte>(atoi(CPLGetXMLValue(psRegion, "Value", "0"))); |
476 | |
|
477 | 0 | poHandle->aoRegions.push_back(std::move(oRegion)); |
478 | 0 | } |
479 | | |
480 | | /* -------------------------------------------------------------------- */ |
481 | | /* Get sparse file length, use maximum bound of regions if not */ |
482 | | /* explicit in file. */ |
483 | | /* -------------------------------------------------------------------- */ |
484 | 0 | poHandle->nOverallLength = |
485 | 0 | CPLScanUIntBig(CPLGetXMLValue(psXMLRoot, "Length", "0"), 32); |
486 | 0 | if (poHandle->nOverallLength == 0) |
487 | 0 | { |
488 | 0 | for (unsigned int i = 0; i < poHandle->aoRegions.size(); i++) |
489 | 0 | { |
490 | 0 | poHandle->nOverallLength = std::max( |
491 | 0 | poHandle->nOverallLength, poHandle->aoRegions[i].nDstOffset + |
492 | 0 | poHandle->aoRegions[i].nLength); |
493 | 0 | } |
494 | 0 | } |
495 | |
|
496 | 0 | CPLDestroyXMLNode(psXMLRoot); |
497 | |
|
498 | 0 | return VSIVirtualHandleUniquePtr(poHandle.release()); |
499 | 647 | } |
500 | | |
501 | | /************************************************************************/ |
502 | | /* Stat() */ |
503 | | /************************************************************************/ |
504 | | |
505 | | int VSISparseFileFilesystemHandler::Stat(const char *pszFilename, |
506 | | VSIStatBufL *psStatBuf, int nFlags) |
507 | | |
508 | 5.28k | { |
509 | 5.28k | auto poFile = Open(pszFilename, "rb", false, nullptr); |
510 | | |
511 | 5.28k | memset(psStatBuf, 0, sizeof(VSIStatBufL)); |
512 | | |
513 | 5.28k | if (poFile == nullptr) |
514 | 5.28k | return -1; |
515 | | |
516 | 0 | poFile->Seek(0, SEEK_END); |
517 | 0 | const vsi_l_offset nLength = poFile->Tell(); |
518 | |
|
519 | 0 | const int nResult = |
520 | 0 | VSIStatExL(pszFilename + strlen("/vsisparse/"), psStatBuf, nFlags); |
521 | |
|
522 | 0 | psStatBuf->st_size = nLength; |
523 | |
|
524 | 0 | return nResult; |
525 | 5.28k | } |
526 | | |
527 | | /************************************************************************/ |
528 | | /* Unlink() */ |
529 | | /************************************************************************/ |
530 | | |
531 | | int VSISparseFileFilesystemHandler::Unlink(const char * /* pszFilename */) |
532 | 0 | { |
533 | 0 | errno = EACCES; |
534 | 0 | return -1; |
535 | 0 | } |
536 | | |
537 | | /************************************************************************/ |
538 | | /* Mkdir() */ |
539 | | /************************************************************************/ |
540 | | |
541 | | int VSISparseFileFilesystemHandler::Mkdir(const char * /* pszPathname */, |
542 | | long /* nMode */) |
543 | 0 | { |
544 | 0 | errno = EACCES; |
545 | 0 | return -1; |
546 | 0 | } |
547 | | |
548 | | /************************************************************************/ |
549 | | /* Rmdir() */ |
550 | | /************************************************************************/ |
551 | | |
552 | | int VSISparseFileFilesystemHandler::Rmdir(const char * /* pszPathname */) |
553 | 0 | { |
554 | 0 | errno = EACCES; |
555 | 0 | return -1; |
556 | 0 | } |
557 | | |
558 | | /************************************************************************/ |
559 | | /* ReadDirEx() */ |
560 | | /************************************************************************/ |
561 | | |
562 | | char **VSISparseFileFilesystemHandler::ReadDirEx(const char * /* pszPath */, |
563 | | int /* nMaxFiles */) |
564 | 0 | { |
565 | 0 | errno = EACCES; |
566 | 0 | return nullptr; |
567 | 0 | } |
568 | | |
569 | | /************************************************************************/ |
570 | | /* VSIInstallSparseFileFilesystemHandler() */ |
571 | | /************************************************************************/ |
572 | | |
573 | | /*! |
574 | | \brief Install /vsisparse/ virtual file handler. |
575 | | |
576 | | \verbatim embed:rst |
577 | | See :ref:`/vsisparse/ documentation <vsisparse>` |
578 | | \endverbatim |
579 | | */ |
580 | | |
581 | | void VSIInstallSparseFileHandler() |
582 | 3 | { |
583 | 3 | VSIFileManager::InstallHandler("/vsisparse/", |
584 | 3 | new VSISparseFileFilesystemHandler); |
585 | 3 | } |