Coverage Report

Created: 2026-04-01 06:20

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/port/cpl_path.cpp
Line
Count
Source
1
/**********************************************************************
2
 *
3
 * Project:  CPL - Common Portability Library
4
 * Purpose:  Portable filename/path parsing, and forming ala "Glob API".
5
 * Author:   Frank Warmerdam, warmerdam@pobox.com
6
 *
7
 **********************************************************************
8
 * Copyright (c) 1999, Frank Warmerdam
9
 * Copyright (c) 2008-2012, Even Rouault <even dot rouault at spatialys.com>
10
 *
11
 * SPDX-License-Identifier: MIT
12
 ****************************************************************************/
13
14
#define ALLOW_DEPRECATED_CPL_PATH_FUNCTIONS
15
16
#include "cpl_port.h"
17
#include "cpl_conv.h"
18
19
#include <cctype>
20
#include <climits>
21
#include <cstddef>
22
#include <cstdio>
23
#include <cstring>
24
#if HAVE_UNISTD_H
25
#include <unistd.h>
26
#endif
27
28
#include <algorithm>
29
#include <string>
30
#include <string_view>
31
32
#include "cpl_atomic_ops.h"
33
#include "cpl_config.h"
34
#include "cpl_error.h"
35
#include "cpl_multiproc.h"
36
#include "cpl_string.h"
37
#include "cpl_vsi.h"
38
39
// Should be size of larged possible filename.
40
constexpr int CPL_PATH_BUF_SIZE = 2048;
41
constexpr int CPL_PATH_BUF_COUNT = 10;
42
43
static const char *CPLStaticBufferTooSmall(char *pszStaticResult)
44
0
{
45
0
    CPLError(CE_Failure, CPLE_AppDefined, "Destination buffer too small");
46
0
    if (pszStaticResult == nullptr)
47
0
        return "";
48
0
    strcpy(pszStaticResult, "");
49
0
    return pszStaticResult;
50
0
}
51
52
/************************************************************************/
53
/*                         CPLGetStaticResult()                         */
54
/************************************************************************/
55
56
static char *CPLGetStaticResult()
57
58
17.4k
{
59
17.4k
    int bMemoryError = FALSE;
60
17.4k
    char *pachBufRingInfo =
61
17.4k
        static_cast<char *>(CPLGetTLSEx(CTLS_PATHBUF, &bMemoryError));
62
17.4k
    if (bMemoryError)
63
0
        return nullptr;
64
17.4k
    if (pachBufRingInfo == nullptr)
65
2
    {
66
2
        pachBufRingInfo = static_cast<char *>(VSI_CALLOC_VERBOSE(
67
2
            1, sizeof(int) + CPL_PATH_BUF_SIZE * CPL_PATH_BUF_COUNT));
68
2
        if (pachBufRingInfo == nullptr)
69
0
            return nullptr;
70
2
        CPLSetTLS(CTLS_PATHBUF, pachBufRingInfo, TRUE);
71
2
    }
72
73
    /* -------------------------------------------------------------------- */
74
    /*      Work out which string in the "ring" we want to use this         */
75
    /*      time.                                                           */
76
    /* -------------------------------------------------------------------- */
77
17.4k
    int *pnBufIndex = reinterpret_cast<int *>(pachBufRingInfo);
78
17.4k
    const size_t nOffset =
79
17.4k
        sizeof(int) + static_cast<size_t>(*pnBufIndex * CPL_PATH_BUF_SIZE);
80
17.4k
    char *pachBuffer = pachBufRingInfo + nOffset;
81
82
17.4k
    *pnBufIndex = (*pnBufIndex + 1) % CPL_PATH_BUF_COUNT;
83
84
17.4k
    return pachBuffer;
85
17.4k
}
86
87
/************************************************************************/
88
/*                       CPLPathReturnTLSString()                       */
89
/************************************************************************/
90
91
static const char *CPLPathReturnTLSString(const std::string &osRes,
92
                                          const char *pszFuncName)
93
17.4k
{
94
17.4k
    if (osRes.size() >= CPL_PATH_BUF_SIZE)
95
0
    {
96
0
        CPLError(CE_Failure, CPLE_AppDefined, "Too long result for %s()",
97
0
                 pszFuncName);
98
0
        return "";
99
0
    }
100
101
17.4k
    char *pszStaticResult = CPLGetStaticResult();
102
17.4k
    if (pszStaticResult == nullptr)
103
0
        return CPLStaticBufferTooSmall(pszStaticResult);
104
17.4k
    memcpy(pszStaticResult, osRes.c_str(), osRes.size() + 1);
105
17.4k
    return pszStaticResult;
106
17.4k
}
107
108
/************************************************************************/
109
/*                        CPLFindFilenameStart()                        */
110
/************************************************************************/
111
112
static int CPLFindFilenameStart(const char *pszFilename, size_t nStart = 0)
113
114
5.41k
{
115
5.41k
    size_t iFileStart = nStart ? nStart : strlen(pszFilename);
116
117
43.2k
    for (; iFileStart > 0 && pszFilename[iFileStart - 1] != '/' &&
118
37.8k
           pszFilename[iFileStart - 1] != '\\';
119
37.8k
         iFileStart--)
120
37.8k
    {
121
37.8k
    }
122
123
5.41k
    return static_cast<int>(iFileStart);
124
5.41k
}
125
126
/************************************************************************/
127
/*                           CPLGetPathSafe()                           */
128
/************************************************************************/
129
130
/**
131
 * Extract directory path portion of filename.
132
 *
133
 * Returns a string containing the directory path portion of the passed
134
 * filename.  If there is no path in the passed filename an empty string
135
 * will be returned (not NULL).
136
 *
137
 * \code{.cpp}
138
 * CPLGetPathSafe( "abc/def.xyz" ) == "abc"
139
 * CPLGetPathSafe( "/abc/def/" ) == "/abc/def"
140
 * CPLGetPathSafe( "/" ) == "/"
141
 * CPLGetPathSafe( "/abc/def" ) == "/abc"
142
 * CPLGetPathSafe( "abc" ) == ""
143
 * \endcode
144
 *
145
 * @param pszFilename the filename potentially including a path.
146
 *
147
 * @return Path.
148
 *
149
 * @since 3.11
150
 */
151
152
std::string CPLGetPathSafe(const char *pszFilename)
153
154
5.41k
{
155
5.41k
    size_t nSuffixPos = 0;
156
5.41k
    if (STARTS_WITH(pszFilename, "/vsicurl/http"))
157
0
    {
158
0
        const char *pszQuestionMark = strchr(pszFilename, '?');
159
0
        if (pszQuestionMark)
160
0
            nSuffixPos = static_cast<size_t>(pszQuestionMark - pszFilename);
161
0
    }
162
5.41k
    else if (STARTS_WITH(pszFilename, "/vsicurl?") &&
163
0
             strstr(pszFilename, "url="))
164
0
    {
165
0
        std::string osRet;
166
0
        const CPLStringList aosTokens(
167
0
            CSLTokenizeString2(pszFilename + strlen("/vsicurl?"), "&", 0));
168
0
        for (int i = 0; i < aosTokens.size(); i++)
169
0
        {
170
0
            if (osRet.empty())
171
0
                osRet = "/vsicurl?";
172
0
            else
173
0
                osRet += '&';
174
0
            if (STARTS_WITH(aosTokens[i], "url=") &&
175
0
                !STARTS_WITH(aosTokens[i], "url=/vsicurl"))
176
0
            {
177
0
                char *pszUnescaped =
178
0
                    CPLUnescapeString(aosTokens[i], nullptr, CPLES_URL);
179
0
                char *pszPath = CPLEscapeString(
180
0
                    CPLGetPathSafe(pszUnescaped + strlen("url=")).c_str(), -1,
181
0
                    CPLES_URL);
182
0
                osRet += "url=";
183
0
                osRet += pszPath;
184
0
                CPLFree(pszPath);
185
0
                CPLFree(pszUnescaped);
186
0
            }
187
0
            else
188
0
            {
189
0
                osRet += aosTokens[i];
190
0
            }
191
0
        }
192
0
        return osRet;
193
0
    }
194
195
5.41k
    const int iFileStart = CPLFindFilenameStart(pszFilename, nSuffixPos);
196
5.41k
    if (iFileStart == 0)
197
0
    {
198
0
        return std::string();
199
0
    }
200
201
5.41k
    std::string osRet(pszFilename, iFileStart);
202
203
5.41k
    if (iFileStart > 1 && (osRet.back() == '/' || osRet.back() == '\\'))
204
5.41k
        osRet.pop_back();
205
206
5.41k
    if (nSuffixPos)
207
0
    {
208
0
        osRet += (pszFilename + nSuffixPos);
209
0
    }
210
211
5.41k
    return osRet;
212
5.41k
}
213
214
/************************************************************************/
215
/*                             CPLGetPath()                             */
216
/************************************************************************/
217
218
/**
219
 * Extract directory path portion of filename.
220
 *
221
 * Returns a string containing the directory path portion of the passed
222
 * filename.  If there is no path in the passed filename an empty string
223
 * will be returned (not NULL).
224
 *
225
 * \code{.cpp}
226
 * CPLGetPath( "abc/def.xyz" ) == "abc"
227
 * CPLGetPath( "/abc/def/" ) == "/abc/def"
228
 * CPLGetPath( "/" ) == "/"
229
 * CPLGetPath( "/abc/def" ) == "/abc"
230
 * CPLGetPath( "abc" ) == ""
231
 * \endcode
232
 *
233
 * @param pszFilename the filename potentially including a path.
234
 *
235
 * @return Path in an internal string which must not be freed.  The string
236
 * may be destroyed by the next CPL filename handling call.  The returned
237
 * will generally not contain a trailing path separator.
238
 *
239
 * @deprecated If using C++, prefer using CPLGetPathSafe() instead
240
 */
