Coverage Report

Created: 2026-08-11 08:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/ogr/ogrsf_frmts/csv/ogrcsvdriver.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  CSV Translator
4
 * Purpose:  Implements OGRCSVDriver.
5
 * Author:   Frank Warmerdam, warmerdam@pobox.com
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2004, Frank Warmerdam <warmerdam@pobox.com>
9
 * Copyright (c) 2010-2013, Even Rouault <even dot rouault at spatialys.com>
10
 *
11
 * SPDX-License-Identifier: MIT
12
 ****************************************************************************/
13
14
#include "cpl_port.h"
15
#include "ogr_csv.h"
16
17
#include <cerrno>
18
#include <cstring>
19
#include <map>
20
#include <string>
21
#include <utility>
22
23
#include "cpl_conv.h"
24
#include "cpl_error.h"
25
#include "cpl_multiproc.h"
26
#include "cpl_string.h"
27
#include "cpl_vsi.h"
28
#include "gdal.h"
29
#include "gdal_priv.h"
30
31
static CPLMutex *hMutex = nullptr;
32
static std::map<CPLString, GDALDataset *> *poMap = nullptr;
33
34
/************************************************************************/
35
/*                        OGRCSVDriverIdentify()                        */
36
/************************************************************************/
37
38
static int OGRCSVDriverIdentify(GDALOpenInfo *poOpenInfo)
39
40
242k
{
41
242k
    if (poOpenInfo->fpL != nullptr)
42
146k
    {
43
146k
        if (poOpenInfo->IsSingleAllowedDriver("CSV"))
44
288
            return TRUE;
45
46
145k
        const CPLString osBaseFilename =
47
145k
            CPLGetFilename(poOpenInfo->pszFilename);
48
145k
        const CPLString osExt =
49
145k
            OGRCSVDataSource::GetRealExtension(poOpenInfo->pszFilename);
50
51
145k
        if (EQUAL(osBaseFilename, "NfdcFacilities.xls") ||
52
145k
            EQUAL(osBaseFilename, "NfdcRunways.xls") ||
53
145k
            EQUAL(osBaseFilename, "NfdcRemarks.xls") ||
54
145k
            EQUAL(osBaseFilename, "NfdcSchedules.xls"))
55
302
        {
56
302
            return TRUE;
57
302
        }
58
145k
        else if ((STARTS_WITH_CI(osBaseFilename, "NationalFile_") ||
59
145k
                  STARTS_WITH_CI(osBaseFilename, "POP_PLACES_") ||
60
145k
                  STARTS_WITH_CI(osBaseFilename, "HIST_FEATURES_") ||
61
145k
                  STARTS_WITH_CI(osBaseFilename, "US_CONCISE_") ||
62
145k
                  STARTS_WITH_CI(osBaseFilename, "AllNames_") ||
63
145k
                  STARTS_WITH_CI(osBaseFilename,
64
145k
                                 "Feature_Description_History_") ||
65
145k
                  STARTS_WITH_CI(osBaseFilename, "ANTARCTICA_") ||
66
145k
                  STARTS_WITH_CI(osBaseFilename, "GOVT_UNITS_") ||
67
145k
                  STARTS_WITH_CI(osBaseFilename, "NationalFedCodes_") ||
68
145k
                  STARTS_WITH_CI(osBaseFilename, "AllStates_") ||
69
145k
                  STARTS_WITH_CI(osBaseFilename, "AllStatesFedCodes_") ||
70
145k
                  (osBaseFilename.size() > 2 &&
71
145k
                   STARTS_WITH_CI(osBaseFilename + 2, "_Features_")) ||
72
145k
                  (osBaseFilename.size() > 2 &&
73
145k
                   STARTS_WITH_CI(osBaseFilename + 2, "_FedCodes_"))) &&
74
321
                 (EQUAL(osExt, "txt") || EQUAL(osExt, "zip")))
75
186
        {
76
186
            return TRUE;
77
186
        }
78
145k
        else if (EQUAL(osBaseFilename, "allCountries.txt") ||
79
145k
                 EQUAL(osBaseFilename, "allCountries.zip"))
80
34
        {
81
34
            return TRUE;
82
34
        }
83
145k
        else if (EQUAL(osExt, "csv") || EQUAL(osExt, "tsv") ||
84
108k
                 EQUAL(osExt, "psv"))
85
36.4k
        {
86
36.4k
            return TRUE;
87
36.4k
        }
88
108k
        else if (STARTS_WITH(poOpenInfo->pszFilename, "/vsizip/") &&
89
0
                 EQUAL(osExt, "zip"))
90
0
        {
91
0
            return -1;  // Unsure.
92
0
        }
93
108k
        else
94
108k
        {
95
108k
            return FALSE;
96
108k
        }
97
145k
    }
98
96.6k
    else if (STARTS_WITH_CI(poOpenInfo->pszFilename, "CSV:"))
99
17.0k
    {
100
17.0k
        return TRUE;
101
17.0k
    }
102
79.6k
    else if (poOpenInfo->bIsDirectory)
103
33.6k
    {
104
33.6k
        if (poOpenInfo->IsSingleAllowedDriver("CSV"))
105
16.7k
            return TRUE;
106
107
16.8k
        return -1;  // Unsure.
108
33.6k
    }
109
110
46.0k
    return FALSE;
111
242k
}
112
113
/************************************************************************/
114
/*                     OGRCSVDriverRemoveFromMap()                      */
115
/************************************************************************/
116
117
void OGRCSVDriverRemoveFromMap(const char *pszName, GDALDataset *poDS)
118
320
{
119
320
    if (poMap == nullptr)
120
320
        return;
121
0
    CPLMutexHolderD(&hMutex);
122
0
    std::map<CPLString, GDALDataset *>::iterator oIter = poMap->find(pszName);
123
0
    if (oIter != poMap->end())
124
0
    {
125
0
        GDALDataset *poOtherDS = oIter->second;
126
0
        if (poDS == poOtherDS)
127
0
            poMap->erase(oIter);
128
0
    }
129
0
}
130
131
/************************************************************************/
132
/*                                Open()                                */
133
/************************************************************************/
134
135
static GDALDataset *OGRCSVDriverOpen(GDALOpenInfo *poOpenInfo)
136
137
43.9k
{
138
43.9k
    if (!OGRCSVDriverIdentify(poOpenInfo))
139
0
        return nullptr;
140
141
43.9k
    if (poMap != nullptr)
142
0
    {
143
0
        CPLMutexHolderD(&hMutex);
144
0
        std::map<CPLString, GDALDataset *>::iterator oIter =
145
0
            poMap->find(poOpenInfo->pszFilename);
146
0
        if (oIter != poMap->end())
147
0
        {
148
0
            GDALDataset *poOtherDS = oIter->second;
149
0
            poOtherDS->FlushCache(false);
150
0
        }
151
0
    }
152
153
43.9k
    auto poDSUniquePtr = std::make_unique<OGRCSVDataSource>();
154
155
43.9k
    if (!poDSUniquePtr->Open(poOpenInfo->pszFilename,
156
43.9k
                             poOpenInfo->eAccess == GA_Update, false,
157
43.9k
                             poOpenInfo->papszOpenOptions,
158
43.9k
                             poOpenInfo->IsSingleAllowedDriver("CSV")))
159
5.50k
    {
160
5.50k
        poDSUniquePtr.reset();
161
5.50k
    }
162
163
43.9k
    auto poDS = poDSUniquePtr.release();
164
165
43.9k
    if (poOpenInfo->eAccess == GA_Update && poDS != nullptr)
166
0
    {
167
0
        CPLMutexHolderD(&hMutex);
168
0
        if (poMap == nullptr)
169
0
            poMap = new std::map<CPLString, GDALDataset *>();
170
0
        if (poMap->find(poOpenInfo->pszFilename) == poMap->end())
171
0
        {
172
0
            (*poMap)[poOpenInfo->pszFilename] = poDS;
173
0
        }
174
0
    }
175
176
43.9k
    return poDS;
177
43.9k
}
178
179
/************************************************************************/
180
/*                               Create()                               */
181
/************************************************************************/
182
183
static GDALDataset *
184
OGRCSVDriverCreate(const char *pszName, CPL_UNUSED int nBands,
185
                   CPL_UNUSED int nXSize, CPL_UNUSED int nYSize,
186
                   CPL_UNUSED GDALDataType eDT, CSLConstList papszOptions)
