Coverage Report

Created: 2026-08-11 08:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/frmts/raw/noaabdataset.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  GDAL
4
 * Purpose:  Implementation of NOAA .b format used for GEOCON / NADCON5 grids
5
 * Author:   Even Rouault <even dot rouault at spatialys.com>
6
 *
7
 ******************************************************************************
8
 * Copyright (c) 2022, Even Rouault <even dot rouault at spatialys.com>
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "cpl_conv.h"
14
#include "cpl_string.h"
15
#include "gdal_frmts.h"
16
#include "gdal_priv.h"
17
#include "rawdataset.h"
18
#include "ogr_srs_api.h"
19
20
#include <limits>
21
22
// Specification of the format is at "paragraph 10.2 ".b" grids (GEOCON and
23
// NADCON 5.0)" of "NOAA Technical Report NOS NGS 63" at
24
// https://geodesy.noaa.gov/library/pdfs/NOAA_TR_NOS_NGS_0063.pdf
25
26
constexpr int HEADER_SIZE = 52;
27
constexpr int FORTRAN_HEADER_SIZE = 4;
28
constexpr int FORTRAN_TRAILER_SIZE = 4;
29
30
/************************************************************************/
31
/* ==================================================================== */
32
/*                          NOAA_B_Dataset                              */
33
/* ==================================================================== */
34
/************************************************************************/
35
36
class NOAA_B_Dataset final : public RawDataset
37
{
38
    OGRSpatialReference m_oSRS{};
39
    GDALGeoTransform m_gt{};
40
41
    CPL_DISALLOW_COPY_ASSIGN(NOAA_B_Dataset)
42
43
    static int IdentifyEx(GDALOpenInfo *poOpenInfo, bool &bBigEndianOut);
44
45
    CPLErr Close(GDALProgressFunc = nullptr, void * = nullptr) override
46
238
    {
47
238
        return GDALPamDataset::Close();
48
238
    }
49
50
  public:
51
    NOAA_B_Dataset()
52
13.8k
    {
53
13.8k
        m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
54
13.8k
    }
55
56
    CPLErr GetGeoTransform(GDALGeoTransform &gt) const override;
57
58
    const OGRSpatialReference *GetSpatialRef() const override
59
213
    {
60
213
        return &m_oSRS;
61
213
    }
62
63
    static GDALDataset *Open(GDALOpenInfo *);
64
    static int Identify(GDALOpenInfo *);
65
};
66
67
/************************************************************************/
68
/* ==================================================================== */
69
/*                          NOAA_B_Dataset                              */
70
/* ==================================================================== */
71
/************************************************************************/
72
73
/************************************************************************/
74
/*                          GetHeaderValues()                           */
75
/************************************************************************/
76
77
static void GetHeaderValues(const GDALOpenInfo *poOpenInfo, double &dfSWLat,
78
                            double &dfSWLon, double &dfDeltaLat,
79
                            double &dfDeltaLon, int32_t &nRows, int32_t &nCols,
80
                            int32_t &iKind, bool bBigEndian)