241
242
const char *CPLGetPath(const char *pszFilename)
243
244
0
{
245
0
    return CPLPathReturnTLSString(CPLGetPathSafe(pszFilename), __FUNCTION__);
246
0
}
247
248
/************************************************************************/
249
/*                           CPLGetDirname()                            */
250
/************************************************************************/
251
252
/**
253
 * Extract directory path portion of filename.
254
 *
255
 * Returns a string containing the directory path portion of the passed
256
 * filename.  If there is no path in the passed filename the dot will be
257
 * returned.  It is the only difference from CPLGetPath().
258
 *
259
 * \code{.cpp}
260
 * CPLGetDirnameSafe( "abc/def.xyz" ) == "abc"
261
 * CPLGetDirnameSafe( "/abc/def/" ) == "/abc/def"
262
 * CPLGetDirnameSafe( "/" ) == "/"
263
 * CPLGetDirnameSafe( "/abc/def" ) == "/abc"
264
 * CPLGetDirnameSafe( "abc" ) == "."
265
 * \endcode
266
 *
267
 * @param pszFilename the filename potentially including a path.
268
 *
269
 * @return Path
270
 *
271
 * @since 3.11
272
 */
273
274
std::string CPLGetDirnameSafe(const char *pszFilename)
275
276
0
{
277
0
    size_t nSuffixPos = 0;
278
0
    if (STARTS_WITH(pszFilename, "/vsicurl/http"))
279
0
    {
280
0
        const char *pszQuestionMark = strchr(pszFilename, '?');
281
0
        if (pszQuestionMark)
282
0
            nSuffixPos = static_cast<size_t>(pszQuestionMark - pszFilename);
283
0
    }
284
0
    else if (STARTS_WITH(pszFilename, "/vsicurl?") &&
285
0
             strstr(pszFilename, "url="))
286
0
    {
287
0
        std::string osRet;
288
0
        const CPLStringList aosTokens(
289
0
            CSLTokenizeString2(pszFilename + strlen("/vsicurl?"), "&", 0));
290
0
        for (int i = 0; i < aosTokens.size(); i++)
291
0
        {
292
0
            if (osRet.empty())
293
0
                osRet = "/vsicurl?";
294
0
            else
295
0
                osRet += '&';
296
0
            if (STARTS_WITH(aosTokens[i], "url=") &&
297
0
                !STARTS_WITH(aosTokens[i], "url=/vsicurl"))
298
0
            {
299
0
                char *pszUnescaped =
300
0
                    CPLUnescapeString(aosTokens[i], nullptr, CPLES_URL);
301
0
                char *pszPath = CPLEscapeString(
302
0
                    CPLGetDirname(pszUnescaped + strlen("url=")), -1,
303
0
                    CPLES_URL);
304
0
                osRet += "url=";
305
0
                osRet += pszPath;
306
0
                CPLFree(pszPath);
307
0
                CPLFree(pszUnescaped);
308
0
            }
309
0
            else
310
0
            {
311
0
                osRet += aosTokens[i];
312
0
            }
313
0
        }
314
0
        return osRet;
315
0
    }
316
317
0
    const int iFileStart = CPLFindFilenameStart(pszFilename, nSuffixPos);
318
0
    if (iFileStart == 0)
319
0
    {
320
0
        return std::string(".");
321
0
    }
322
323
0
    std::string osRet(pszFilename, iFileStart);
324
325
0
    if (iFileStart > 1 && (osRet.back() == '/' || osRet.back() == '\\'))
326
0
        osRet.pop_back();
327
328
0
    if (nSuffixPos)
329
0
    {
330
0
        osRet += (pszFilename + nSuffixPos);
331
0
    }
332
333
0
    return osRet;
334
0
}
335
336
/************************************************************************/
337
/*                           CPLGetDirname()                            */
338
/************************************************************************/
339
340
/**
341
 * Extract directory path portion of filename.
342
 *
343
 * Returns a string containing the directory path portion of the passed
344
 * filename.  If there is no path in the passed filename the dot will be
345
 * returned.  It is the only difference from CPLGetPath().
346
 *
347
 * \code{.cpp}
348
 * CPLGetDirname( "abc/def.xyz" ) == "abc"
349
 * CPLGetDirname( "/abc/def/" ) == "/abc/def"
350
 * CPLGetDirname( "/" ) == "/"
351
 * CPLGetDirname( "/abc/def" ) == "/abc"
352
 * CPLGetDirname( "abc" ) == "."
353
 * \endcode
354
 *
355
 * @param pszFilename the filename potentially including a path.
356
 *
357
 * @return Path in an internal string which must not be freed.  The string
358
 * may be destroyed by the next CPL filename handling call.  The returned
359
 * will generally not contain a trailing path separator.
360
 */
361
362
const char *CPLGetDirname(const char *pszFilename)
363
364
0
{
365
0
    return CPLPathReturnTLSString(CPLGetDirnameSafe(pszFilename), __FUNCTION__);
366
0
}
367
368
/************************************************************************/
369
/*                           CPLGetFilename()                           */
370
/************************************************************************/
371
372
/**
373
 * Extract non-directory portion of filename.
374
 *
375
 * Returns a string containing the bare filename portion of the passed
376
 * filename.  If there is no filename (passed value ends in trailing directory
377
 * separator) an empty string is returned.
378
 *
379
 * \code{.cpp}
380
 * CPLGetFilename( "abc/def.xyz" ) == "def.xyz"
381
 * CPLGetFilename( "/abc/def/" ) == ""
382
 * CPLGetFilename( "abc/def" ) == "def"
383
 * \endcode
384
 *
385
 * @param pszFullFilename the full filename potentially including a path.
386
 *
387
 * @return just the non-directory portion of the path (points back into
388
 * original string).
389
 */
390
391
const char *CPLGetFilename(const char *pszFullFilename)
392
393
0
{
394
0
    const int iFileStart = CPLFindFilenameStart(pszFullFilename);
395
396
0
    return pszFullFilename + iFileStart;
397
0
}
398
399
/************************************************************************/
400
/*                         CPLGetBasenameSafe()                         */
401
/************************************************************************/
402
403
/**
404
 * Extract basename (non-directory, non-extension) portion of filename.
405
 *
406
 * Returns a string containing the file basename portion of the passed
407
 * name.  If there is no basename (passed value ends in trailing directory
408
 * separator, or filename starts with a dot) an empty string is returned.
409
 *
410
 * \code{.cpp}
411
 * CPLGetBasename( "abc/def.xyz" ) == "def"
412
 * CPLGetBasename( "abc/def" ) == "def"
413
 * CPLGetBasename( "abc/def/" ) == ""
414
 * \endcode
415
 *
416
 * @param pszFullFilename the full filename potentially including a path.
417
 *
418
 * @return just the non-directory, non-extension portion of the path
419
 *
420
 * @since 3.11
421
 */
422
423
std::string CPLGetBasenameSafe(const char *pszFullFilename)
424
425
0
{
426
0
    const size_t iFileStart =
427
0
        static_cast<size_t>(CPLFindFilenameStart(pszFullFilename));
428
429
0
    size_t iExtStart = strlen(pszFullFilename);
430
0
    for (; iExtStart > iFileStart && pszFullFilename[iExtStart] != '.';
431
0
         iExtStart--)
432
0
    {
433
0
    }
434
435
0
    if (iExtStart == iFileStart)
436
0
        iExtStart = strlen(pszFullFilename);
437
438
0
    const size_t nLength = iExtStart - iFileStart;
439
0
    return std::string(pszFullFilename + iFileStart, nLength);
440
0
}
441
442
/************************************************************************/
443
/*                           CPLGetBasename()                           */
444
/************************************************************************/
445
446
/**
447
 * Extract basename (non-directory, non-extension) portion of filename.
448
 *
449
 * Returns a string containing the file basename portion of the passed
450
 * name.  If there is no basename (passed value ends in trailing directory
451
 * separator, or filename starts with a dot) an empty string is returned.
452
 *
453
 * \code{.cpp}
454
 * CPLGetBasename( "abc/def.xyz" ) == "def"
455
 * CPLGetBasename( "abc/def" ) == "def"
456
 * CPLGetBasename( "abc/def/" ) == ""
457
 * \endcode
458
 *
459
 * @param pszFullFilename the full filename potentially including a path.
460
 *
461
 * @return just the non-directory, non-extension portion of the path in
462
 * an internal string which must not be freed.  The string
463
 * may be destroyed by the next CPL filename handling call.
464
 *
465
 * @deprecated If using C++, prefer using CPLGetBasenameSafe() instead
466
 */
467
468
const char *CPLGetBasename(const char *pszFullFilename)
469
470
0
{
471
0
    return CPLPathReturnTLSString(CPLGetBasenameSafe(pszFullFilename),
472
0
                                  __FUNCTION__);
473
0
}
474
475
/************************************************************************/
476
/*                        CPLGetExtensionSafe()                         */
477
/************************************************************************/
478
479
/**
480
 * Extract filename extension from full filename.
481
 *
482
 * Returns a string containing the extension portion of the passed
483
 * name.  If there is no extension (the filename has no dot) an empty string
484
 * is returned.  The returned extension will not include the period.
485
 *
486
 * \code{.cpp}
487
 * CPLGetExtensionSafe( "abc/def.xyz" ) == "xyz"
488
 * CPLGetExtensionSafe( "abc/def" ) == ""
489
 * \endcode
490
 *
491
 * @param pszFullFilename the full filename potentially including a path.
492
 *
493
 * @return just the extension portion of the path.
494
 *
495
 * @since 3.11
496
 */