187
309
{
188
    // First, ensure there isn't any such file yet.
189
309
    VSIStatBufL sStatBuf;
190
191
309
    if (strcmp(pszName, "/dev/stdout") == 0)
192
0
        pszName = "/vsistdout/";
193
194
309
    if (VSIStatL(pszName, &sStatBuf) == 0)
195
0
    {
196
0
        CPLError(CE_Failure, CPLE_AppDefined,
197
0
                 "It seems a file system object called '%s' already exists.",
198
0
                 pszName);
199
200
0
        return nullptr;
201
0
    }
202
203
    // If the target is not a simple .csv then create it as a directory.
204
309
    CPLString osDirName;
205
206
309
    if (EQUAL(CPLGetExtensionSafe(pszName).c_str(), "csv"))
207
266
    {
208
266
        osDirName = CPLGetPathSafe(pszName);
209
266
        if (osDirName == "")
210
0
            osDirName = ".";
211
212
        // HACK: CPLGetPathSafe("/vsimem/foo.csv") = "/vsimem", but this is not
213
        // recognized afterwards as a valid directory name.
214
266
        if (osDirName == "/vsimem")
215
261
            osDirName = "/vsimem/";
216
266
    }
217
43
    else
218
43
    {
219
43
        if (STARTS_WITH(pszName, "/vsizip/"))
220
0
        {
221
            // Do nothing.
222
0
        }
223
43
        else if (!EQUAL(pszName, "/vsistdout/") && VSIMkdir(pszName, 0755) != 0)
224
0
        {
225
0
            CPLError(CE_Failure, CPLE_AppDefined,
226
0
                     "Failed to create directory %s:\n%s", pszName,
227
0
                     VSIStrerror(errno));
228
0
            return nullptr;
229
0
        }
230
43
        osDirName = pszName;
231
43
    }
232
233
    // Force it to open as a datasource.
234
309
    auto poDS = std::make_unique<OGRCSVDataSource>();
235
236
309
    if (EQUAL(CPLGetExtensionSafe(pszName).c_str(), "csv"))
237
266
    {
238
266
        poDS->CreateForSingleFile(osDirName, pszName);
239
266
    }
240
43
    else if (!poDS->Open(osDirName, /* bUpdate = */ true,
241
43
                         /* bForceAccept = */ true, nullptr,
242
43
                         /* bSingleDriver = */ true))
243
0
    {
244
0
        return nullptr;
245
0
    }
246
247
309
    const char *pszGeometry = CSLFetchNameValue(papszOptions, "GEOMETRY");
248
309
    if (pszGeometry != nullptr && EQUAL(pszGeometry, "AS_WKT"))
249
0
        poDS->EnableGeometryFields();
250
251
309
    return poDS.release();
252
309
}
253
254
/************************************************************************/
255
/*                               Delete()                               */
256
/************************************************************************/
257
258
static CPLErr OGRCSVDriverDelete(const char *pszFilename)
259
260
258
{
261
258
    return CPLUnlinkTree(pszFilename) == 0 ? CE_None : CE_Failure;
262
258
}
263
264
/************************************************************************/
265
/*                         OGRCSVDriverUnload()                         */
266
/************************************************************************/
267
268
static void OGRCSVDriverUnload(GDALDriver *)
269
0
{
270
0
    if (hMutex != nullptr)
271
0
        CPLDestroyMutex(hMutex);
272
0
    hMutex = nullptr;
273
0
    delete poMap;
274
0
    poMap = nullptr;
275
0
}
276
277
/************************************************************************/
278
/*                           RegisterOGRCSV()                           */
279
/************************************************************************/
280
281
#define XSTRINGIFY(x) #x
282
#define STRINGIFY(x) XSTRINGIFY(x)
283
284
void RegisterOGRCSV()
285
286
24
{
287
24
    if (GDALGetDriverByName("CSV") != nullptr)
288
0
        return;
289
290
24
    GDALDriver *poDriver = new GDALDriver();
291
292
24
    poDriver->SetDescription("CSV");
293
24
    poDriver->SetMetadataItem(GDAL_DCAP_VECTOR, "YES");
294
24
    poDriver->SetMetadataItem(GDAL_DCAP_CREATE_LAYER, "YES");
295
24
    poDriver->SetMetadataItem(GDAL_DCAP_DELETE_LAYER, "YES");
296
24
    poDriver->SetMetadataItem(GDAL_DCAP_CREATE_FIELD, "YES");
297
24
    poDriver->SetMetadataItem(GDAL_DCAP_DELETE_FIELD, "YES");
298
24
    poDriver->SetMetadataItem(GDAL_DCAP_REORDER_FIELDS, "YES");
299
24
    poDriver->SetMetadataItem(GDAL_DMD_CREATION_FIELD_DEFN_FLAGS,
300
24
                              "WidthPrecision");
301
24
    poDriver->SetMetadataItem(GDAL_DMD_ALTER_FIELD_DEFN_FLAGS,
302
24
                              "Name Type WidthPrecision");
303
24
    poDriver->SetMetadataItem(GDAL_DCAP_MULTIPLE_VECTOR_LAYERS_IN_DIRECTORY,
304
24
                              "YES");
305
306
24
    poDriver->SetMetadataItem(GDAL_DCAP_CURVE_GEOMETRIES, "YES");
307
24
    poDriver->SetMetadataItem(GDAL_DCAP_MEASURED_GEOMETRIES, "YES");
308
24
    poDriver->SetMetadataItem(GDAL_DCAP_Z_GEOMETRIES, "YES");
309
310
24
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
311
24
                              "Comma Separated Value (.csv)");
