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