497
498
std::string CPLGetExtensionSafe(const char *pszFullFilename)
499
500
0
{
501
0
    if (pszFullFilename[0] == '\0')
502
0
        return std::string();
503
504
0
    size_t iFileStart =
505
0
        static_cast<size_t>(CPLFindFilenameStart(pszFullFilename));
506
0
    size_t iExtStart = strlen(pszFullFilename);
507
0
    for (; iExtStart > iFileStart && pszFullFilename[iExtStart] != '.';
508
0
         iExtStart--)
509
0
    {
510
0
    }
511
512
0
    if (iExtStart == iFileStart)
513
0
        iExtStart = strlen(pszFullFilename) - 1;
514
515
    // If the extension is too long, it is very much likely not an extension,
516
    // but another component of the path
517
0
    const size_t knMaxExtensionSize = 10;
518
0
    if (strlen(pszFullFilename + iExtStart + 1) > knMaxExtensionSize)
519
0
        return "";
520
521
0
    return std::string(pszFullFilename + iExtStart + 1);
522
0
}
523
524
/************************************************************************/
525
/*                          CPLGetExtension()                           */
526
/************************************************************************/
527
528
/**
529
 * Extract filename extension from full filename.
530
 *
531
 * Returns a string containing the extension portion of the passed
532
 * name.  If there is no extension (the filename has no dot) an empty string
533
 * is returned.  The returned extension will not include the period.
534
 *
535
 * \code{.cpp}
536
 * CPLGetExtension( "abc/def.xyz" ) == "xyz"
537
 * CPLGetExtension( "abc/def" ) == ""
538
 * \endcode
539
 *
540
 * @param pszFullFilename the full filename potentially including a path.
541
 *
542
 * @return just the extension portion of the path in
543
 * an internal string which must not be freed.  The string
544
 * may be destroyed by the next CPL filename handling call.
545
 *
546
 * @deprecated If using C++, prefer using CPLGetExtensionSafe() instead
547
 */
548
549
const char *CPLGetExtension(const char *pszFullFilename)
550
551
0
{
552
0
    return CPLPathReturnTLSString(CPLGetExtensionSafe(pszFullFilename),
553
0
                                  __FUNCTION__);
554
0
}
555
556
/************************************************************************/
557
/*                          CPLGetCurrentDir()                          */
558
/************************************************************************/
559
560
/**
561
 * Get the current working directory name.
562
 *
563
 * @return a pointer to buffer, containing current working directory path
564
 * or NULL in case of error.  User is responsible to free that buffer
565
 * after usage with CPLFree() function.
566
 * If HAVE_GETCWD macro is not defined, the function returns NULL.
567
 **/
568
569
#ifdef _WIN32
570
char *CPLGetCurrentDir()
571
{
572
    const size_t nPathMax = _MAX_PATH;
573
    wchar_t *pwszDirPath =
574
        static_cast<wchar_t *>(VSI_MALLOC_VERBOSE(nPathMax * sizeof(wchar_t)));
575
    char *pszRet = nullptr;
576
    if (pwszDirPath != nullptr && _wgetcwd(pwszDirPath, nPathMax) != nullptr)
577
    {
578
        pszRet = CPLRecodeFromWChar(pwszDirPath, CPL_ENC_UCS2, CPL_ENC_UTF8);
579
    }
580
    CPLFree(pwszDirPath);
581
    return pszRet;
582
}
583
#elif defined(HAVE_GETCWD)
584
char *CPLGetCurrentDir()
585
0
{
586
0
#if PATH_MAX
587
0
    const size_t nPathMax = PATH_MAX;
588
#else
589
    const size_t nPathMax = 8192;
590
#endif
591
592
0
    char *pszDirPath = static_cast<char *>(VSI_MALLOC_VERBOSE(nPathMax));
593
0
    if (!pszDirPath)
594
0
        return nullptr;
595
596
0
    return getcwd(pszDirPath, nPathMax);
597
0
}
598
#else   // !HAVE_GETCWD
599
char *CPLGetCurrentDir()
600
{
601
    return nullptr;
602
}
603
#endif  // HAVE_GETCWD
604
605
/************************************************************************/
606
/*                         CPLResetExtension()                          */
607
/************************************************************************/
608
609
/**
610
 * Replace the extension with the provided one.
611
 *
612
 * @param pszPath the input path, this string is not altered.
613
 * @param pszExt the new extension to apply to the given path.
614
 *
615
 * @return an altered filename with the new extension.
616
 *
617
 * @since 3.11
618
 */
619
620
std::string CPLResetExtensionSafe(const char *pszPath, const char *pszExt)
621
622
0
{
623
0
    std::string osRet(pszPath);
624
625
    /* -------------------------------------------------------------------- */
626
    /*      First, try and strip off any existing extension.                */
627
    /* -------------------------------------------------------------------- */
628
629
0
    for (size_t i = osRet.size(); i > 0;)
630
0
    {
631
0
        --i;
632
0
        if (osRet[i] == '.')
633
0
        {
634
0
            osRet.resize(i);
635
0
            break;
636
0
        }
637
0
        else if (osRet[i] == '/' || osRet[i] == '\\' || osRet[i] == ':')
638
0
        {
639
0
            break;
640
0
        }
641
0
    }
642
643
    /* -------------------------------------------------------------------- */
644
    /*      Append the new extension.                                       */
645
    /* -------------------------------------------------------------------- */
646
0
    osRet += '.';
647
0
    osRet += pszExt;
648
649
0
    return osRet;
650
0
}
651
652
/************************************************************************/
653
/*                         CPLResetExtension()                          */
654
/************************************************************************/
655
656
/**
657
 * Replace the extension with the provided one.
658
 *
659
 * @param pszPath the input path, this string is not altered.
660
 * @param pszExt the new extension to apply to the given path.
661
 *
662
 * @return an altered filename with the new extension.    Do not
663
 * modify or free the returned string.  The string may be destroyed by the
664
 * next CPL call.
665
 *
666
 * @deprecated If using C++, prefer using CPLResetExtensionSafe() instead
667
 */
668
669
const char *CPLResetExtension(const char *pszPath, const char *pszExt)
670
671
0
{
672
0
    return CPLPathReturnTLSString(CPLResetExtensionSafe(pszPath, pszExt),
673
0
                                  __FUNCTION__);
674
0
}
675
676
/************************************************************************/
677
/*                        CPLFormFilenameSafe()                         */
678
/************************************************************************/
679
680
/**
681
 * Build a full file path from a passed path, file basename and extension.
682
 *
683
 * The path, and extension are optional.  The basename may in fact contain
684
 * an extension if desired.
685
 *
686
 * \code{.cpp}
687
 * CPLFormFilenameSafe("abc/xyz", "def", ".dat" ) == "abc/xyz/def.dat"
688
 * CPLFormFilenameSafe(NULL,"def", NULL ) == "def"
689
 * CPLFormFilenameSafe(NULL, "abc/def.dat", NULL ) == "abc/def.dat"
690
 * CPLFormFilenameSafe("/abc/xyz/", "def.dat", NULL ) == "/abc/xyz/def.dat"
691
 * CPLFormFilenameSafe("/a/b/c", "../d", NULL ) == "/a/b/d" (since 3.10.1)
692
 * \endcode
693
 *
694
 * @param pszPath directory path to the directory containing the file.  This
695
 * may be relative or absolute, and may have a trailing path separator or
696
 * not.  May be NULL.
697
 *
698
 * @param pszBasename file basename.  May optionally have path and/or
699
 * extension.  Must *NOT* be NULL.
700
 *
701
 * @param pszExtension file extension, optionally including the period.  May
702
 * be NULL.
703
 *
704
 * @return a fully formed filename.
705
 *
706
 * @since 3.11
707
 */
708
709
std::string CPLFormFilenameSafe(const char *pszPath, const char *pszBasename,
710
                                const char *pszExtension)