312
24
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSIONS, "csv tsv psv");
313
24
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/vector/csv.html");
314
24
    poDriver->SetMetadataItem(GDAL_DMD_SUPPORTED_SQL_DIALECTS, "OGRSQL SQLITE");
315
24
    poDriver->SetMetadataItem(GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_SIGN,
316
24
                              "YES");
317
24
    poDriver->SetMetadataItem(
318
24
        GDAL_DMD_NUMERIC_FIELD_WIDTH_INCLUDES_DECIMAL_SEPARATOR, "YES");
319
320
24
    poDriver->SetMetadataItem(GDAL_DMD_CREATIONOPTIONLIST,
321
24
                              "<CreationOptionList>"
322
24
                              "  <Option name='GEOMETRY' type='string-select' "
323
24
                              "description='how to encode geometry fields'>"
324
24
                              "    <Value>AS_WKT</Value>"
325
24
                              "  </Option>"
326
24
                              "</CreationOptionList>");
327
328
24
    poDriver->SetMetadataItem(
329
24
        GDAL_DS_LAYER_CREATIONOPTIONLIST,
330
24
        "<LayerCreationOptionList>"
331
24
        "  <Option name='SEPARATOR' type='string-select' description='field "
332
24
        "separator' default='COMMA'>"
333
24
        "    <Value>COMMA</Value>"
334
24
        "    <Value>SEMICOLON</Value>"
335
24
        "    <Value>TAB</Value>"
336
24
        "    <Value>SPACE</Value>"
337
24
        "  </Option>"
338
#ifdef _WIN32
339
        "  <Option name='LINEFORMAT' type='string-select' "
340
        "description='end-of-line sequence' default='CRLF'>"
341
#else
342
24
        "  <Option name='LINEFORMAT' type='string-select' "
343
24
        "description='end-of-line sequence' default='LF'>"
344
24
#endif
345
24
        "    <Value>CRLF</Value>"
346
24
        "    <Value>LF</Value>"
347
24
        "  </Option>"
348
24
        "  <Option name='GEOMETRY' type='string-select' description='how to "
349
24
        "encode geometry fields'>"
350
24
        "    <Value>AS_WKT</Value>"
351
24
        "    <Value>AS_XYZ</Value>"
352
24
        "    <Value>AS_XY</Value>"
353
24
        "    <Value>AS_YX</Value>"
354
24
        "    <Value>NONE</Value>"
355
24
        "  </Option>"
356
24
        "  <Option name='CREATE_CSVT' type='boolean' description='whether to "
357
24
        "create a .csvt file' default='NO'/>"
358
24
        "  <Option name='HEADER' alias='HEADERS' type='boolean' "
359
24
        "description='Whether the first line of the file contains column names "
360
24
        "or not' default='YES'/>"
361
24
        "  <Option name='WRITE_BOM' type='boolean' description='whether to "
362
24
        "write a UTF-8 BOM prefix' default='NO'/>"
363
24
        "  <Option name='GEOMETRY_NAME' type='string' description='Name of "
364
24
        "geometry column. Only used if GEOMETRY=AS_WKT' default='WKT'/>"
365
24
        "  <Option name='STRING_QUOTING' type='string-select' "
366
24
        "description='whether to double-quote strings. IF_AMBIGUOUS means that "
367
24
        "string values that look like numbers will be quoted (it also implies "
368
24
        "IF_NEEDED).' default='IF_AMBIGUOUS'>"
369
24
        "    <Value>IF_NEEDED</Value>"
370
24
        "    <Value>IF_AMBIGUOUS</Value>"
371
24
        "    <Value>ALWAYS</Value>"
372
24
        "  </Option>"
373
24
        "</LayerCreationOptionList>");
