Coverage Report

Created: 2025-08-28 06:57

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