711
712
17.4k
{
713
17.4k
    if (pszBasename[0] == '.' &&
714
0
        (pszBasename[1] == '/' || pszBasename[1] == '\\'))
715
0
        pszBasename += 2;
716
717
17.4k
    const char *pszAddedPathSep = "";
718
17.4k
    const char *pszAddedExtSep = "";
719
720
17.4k
    if (pszPath == nullptr)
721
0
        pszPath = "";
722
17.4k
    size_t nLenPath = strlen(pszPath);
723
724
17.4k
    const char *pszQuestionMark = nullptr;
725
17.4k
    if (STARTS_WITH_CI(pszPath, "/vsicurl/http"))
726
0
    {
727
0
        pszQuestionMark = strchr(pszPath, '?');
728
0
        if (pszQuestionMark)
729
0
        {
730
0
            nLenPath = pszQuestionMark - pszPath;
731
0
        }
732
0
        pszAddedPathSep = "/";
733
0
    }
734
735
17.4k
    if (!CPLIsFilenameRelative(pszPath) && pszBasename[0] == '.' &&
736
0
        pszBasename[1] == '.' &&
737
0
        (pszBasename[2] == 0 || pszBasename[2] == '\\' ||
738
0
         pszBasename[2] == '/'))
739
0
    {
740
        // "/a/b/" + "..[/something]" --> "/a[/something]"
741
        // "/a/b" + "..[/something]" --> "/a[/something]"
742
0
        if (pszPath[nLenPath - 1] == '\\' || pszPath[nLenPath - 1] == '/')
743
0
            nLenPath--;
744
0
        while (true)
745
0
        {
746
0
            const char *pszBasenameOri = pszBasename;
747
0
            const size_t nLenPathOri = nLenPath;
748
0
            while (nLenPath > 0 && pszPath[nLenPath - 1] != '\\' &&
749
0
                   pszPath[nLenPath - 1] != '/')
750
0
            {
751
0
                nLenPath--;
752
0
            }
753
0
            if (nLenPath == 1 && pszPath[0] == '/')
754
0
            {
755
0
                pszBasename += 2;
756
0
                if (pszBasename[0] == '/' || pszBasename[0] == '\\')
757
0
                    pszBasename++;
758
0
                if (*pszBasename == '.')
759
0
                {
760
0
                    pszBasename = pszBasenameOri;
761
0
                    nLenPath = nLenPathOri;
762
0
                    if (pszAddedPathSep[0] == 0)
763
0
                        pszAddedPathSep = "/";
764
0
                }
765
0
                break;
766
0
            }
767
0
            else if ((nLenPath > 1 && pszPath[0] == '/') ||
768
0
                     (nLenPath > 2 && pszPath[1] == ':') ||
769
0
                     (nLenPath > 6 && strncmp(pszPath, "\\\\$\\", 4) == 0))
770
0
            {
771
0
                nLenPath--;
772
0
                pszBasename += 2;
773
0
                if ((pszBasename[0] == '/' || pszBasename[0] == '\\') &&
774
0
                    pszBasename[1] == '.' && pszBasename[2] == '.')
775
0
                {
776
0
                    pszBasename++;
777
0
                }
778
0
                else
779
0
                {
780
0
                    break;
781
0
                }
782
0
            }
783
0
            else
784
0
            {
785
                // cppcheck-suppress redundantAssignment
786
0
                pszBasename = pszBasenameOri;
787
0
                nLenPath = nLenPathOri;
788
0
                if (pszAddedPathSep[0] == 0)
789
0
                    pszAddedPathSep = pszPath[0] == '/'
790
0
                                          ? "/"
791
0
                                          : VSIGetDirectorySeparator(pszPath);
792
0
                break;
793
0
            }
794
0
        }
795
0
    }
796
17.4k
    else if (nLenPath > 0 && pszPath[nLenPath - 1] != '/' &&
797
16.2k
             pszPath[nLenPath - 1] != '\\')
798
16.2k
    {
799
16.2k
        if (pszAddedPathSep[0] == 0)
800
16.2k
            pszAddedPathSep = VSIGetDirectorySeparator(pszPath);
801
16.2k
    }
802
803
17.4k
    if (pszExtension == nullptr)
804
17.4k
        pszExtension = "";
805
0
    else if (pszExtension[0] != '.' && strlen(pszExtension) > 0)
806
0
        pszAddedExtSep = ".";
807
808
17.4k
    std::string osRes;
809
17.4k
    osRes.reserve(nLenPath + strlen(pszAddedPathSep) + strlen(pszBasename) +
810
17.4k
                  strlen(pszAddedExtSep) + strlen(pszExtension) +
811
17.4k
                  (pszQuestionMark ? strlen(pszQuestionMark) : 0));
812
17.4k
    osRes.assign(pszPath, nLenPath);
813
17.4k
    osRes += pszAddedPathSep;
814
17.4k
    osRes += pszBasename;
815
17.4k
    osRes += pszAddedExtSep;
816
17.4k
    osRes += pszExtension;
817
818
17.4k
    if (pszQuestionMark)
819
0
    {
820
0
        osRes += pszQuestionMark;
821
0
    }
822
823
17.4k
    return osRes;
824
17.4k
}
825
826
/************************************************************************/
827
/*                          CPLFormFilename()                           */
828
/************************************************************************/
829
830
/**
831
 * Build a full file path from a passed path, file basename and extension.
832
 *
833
 * The path, and extension are optional.  The basename may in fact contain
834
 * an extension if desired.
835
 *
836
 * \code{.cpp}
837
 * CPLFormFilename("abc/xyz", "def", ".dat" ) == "abc/xyz/def.dat"
838
 * CPLFormFilename(NULL,"def", NULL ) == "def"
839
 * CPLFormFilename(NULL, "abc/def.dat", NULL ) == "abc/def.dat"
840
 * CPLFormFilename("/abc/xyz/", "def.dat", NULL ) == "/abc/xyz/def.dat"
841
 * CPLFormFilename("/a/b/c", "../d", NULL ) == "/a/b/d" (since 3.10.1)
842
 * \endcode
843
 *
844
 * @param pszPath directory path to the directory containing the file.  This
845
 * may be relative or absolute, and may have a trailing path separator or
846
 * not.  May be NULL.
847
 *
848
 * @param pszBasename file basename.  May optionally have path and/or
849
 * extension.  Must *NOT* be NULL.
850
 *
851
 * @param pszExtension file extension, optionally including the period.  May
852
 * be NULL.
853
 *
854
 * @return a fully formed filename in an internal static string.  Do not
855
 * modify or free the returned string.  The string may be destroyed by the
856
 * next CPL call.
857
 *
858
 * @deprecated If using C++, prefer using CPLFormFilenameSafe() instead
859
 */
860
const char *CPLFormFilename(const char *pszPath, const char *pszBasename,
861
                            const char *pszExtension)
862
863
0
{
864
0
    return CPLPathReturnTLSString(
865
0
        CPLFormFilenameSafe(pszPath, pszBasename, pszExtension), __FUNCTION__);
866
0
}
867
868
/************************************************************************/
869
/*                       CPLFormCIFilenameSafe()                        */
870
/************************************************************************/
871
872
/**
873
 * Case insensitive file searching, returning full path.
874
 *
875
 * This function tries to return the path to a file regardless of
876
 * whether the file exactly matches the basename, and extension case, or
877
 * is all upper case, or all lower case.  The path is treated as case
878
 * sensitive.  This function is equivalent to CPLFormFilename() on
879
 * case insensitive file systems (like Windows).
880
 *
881
 * @param pszPath directory path to the directory containing the file.  This
882
 * may be relative or absolute, and may have a trailing path separator or
883
 * not.  May be NULL.
884
 *
885
 * @param pszBasename file basename.  May optionally have path and/or
886
 * extension.  May not be NULL.
887
 *
888
 * @param pszExtension file extension, optionally including the period.  May
889
 * be NULL.
890
 *
891
 * @return a fully formed filename.
892
 *
893
 * @since 3.11
894
 */
895
896
std::string CPLFormCIFilenameSafe(const char *pszPath, const char *pszBasename,
897
                                  const char *pszExtension)
898
899
0
{
900
    // On case insensitive filesystems, just default to CPLFormFilename().
901
0
    if (!VSIIsCaseSensitiveFS(pszPath))
902
0
        return CPLFormFilenameSafe(pszPath, pszBasename, pszExtension);
903
904
0
    const char *pszAddedExtSep = "";
905
0
    size_t nLen = strlen(pszBasename) + 2;
906
907
0
    if (pszExtension != nullptr)
908
0
        nLen += strlen(pszExtension);
909
910
0
    char *pszFilename = static_cast<char *>(VSI_MALLOC_VERBOSE(nLen));
911
0
    if (pszFilename == nullptr)
912
0
        return "";
913
914
0
    if (pszExtension == nullptr)
915
0
        pszExtension = "";
916
0
    else if (pszExtension[0] != '.' && strlen(pszExtension) > 0)
917
0
        pszAddedExtSep = ".";
918
919
0
    snprintf(pszFilename, nLen, "%s%s%s", pszBasename, pszAddedExtSep,
920
0
             pszExtension);
921
922
0
    std::string osRet = CPLFormFilenameSafe(pszPath, pszFilename, nullptr);
923
0
    VSIStatBufL sStatBuf;
924
0
    int nStatRet = VSIStatExL(osRet.c_str(), &sStatBuf, VSI_STAT_EXISTS_FLAG);
925
926
0
    if (nStatRet != 0)
927
0
    {
928
0
        for (size_t i = 0; pszFilename[i] != '\0'; i++)
929
0
        {
930
0
            pszFilename[i] = static_cast<char>(CPLToupper(pszFilename[i]));
931
0
        }
932
933
0
        std::string osTmpPath(
934
0
            CPLFormFilenameSafe(pszPath, pszFilename, nullptr));
935
0
        nStatRet =
936
0
            VSIStatExL(osTmpPath.c_str(), &sStatBuf, VSI_STAT_EXISTS_FLAG);
937
0
        if (nStatRet == 0)
938
0
            osRet = std::move(osTmpPath);
939
0
    }
940
941
0
    if (nStatRet != 0)
942
0
    {
943
0
        for (size_t i = 0; pszFilename[i] != '\0'; i++)
944
0
        {
945
0
            pszFilename[i] = static_cast<char>(
946
0
                CPLTolower(static_cast<unsigned char>(pszFilename[i])));
947
0
        }
948
949
0
        std::string osTmpPath(
950
0
            CPLFormFilenameSafe(pszPath, pszFilename, nullptr));
951
0
        nStatRet =
952
0
            VSIStatExL(osTmpPath.c_str(), &sStatBuf, VSI_STAT_EXISTS_FLAG);
953
0
        if (nStatRet == 0)
954
0
            osRet = std::move(osTmpPath);
955
0
    }
956
957
0
    if (nStatRet != 0)
958
0
        osRet = CPLFormFilenameSafe(pszPath, pszBasename, pszExtension);
959
960
0
    CPLFree(pszFilename);
961
962
0
    return osRet;
963
0
}
964
965
/************************************************************************/
966
/*                         CPLFormCIFilename()                          */
967
/************************************************************************/
968
969
/**
970
 * Case insensitive file searching, returning full path.
971
 *
972
 * This function tries to return the path to a file regardless of
973
 * whether the file exactly matches the basename, and extension case, or
974
 * is all upper case, or all lower case.  The path is treated as case
975
 * sensitive.  This function is equivalent to CPLFormFilename() on
976
 * case insensitive file systems (like Windows).
977
 *
978
 * @param pszPath directory path to the directory containing the file.  This
979
 * may be relative or absolute, and may have a trailing path separator or
980
 * not.  May be NULL.
981
 *
982
 * @param pszBasename file basename.  May optionally have path and/or
983
 * extension.  May not be NULL.
984
 *
985
 * @param pszExtension file extension, optionally including the period.  May
986
 * be NULL.
987
 *
988
 * @return a fully formed filename in an internal static string.  Do not
989
 * modify or free the returned string.  The string may be destroyed by the
990
 * next CPL call.
991
 *
992
 * @deprecated If using C++, prefer using CPLFormCIFilenameSafe() instead
993
*/
994
995
const char *CPLFormCIFilename(const char *pszPath, const char *pszBasename,
996
                              const char *pszExtension)
