/src/gdal/port/cpl_vsil_sparsefile.cpp
Line | Count | Source |
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 <limits> |
24 | | #include <map> |
25 | | #include <memory> |
26 | | #include <vector> |
27 | | |
28 | | #include "cpl_conv.h" |
29 | | #include "cpl_error.h" |
30 | | #include "cpl_minixml.h" |
31 | | #include "cpl_multiproc.h" |
32 | | #include "cpl_string.h" |
33 | | #include "cpl_vsi_virtual.h" |
34 | | |
35 | | class SFRegion |
36 | | { |
37 | | public: |
38 | | CPLString osFilename{}; |
39 | | VSILFILE *fp = nullptr; |
40 | | uint64_t nDstOffset = 0; |
41 | | uint64_t nSrcOffset = 0; |
42 | | uint64_t nLength = 0; |
43 | | GByte byValue = 0; |
44 | | bool bTriedOpen = false; |
45 | | }; |
46 | | |
47 | | /************************************************************************/ |
48 | | /* ==================================================================== */ |
49 | | /* VSISparseFileHandle */ |
50 | | /* ==================================================================== */ |
51 | | /************************************************************************/ |
52 | | |
53 | | class VSISparseFileFilesystemHandler; |
54 | | |
55 | | class VSISparseFileHandle final : public VSIVirtualHandle |
56 | | { |
57 | | CPL_DISALLOW_COPY_ASSIGN(VSISparseFileHandle) |
58 | | |
59 | | VSISparseFileFilesystemHandler *m_poFS = nullptr; |
60 | | bool bEOF = false; |
61 | | bool bError = false; |
62 | | |
63 | | public: |
64 | | explicit VSISparseFileHandle(VSISparseFileFilesystemHandler *poFS) |
65 | 0 | : m_poFS(poFS) |
66 | 0 | { |
67 | 0 | } |
68 | | |
69 | | ~VSISparseFileHandle() override; |
70 | | |
71 | | uint64_t nOverallLength = 0; |
72 | | uint64_t nCurOffset = 0; |
73 | | |
74 | | std::vector<SFRegion> aoRegions{}; |
75 | | |
76 | | int Seek(vsi_l_offset nOffset, int nWhence) override; |
77 | | vsi_l_offset Tell() override; |
78 | | size_t Read(void *pBuffer, size_t nBytes) override; |
79 | | size_t Write(const void *pBuffer, size_t nBytes) override; |
80 | | void ClearErr() override; |
81 | | int Eof() override; |
82 | | int Error() override; |
83 | | int Close() override; |
84 | | }; |
85 | | |
86 | | /************************************************************************/ |
87 | | /* ==================================================================== */ |
88 | | /* VSISparseFileFilesystemHandler */ |
89 | | /* ==================================================================== */ |
90 | | /************************************************************************/ |
91 | | |
92 | | class VSISparseFileFilesystemHandler final : public VSIFilesystemHandler |
93 | | { |
94 | | std::map<GIntBig, int> oRecOpenCount{}; |
95 | | CPL_DISALLOW_COPY_ASSIGN(VSISparseFileFilesystemHandler) |
96 | | |
97 | | public: |
98 | 2 | VSISparseFileFilesystemHandler() = default; |
99 | 0 | ~VSISparseFileFilesystemHandler() override = default; |
100 | | |
101 | | int DecomposePath(const char *pszPath, CPLString &osFilename, |
102 | | vsi_l_offset &nSparseFileOffset, |
103 | | vsi_l_offset &nSparseFileSize); |
104 | | |
105 | | VSIVirtualHandleUniquePtr Open(const char *pszFilename, |
106 | | const char *pszAccess, bool bSetError, |
107 | | CSLConstList /* papszOptions */) override; |
108 | | int Stat(const char *pszFilename, VSIStatBufL *pStatBuf, |
109 | | int nFlags) override; |
110 | | int Unlink(const char *pszFilename) override; |
111 | | int Mkdir(const char *pszDirname, long nMode) override; |
112 | | int Rmdir(const char *pszDirname) override; |
113 | | char **ReadDirEx(const char *pszDirname, int nMaxFiles) override; |
114 | | |
115 | | int GetRecCounter() |
116 | 0 | { |
117 | 0 | return oRecOpenCount[CPLGetPID()]; |
118 | 0 | } |
119 | | |
120 | | void IncRecCounter() |
121 | 0 | { |
122 | 0 | oRecOpenCount[CPLGetPID()]++; |
123 | 0 | } |
124 | | |
125 | | void DecRecCounter() |
126 | 0 | { |
127 | 0 | oRecOpenCount[CPLGetPID()]--; |
128 | 0 | } |
129 | | }; |
130 | | |
131 | | /************************************************************************/ |
132 | | /* ==================================================================== */ |
133 | | /* VSISparseFileHandle */ |
134 | | /* ==================================================================== */ |
135 | | /************************************************************************/ |
136 | | |
137 | | /************************************************************************/ |
138 | | /* ~VSISparseFileHandle() */ |
139 | | /************************************************************************/ |
140 | | |
141 | | VSISparseFileHandle::~VSISparseFileHandle() |
142 | 0 | { |
143 | 0 | VSISparseFileHandle::Close(); |
144 | 0 | } |
145 | | |
146 | | /************************************************************************/ |
147 | | /* Close() */ |
148 | | /************************************************************************/ |
149 | | |
150 | | int VSISparseFileHandle::Close() |
151 | | |
152 | 0 | { |
153 | 0 | for (unsigned int i = 0; i < aoRegions.size(); i++) |
154 | 0 | { |
155 | 0 | if (aoRegions[i].fp != nullptr) |
156 | 0 | CPL_IGNORE_RET_VAL(VSIFCloseL(aoRegions[i].fp)); |
157 | 0 | } |
158 | 0 | aoRegions.clear(); |
159 | |
|
160 | 0 | return 0; |
161 | 0 | } |
162 | | |
163 | | /************************************************************************/ |
164 | | /* Seek() */ |
165 | | /************************************************************************/ |
166 | | |
167 | | int VSISparseFileHandle::Seek(vsi_l_offset nOffset, int nWhence) |
168 | | |
169 | 0 | { |
170 | 0 | bEOF = false; |
171 | 0 | if (nWhence == SEEK_SET) |
172 | 0 | nCurOffset = nOffset; |
173 | 0 | else if (nWhence == SEEK_CUR) |
174 | 0 | { |
175 | 0 | nCurOffset += nOffset; |
176 | 0 | } |
177 | 0 | else if (nWhence == SEEK_END) |
178 | 0 | { |
179 | 0 | nCurOffset = nOverallLength + nOffset; |
180 | 0 | } |
181 | 0 | else |
182 | 0 | { |
183 | 0 | errno = EINVAL; |
184 | 0 | return -1; |
185 | 0 | } |
186 | | |
187 | 0 | return 0; |
188 | 0 | } |
189 | | |
190 | | /************************************************************************/ |
191 | | /* Tell() */ |
192 | | /************************************************************************/ |
193 | | |
194 | | vsi_l_offset VSISparseFileHandle::Tell() |
195 | | |
196 | 0 | { |
197 | 0 | return nCurOffset; |
198 | 0 | } |
199 | | |
200 | | /************************************************************************/ |
201 | | /* Read() */ |
202 | | /************************************************************************/ |
203 | | |
204 | | size_t VSISparseFileHandle::Read(void *pBuffer, size_t nBytes) |
205 | | |
206 | 0 | { |
207 | 0 | if (nCurOffset >= nOverallLength) |
208 | 0 | { |
209 | 0 | bEOF = true; |
210 | 0 | return 0; |
211 | 0 | } |
212 | | |
213 | | /* -------------------------------------------------------------------- */ |
214 | | /* Find what region we are in, searching linearly from the */ |
215 | | /* start. */ |
216 | | /* -------------------------------------------------------------------- */ |
217 | 0 | unsigned int iRegion = 0; // Used after for. |
218 | |
|
219 | 0 | for (; iRegion < aoRegions.size(); iRegion++) |
220 | 0 | { |
221 | 0 | if (nCurOffset >= aoRegions[iRegion].nDstOffset && |
222 | 0 | nCurOffset < |
223 | 0 | aoRegions[iRegion].nDstOffset + aoRegions[iRegion].nLength) |
224 | 0 | break; |
225 | 0 | } |
226 | |
|
227 | 0 | size_t nBytesRequested = nBytes; |
228 | 0 | if (nBytesRequested == 0) |
229 | 0 | { |
230 | 0 | return 0; |
231 | 0 | } |
232 | 0 | if (nCurOffset + nBytesRequested > nOverallLength) |
233 | 0 | { |
234 | 0 | nBytesRequested = static_cast<size_t>(nOverallLength - nCurOffset); |
235 | 0 | bEOF = true; |
236 | 0 | } |
237 | | |
238 | | /* -------------------------------------------------------------------- */ |
239 | | /* Default to zeroing the buffer if no corresponding region was */ |
240 | | /* found. */ |
241 | | /* -------------------------------------------------------------------- */ |
242 | 0 | if (iRegion == aoRegions.size()) |
243 | 0 | { |
244 | 0 | memset(pBuffer, 0, nBytesRequested); |
245 | 0 | nCurOffset += nBytesRequested; |
246 | 0 | return nBytesRequested; |
247 | 0 | } |
248 | | |
249 | | /* -------------------------------------------------------------------- */ |
250 | | /* If this request crosses region boundaries, split it into two */ |
251 | | /* requests. */ |
252 | | /* -------------------------------------------------------------------- */ |
253 | 0 | size_t nBytesReturnCount = 0; |
254 | 0 | const uint64_t nEndOffsetOfRegion = |
255 | 0 | aoRegions[iRegion].nDstOffset + aoRegions[iRegion].nLength; |
256 | |
|
257 | 0 | if (nCurOffset + nBytesRequested > nEndOffsetOfRegion) |
258 | 0 | { |
259 | 0 | const size_t nExtraBytes = static_cast<size_t>( |
260 | 0 | nCurOffset + nBytesRequested - nEndOffsetOfRegion); |
261 | | // Recurse to get the rest of the request. |
262 | |
|
263 | 0 | const uint64_t nCurOffsetSave = nCurOffset; |
264 | 0 | nCurOffset += nBytesRequested - nExtraBytes; |
265 | 0 | bool bEOFSave = bEOF; |
266 | 0 | bEOF = false; |
267 | 0 | const size_t nBytesRead = this->Read(static_cast<char *>(pBuffer) + |
268 | 0 | nBytesRequested - nExtraBytes, |
269 | 0 | nExtraBytes); |
270 | 0 | nCurOffset = nCurOffsetSave; |
271 | 0 | bEOF = bEOFSave; |
272 | 0 | if (nBytesRead < nExtraBytes) |
273 | 0 | { |
274 | | // A short read in a region of a sparse file is always an error |
275 | 0 | bError = true; |
276 | 0 | } |
277 | |
|
278 | 0 | nBytesReturnCount += nBytesRead; |
279 | 0 | nBytesRequested -= nExtraBytes; |
280 | 0 | } |
281 | | |
282 | | /* -------------------------------------------------------------------- */ |
283 | | /* Handle a constant region. */ |
284 | | /* -------------------------------------------------------------------- */ |
285 | 0 | if (aoRegions[iRegion].osFilename.empty()) |
286 | 0 | { |
287 | 0 | memset(pBuffer, aoRegions[iRegion].byValue, |
288 | 0 | static_cast<size_t>(nBytesRequested)); |
289 | |
|
290 | 0 | nBytesReturnCount += nBytesRequested; |
291 | 0 | } |
292 | | |
293 | | /* -------------------------------------------------------------------- */ |
294 | | /* Otherwise handle as a file. */ |
295 | | /* -------------------------------------------------------------------- */ |
296 | 0 | else |
297 | 0 | { |
298 | 0 | if (aoRegions[iRegion].fp == nullptr) |
299 | 0 | { |
300 | 0 | if (!aoRegions[iRegion].bTriedOpen) |
301 | 0 | { |
302 | 0 | aoRegions[iRegion].fp = |
303 | 0 | VSIFOpenL(aoRegions[iRegion].osFilename, "r"); |
304 | 0 | if (aoRegions[iRegion].fp == nullptr) |
305 | 0 | { |
306 | 0 | CPLDebug("/vsisparse/", "Failed to open '%s'.", |
307 | 0 | aoRegions[iRegion].osFilename.c_str()); |
308 | 0 | } |
309 | 0 | aoRegions[iRegion].bTriedOpen = true; |
310 | 0 | } |
311 | 0 | if (aoRegions[iRegion].fp == nullptr) |
312 | 0 | { |
313 | 0 | bError = true; |
314 | 0 | return 0; |
315 | 0 | } |
316 | 0 | } |
317 | | |
318 | 0 | if (aoRegions[iRegion].fp->Seek(nCurOffset - |
319 | 0 | aoRegions[iRegion].nDstOffset + |
320 | 0 | aoRegions[iRegion].nSrcOffset, |
321 | 0 | SEEK_SET) != 0) |
322 | 0 | { |
323 | 0 | bError = true; |
324 | 0 | return 0; |
325 | 0 | } |
326 | | |
327 | 0 | m_poFS->IncRecCounter(); |
328 | 0 | const size_t nBytesRead = |
329 | 0 | aoRegions[iRegion].fp->Read(pBuffer, nBytesRequested); |
330 | 0 | m_poFS->DecRecCounter(); |
331 | 0 | if (nBytesRead < 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; |
343 | 0 | } |
344 | | |
345 | | /************************************************************************/ |
346 | | /* Write() */ |
347 | | /************************************************************************/ |
348 | | |
349 | | size_t VSISparseFileHandle::Write(const void * /* pBuffer */, |
350 | | size_t /* nBytes */) |
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 | 0 | { |
407 | 0 | if (!STARTS_WITH_CI(pszFilename, "/vsisparse/")) |
408 | 0 | return nullptr; |
409 | | |
410 | 0 | if (!EQUAL(pszAccess, "r") && !EQUAL(pszAccess, "rb")) |
411 | 0 | { |
412 | 0 | errno = EACCES; |
413 | 0 | return nullptr; |
414 | 0 | } |
415 | | |
416 | | // Arbitrary number. |
417 | 0 | if (GetRecCounter() == 32) |
418 | 0 | return nullptr; |
419 | | |
420 | 0 | const CPLString osSparseFilePath = pszFilename + 11; |
421 | | |
422 | | /* -------------------------------------------------------------------- */ |
423 | | /* Does this file even exist? */ |
424 | | /* -------------------------------------------------------------------- */ |
425 | 0 | if (VSIFilesystemHandler::OpenStatic(osSparseFilePath, "rb") == nullptr) |
426 | 0 | return nullptr; |
427 | | |
428 | | /* -------------------------------------------------------------------- */ |
429 | | /* Read the XML file. */ |
430 | | /* -------------------------------------------------------------------- */ |
431 | 0 | CPLXMLTreeCloser psXMLRoot(CPLParseXMLFile(osSparseFilePath)); |
432 | |
|
433 | 0 | if (psXMLRoot == nullptr) |
434 | 0 | 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 | 0 | oRegion.nDstOffset = std::strtoull( |
465 | 0 | CPLGetXMLValue(psRegion, "DestinationOffset", "0"), nullptr, 10); |
466 | |
|
467 | 0 | oRegion.nSrcOffset = std::strtoull( |
468 | 0 | CPLGetXMLValue(psRegion, "SourceOffset", "0"), nullptr, 10); |
469 | |
|
470 | 0 | oRegion.nLength = std::strtoull( |
471 | 0 | CPLGetXMLValue(psRegion, "RegionLength", "0"), nullptr, 10); |
472 | |
|
473 | 0 | oRegion.byValue = |
474 | 0 | static_cast<GByte>(atoi(CPLGetXMLValue(psRegion, "Value", "0"))); |
475 | |
|
476 | 0 | poHandle->aoRegions.push_back(std::move(oRegion)); |
477 | 0 | } |
478 | | |
479 | | /* -------------------------------------------------------------------- */ |
480 | | /* Get sparse file length, use maximum bound of regions if not */ |
481 | | /* explicit in file. */ |
482 | | /* -------------------------------------------------------------------- */ |
483 | 0 | poHandle->nOverallLength = std::strtoull( |
484 | 0 | CPLGetXMLValue(psXMLRoot.get(), "Length", "0"), nullptr, 10); |
485 | 0 | if (poHandle->nOverallLength == 0) |
486 | 0 | { |
487 | 0 | for (unsigned int i = 0; i < poHandle->aoRegions.size(); i++) |
488 | 0 | { |
489 | 0 | if (poHandle->aoRegions[i].nDstOffset > |
490 | 0 | std::numeric_limits<uint64_t>::max() - |
491 | 0 | poHandle->aoRegions[i].nLength) |
492 | 0 | { |
493 | 0 | return nullptr; |
494 | 0 | } |
495 | 0 | poHandle->nOverallLength = std::max( |
496 | 0 | poHandle->nOverallLength, poHandle->aoRegions[i].nDstOffset + |
497 | 0 | poHandle->aoRegions[i].nLength); |
498 | 0 | } |
499 | 0 | } |
500 | | |
501 | 0 | return VSIVirtualHandleUniquePtr(poHandle.release()); |
502 | 0 | } |
503 | | |
504 | | /************************************************************************/ |
505 | | /* Stat() */ |
506 | | /************************************************************************/ |
507 | | |
508 | | int VSISparseFileFilesystemHandler::Stat(const char *pszFilename, |
509 | | VSIStatBufL *psStatBuf, int nFlags) |
510 | | |
511 | 0 | { |
512 | 0 | auto poFile = Open(pszFilename, "rb", false, nullptr); |
513 | |
|
514 | 0 | memset(psStatBuf, 0, sizeof(VSIStatBufL)); |
515 | |
|
516 | 0 | if (poFile == nullptr) |
517 | 0 | return -1; |
518 | | |
519 | 0 | poFile->Seek(0, SEEK_END); |
520 | 0 | const vsi_l_offset nLength = poFile->Tell(); |
521 | |
|
522 | 0 | const int nResult = |
523 | 0 | VSIStatExL(pszFilename + strlen("/vsisparse/"), psStatBuf, nFlags); |
524 | |
|
525 | 0 | psStatBuf->st_size = nLength; |
526 | |
|
527 | 0 | return nResult; |
528 | 0 | } |
529 | | |
530 | | /************************************************************************/ |
531 | | /* Unlink() */ |
532 | | /************************************************************************/ |
533 | | |
534 | | int VSISparseFileFilesystemHandler::Unlink(const char * /* pszFilename */) |
535 | 0 | { |
536 | 0 | errno = EACCES; |
537 | 0 | return -1; |
538 | 0 | } |
539 | | |
540 | | /************************************************************************/ |
541 | | /* Mkdir() */ |
542 | | /************************************************************************/ |
543 | | |
544 | | int VSISparseFileFilesystemHandler::Mkdir(const char * /* pszPathname */, |
545 | | long /* nMode */) |
546 | 0 | { |
547 | 0 | errno = EACCES; |
548 | 0 | return -1; |
549 | 0 | } |
550 | | |
551 | | /************************************************************************/ |
552 | | /* Rmdir() */ |
553 | | /************************************************************************/ |
554 | | |
555 | | int VSISparseFileFilesystemHandler::Rmdir(const char * /* pszPathname */) |
556 | 0 | { |
557 | 0 | errno = EACCES; |
558 | 0 | return -1; |
559 | 0 | } |
560 | | |
561 | | /************************************************************************/ |
562 | | /* ReadDirEx() */ |
563 | | /************************************************************************/ |
564 | | |
565 | | char **VSISparseFileFilesystemHandler::ReadDirEx(const char * /* pszPath */, |
566 | | int /* nMaxFiles */) |
567 | 0 | { |
568 | 0 | errno = EACCES; |
569 | 0 | return nullptr; |
570 | 0 | } |
571 | | |
572 | | /************************************************************************/ |
573 | | /* VSIInstallSparseFileFilesystemHandler() */ |
574 | | /************************************************************************/ |
575 | | |
576 | | /*! |
577 | | \brief Install /vsisparse/ virtual file handler. |
578 | | |
579 | | \verbatim embed:rst |
580 | | See :ref:`/vsisparse/ documentation <vsisparse>` |
581 | | \endverbatim |
582 | | */ |
583 | | |
584 | | void VSIInstallSparseFileHandler() |
585 | 2 | { |
586 | 2 | VSIFileManager::InstallHandler( |
587 | 2 | "/vsisparse/", std::make_shared<VSISparseFileFilesystemHandler>()); |
588 | 2 | } |