81
219k
{
82
219k
    const auto ReadFloat64 = [bBigEndian](const GByte *&ptr)
83
878k
    {
84
878k
        double v;
85
878k
        memcpy(&v, ptr, sizeof(v));
86
878k
        ptr += sizeof(v);
87
878k
        if (bBigEndian)
88
878k
            CPL_MSBPTR64(&v);
89
466k
        else
90
878k
            CPL_LSBPTR64(&v);
91
878k
        return v;
92
878k
    };
93
94
219k
    const auto ReadInt32 = [bBigEndian](const GByte *&ptr)
95
659k
    {
96
659k
        int32_t v;
97
659k
        memcpy(&v, ptr, sizeof(v));
98
659k
        ptr += sizeof(v);
99
659k
        if (bBigEndian)
100
659k
            CPL_MSBPTR32(&v);
101
349k
        else
102
659k
            CPL_LSBPTR32(&v);
103
659k
        return v;
104
659k
    };
105
106
219k
    const GByte *ptr = poOpenInfo->pabyHeader + FORTRAN_HEADER_SIZE;
107
108
219k
    dfSWLat = ReadFloat64(ptr);
109
219k
    dfSWLon = ReadFloat64(ptr);
110
219k
    dfDeltaLat = ReadFloat64(ptr);
111
219k
    dfDeltaLon = ReadFloat64(ptr);
112
113
219k
    nRows = ReadInt32(ptr);
114
219k
    nCols = ReadInt32(ptr);
115
219k
    iKind = ReadInt32(ptr);
116
219k
}
117
118
/************************************************************************/
119
/*                              Identify()                              */
120
/************************************************************************/
121
122
int NOAA_B_Dataset::IdentifyEx(GDALOpenInfo *poOpenInfo, bool &bBigEndianOut)
123
124
535k
{
125
535k
    if (poOpenInfo->nHeaderBytes < HEADER_SIZE)
126
432k
        return FALSE;
127
128
#if !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION)
129
    if (!poOpenInfo->IsExtensionEqualToCI("b"))
130
        return FALSE;
131
#endif
132
133
    // Sanity checks on header
134
102k
    double dfSWLat;
135
102k
    double dfSWLon;
136
102k
    double dfDeltaLat;
137
102k
    double dfDeltaLon;
138
102k
    int32_t nRows;
139
102k
    int32_t nCols;
140
102k
    int32_t iKind;
141
142
    // Fun... nadcon5 files are encoded in big-endian, but vertcon3 files...
143
    // in little-endian. We could probably figure that out directly from the
144
    // 4 bytes which are 0x00 0x00 0x00 0x2C for nadcon5, and the reverse for
145
    // vertcon3, but the semantics of those 4 bytes is undocumented.
146
    // So try both possibilities and rely on sanity checks.
147
280k
    for (int i = 0; i < 2; ++i)
148
205k
    {
149
205k
        const bool bBigEndian = i == 0 ? true : false;
150
205k
        GetHeaderValues(poOpenInfo, dfSWLat, dfSWLon, dfDeltaLat, dfDeltaLon,
151
205k
                        nRows, nCols, iKind, bBigEndian);
152
205k
        if (!(fabs(dfSWLat) <= 90))
153
122k
            continue;
154
83.3k
        if (!(fabs(dfSWLon) <=
155
83.3k
              360))  // NADCON5 grids typically have SWLon > 180
156
20.7k
            continue;
157
62.5k
        if (!(dfDeltaLat > 0 && dfDeltaLat <= 1))
158
16.4k
            continue;
159
46.0k
        if (!(dfDeltaLon > 0 && dfDeltaLon <= 1))
160
6.11k
            continue;
161
39.9k
        if (!(nRows > 0 && dfSWLat + (nRows - 1) * dfDeltaLat <= 90))
162
1.40k
            continue;
163
38.5k
        if (!(nCols > 0 && (nCols - 1) * dfDeltaLon <= 360))
164
789
            continue;
165
37.7k
        if (!(iKind >= -1 && iKind <= 2))
166
9.46k
            continue;
167
168
28.3k
        bBigEndianOut = bBigEndian;
169
28.3k
        return TRUE;
170
37.7k
    }
171
74.6k
    return FALSE;
172
102k
}
173
174
int NOAA_B_Dataset::Identify(GDALOpenInfo *poOpenInfo)
175
176
521k
{
177
521k
    bool bBigEndian = false;
178
521k
    return IdentifyEx(poOpenInfo, bBigEndian);
179
521k
}
180
181
/************************************************************************/
182
/*                          GetGeoTransform()                           */
183
/************************************************************************/
184
185
CPLErr NOAA_B_Dataset::GetGeoTransform(GDALGeoTransform &gt) const
186
187
232
{
188
232
    gt = m_gt;
189
232
    return CE_None;
190
232
}
191
192
/************************************************************************/
193
/*                                Open()                                */
194
/************************************************************************/
195
196
GDALDataset *NOAA_B_Dataset::Open(GDALOpenInfo *poOpenInfo)
197
198
14.1k
{
199
14.1k
    bool bBigEndian = false;
200
14.1k
    if (!IdentifyEx(poOpenInfo, bBigEndian) || poOpenInfo->fpL == nullptr ||
201
14.1k
        poOpenInfo->eAccess == GA_Update)
202
0
    {
203
0
        return nullptr;
204
0
    }
205
206
    /* -------------------------------------------------------------------- */
207
    /*      Read the header.                                                */
208
    /* -------------------------------------------------------------------- */
209
14.1k
    double dfSWLat;
210
14.1k
    double dfSWLon;
211
14.1k
    double dfDeltaLat;
212
14.1k
    double dfDeltaLon;
213
14.1k
    int32_t nRows;
214
14.1k
    int32_t nCols;
215
14.1k
    int32_t iKind;
216
14.1k
    GetHeaderValues(poOpenInfo, dfSWLat, dfSWLon, dfDeltaLat, dfDeltaLon, nRows,
217
14.1k
                    nCols, iKind, bBigEndian);
218
219
14.1k
    if (iKind == -1)
220
60
    {
221
60
        CPLError(CE_Failure, CPLE_NotSupported,
222
60
                 "KIND = -1 in NOAA .b dataset not supported");
223
60
        return nullptr;
224
60
    }
225
226
14.0k
    const GDALDataType eDT =
227
        // iKind == -1 ? GDT_Int16 :
228
14.0k
        iKind == 0   ? GDT_Int32
229
14.0k
        : iKind == 1 ? GDT_Float32
230
196
                     : GDT_Int16;
231
14.0k
    const int nDTSize = GDALGetDataTypeSizeBytes(eDT);
232
14.0k
    if (!GDALCheckDatasetDimensions(nCols, nRows) ||
233
14.0k
        (nDTSize > 0 && static_cast<vsi_l_offset>(nCols) * nRows >
234
14.0k
                            std::numeric_limits<vsi_l_offset>::max() / nDTSize))
235
0
    {
236
0
        return nullptr;
237
0
    }
238
14.0k
    if (nDTSize > 0 && nCols > (std::numeric_limits<int>::max() -
239
14.0k
                                FORTRAN_HEADER_SIZE - FORTRAN_TRAILER_SIZE) /
240
14.0k
                                   nDTSize)
241
259
    {
242
259
        return nullptr;
243
259
    }
244
13.8k
    const int nLineSize =
245
13.8k
        FORTRAN_HEADER_SIZE + nCols * nDTSize + FORTRAN_TRAILER_SIZE;
246
247
    /* -------------------------------------------------------------------- */
248
    /*      Create a corresponding GDALDataset.                             */
249
    /* -------------------------------------------------------------------- */
250
13.8k
    auto poDS = std::make_unique<NOAA_B_Dataset>();
251
252
13.8k
    poDS->nRasterXSize = nCols;
253
13.8k
    poDS->nRasterYSize = nRows;
254
255
    // Adjust longitude > 180 to [-180, 180] range
256
13.8k
    if (dfSWLon > 180)
257
0
        dfSWLon -= 360;
258
259
    // Convert from south-west center-of-pixel convention to
260
    // north-east pixel-corner convention
261
13.8k
    poDS->m_gt.xorig = dfSWLon - dfDeltaLon / 2;
262
13.8k
    poDS->m_gt.xscale = dfDeltaLon;
263
13.8k
    poDS->m_gt.xrot = 0.0;
264
13.8k
    poDS->m_gt.yorig = dfSWLat + (nRows - 1) * dfDeltaLat + dfDeltaLat / 2;
265
13.8k
    poDS->m_gt.yrot = 0.0;
266
13.8k
    poDS->m_gt.yscale = -dfDeltaLat;
267
268
    /* -------------------------------------------------------------------- */
269
    /*      Create band information object.                                 */
270
    /* -------------------------------------------------------------------- */
271
272
    // Borrow file handle
273
13.8k
    VSILFILE *fpImage = poOpenInfo->fpL;
274
13.8k
    poOpenInfo->fpL = nullptr;
275
276
    // Records are presented from the southern-most to the northern-most
277
13.8k
    auto poBand = RawRasterBand::Create(
278
13.8k
        poDS.get(), 1, fpImage,
279
        // skip to beginning of northern-most line
280
13.8k
        HEADER_SIZE +
281
13.8k
            static_cast<vsi_l_offset>(poDS->nRasterYSize - 1) * nLineSize +
282
13.8k
            FORTRAN_HEADER_SIZE,
283
13.8k
        nDTSize, -nLineSize, eDT,
284
13.8k
        bBigEndian ? RawRasterBand::ByteOrder::ORDER_BIG_ENDIAN
285
13.8k
                   : RawRasterBand::ByteOrder::ORDER_LITTLE_ENDIAN,
286
13.8k
        RawRasterBand::OwnFP::YES);
287
13.8k
    if (!poBand)
288
0
        return nullptr;
289
13.8k
    poDS->SetBand(1, std::move(poBand));
290
291
    /* -------------------------------------------------------------------- */
292
    /*      Guess CRS from filename.                                        */
293
    /* -------------------------------------------------------------------- */
294
13.8k
    const std::string osFilename(CPLGetFilename(poOpenInfo->pszFilename));
295
296
13.8k
    static const struct
297
13.8k
    {
298
13.8k
        const char *pszPrefix;
299
13.8k
        int nEPSGCode;
300
13.8k
    }
301
    // Cf https://geodesy.noaa.gov/pub/nadcon5/20160901release/Builds/
302
13.8k
    asFilenameToCRS[] = {
303
13.8k
        {"nadcon5.nad27.", 4267},       // NAD27
304
13.8k
        {"nadcon5.pr40.", 4139},        // Puerto Rico (1940)
305
13.8k
        {"nadcon5.ohd.", 4135},         // Old Hawaian
306
13.8k
        {"nadcon5.sl1952.", 4136},      // Saint Lawrence Island (1952)
307
13.8k
        {"nadcon5.sp1952.", 4137},      // Saint Paul Island (1952)
308
13.8k
        {"nadcon5.sg1952.", 4138},      // Saint George Island (1952)
309
13.8k
        {"nadcon5.as62.", 4169},        // American Samoa 1962
310
13.8k
        {"nadcon5.gu63.", 4675},        // Guam 1963
311
13.8k
        {"nadcon5.nad83_1986.", 4269},  // NAD83
312
13.8k
        {"nadcon5.nad83_harn.", 4152},  // NAD83(HARN)
313
13.8k
        {"nadcon5.nad83_1992.",
314
13.8k
         4152},  // NAD83(1992) for Alaska is NAD83(HARN) in EPSG
315
13.8k
        {"nadcon5.nad83_1993.",
316
13.8k
         4152},  // NAD83(1993) for American Samoa, PRVI, Guam and Hawaii is
317
                 // NAD83(HARN) in EPSG
318
13.8k
        {"nadcon5.nad83_1997.", 8545},  // NAD83(HARN Corrected)
319
13.8k
        {"nadcon5.nad83_fbn.", 8860},   // NAD83(FBN)
320
13.8k
        {"nadcon5.nad83_2002.",
321
13.8k
         8860},  // NAD83(2002) for Alaska, PRVI and Guam is NAD83(FBN) in EPSG
322
13.8k
        {"nadcon5.nad83_2007.", 4759},  // NAD83(NSRS2007)
323
13.8k
    };
324
325
13.8k
    for (const auto &sPair : asFilenameToCRS)
326
221k
    {
327
221k
        if (STARTS_WITH_CI(osFilename.c_str(), sPair.pszPrefix))
328
5
        {
329
5
            poDS->m_oSRS.importFromEPSG(sPair.nEPSGCode);
330
5
            break;
331
5
        }
332
221k
    }
333
13.8k
    if (poDS->m_oSRS.IsEmpty())
334
13.8k
    {
335
13.8k
        poDS->m_oSRS.importFromWkt(
336
13.8k
            "GEOGCRS[\"Unspecified geographic CRS\",DATUM[\"Unspecified datum "
337
13.8k
            "based on GRS80 ellipsoid\",ELLIPSOID[\"GRS "
338
13.8k
            "1980\",6378137,298.257222101]],CS[ellipsoidal,2],AXIS[\"geodetic "
339
13.8k
            "latitude (Lat)\",north,ANGLEUNIT[\"degree\",0.0174532925199433]], "
340
13.8k
            "       AXIS[\"geodetic longitude "
341
13.8k
            "(Lon)\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]]]");
342
13.8k
    }
343
344
    /* -------------------------------------------------------------------- */
345
    /*      Initialize any PAM information.                                 */
346
    /* -------------------------------------------------------------------- */
347
13.8k
    poDS->SetDescription(poOpenInfo->pszFilename);
348
13.8k
    poDS->TryLoadXML();
349
350
    /* -------------------------------------------------------------------- */
351
    /*      Check for overviews.                                            */
352
    /* -------------------------------------------------------------------- */
353
13.8k
    poDS->oOvManager.Initialize(poDS.get(), poOpenInfo->pszFilename);
354
355
13.8k
    return poDS.release();
356
13.8k
}
357
358
/************************************************************************/
359
/*                        GDALRegister_NOAA_B()                         */
360
/************************************************************************/
361
362
void GDALRegister_NOAA_B()
363
22
{
364
22
    if (GDALGetDriverByName("NOAA_B") != nullptr)
365
0
        return;
366
367
22
    GDALDriver *poDriver = new GDALDriver();
368
369
22
    poDriver->SetDescription("NOAA_B");
370
22
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
371
22
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME,
372
22
                              "NOAA GEOCON/NADCON5 .b format");
373
22
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "b");
374
22
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
375
22
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC, "drivers/raster/noaa_b.html");
376
377
22
    poDriver->pfnIdentify = NOAA_B_Dataset::Identify;
378
22
    poDriver->pfnOpen = NOAA_B_Dataset::Open;
379
380
22
    GetGDALDriverManager()->RegisterDriver(poDriver);
381
22
}