997
998
0
{
999
0
    return CPLPathReturnTLSString(
1000
0
        CPLFormCIFilenameSafe(pszPath, pszBasename, pszExtension),
1001
0
        __FUNCTION__);
1002
0
}
1003
1004
/************************************************************************/
1005
/*                   CPLProjectRelativeFilenameSafe()                   */
1006
/************************************************************************/
1007
1008
/**
1009
 * Find a file relative to a project file.
1010
 *
1011
 * Given the path to a "project" directory, and a path to a secondary file
1012
 * referenced from that project, build a path to the secondary file
1013
 * that the current application can use.  If the secondary path is already
1014
 * absolute, rather than relative, then it will be returned unaltered.
1015
 *
1016
 * Examples:
1017
 * \code{.cpp}
1018
 * CPLProjectRelativeFilenameSafe("abc/def", "tmp/abc.gif") == "abc/def/tmp/abc.gif"
1019
 * CPLProjectRelativeFilenameSafe("abc/def", "/tmp/abc.gif") == "/tmp/abc.gif"
1020
 * CPLProjectRelativeFilenameSafe("/xy", "abc.gif") == "/xy/abc.gif"
1021
 * CPLProjectRelativeFilenameSafe("/abc/def", "../abc.gif") == "/abc/def/../abc.gif"
1022
 * CPLProjectRelativeFilenameSafe("C:\WIN", "abc.gif") == "C:\WIN\abc.gif"
1023
 * \endcode
1024
 *
1025
 * @param pszProjectDir the directory relative to which the secondary files
1026
 * path should be interpreted.
1027
 * @param pszSecondaryFilename the filename (potentially with path) that
1028
 * is to be interpreted relative to the project directory.
1029
 *
1030
 * @return a composed path to the secondary file.
1031
 *
1032
 * @since 3.11
1033
 */
1034
1035
std::string CPLProjectRelativeFilenameSafe(const char *pszProjectDir,
1036
                                           const char *pszSecondaryFilename)
1037
1038
0
{
1039
0
    if (pszProjectDir == nullptr || pszProjectDir[0] == 0 ||
1040
0
        !CPLIsFilenameRelative(pszSecondaryFilename))
1041
0
    {
1042
0
        return pszSecondaryFilename;
1043
0
    }
1044
1045
0
    std::string osRes(pszProjectDir);
1046
0
    if (osRes.back() != '/' && osRes.back() != '\\')
1047
0
    {
1048
0
        osRes += VSIGetDirectorySeparator(pszProjectDir);
1049
0
    }
1050
1051
0
    osRes += pszSecondaryFilename;
1052
0
    return osRes;
1053
0
}
1054
1055
/************************************************************************/
1056
/*                     CPLProjectRelativeFilename()                     */
1057
/************************************************************************/
1058
1059
/**
1060
 * Find a file relative to a project file.
1061
 *
1062
 * Given the path to a "project" directory, and a path to a secondary file
1063
 * referenced from that project, build a path to the secondary file
1064
 * that the current application can use.  If the secondary path is already
1065
 * absolute, rather than relative, then it will be returned unaltered.
1066
 *
1067
 * Examples:
1068
 * \code{.cpp}
1069
 * CPLProjectRelativeFilename("abc/def", "tmp/abc.gif") == "abc/def/tmp/abc.gif"
1070
 * CPLProjectRelativeFilename("abc/def", "/tmp/abc.gif") == "/tmp/abc.gif"
1071
 * CPLProjectRelativeFilename("/xy", "abc.gif") == "/xy/abc.gif"
1072
 * CPLProjectRelativeFilename("/abc/def", "../abc.gif") == "/abc/def/../abc.gif"
1073
 * CPLProjectRelativeFilename("C:\WIN", "abc.gif") == "C:\WIN\abc.gif"
1074
 * \endcode
1075
 *
1076
 * @param pszProjectDir the directory relative to which the secondary files
1077
 * path should be interpreted.
1078
 * @param pszSecondaryFilename the filename (potentially with path) that
1079
 * is to be interpreted relative to the project directory.
1080
 *
1081
 * @return a composed path to the secondary file.  The returned string is
1082
 * internal and should not be altered, freed, or depending on past the next
1083
 * CPL call.
1084
 *
1085
 * @deprecated If using C++, prefer using CPLProjectRelativeFilenameSafe() instead
1086
 */
1087
1088
const char *CPLProjectRelativeFilename(const char *pszProjectDir,
1089
                                       const char *pszSecondaryFilename)
1090
1091
0
{
1092
0
    return CPLPathReturnTLSString(
1093
0
        CPLProjectRelativeFilenameSafe(pszProjectDir, pszSecondaryFilename),
1094
0
        __FUNCTION__);
1095
0
}
1096
1097
/************************************************************************/
1098
/*                       CPLIsFilenameRelative()                        */
1099
/************************************************************************/
1100
1101
/**
1102
 * Is filename relative or absolute?
1103
 *
1104
 * The test is filesystem convention agnostic.  That is it will test for
1105
 * Unix style and windows style path conventions regardless of the actual
1106
 * system in use.
1107
 *
1108
 * @param pszFilename the filename with path to test.
1109
 *
1110
 * @return TRUE if the filename is relative or FALSE if it is absolute.
1111
 */
1112
1113
int CPLIsFilenameRelative(const char *pszFilename)
1114
1115
17.4k
{
1116
17.4k
    if ((pszFilename[0] != '\0' &&
1117
16.2k
         (STARTS_WITH(pszFilename + 1, ":\\") ||
1118
16.2k
          STARTS_WITH(pszFilename + 1, ":/") ||
1119
16.2k
          strstr(pszFilename + 1, "://")  // http://, ftp:// etc....
1120
16.2k
          )) ||
1121
17.4k
        STARTS_WITH(pszFilename, "\\\\?\\")  // Windows extended Length Path.
1122
17.4k
        || pszFilename[0] == '\\' || pszFilename[0] == '/')
1123
23
        return FALSE;
1124
1125
17.4k
    return TRUE;
1126
17.4k
}
1127
1128
/************************************************************************/
1129
/*                       CPLExtractRelativePath()                       */
1130
/************************************************************************/
1131
1132
/**
1133
 * Get relative path from directory to target file.
1134
 *
1135
 * Computes a relative path for pszTarget relative to pszBaseDir.
1136
 * Currently this only works if they share a common base path.  The returned
1137
 * path is normally into the pszTarget string.  It should only be considered
1138
 * valid as long as pszTarget is valid or till the next call to
1139
 * this function, whichever comes first.
1140
 *
1141
 * @param pszBaseDir the name of the directory relative to which the path
1142
 * should be computed.  pszBaseDir may be NULL in which case the original
1143
 * target is returned without relativizing.
1144
 *
1145
 * @param pszTarget the filename to be changed to be relative to pszBaseDir.
1146
 *
1147
 * @param pbGotRelative Pointer to location in which a flag is placed
1148
 * indicating that the returned path is relative to the basename (TRUE) or
1149
 * not (FALSE).  This pointer may be NULL if flag is not desired.
1150
 *
1151
 * @return an adjusted path or the original if it could not be made relative
1152
 * to the pszBaseFile's path.
1153
 **/
1154
1155
const char *CPLExtractRelativePath(const char *pszBaseDir,
1156
                                   const char *pszTarget, int *pbGotRelative)