374
375
24
    poDriver->SetMetadataItem(
376
24
        GDAL_DMD_OPENOPTIONLIST,
377
24
        "<OpenOptionList>"
378
24
        "  <Option name='SEPARATOR' type='string-select' "
379
24
        "description='field separator' default='AUTO'>"
380
24
        "    <Value>AUTO</Value>"
381
24
        "    <Value>COMMA</Value>"
382
24
        "    <Value>SEMICOLON</Value>"
383
24
        "    <Value>TAB</Value>"
384
24
        "    <Value>SPACE</Value>"
385
24
        "    <Value>PIPE</Value>"
386
24
        "  </Option>"
387
24
        "  <Option name='MERGE_SEPARATOR' type='boolean' description='whether "
388
24
        "to merge consecutive separators' default='NO'/>"
389
24
        "  <Option name='AUTODETECT_TYPE' type='boolean' description='whether "
390
24
        "to guess data type from first bytes of the file' default='NO'/>"
391
24
        "  <Option name='KEEP_SOURCE_COLUMNS' type='boolean' "
392
24
        "description='whether to add original columns whose guessed data type "
393
24
        "is not String. Only used if AUTODETECT_TYPE=YES' default='NO'/>"
394
24
        "  <Option name='AUTODETECT_WIDTH' type='string-select' "
395
24
        "description='whether to auto-detect width/precision. Only used if "
396
24
        "AUTODETECT_TYPE=YES' default='NO'>"
397
24
        "    <Value>YES</Value>"
398
24
        "    <Value>NO</Value>"
399
24
        "    <Value>STRING_ONLY</Value>"
400
24
        "  </Option>"
401
24
        "  <Option name='AUTODETECT_SIZE_LIMIT' type='int' description='number "
402
24
        "of bytes to inspect for auto-detection of data type. Only used if "
403
24
        "AUTODETECT_TYPE=YES' default='1000000'/>"
404
24
        "  <Option name='QUOTED_FIELDS_AS_STRING' type='boolean' "
405
24
        "description='Only used if AUTODETECT_TYPE=YES. Whether to enforce "
406
24
        "quoted fields as string fields.' default='NO'/>"
407
24
        "  <Option name='X_POSSIBLE_NAMES' type='string' description='Comma "
408
24
        "separated list of possible names for X/longitude coordinate of a "
409
24
        "point.'/>"
410
24
        "  <Option name='Y_POSSIBLE_NAMES' type='string' description='Comma "
411
24
        "separated list of possible names for Y/latitude coordinate of a "
412
24
        "point.'/>"
413
24
        "  <Option name='Z_POSSIBLE_NAMES' type='string' description='Comma "
414
24
        "separated list of possible names for Z/elevation coordinate of a "
415
24
        "point.'/>"
416
24
        "  <Option name='GEOM_POSSIBLE_NAMES' type='string' description='Comma "
417
24
        "separated list of possible names for geometry columns.' "
418
24
        "default='WKT'/>"
419
24
        "  <Option name='KEEP_GEOM_COLUMNS' type='boolean' "
420
24
        "description='whether to add original x/y/geometry columns as regular "
421
24
        "fields.' default='YES'/>"
422
24
        "  <Option name='HEADERS' type='string-select' description='Whether "
423
24
        "the first line of the file contains column names or not' "
424
24
        "default='AUTO'>"
425
24
        "    <Value>YES</Value>"
426
24
        "    <Value>NO</Value>"
427
24
        "    <Value>AUTO</Value>"
428
24
        "  </Option>"
429
24
        "  <Option name='EMPTY_STRING_AS_NULL' type='boolean' "
430
24
        "description='Whether to consider empty strings as null fields on "
431
24
        "reading' default='NO'/>"
432
24
        "  <Option name='MAX_LINE_SIZE' type='int' description='Maximum number "
433
24
        "of bytes for a line (-1=unlimited)' default='" STRINGIFY(
434
24
            OGR_CSV_DEFAULT_MAX_LINE_SIZE) "'/>"
435
24
                                           "  <Option name='OGR_SCHEMA' "
436
24
                                           "type='string' description='"
437
24
                                           "Partially or totally overrides the "
438
24
                                           "auto-detected schema to use for "
439
24
                                           "creating the layer. "
440
24
                                           "The overrides are defined as a "
441
24
                                           "JSON list of field definitions. "
442
24
                                           "This can be a filename or a JSON "
443
24
                                           "string or a URL.'/>"
444
24
                                           "</OpenOptionList>");
