Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: CPL - Common Portability Library
4 : * Purpose: Implement VSI large file api for tar files (.tar).
5 : * Author: Even Rouault, even.rouault at spatialys.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2010-2014, Even Rouault <even dot rouault at spatialys.com>
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : //! @cond Doxygen_Suppress
14 :
15 : #include "cpl_port.h"
16 : #include "cpl_vsi.h"
17 :
18 : #include <cstring>
19 :
20 : #include <fcntl.h>
21 :
22 : #include <memory>
23 : #include <string>
24 : #include <string_view>
25 : #include <vector>
26 :
27 : #include "cpl_conv.h"
28 : #include "cpl_error.h"
29 : #include "cpl_string.h"
30 : #include "cpl_vsi_virtual.h"
31 :
32 : #if (defined(DEBUG) || defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)) && \
33 : !defined(HAVE_FUZZER_FRIENDLY_ARCHIVE)
34 : /* This is a completely custom archive format that is rather inefficient */
35 : /* but supports random insertions or deletions, since it doesn't record */
36 : /* explicit file size or rely on files starting on a particular boundary */
37 : #define HAVE_FUZZER_FRIENDLY_ARCHIVE 1
38 : #endif
39 :
40 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
41 : constexpr int HALF_BUFFER_SIZE = 1024;
42 : constexpr int BUFFER_SIZE = 2 * HALF_BUFFER_SIZE;
43 : #endif
44 :
45 : /************************************************************************/
46 : /* ==================================================================== */
47 : /* VSITarEntryFileOffset */
48 : /* ==================================================================== */
49 : /************************************************************************/
50 :
51 70 : class VSITarEntryFileOffset final : public VSIArchiveEntryFileOffset
52 : {
53 : public:
54 : GUIntBig m_nOffset = 0;
55 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
56 : GUIntBig m_nFileSize = 0;
57 : CPLString m_osFileName{};
58 : #endif
59 :
60 61 : explicit VSITarEntryFileOffset(GUIntBig nOffset) : m_nOffset(nOffset)
61 : {
62 61 : }
63 :
64 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
65 5 : VSITarEntryFileOffset(GUIntBig nOffset, GUIntBig nFileSize,
66 : const CPLString &osFileName)
67 5 : : m_nOffset(nOffset), m_nFileSize(nFileSize), m_osFileName(osFileName)
68 : {
69 5 : }
70 : #endif
71 :
72 : ~VSITarEntryFileOffset() override;
73 : };
74 :
75 : VSITarEntryFileOffset::~VSITarEntryFileOffset() = default;
76 :
77 : /************************************************************************/
78 : /* ==================================================================== */
79 : /* VSITarReader */
80 : /* ==================================================================== */
81 : /************************************************************************/
82 :
83 : class VSITarReader final : public VSIArchiveReader
84 : {
85 : private:
86 : CPL_DISALLOW_COPY_ASSIGN(VSITarReader)
87 :
88 : VSILFILE *fp = nullptr;
89 : GUIntBig nCurOffset = 0;
90 : GUIntBig nNextFileSize = 0;
91 : CPLString osNextFileName{};
92 : GIntBig nModifiedTime = 0;
93 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
94 : bool m_bIsFuzzerFriendly = false;
95 : GByte m_abyBuffer[BUFFER_SIZE + 1] = {};
96 : int m_abyBufferIdx = 0;
97 : int m_abyBufferSize = 0;
98 : GUIntBig m_nCurOffsetOld = 0;
99 : #endif
100 :
101 : public:
102 : explicit VSITarReader(const char *pszTarFileName);
103 : ~VSITarReader() override;
104 :
105 83 : int IsValid()
106 : {
107 83 : return fp != nullptr;
108 : }
109 :
110 : int GotoFirstFile() override;
111 : int GotoNextFile() override;
112 : VSIArchiveEntryFileOffset *GetFileOffset() override;
113 :
114 70 : GUIntBig GetFileSize() override
115 : {
116 70 : return nNextFileSize;
117 : }
118 :
119 53 : CPLString GetFileName() override
120 : {
121 53 : return osNextFileName;
122 : }
123 :
124 44 : GIntBig GetModifiedTime() override
125 : {
126 44 : return nModifiedTime;
127 : }
128 :
129 : int GotoFileOffset(VSIArchiveEntryFileOffset *pOffset) override;
130 : };
131 :
132 : /************************************************************************/
133 : /* VSIIsTGZ() */
134 : /************************************************************************/
135 :
136 114 : static bool VSIIsTGZ(const char *pszFilename)
137 : {
138 : return (
139 228 : !STARTS_WITH_CI(pszFilename, "/vsigzip/") &&
140 114 : ((strlen(pszFilename) > 4 &&
141 114 : STARTS_WITH_CI(pszFilename + strlen(pszFilename) - 4, ".tgz")) ||
142 110 : (strlen(pszFilename) > 7 &&
143 224 : STARTS_WITH_CI(pszFilename + strlen(pszFilename) - 7, ".tar.gz"))));
144 : }
145 :
146 : /************************************************************************/
147 : /* VSITarReader() */
148 : /************************************************************************/
149 :
150 : // TODO(schwehr): What is this ***NEWFILE*** thing?
151 : // And make it a symbolic constant.
152 :
153 83 : VSITarReader::VSITarReader(const char *pszTarFileName)
154 83 : : fp(VSIFOpenL(pszTarFileName, "rb"))
155 : {
156 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
157 83 : if (fp != nullptr)
158 : {
159 83 : GByte abySignature[24] = {};
160 83 : m_bIsFuzzerFriendly =
161 166 : (VSIFReadL(abySignature, 1, 24, fp) == 24) &&
162 83 : (memcmp(abySignature, "FUZZER_FRIENDLY_ARCHIVE\n", 24) == 0 ||
163 80 : memcmp(abySignature, "***NEWFILE***:", strlen("***NEWFILE***:")) ==
164 : 0);
165 83 : CPL_IGNORE_RET_VAL(VSIFSeekL(fp, 0, SEEK_SET));
166 : }
167 : #endif
168 83 : }
169 :
170 : /************************************************************************/
171 : /* ~VSITarReader() */
172 : /************************************************************************/
173 :
174 166 : VSITarReader::~VSITarReader()
175 : {
176 83 : if (fp)
177 83 : CPL_IGNORE_RET_VAL(VSIFCloseL(fp));
178 166 : }
179 :
180 : /************************************************************************/
181 : /* GetFileOffset() */
182 : /************************************************************************/
183 :
184 66 : VSIArchiveEntryFileOffset *VSITarReader::GetFileOffset()
185 : {
186 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
187 66 : if (m_bIsFuzzerFriendly)
188 : {
189 : return new VSITarEntryFileOffset(nCurOffset, nNextFileSize,
190 5 : osNextFileName);
191 : }
192 : #endif
193 61 : return new VSITarEntryFileOffset(nCurOffset);
194 : }
195 :
196 : /************************************************************************/
197 : /* IsNumericFieldTerminator() */
198 : /************************************************************************/
199 :
200 843 : static bool IsNumericFieldTerminator(GByte byVal)
201 : {
202 : // See https://github.com/Keruspe/tar-parser.rs/blob/master/tar.specs#L202
203 843 : return byVal == '\0' || byVal == ' ';
204 : }
205 :
206 : /************************************************************************/
207 : /* GotoNextFile() */
208 : /************************************************************************/
209 :
210 179 : int VSITarReader::GotoNextFile()
211 : {
212 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
213 179 : if (m_bIsFuzzerFriendly)
214 : {
215 11 : const int nNewFileMarkerSize =
216 : static_cast<int>(strlen("***NEWFILE***:"));
217 : while (true)
218 : {
219 26 : if (m_abyBufferIdx >= m_abyBufferSize)
220 : {
221 16 : if (m_abyBufferSize == 0)
222 : {
223 6 : m_abyBufferSize = static_cast<int>(
224 6 : VSIFReadL(m_abyBuffer, 1, BUFFER_SIZE, fp));
225 6 : if (m_abyBufferSize == 0)
226 11 : return FALSE;
227 6 : m_abyBuffer[m_abyBufferSize] = '\0';
228 : }
229 : else
230 : {
231 10 : if (m_abyBufferSize < BUFFER_SIZE)
232 : {
233 8 : if (nCurOffset > 0 && nCurOffset != m_nCurOffsetOld)
234 : {
235 5 : nNextFileSize = VSIFTellL(fp);
236 5 : if (nNextFileSize >= nCurOffset)
237 : {
238 5 : nNextFileSize -= nCurOffset;
239 5 : m_nCurOffsetOld = nCurOffset;
240 5 : return TRUE;
241 : }
242 : }
243 3 : return FALSE;
244 : }
245 2 : memcpy(m_abyBuffer, m_abyBuffer + HALF_BUFFER_SIZE,
246 : HALF_BUFFER_SIZE);
247 2 : m_abyBufferSize = static_cast<int>(
248 2 : VSIFReadL(m_abyBuffer + HALF_BUFFER_SIZE, 1,
249 : HALF_BUFFER_SIZE, fp));
250 2 : if (m_abyBufferSize == 0)
251 0 : return FALSE;
252 2 : m_abyBufferIdx = 0;
253 2 : m_abyBufferSize += HALF_BUFFER_SIZE;
254 2 : m_abyBuffer[m_abyBufferSize] = '\0';
255 : }
256 : }
257 :
258 18 : std::string_view abyBuffer(reinterpret_cast<char *>(m_abyBuffer),
259 18 : m_abyBufferSize);
260 18 : const auto posNewFile = abyBuffer.find(
261 : std::string_view("***NEWFILE***:", nNewFileMarkerSize),
262 18 : m_abyBufferIdx);
263 18 : if (posNewFile == std::string::npos)
264 : {
265 5 : m_abyBufferIdx = m_abyBufferSize;
266 : }
267 : else
268 : {
269 13 : m_abyBufferIdx = static_cast<int>(posNewFile);
270 : // 2: space for at least one-char filename and '\n'
271 13 : if (m_abyBufferIdx < m_abyBufferSize - (nNewFileMarkerSize + 2))
272 : {
273 11 : if (nCurOffset > 0 && nCurOffset != m_nCurOffsetOld)
274 : {
275 3 : nNextFileSize = VSIFTellL(fp);
276 3 : nNextFileSize -= m_abyBufferSize;
277 3 : nNextFileSize += m_abyBufferIdx;
278 3 : if (nNextFileSize >= nCurOffset)
279 : {
280 3 : nNextFileSize -= nCurOffset;
281 3 : m_nCurOffsetOld = nCurOffset;
282 3 : return TRUE;
283 : }
284 : }
285 8 : m_abyBufferIdx += nNewFileMarkerSize;
286 8 : const int nFilenameStartIdx = m_abyBufferIdx;
287 45 : for (; m_abyBufferIdx < m_abyBufferSize &&
288 45 : m_abyBuffer[m_abyBufferIdx] != '\n';
289 37 : ++m_abyBufferIdx)
290 : {
291 : // Do nothing.
292 : }
293 8 : if (m_abyBufferIdx < m_abyBufferSize)
294 : {
295 8 : const char *pszFilename =
296 8 : reinterpret_cast<const char *>(m_abyBuffer +
297 8 : nFilenameStartIdx);
298 : osNextFileName.assign(
299 : pszFilename,
300 : CPLStrnlen(pszFilename,
301 8 : m_abyBufferIdx - nFilenameStartIdx));
302 24 : if (osNextFileName.empty() || osNextFileName == "." ||
303 16 : osNextFileName.find("..") != std::string::npos ||
304 24 : osNextFileName.find("//") != std::string::npos ||
305 8 : osNextFileName.find("\\\\") != std::string::npos)
306 : {
307 0 : CPLError(CE_Failure, CPLE_AppDefined,
308 : "Invalid filename");
309 0 : return false;
310 : }
311 8 : nCurOffset = VSIFTellL(fp);
312 8 : nCurOffset -= m_abyBufferSize;
313 8 : nCurOffset += m_abyBufferIdx + 1;
314 : }
315 : }
316 : else
317 : {
318 2 : m_abyBufferIdx = m_abyBufferSize;
319 : }
320 : }
321 15 : }
322 : }
323 : #endif
324 :
325 168 : osNextFileName.clear();
326 : while (true)
327 : {
328 173 : GByte abyHeader[512] = {};
329 173 : if (VSIFReadL(abyHeader, 512, 1, fp) != 1)
330 32 : return FALSE;
331 :
332 513 : if (!(abyHeader[100] == 0x80 ||
333 171 : IsNumericFieldTerminator(
334 171 : abyHeader[107])) || /* start/end of filemode */
335 169 : !(abyHeader[108] == 0x80 ||
336 167 : IsNumericFieldTerminator(
337 167 : abyHeader[115])) || /* start/end of owner ID */
338 169 : !(abyHeader[116] == 0x80 ||
339 167 : IsNumericFieldTerminator(
340 167 : abyHeader[123])) || /* start/end of group ID */
341 511 : !IsNumericFieldTerminator(abyHeader[135]) || /* end of file size */
342 169 : !IsNumericFieldTerminator(abyHeader[147])) /* end of mtime */
343 : {
344 2 : return FALSE;
345 : }
346 169 : if (!(abyHeader[124] == ' ' ||
347 169 : (abyHeader[124] >= '0' && abyHeader[124] <= '7')))
348 28 : return FALSE;
349 :
350 141 : if (osNextFileName.empty())
351 : {
352 : osNextFileName.assign(
353 : reinterpret_cast<const char *>(abyHeader),
354 136 : CPLStrnlen(reinterpret_cast<const char *>(abyHeader), 100));
355 : }
356 :
357 141 : nNextFileSize = 0;
358 1692 : for (int i = 0; i < 11; i++)
359 : {
360 1551 : if (abyHeader[124 + i] != ' ')
361 : {
362 1551 : if (nNextFileSize > static_cast<GUIntBig>(GINTBIG_MAX / 8) ||
363 1551 : abyHeader[124 + i] < '0' || abyHeader[124 + i] >= '8')
364 : {
365 0 : CPLError(CE_Failure, CPLE_AppDefined,
366 : "Invalid file size for %s",
367 : osNextFileName.c_str());
368 0 : return FALSE;
369 : }
370 1551 : nNextFileSize = nNextFileSize * 8 + (abyHeader[124 + i] - '0');
371 : }
372 : }
373 141 : if (nNextFileSize > GINTBIG_MAX)
374 : {
375 0 : CPLError(CE_Failure, CPLE_AppDefined, "Invalid file size for %s",
376 : osNextFileName.c_str());
377 0 : return FALSE;
378 : }
379 :
380 141 : nModifiedTime = 0;
381 1692 : for (int i = 0; i < 11; i++)
382 : {
383 1551 : if (abyHeader[136 + i] != ' ')
384 : {
385 1551 : if (nModifiedTime > GINTBIG_MAX / 8 ||
386 1551 : abyHeader[136 + i] < '0' || abyHeader[136 + i] >= '8' ||
387 1551 : nModifiedTime * 8 >
388 1551 : GINTBIG_MAX - (abyHeader[136 + i] - '0'))
389 : {
390 0 : CPLError(CE_Failure, CPLE_AppDefined,
391 : "Invalid mtime for %s", osNextFileName.c_str());
392 0 : return FALSE;
393 : }
394 1551 : nModifiedTime = nModifiedTime * 8 + (abyHeader[136 + i] - '0');
395 : }
396 : }
397 :
398 141 : if (abyHeader[156] == 'L' && nNextFileSize > 0 && nNextFileSize < 32768)
399 : {
400 : // If this is a large filename record, then read the filename
401 5 : osNextFileName.clear();
402 5 : osNextFileName.resize(
403 5 : static_cast<size_t>(((nNextFileSize + 511) / 512) * 512));
404 5 : if (VSIFReadL(&osNextFileName[0], osNextFileName.size(), 1, fp) !=
405 : 1)
406 0 : return FALSE;
407 5 : osNextFileName.resize(static_cast<size_t>(nNextFileSize));
408 5 : if (osNextFileName.back() == '\0')
409 5 : osNextFileName.pop_back();
410 : }
411 : else
412 : {
413 : // Is it a ustar extension ?
414 : // Cf https://en.wikipedia.org/wiki/Tar_(computing)#UStar_format
415 136 : if (memcmp(abyHeader + 257, "ustar\0", 6) == 0 &&
416 2 : abyHeader[345] != '\0')
417 : {
418 2 : std::string osFilenamePrefix;
419 : osFilenamePrefix.assign(
420 : reinterpret_cast<const char *>(abyHeader + 345),
421 : CPLStrnlen(reinterpret_cast<const char *>(abyHeader + 345),
422 2 : 155));
423 2 : osNextFileName = osFilenamePrefix + '/' + osNextFileName;
424 : }
425 :
426 136 : break;
427 : }
428 5 : }
429 :
430 136 : nCurOffset = VSIFTellL(fp);
431 :
432 136 : const GUIntBig nBytesToSkip = ((nNextFileSize + 511) / 512) * 512;
433 136 : if (nBytesToSkip > (~(static_cast<GUIntBig>(0))) - nCurOffset)
434 : {
435 0 : CPLError(CE_Failure, CPLE_AppDefined, "Bad .tar structure");
436 0 : return FALSE;
437 : }
438 :
439 136 : if (VSIFSeekL(fp, nBytesToSkip, SEEK_CUR) < 0)
440 0 : return FALSE;
441 :
442 136 : return TRUE;
443 : }
444 :
445 : /************************************************************************/
446 : /* GotoFirstFile() */
447 : /************************************************************************/
448 :
449 112 : int VSITarReader::GotoFirstFile()
450 : {
451 112 : if (VSIFSeekL(fp, 0, SEEK_SET) < 0)
452 0 : return FALSE;
453 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
454 112 : m_abyBufferIdx = 0;
455 112 : m_abyBufferSize = 0;
456 112 : nCurOffset = 0;
457 112 : m_nCurOffsetOld = 0;
458 112 : osNextFileName = "";
459 112 : nNextFileSize = 0;
460 : #endif
461 112 : return GotoNextFile();
462 : }
463 :
464 : /************************************************************************/
465 : /* GotoFileOffset() */
466 : /************************************************************************/
467 :
468 22 : int VSITarReader::GotoFileOffset(VSIArchiveEntryFileOffset *pOffset)
469 : {
470 22 : VSITarEntryFileOffset *pTarEntryOffset =
471 : static_cast<VSITarEntryFileOffset *>(pOffset);
472 : #ifdef HAVE_FUZZER_FRIENDLY_ARCHIVE
473 22 : if (m_bIsFuzzerFriendly)
474 : {
475 0 : if (VSIFSeekL(fp,
476 0 : pTarEntryOffset->m_nOffset + pTarEntryOffset->m_nFileSize,
477 0 : SEEK_SET) < 0)
478 0 : return FALSE;
479 0 : m_abyBufferIdx = 0;
480 0 : m_abyBufferSize = 0;
481 0 : nCurOffset = pTarEntryOffset->m_nOffset;
482 0 : m_nCurOffsetOld = pTarEntryOffset->m_nOffset;
483 0 : osNextFileName = pTarEntryOffset->m_osFileName;
484 0 : nNextFileSize = pTarEntryOffset->m_nFileSize;
485 0 : return TRUE;
486 : }
487 : #endif
488 44 : if (pTarEntryOffset->m_nOffset < 512 ||
489 22 : VSIFSeekL(fp, pTarEntryOffset->m_nOffset - 512, SEEK_SET) < 0)
490 0 : return FALSE;
491 22 : return GotoNextFile();
492 : }
493 :
494 : /************************************************************************/
495 : /* ==================================================================== */
496 : /* VSITarFilesystemHandler */
497 : /* ==================================================================== */
498 : /************************************************************************/
499 :
500 : class VSITarFilesystemHandler final : public VSIArchiveFilesystemHandler
501 : {
502 : public:
503 696 : const char *GetPrefix() const override
504 : {
505 696 : return "/vsitar";
506 : }
507 :
508 : std::vector<CPLString> GetExtensions() const override;
509 : std::unique_ptr<VSIArchiveReader>
510 : CreateReader(const char *pszTarFileName) override;
511 :
512 : VSIVirtualHandleUniquePtr Open(const char *pszFilename,
513 : const char *pszAccess, bool bSetError,
514 : CSLConstList /* papszOptions */) override;
515 : };
516 :
517 : /************************************************************************/
518 : /* GetExtensions() */
519 : /************************************************************************/
520 :
521 108 : std::vector<CPLString> VSITarFilesystemHandler::GetExtensions() const
522 : {
523 108 : std::vector<CPLString> oList;
524 108 : oList.push_back(".tar.gz");
525 108 : oList.push_back(".tar");
526 108 : oList.push_back(".tgz");
527 108 : return oList;
528 : }
529 :
530 : /************************************************************************/
531 : /* CreateReader() */
532 : /************************************************************************/
533 :
534 : std::unique_ptr<VSIArchiveReader>
535 83 : VSITarFilesystemHandler::CreateReader(const char *pszTarFileName)
536 : {
537 166 : CPLString osTarInFileName;
538 :
539 83 : if (VSIIsTGZ(pszTarFileName))
540 : {
541 12 : osTarInFileName = "/vsigzip/";
542 12 : osTarInFileName += pszTarFileName;
543 : }
544 : else
545 71 : osTarInFileName = pszTarFileName;
546 :
547 166 : auto poReader = std::make_unique<VSITarReader>(osTarInFileName);
548 83 : if (!poReader->IsValid() || !poReader->GotoFirstFile())
549 : {
550 16 : return nullptr;
551 : }
552 :
553 67 : return poReader;
554 : }
555 :
556 : /************************************************************************/
557 : /* Open() */
558 : /************************************************************************/
559 :
560 : VSIVirtualHandleUniquePtr
561 58 : VSITarFilesystemHandler::Open(const char *pszFilename, const char *pszAccess,
562 : bool bSetError, CSLConstList /* papszOptions */)
563 : {
564 :
565 58 : if (strchr(pszAccess, 'w') != nullptr || strchr(pszAccess, '+') != nullptr)
566 : {
567 0 : CPLError(CE_Failure, CPLE_AppDefined,
568 : "Only read-only mode is supported for /vsitar");
569 0 : return nullptr;
570 : }
571 :
572 116 : CPLString osTarInFileName;
573 : std::unique_ptr<char, VSIFreeReleaser> tarFilename(
574 116 : SplitFilename(pszFilename, osTarInFileName, true, bSetError));
575 58 : if (tarFilename == nullptr)
576 2 : return nullptr;
577 :
578 112 : auto poReader = OpenArchiveFile(tarFilename.get(), osTarInFileName);
579 56 : if (poReader == nullptr)
580 : {
581 25 : return nullptr;
582 : }
583 :
584 62 : CPLString osSubFileName("/vsisubfile/");
585 : VSITarEntryFileOffset *pOffset =
586 31 : reinterpret_cast<VSITarEntryFileOffset *>(poReader->GetFileOffset());
587 31 : osSubFileName += CPLString().Printf(CPL_FRMT_GUIB, pOffset->m_nOffset);
588 31 : osSubFileName += "_";
589 31 : osSubFileName += CPLString().Printf(CPL_FRMT_GUIB, poReader->GetFileSize());
590 31 : osSubFileName += ",";
591 31 : delete pOffset;
592 :
593 31 : if (VSIIsTGZ(tarFilename.get()))
594 : {
595 7 : osSubFileName += "/vsigzip/";
596 7 : osSubFileName += tarFilename.get();
597 : }
598 : else
599 24 : osSubFileName += tarFilename.get();
600 :
601 31 : return VSIFilesystemHandler::OpenStatic(osSubFileName, "rb");
602 : }
603 :
604 : //! @endcond
605 :
606 : /************************************************************************/
607 : /* VSIInstallTarFileHandler() */
608 : /************************************************************************/
609 :
610 : /*!
611 : \brief Install /vsitar/ file system handler.
612 :
613 : A special file handler is installed that allows reading on-the-fly in TAR
614 : (regular .tar, or compressed .tar.gz/.tgz) archives.
615 :
616 : All portions of the file system underneath the base path "/vsitar/" will be
617 : handled by this driver.
618 :
619 : \verbatim embed:rst
620 : See :ref:`/vsitar/ documentation <vsitar>`
621 : \endverbatim
622 :
623 : @since GDAL 1.8.0
624 : */
625 :
626 1754 : void VSIInstallTarFileHandler(void)
627 : {
628 1754 : VSIFileManager::InstallHandler("/vsitar/", new VSITarFilesystemHandler());
629 1754 : }
|