1157
1158
0
{
1159
    /* -------------------------------------------------------------------- */
1160
    /*      If we don't have a basedir, then we can't relativize the path.  */
1161
    /* -------------------------------------------------------------------- */
1162
0
    if (pszBaseDir == nullptr)
1163
0
    {
1164
0
        if (pbGotRelative != nullptr)
1165
0
            *pbGotRelative = FALSE;
1166
1167
0
        return pszTarget;
1168
0
    }
1169
1170
0
    const size_t nBasePathLen = strlen(pszBaseDir);
1171
1172
    /* -------------------------------------------------------------------- */
1173
    /*      One simple case is when the base dir is '.' and the target      */
1174
    /*      filename is relative.                                           */
1175
    /* -------------------------------------------------------------------- */
1176
0
    if ((nBasePathLen == 0 || EQUAL(pszBaseDir, ".")) &&
1177
0
        CPLIsFilenameRelative(pszTarget))
1178
0
    {
1179
0
        if (pbGotRelative != nullptr)
1180
0
            *pbGotRelative = TRUE;
1181
1182
0
        return pszTarget;
1183
0
    }
1184
1185
    /* -------------------------------------------------------------------- */
1186
    /*      By this point, if we don't have a base path, we can't have a    */
1187
    /*      meaningful common prefix.                                       */
1188
    /* -------------------------------------------------------------------- */
1189
0
    if (nBasePathLen == 0)
1190
0
    {
1191
0
        if (pbGotRelative != nullptr)
1192
0
            *pbGotRelative = FALSE;
1193
1194
0
        return pszTarget;
1195
0
    }
1196
1197
    /* -------------------------------------------------------------------- */
1198
    /*      If we don't have a common path prefix, then we can't get a      */
1199
    /*      relative path.                                                  */
1200
    /* -------------------------------------------------------------------- */
1201
0
    if (!EQUALN(pszBaseDir, pszTarget, nBasePathLen) ||
1202
0
        (pszTarget[nBasePathLen] != '\\' && pszTarget[nBasePathLen] != '/'))
1203
0
    {
1204
0
        if (pbGotRelative != nullptr)
1205
0
            *pbGotRelative = FALSE;
1206
1207
0
        return pszTarget;
1208
0
    }
1209
1210
    /* -------------------------------------------------------------------- */
1211
    /*      We have a relative path.  Strip it off to get a string to       */
1212
    /*      return.                                                         */
1213
    /* -------------------------------------------------------------------- */
1214
0
    if (pbGotRelative != nullptr)
1215
0
        *pbGotRelative = TRUE;
1216
1217
0
    return pszTarget + nBasePathLen + 1;
1218
0
}
1219
1220
/************************************************************************/
1221
/*                     CPLCleanTrailingSlashSafe()                      */
1222
/************************************************************************/
1223
1224
/**
1225
 * Remove trailing forward/backward slash from the path for UNIX/Windows resp.
1226
 *
1227
 * Returns a string containing the portion of the passed path string with
1228
 * trailing slash removed. If there is no path in the passed filename
1229
 * an empty string will be returned (not NULL).
1230
 *
1231
 * \code{.cpp}
1232
 * CPLCleanTrailingSlashSafe( "abc/def/" ) == "abc/def"
1233
 * CPLCleanTrailingSlashSafe( "abc/def" ) == "abc/def"
1234
 * CPLCleanTrailingSlashSafe( "c:\\abc\\def\\" ) == "c:\\abc\\def"
1235
 * CPLCleanTrailingSlashSafe( "c:\\abc\\def" ) == "c:\\abc\\def"
1236
 * CPLCleanTrailingSlashSafe( "abc" ) == "abc"
1237
 * \endcode
1238
 *
1239
 * @param pszPath the path to be cleaned up
1240
 *
1241
 * @return Path
1242
 *
1243
 * @since 3.11
1244
 */
1245
1246
std::string CPLCleanTrailingSlashSafe(const char *pszPath)
1247
1248
0
{
1249
0
    std::string osRes(pszPath);
1250
0
    if (!osRes.empty() && (osRes.back() == '\\' || osRes.back() == '/'))
1251
0
        osRes.pop_back();
1252
0
    return osRes;
1253
0
}
1254
1255
/************************************************************************/
1256
/*                       CPLCleanTrailingSlash()                        */
1257
/************************************************************************/
1258
1259
/**
1260
 * Remove trailing forward/backward slash from the path for UNIX/Windows resp.
1261
 *
1262
 * Returns a string containing the portion of the passed path string with
1263
 * trailing slash removed. If there is no path in the passed filename
1264
 * an empty string will be returned (not NULL).
1265
 *
1266
 * \code{.cpp}
1267
 * CPLCleanTrailingSlash( "abc/def/" ) == "abc/def"
1268
 * CPLCleanTrailingSlash( "abc/def" ) == "abc/def"
1269
 * CPLCleanTrailingSlash( "c:\\abc\\def\\" ) == "c:\\abc\\def"
1270
 * CPLCleanTrailingSlash( "c:\\abc\\def" ) == "c:\\abc\\def"
1271
 * CPLCleanTrailingSlash( "abc" ) == "abc"
1272
 * \endcode
1273
 *
1274
 * @param pszPath the path to be cleaned up
1275
 *
1276
 * @return Path in an internal string which must not be freed.  The string
1277
 * may be destroyed by the next CPL filename handling call.
1278
 *
1279
 * @deprecated If using C++, prefer using CPLCleanTrailingSlashSafe() instead
1280
 */
1281
1282
const char *CPLCleanTrailingSlash(const char *pszPath)
1283
1284
0
{
1285
0
    return CPLPathReturnTLSString(CPLCleanTrailingSlashSafe(pszPath),
1286
0
                                  __FUNCTION__);
1287
0
}
1288
1289
/************************************************************************/
1290
/*                       CPLCorrespondingPaths()                        */
1291
/************************************************************************/
1292
1293
/**
1294
 * Identify corresponding paths.
1295
 *
1296
 * Given a prototype old and new filename this function will attempt
1297
 * to determine corresponding names for a set of other old filenames that
1298
 * will rename them in a similar manner.  This correspondence assumes there
1299
 * are two possibly kinds of renaming going on.  A change of path, and a
1300
 * change of filename stem.
1301
 *
1302
 * If a consistent renaming cannot be established for all the files this
1303
 * function will return indicating an error.
1304
 *
1305
 * The returned file list becomes owned by the caller and should be destroyed
1306
 * with CSLDestroy().
1307
 *
1308
 * @param pszOldFilename path to old prototype file.
1309
 * @param pszNewFilename path to new prototype file.
1310
 * @param papszFileList list of other files associated with pszOldFilename to
1311
 * rename similarly.
1312
 *
1313
 * @return a list of files corresponding to papszFileList but renamed to
1314
 * correspond to pszNewFilename.
1315
 */
1316
1317
char **CPLCorrespondingPaths(const char *pszOldFilename,
1318
                             const char *pszNewFilename,
1319
                             CSLConstList papszFileList)
1320
1321
0
{
1322
0
    if (CSLCount(papszFileList) == 0)
1323
0
        return nullptr;
1324
1325
0
    VSIStatBufL sStatBuf;
1326
0
    if (VSIStatL(pszOldFilename, &sStatBuf) == 0 && VSI_ISDIR(sStatBuf.st_mode))
1327
0
    {
1328
0
        CPLStringList aosNewList;
1329
0
        std::string_view svOldFilename(pszOldFilename);
1330
0
        for (int i = 0; papszFileList[i] != nullptr; i++)
1331
0
        {
1332
0
            if (cpl::starts_with(std::string_view(papszFileList[i]),
1333
0
                                 svOldFilename) &&
1334
0
                (papszFileList[i][svOldFilename.size()] == '/' ||
1335
0
                 papszFileList[i][svOldFilename.size()] == '\\'))
1336
0
            {
1337
                // If the old file list contains entries like oldpath/filename,
1338
                // generate newpath/filename
1339
0
                aosNewList.push_back(CPLFormFilenameSafe(
1340
0
                    pszNewFilename, papszFileList[i] + svOldFilename.size() + 1,
1341
0
                    nullptr));
1342
0
            }
1343
0
            else
1344
0
            {
1345
0
                CPLError(CE_Failure, CPLE_AppDefined,
1346
0
                         "Unable to copy/rename fileset due to unexpected "
1347
0
                         "source filename.");
1348
0
                return nullptr;
1349
0
            }
1350
0
        }
1351
0
        return aosNewList.StealList();
1352
0
    }
1353
1354
    /* -------------------------------------------------------------------- */
1355
    /*      There is a special case for a one item list which exactly       */
1356
    /*      matches the old name, to rename to the new name.                */
1357
    /* -------------------------------------------------------------------- */
1358
0
    if (CSLCount(papszFileList) == 1 &&
1359
0
        strcmp(pszOldFilename, papszFileList[0]) == 0)
1360
0
    {
1361
0
        return CSLAddString(nullptr, pszNewFilename);
1362
0
    }
1363
1364
0
    const std::string osOldPath = CPLGetPathSafe(pszOldFilename);
1365
0
    const std::string osOldBasename = CPLGetBasenameSafe(pszOldFilename);
1366
0
    const std::string osNewBasename = CPLGetBasenameSafe(pszNewFilename);
1367
1368
    /* -------------------------------------------------------------------- */
1369
    /*      If the basename is changing, verify that all source files       */
1370
    /*      have the same starting basename.                                */
1371
    /* -------------------------------------------------------------------- */
1372
0
    if (osOldBasename != osNewBasename)
1373
0
    {
1374
0
        for (int i = 0; papszFileList[i] != nullptr; i++)
1375
0
        {
1376
0
            if (osOldBasename == CPLGetBasenameSafe(papszFileList[i]))
1377
0
                continue;
1378
1379
0
            const std::string osFilePath = CPLGetPathSafe(papszFileList[i]);
1380
0
            const std::string osFileName = CPLGetFilename(papszFileList[i]);
1381
1382
0
            if (!EQUALN(osFileName.c_str(), osOldBasename.c_str(),
1383
0
                        osOldBasename.size()) ||
1384
0
                !EQUAL(osFilePath.c_str(), osOldPath.c_str()) ||
1385
0
                osFileName[osOldBasename.size()] != '.')
1386
0
            {
1387
0
                CPLError(
1388
0
                    CE_Failure, CPLE_AppDefined,
1389
0
                    "Unable to copy/rename fileset due irregular basenames.");
1390
0
                return nullptr;
1391
0
            }
1392
0
        }
1393
0
    }
1394
1395
    /* -------------------------------------------------------------------- */
1396
    /*      If the filename portions differs, ensure they only differ in    */
1397
    /*      basename.                                                       */
1398
    /* -------------------------------------------------------------------- */
1399
0
    if (osOldBasename != osNewBasename)
1400
0
    {
1401
0
        const std::string osOldExtra =
1402
0
            CPLGetFilename(pszOldFilename) + osOldBasename.size();
1403
0
        const std::string osNewExtra =
1404
0
            CPLGetFilename(pszNewFilename) + osNewBasename.size();
1405
1406
0
        if (osOldExtra != osNewExtra)
1407
0
        {
1408
0
            CPLError(CE_Failure, CPLE_AppDefined,
1409
0
                     "Unable to copy/rename fileset due to irregular filename "
1410
0
                     "correspondence.");
1411
0
            return nullptr;
1412
0
        }
1413
0
    }
1414
1415
    /* -------------------------------------------------------------------- */
1416
    /*      Generate the new filenames.                                     */
1417
    /* -------------------------------------------------------------------- */
1418
0
    char **papszNewList = nullptr;
1419
0
    const std::string osNewPath = CPLGetPathSafe(pszNewFilename);
1420
1421
0
    for (int i = 0; papszFileList[i] != nullptr; i++)
1422
0
    {
1423
0
        const std::string osOldFilename = CPLGetFilename(papszFileList[i]);
1424
1425
0
        const std::string osNewFilename =
1426
0
            osOldBasename == osNewBasename
1427
0
                ? CPLFormFilenameSafe(osNewPath.c_str(), osOldFilename.c_str(),
1428
0
                                      nullptr)
1429
0
                : CPLFormFilenameSafe(osNewPath.c_str(), osNewBasename.c_str(),
1430
0
                                      osOldFilename.c_str() +
1431
0
                                          osOldBasename.size());
1432
1433
0
        papszNewList = CSLAddString(papszNewList, osNewFilename.c_str());
1434
0
    }
1435
1436
0
    return papszNewList;
1437
0
}
1438
1439
/************************************************************************/
1440
/*                    CPLGenerateTempFilenameSafe()                     */
1441
/************************************************************************/
1442
1443
/**
1444
 * Generate temporary file name.
1445
 *
1446
 * Returns a filename that may be used for a temporary file.  The location
1447
 * of the file tries to follow operating system semantics but may be
1448
 * forced via the CPL_TMPDIR configuration option.
1449
 *
1450
 * @param pszStem if non-NULL this will be part of the filename.
1451
 *
1452
 * @return a filename
1453
 *
1454
 * @since 3.11
1455
 */
