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