445
446
24
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
447
24
    poDriver->SetMetadataItem(GDAL_DMD_CREATIONFIELDDATATYPES,
448
24
                              "Integer Integer64 Real String Date DateTime "
449
24
                              "Time IntegerList Integer64List RealList "
450
24
                              "StringList");
451
24
    poDriver->SetMetadataItem(GDAL_DMD_CREATIONFIELDDATASUBTYPES,
452
24
                              "Boolean Int16 Float32");
453
24
    poDriver->SetMetadataItem(GDAL_DCAP_HONOR_GEOM_COORDINATE_PRECISION, "YES");
454
455
24
    poDriver->SetMetadataItem(GDAL_DCAP_UPDATE, "YES");
456
24
    poDriver->SetMetadataItem(GDAL_DMD_UPDATE_ITEMS, "Features");
457
458
24
    poDriver->pfnOpen = OGRCSVDriverOpen;
459
24
    poDriver->pfnIdentify = OGRCSVDriverIdentify;
460
24
    poDriver->pfnCreate = OGRCSVDriverCreate;
461
24
    poDriver->pfnDelete = OGRCSVDriverDelete;
462
24
    poDriver->pfnUnloadDriver = OGRCSVDriverUnload;
463
464
24
    GetGDALDriverManager()->RegisterDriver(poDriver);
465
24
}