1456
1457
std::string CPLGenerateTempFilenameSafe(const char *pszStem)
1458
1459
17.4k
{
1460
17.4k
    const char *pszDir = CPLGetConfigOption("CPL_TMPDIR", nullptr);
1461
1462
17.4k
    if (pszDir == nullptr)
1463
17.4k
        pszDir = CPLGetConfigOption("TMPDIR", nullptr);
1464
1465
17.4k
    if (pszDir == nullptr)
1466
17.3k
        pszDir = CPLGetConfigOption("TEMP", nullptr);
1467
1468
17.4k
    if (pszDir == nullptr)
1469
16.1k
        pszDir = ".";
1470
1471
17.4k
    if (pszStem == nullptr)
1472
17.4k
        pszStem = "";
1473
1474
17.4k
    static int nTempFileCounter = 0;
1475
17.4k
    CPLString osFilename;
1476
17.4k
    osFilename.Printf("%s_%d_%d", pszStem, CPLGetCurrentProcessID(),
1477
17.4k
                      CPLAtomicInc(&nTempFileCounter));
1478
1479
17.4k
    return CPLFormFilenameSafe(pszDir, osFilename.c_str(), nullptr);
1480
17.4k
}
1481
1482
/************************************************************************/
1483
/*                      CPLGenerateTempFilename()                       */
1484
/************************************************************************/
1485
1486
/**
1487
 * Generate temporary file name.
1488
 *
1489
 * Returns a filename that may be used for a temporary file.  The location
1490
 * of the file tries to follow operating system semantics but may be
1491
 * forced via the CPL_TMPDIR configuration option.
1492
 *
1493
 * @param pszStem if non-NULL this will be part of the filename.
1494
 *
1495
 * @return a filename which is valid till the next CPL call in this thread.
1496
 *
1497
 * @deprecated If using C++, prefer using CPLCleanTrailingSlashSafe() instead
1498
 */
1499
1500
const char *CPLGenerateTempFilename(const char *pszStem)
1501
1502
17.4k
{
1503
17.4k
    return CPLPathReturnTLSString(CPLGenerateTempFilenameSafe(pszStem),
1504
17.4k
                                  __FUNCTION__);
1505
17.4k
}
1506
1507
/************************************************************************/
1508
/*                         CPLExpandTildeSafe()                         */
1509
/************************************************************************/
1510
1511
/**
1512
 * Expands ~/ at start of filename.
1513
 *
1514
 * Assumes that the HOME configuration option is defined.
1515
 *
1516
 * @param pszFilename filename potentially starting with ~/
1517
 *
1518
 * @return an expanded filename.
1519
 *
1520
 * @since GDAL 3.11
1521
 */
1522
1523
std::string CPLExpandTildeSafe(const char *pszFilename)
1524
1525
0
{
1526
0
    if (!STARTS_WITH_CI(pszFilename, "~/"))
1527
0
        return pszFilename;
1528
1529
0
    const char *pszHome = CPLGetConfigOption("HOME", nullptr);
1530
0
    if (pszHome == nullptr)
1531
0
        return pszFilename;
1532
1533
0
    return CPLFormFilenameSafe(pszHome, pszFilename + 2, nullptr);
1534
0
}
1535
1536
/************************************************************************/
1537
/*                           CPLExpandTilde()                           */
1538
/************************************************************************/
1539
1540
/**
1541
 * Expands ~/ at start of filename.
1542
 *
1543
 * Assumes that the HOME configuration option is defined.
1544
 *
1545
 * @param pszFilename filename potentially starting with ~/
1546
 *
1547
 * @return an expanded filename.
1548
 *
1549
 *
1550
 * @deprecated If using C++, prefer using CPLExpandTildeSafe() instead
1551
 */
1552
1553
const char *CPLExpandTilde(const char *pszFilename)
1554
1555
0
{
1556
0
    return CPLPathReturnTLSString(CPLExpandTildeSafe(pszFilename),
1557
0
                                  __FUNCTION__);
1558
0
}
1559
1560
/************************************************************************/
1561
/*                           CPLGetHomeDir()                            */
1562
/************************************************************************/
1563
1564
/**
1565
 * Return the path to the home directory
1566
 *
1567
 * That is the value of the USERPROFILE environment variable on Windows,
1568
 * or HOME on other platforms.
1569
 *
1570
 * @return the home directory, or NULL.
1571
 *
1572
 */
1573
1574
const char *CPLGetHomeDir()
1575
1576
0
{
1577
#ifdef _WIN32
1578
    return CPLGetConfigOption("USERPROFILE", nullptr);
1579
#else
1580
0
    return CPLGetConfigOption("HOME", nullptr);
1581
0
#endif
1582
0
}
1583
1584
/************************************************************************/
1585
/*                     CPLLaunderForFilenameSafe()                      */
1586
/************************************************************************/
1587
1588
/**
1589
 * Launder a string to be compatible of a filename.
1590
 *
1591
 * @param pszName The input string to launder.
1592
 * @param pszOutputPath The directory where the file would be created.
1593
 *                      Unused for now. May be NULL.
1594
 * @return the laundered name.
1595
 *
1596
 * @since GDAL 3.11
1597
 */
1598
1599
std::string CPLLaunderForFilenameSafe(const char *pszName,
1600
                                      CPL_UNUSED const char *pszOutputPath)
1601
0
{
1602
0
    return CPLLaunderForFilenameSafe(pszName, '_', nullptr);
1603
0
}
1604
1605
/************************************************************************/
1606
/*                     CPLLaunderForFilenameSafe()                      */
1607
/************************************************************************/
1608
1609
/** Return a string that is compatible with a filename on Linux, Windows and
1610
 * MacOS.
1611
 *
1612
 * Reserved characters '<', '>', ':', '"', '/', '\\', '|', '?', '*', '^', and
1613
 * ASCII control characters are replaced by the replacement character, or
1614
 * removed if it is NUL.
1615
 *
1616
 * Reserved names (".", "..", "CON", "PRN", etc.) are suffixed with the
1617
 * replacement character (or underscore).
1618
 *
1619
 * If the string ends with a final space or dot, the replacement character
1620
 * (or underscore) will be appended.
1621
 *
1622
 * @param osInput Input string.
1623
 * @param chReplacementChar Character to substitute to characters that are not
1624
 *                          compatible of a file name, or NUL character to
1625
 *                          remove them.
1626
 * @param pszExtraReservedCharacters String with extra reserved characters that
1627
 *                                   are replaced by the replacement character,
1628
 *                                   or removed if it is NUL. Or nullptr.
1629
 * @since GDAL 3.13
1630
 */
1631
std::string CPLLaunderForFilenameSafe(const std::string &osInput,
1632
                                      char chReplacementChar,
1633
                                      const char *pszExtraReservedCharacters)
1634
0
{
1635
    // Cf https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
1636
0
    std::string ret;
1637
0
    ret.reserve(osInput.size());
1638
0
    for (char c : osInput)
1639
0
    {
1640
0
        if (static_cast<unsigned>(c) < 32 || c == 127 || c == '<' || c == '>' ||
1641
0
            c == ':' || c == '"' || c == '/' || c == '\\' || c == '|' ||
1642
0
            c == '?' ||
1643
0
            c == '*'
1644
            // '^' invalid on FAT
1645
0
            || c == '^')
1646
0
        {
1647
0
            if (chReplacementChar)
1648
0
                ret += chReplacementChar;
1649
0
        }
1650
0
        else if (pszExtraReservedCharacters &&
1651
0
                 strchr(pszExtraReservedCharacters, c))
1652
0
        {
1653
0
            if (chReplacementChar)
1654
0
                ret += chReplacementChar;
1655
0
        }
1656
0
        else
1657
0
        {
1658
0
            ret += c;
1659
0
        }
1660
0
    }
1661
1662
    // Windows reserved filenames (case-insensitive)
1663
0
    const char *const apszReservedNames[] = {
1664
0
        "CON",  "PRN",  "AUX",  "NUL",  "COM1", "COM2", "COM3",   "COM4",
1665
0
        "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2",   "LPT3",
1666
0
        "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "CONIN$", "CONOUT$",
1667
0
    };
1668
0
    for (const char *pszReservedName : apszReservedNames)
1669
0
    {
1670
0
        if (EQUAL(ret.c_str(), pszReservedName))
1671
0
        {
1672
0
            ret += chReplacementChar ? chReplacementChar : '_';
1673
0
            break;
1674
0
        }
1675
0
    }
1676
1677
    // Windows rule: no filename ending with space or dot
1678
    // This also prevents "." and ".." which are invalid on POSIX
1679
0
    if (!ret.empty() && (ret.back() == ' ' || ret.back() == '.'))
1680
0
        ret += chReplacementChar ? chReplacementChar : '_';
1681
1682
0
    return ret;
1683
0
}
1684
1685
/************************************************************************/
1686
/*                       CPLLaunderForFilename()                        */
1687
/************************************************************************/
1688
1689
/**
1690
 * Launder a string to be compatible of a filename.
1691
 *
1692
 * @param pszName The input string to launder.
1693
 * @param pszOutputPath The directory where the file would be created.
1694
 *                      Unused for now. May be NULL.
1695
 * @return the laundered name.
1696
 *
1697
 * @since GDAL 3.1
1698
 *
1699
 * @deprecated If using C++, prefer using CPLLaunderForFilenameSafe() instead
1700
 */
1701
1702
const char *CPLLaunderForFilename(const char *pszName,
1703
                                  const char *pszOutputPath)
1704
0
{
1705
0
    return CPLPathReturnTLSString(
1706
0
        CPLLaunderForFilenameSafe(pszName, pszOutputPath), __FUNCTION__);
1707
0
}
1708
1709
/************************************************************************/
1710
/*                        CPLHasPathTraversal()                         */
1711
/************************************************************************/
1712
1713
/**
1714
 * Return whether the filename contains a path traversal pattern.
1715
 *
1716
 * i.e. if it contains "../" or "..\\".
1717
 *
1718
 * The CPL_ENABLE_PATH_TRAVERSAL_DETECTION configuration option can be set
1719
 * to NO to disable this check, although this is not recommended when dealing
1720
 * with un-trusted input.
1721
 *
1722
 * @param pszFilename The input string to check.
1723
 * @return true if a path traversal pattern is detected.
1724
 *
1725
 * @since GDAL 3.12
1726
 */
1727
1728
bool CPLHasPathTraversal(const char *pszFilename)
1729
0
{
1730
0
    const char *pszDotDot = strstr(pszFilename, "..");
1731
0
    if (pszDotDot &&
1732
0
        (pszDotDot == pszFilename ||
1733
0
         pszFilename[pszDotDot - pszFilename - 1] == '/' ||
1734
0
         pszFilename[pszDotDot - pszFilename - 1] == '\\') &&
1735
0
        (pszDotDot[2] == 0 || pszDotDot[2] == '/' || pszDotDot[2] == '\\'))
1736
0
    {
1737
0
        if (CPLTestBool(CPLGetConfigOption(
1738
0
                "CPL_ENABLE_PATH_TRAVERSAL_DETECTION", "YES")))
1739
0
        {
1740
0
            CPLDebug("CPL", "Path traversal detected for %s", pszFilename);
1741
0
            return true;
1742
0
        }
1743
0
        else
1744
0
        {
1745
0
            CPLDebug("CPL",
1746
0
                     "Path traversal detected for %s but ignored given that "
1747
0
                     "CPL_ENABLE_PATH_TRAVERSAL_DETECTION is disabled",
1748
0
                     pszFilename);
1749
0
        }
1750
0
    }
1751
0
    return false;
1752
0
}
1753
1754
/************************************************************************/
1755
/*                   CPLHasUnbalancedPathTraversal()                    */
1756
/************************************************************************/
1757
1758
/**
1759
 * Return whether the filename contains a unbalanced path traversal pattern.
1760
 *
1761
 * i.e. if it contains more "../" or "..\\" than preceding nesting.
1762
 *
1763
 *
1764
 * @param pszFilename The input string to check.
1765
 * @return true if a path traversal pattern is detected.
1766
 *
1767
 * @since GDAL 3.12
1768
 */
1769
1770
bool CPLHasUnbalancedPathTraversal(const char *pszFilename)
1771
0
{
1772
0
    size_t nNestLevel = 0;
1773
0
    int i = 0;
1774
0
    if (pszFilename[0] == '.' &&
1775
0
        (pszFilename[1] == '/' || pszFilename[1] == '\\'))
1776
0
        i += 2;
1777
0
    else if (pszFilename[0] == '/' || pszFilename[0] == '\\')
1778
0
        ++i;
1779
0
    for (; pszFilename[i]; ++i)
1780
0
    {
1781
0
        if (pszFilename[i] == '/' || pszFilename[i] == '\\')
1782
0
        {
1783
0
            if (pszFilename[i + 1] == '/' || pszFilename[i + 1] == '\\')
1784
0
            {
1785
0
                continue;
1786
0
            }
1787
0
            if (pszFilename[i + 1] != 0)
1788
0
                ++nNestLevel;
1789
0
        }
1790
0
        else if (pszFilename[i] == '.' && pszFilename[i + 1] == '.' &&
1791
0
                 (pszFilename[i + 2] == '/' || pszFilename[i + 2] == '\\' ||
1792
0
                  pszFilename[i + 2] == 0))
1793
0
        {
1794
0
            if (nNestLevel == 0)
1795
0
            {
1796
0
                CPLDebug("CPL", "Path traversal detected for %s", pszFilename);
1797
0
                return true;
1798
0
            }
1799
0
            if (pszFilename[i + 2] == 0)
1800
0
                break;
1801
0
            i += 2;
1802
0
            --nNestLevel;
1803
0
        }
1804
0
    }
1805
1806
0
    return false;
1807
0
}
1808
1809
/************************************************************************/
1810
/*                       CPLLexicallyNormalize()                        */
1811
/************************************************************************/
1812
1813
/**
1814
 * Return a path where "/./" or "/../" sequences are removed.
1815
 *
1816
 * No filesystem access is done.
1817
 *
1818
 * @param svPath Input path
1819
 * @param sep1 Path separator (typically slash or backslash)
1820
 * @param sep2 Secondary path separator (typically slash or backslash), or NUL
1821
 * @return compacted path
1822
 *
1823
 * @since GDAL 3.13
1824
 */
1825
std::string CPLLexicallyNormalize(std::string_view svPath, char sep1, char sep2)
1826
0
{
1827
0
    struct Token
1828
0
    {
1829
0
        size_t iStart = 0;  // index of start of token with svPath
1830
0
        size_t nLen = 0;    // length of token (excluding ending separator)
1831
0
        char chSep = 0;     // separator at end of token, or 0 if there is none
1832
0
    };
1833
1834
0
    std::vector<Token> tokens;
1835
1836
0
    const auto CompactTokens = [&tokens, &svPath]()
1837
0
    {
1838
0
        Token &t = tokens.back();
1839
0
        if (t.nLen == 1 && svPath[t.iStart] == '.')
1840
0
        {
1841
0
            tokens.pop_back();
1842
0
        }
1843
0
        else if (t.nLen == 2 && svPath[t.iStart] == '.' &&
1844
0
                 svPath[t.iStart + 1] == '.')
1845
0
        {
1846
0
            if (tokens.size() >= 2)
1847
0
                tokens.resize(tokens.size() - 2);
1848
0
        }
1849
0
    };
1850
1851
0
    bool lastCharIsSep = false;
1852
0
    for (size_t i = 0; i < svPath.size(); ++i)
1853
0
    {
1854
0
        const char c = svPath[i];
1855
0
        if (c == sep1 || c == sep2)
1856
0
        {
1857
0
            if (!lastCharIsSep)
1858
0
            {
1859
0
                if (tokens.empty())
1860
0
                {
1861
0
                    Token t;
1862
0
                    t.chSep = c;
1863
0
                    tokens.push_back(t);
1864
0
                }
1865
0
                else
1866
0
                {
1867
0
                    Token &t = tokens.back();
1868
0
                    t.chSep = c;
1869
0
                    CompactTokens();
1870
0
                }
1871
0
                lastCharIsSep = true;
1872
0
            }
1873
0
        }
1874
0
        else
1875
0
        {
1876
0
            if (tokens.empty() || lastCharIsSep)
1877
0
            {
1878
0
                Token t;
1879
0
                t.iStart = i;
1880
0
                t.nLen = 1;
1881
0
                tokens.push_back(t);
1882
0
            }
1883
0
            else
1884
0
            {
1885
0
                Token &t = tokens.back();
1886
0
                ++t.nLen;
1887
0
            }
1888
0
            lastCharIsSep = false;
1889
0
        }
1890
0
    }
1891
0
    if (!tokens.empty())
1892
0
    {
1893
0
        CompactTokens();
1894
0
    }
1895
1896
0
    std::string s;
1897
0
    s.reserve(svPath.size());
1898
0
    for (const auto &t : tokens)
1899
0
    {
1900
0
        if (t.nLen)
1901
0
            s.append(svPath.substr(t.iStart, t.nLen));
1902
0
        if (t.chSep)
1903
0
            s.push_back(t.chSep);
1904
0
    }
1905
0
    return s;
1906
0
}