Coverage Report

Created: 2026-08-11 08:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gdal/frmts/nitf/rpftocwriter.cpp
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  NITF Read/Write Library
4
 * Purpose:  Creates A.TOC RPF index for CADRG frames
5
 * Author:   Even Rouault, even dot rouault at spatialys dot com
6
 *
7
 **********************************************************************
8
 * Copyright (c) 2026, T-Kartor
9
 *
10
 * SPDX-License-Identifier: MIT
11
 ****************************************************************************/
12
13
#include "cpl_vsi.h"
14
#include "gdal_dataset.h"
15
#include "nitflib.h"
16
#include "offsetpatcher.h"
17
#include "rpfframewriter.h"
18
#include "rpftocwriter.h"
19
20
#include <algorithm>
21
#include <cinttypes>
22
#include <limits>
23
#include <map>
24
#include <utility>
25
26
namespace
27
{
28
struct FrameDesc
29
{
30
    int nZone = 0;
31
    int nReciprocalScale = 0;
32
    int nFrameX = 0;
33
    int nFrameY = 0;
34
    double dfMinX = 0;
35
    double dfMinY = 0;
36
    std::string osRelativeFilename{};  // relative to osInputDirectory
37
    char chClassification = 'U';
38
};
39
40
struct MinMaxFrameXY
41
{
42
    int MinX = std::numeric_limits<int>::max();
43
    int MinY = std::numeric_limits<int>::max();
44
    int MaxX = 0;
45
    int MaxY = 0;
46
};
47
48
struct ScaleZone
49
{
50
    int nReciprocalScale = 0;
51
    int nZone = 0;
52
53
    bool operator<(const ScaleZone &other) const
54
0
    {
55
        // Sort reciprocal scale by decreasing order. This is apparently needed for
56
        // some viewers like Falcon Lite to be able to display A.TOC files
57
        // with multiple scales.
58
0
        return nReciprocalScale > other.nReciprocalScale ||
59
0
               (nReciprocalScale == other.nReciprocalScale &&
60
0
                nZone < other.nZone);
61
0
    }
62
};
63
64
}  // namespace
65
66
/************************************************************************/
67
/*                  Create_RPFTOC_LocationComponent()                   */
68
/************************************************************************/
69
70
static void
71
Create_RPFTOC_LocationComponent(GDALOffsetPatcher::OffsetPatcher &offsetPatcher)
72
0
{
73
0
    auto poBuffer = offsetPatcher.CreateBuffer(
74
0
        "LocationComponent", /* bEndiannessIsLittle = */ false);
75
0
    CPLAssert(poBuffer);
76
0
    poBuffer->DeclareOffsetAtCurrentPosition("LOCATION_COMPONENT_LOCATION");
77
78
0
    static const struct
79
0
    {
80
0
        uint16_t locationId;
81
0
        const char *locationBufferName;
82
0
        const char *locationOffsetName;
83
0
    } asLocations[] = {
84
0
        {LID_BoundaryRectangleSectionSubheader /* 148 */,
85
0
         "BoundaryRectangleSectionSubheader",
86
0
         "BOUNDARY_RECTANGLE_SECTION_SUBHEADER_LOCATION"},
87
0
        {LID_BoundaryRectangleTable /* 149 */, "BoundaryRectangleTable",
88
0
         "BOUNDARY_RECTANGLE_TABLE_LOCATION"},
89
0
        {LID_FrameFileIndexSectionSubHeader /* 150 */,
90
0
         "FrameFileIndexSectionSubHeader",
91
0
         "FRAME_FILE_INDEX_SECTION_SUBHEADER_LOCATION"},
92
0
        {LID_FrameFileIndexSubsection /* 151 */, "FrameFileIndexSubsection",
93
0
         "FRAME_FILE_INDEX_SUBSECTION_LOCATION"},
94
0
    };
95
96
0
    std::string sumOfSizes;
97
0
    uint16_t nComponents = 0;
98
0
    for (const auto &sLocation : asLocations)
99
0
    {
100
0
        ++nComponents;
101
0
        if (!sumOfSizes.empty())
102
0
            sumOfSizes += '+';
103
0
        sumOfSizes += sLocation.locationBufferName;
104
0
    }
105
106
0
    constexpr uint16_t COMPONENT_LOCATION_OFFSET = 14;
107
0
    constexpr uint16_t COMPONENT_LOCATION_RECORD_LENGTH = 10;
108
0
    poBuffer->AppendUInt16RefForSizeOfBuffer("LocationComponent");
109
0
    poBuffer->AppendUInt32(COMPONENT_LOCATION_OFFSET);
110
0
    poBuffer->AppendUInt16(nComponents);
111
0
    poBuffer->AppendUInt16(COMPONENT_LOCATION_RECORD_LENGTH);
112
    // COMPONENT_AGGREGATE_LENGTH
113
0
    poBuffer->AppendUInt32RefForSizeOfBuffer(sumOfSizes);
114
115
0
    for (const auto &sLocation : asLocations)
116
0
    {
117
0
        poBuffer->AppendUInt16(sLocation.locationId);
118
0
        poBuffer->AppendUInt32RefForSizeOfBuffer(sLocation.locationBufferName);
119
0
        poBuffer->AppendUInt32RefForOffset(sLocation.locationOffsetName);
120
0
    }
121
0
}
122
123
/************************************************************************/
124
/*          Create_RPFTOC_BoundaryRectangleSectionSubheader()           */
125
/************************************************************************/
126
127
static void Create_RPFTOC_BoundaryRectangleSectionSubheader(
128
    GDALOffsetPatcher::OffsetPatcher &offsetPatcher,
129
    size_t nNumberOfBoundaryRectangles)
130
0
{
131
0
    auto poBuffer = offsetPatcher.CreateBuffer(
132
0
        "BoundaryRectangleSectionSubheader", /* bEndiannessIsLittle = */ false);
133
0
    CPLAssert(poBuffer);
134
0
    poBuffer->DeclareOffsetAtCurrentPosition(
135
0
        "BOUNDARY_RECTANGLE_SECTION_SUBHEADER_LOCATION");
136
0
    constexpr uint32_t BOUNDARY_RECTANGLE_TABLE_OFFSET = 0;
137
0
    poBuffer->AppendUInt32(BOUNDARY_RECTANGLE_TABLE_OFFSET);
138
0
    poBuffer->AppendUInt16(static_cast<uint16_t>(nNumberOfBoundaryRectangles));
139
0
    constexpr uint16_t BOUNDARY_RECTANGLE_RECORD_LENGTH = 132;
140
0
    poBuffer->AppendUInt16(BOUNDARY_RECTANGLE_RECORD_LENGTH);
141
0
}
142
143
/************************************************************************/
144
/*                           StrPadTruncate()                           */
145
/************************************************************************/
146
147
#ifndef StrPadTruncate_defined
148
#define StrPadTruncate_defined
149
150
static std::string StrPadTruncate(const std::string &osIn, size_t nSize)
151
0
{
152
0
    std::string osOut(osIn);
153
0
    osOut.resize(nSize, ' ');
154
0
    return osOut;
155
0
}
156
#endif
157
158
/************************************************************************/
159
/*                Create_RPFTOC_BoundaryRectangleTable()                */
160
/************************************************************************/
161
162
static void Create_RPFTOC_BoundaryRectangleTable(
163
    GDALOffsetPatcher::OffsetPatcher &offsetPatcher,
164
    const std::string &osProducer,
165
    const std::map<ScaleZone, MinMaxFrameXY> &oMapScaleZoneToMinMaxFrameXY)
166
0
{
167
0
    auto poBuffer = offsetPatcher.CreateBuffer(
168
0
        "BoundaryRectangleTable", /* bEndiannessIsLittle = */ false);
169
0
    CPLAssert(poBuffer);
170
0
    poBuffer->DeclareOffsetAtCurrentPosition(
171
0
        "BOUNDARY_RECTANGLE_TABLE_LOCATION");
172
173
0
    for (const auto &[scaleZone, extent] : oMapScaleZoneToMinMaxFrameXY)
174
0
    {
175
0
        poBuffer->AppendString("CADRG");  // PRODUCT_DATA_TYPE
176
0
        poBuffer->AppendString("55:1 ");  // COMPRESSION_RATIO
177
178
0
        std::string osScaleOrResolution;
179
0
        const int nReciprocalScale = scaleZone.nReciprocalScale;
180
0
        if (nReciprocalScale >= Million && (nReciprocalScale % Million) == 0)
181
0
            osScaleOrResolution =
182
0
                CPLSPrintf("1:%dM", nReciprocalScale / Million);
183
0
        else if (nReciprocalScale >= Kilo && (nReciprocalScale % Kilo) == 0)
184
0
            osScaleOrResolution = CPLSPrintf("1:%dK", nReciprocalScale / Kilo);
185
0
        else
186
0
            osScaleOrResolution = CPLSPrintf("1:%d", nReciprocalScale);
187
0
        poBuffer->AppendString(StrPadTruncate(osScaleOrResolution, 12));
188
189
0
        const int nZone = scaleZone.nZone;
190
0
        poBuffer->AppendString(CPLSPrintf("%c", RPFCADRGZoneNumToChar(nZone)));
191
0
        poBuffer->AppendString(StrPadTruncate(osProducer, 5));
192
193
0
        double dfXMin = 0;
194
0
        double dfYMin = 0;
195
0
        double dfXMax = 0;
196
0
        double dfYMax = 0;
197
0
        double dfUnused = 0;
198
0
        RPFGetCADRGFrameExtent(nZone, nReciprocalScale, extent.MinX,
199
0
                               extent.MinY, dfXMin, dfYMin, dfUnused, dfUnused);
200
0
        RPFGetCADRGFrameExtent(nZone, nReciprocalScale, extent.MaxX,
201
0
                               extent.MaxY, dfUnused, dfUnused, dfXMax, dfYMax);
202
203
0
        double dfULX = dfXMin;
204
0
        double dfULY = dfYMax;
205
0
        double dfLLX = dfXMin;
206
0
        double dfLLY = dfYMin;
207
0
        double dfURX = dfXMax;
208
0
        double dfURY = dfYMax;
209
0
        double dfLRX = dfXMax;
210
0
        double dfLRY = dfYMin;
211
212
0
        if (nZone == MAX_ZONE_NORTHERN_HEMISPHERE || nZone == MAX_ZONE)
213
0
        {
214
0
            OGRSpatialReference oPolarSRS;
215
0
            oPolarSRS.importFromWkt(nZone == MAX_ZONE_NORTHERN_HEMISPHERE
216
0
                                        ? pszNorthPolarProjection
217
0
                                        : pszSouthPolarProjection);
218
0
            OGRSpatialReference oSRS_WGS84;
219
0
            oSRS_WGS84.SetWellKnownGeogCS("WGS84");
220
0
            oSRS_WGS84.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
221
0
            auto poCT = std::unique_ptr<OGRCoordinateTransformation>(
222
0
                OGRCreateCoordinateTransformation(&oPolarSRS, &oSRS_WGS84));
223
0
            poCT->Transform(1, &dfULX, &dfULY);
224
0
            poCT->Transform(1, &dfLLX, &dfLLY);
225
0
            poCT->Transform(1, &dfURX, &dfURY);
226
0
            poCT->Transform(1, &dfLRX, &dfLRY);
227
0
        }
228
229
0
        poBuffer->AppendFloat64(dfULY);  // NORTHWEST_LATITUDE
230
0
        poBuffer->AppendFloat64(dfULX);  // NORTHWEST_LONGITUDE
231
232
0
        poBuffer->AppendFloat64(dfLLY);  // SOUTHWEST_LATITUDE
233
0
        poBuffer->AppendFloat64(dfLLX);  // SOUTHWEST_LONGITUDE
234
235
0
        poBuffer->AppendFloat64(dfURY);  // NORTHEAST_LATITUDE
236
0
        poBuffer->AppendFloat64(dfURX);  // NORTHEAST_LONGITUDE
237
238
0
        poBuffer->AppendFloat64(dfLRY);  // SOUTHEAST_LATITUDE
239
0
        poBuffer->AppendFloat64(dfLRX);  // SOUTHEAST_LONGITUDE
240
241
0
        double latResolution = 0;
242
0
        double lonResolution = 0;
243
0
        double latInterval = 0;
244
0
        double lonInterval = 0;
245
0
        RPFGetCADRGResolutionAndInterval(nZone, nReciprocalScale, latResolution,
246
0
                                         lonResolution, latInterval,
247
0
                                         lonInterval);
248
249
0
        poBuffer->AppendFloat64(latResolution);
250
0
        poBuffer->AppendFloat64(lonResolution);
251
0
        poBuffer->AppendFloat64(latInterval);
252
0
        poBuffer->AppendFloat64(lonInterval);
253
254
0
        const int nCountY = extent.MaxY - extent.MinY + 1;
255
0
        poBuffer->AppendUInt32(static_cast<uint32_t>(nCountY));
256
257
0
        const int nCountX = extent.MaxX - extent.MinX + 1;
258
0
        poBuffer->AppendUInt32(static_cast<uint32_t>(nCountX));
259
0
    }
260
0
}
261
262
/************************************************************************/
263
/*            Create_RPFTOC_FrameFileIndexSectionSubHeader()            */
264
/************************************************************************/
265
266
static void Create_RPFTOC_FrameFileIndexSectionSubHeader(
267
    GDALOffsetPatcher::OffsetPatcher &offsetPatcher,
268
    char chHighestClassification, size_t nCountFrames, uint16_t nCountSubdirs)
269
0
{
270
0
    auto poBuffer = offsetPatcher.CreateBuffer(
271
0
        "FrameFileIndexSectionSubHeader", /* bEndiannessIsLittle = */ false);
272
0
    CPLAssert(poBuffer);
273
0
    poBuffer->DeclareOffsetAtCurrentPosition(
274
0
        "FRAME_FILE_INDEX_SECTION_SUBHEADER_LOCATION");
275
276
0
    poBuffer->AppendString(CPLSPrintf("%c", chHighestClassification));
277
0
    constexpr uint32_t FRAME_FILE_INDEX_TABLE_OFFSET = 0;
278
0
    poBuffer->AppendUInt32(FRAME_FILE_INDEX_TABLE_OFFSET);
279
0
    poBuffer->AppendUInt32(static_cast<uint32_t>(nCountFrames));
280
0
    poBuffer->AppendUInt16(nCountSubdirs);
281
0
    constexpr uint16_t FRAME_FILE_INDEX_RECORD_LENGTH = 33;
282
0
    poBuffer->AppendUInt16(FRAME_FILE_INDEX_RECORD_LENGTH);
283
0
}
284
285
/************************************************************************/
286
/*                             GetGEOREF()                              */
287
/************************************************************************/
288
289
/** Return coordinate as a World Geographic Reference System (GEOREF) string
290
 * as described in paragraph 5.4 of DMA TM 8358.1
291
 * (https://everyspec.com/DoD/DOD-General/download.php?spec=DMA_TM-8358.1.006300.PDF)
292
 */
293
static std::string GetGEOREF(double dfLon, double dfLat)
294
0
{
295
    // clang-format off
296
    // letters 'I' and 'O' are omitted to avoid confusiong with one and zero
297
0
    constexpr char ALPHABET_WITHOUT_IO[] = {
298
0
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',      'J',
299
0
        'K', 'L', 'M', 'N',      'P', 'Q', 'R', 'S', 'T',
300
0
        'U', 'V', 'W', 'X', 'Y', 'Z'
301
0
    };
302
    // clang-format on
303
304
0
    std::string osRes;
305
306
0
    constexpr double LON_ORIGIN = -180;
307
0
    constexpr double LAT_ORIGIN = -90;
308
0
    constexpr int QUADRANGLE_SIZE = 15;  // degree
309
0
    constexpr double EPSILON = 1e-5;
310
311
    // Longitude zone
312
0
    {
313
0
        const int nIdx =
314
0
            static_cast<int>(dfLon - LON_ORIGIN + EPSILON) / QUADRANGLE_SIZE;
315
0
        CPLAssert(nIdx >= 0 && nIdx < 24);
316
0
        osRes += ALPHABET_WITHOUT_IO[nIdx];
317
0
    }
318
319
    // Latitude band
320
0
    {
321
0
        const int nIdx =
322
0
            static_cast<int>(dfLat - LAT_ORIGIN + EPSILON) / QUADRANGLE_SIZE;
323
0
        CPLAssert(nIdx >= 0 && nIdx < 12);
324
0
        osRes += ALPHABET_WITHOUT_IO[nIdx];
325
0
    }
326
327
    // Longitude index within 15x15 degree quadrangle
328
0
    {
329
0
        const int nIdx =
330
0
            static_cast<int>(dfLon - LON_ORIGIN + EPSILON) % QUADRANGLE_SIZE;
331
0
        osRes += ALPHABET_WITHOUT_IO[nIdx];
332
0
    }
333
334
    // Latitude index within 15x15 degree quadrangle
335
0
    {
336
0
        const int nIdx =
337
0
            static_cast<int>(dfLat - LAT_ORIGIN + EPSILON) % QUADRANGLE_SIZE;
338
0
        osRes += ALPHABET_WITHOUT_IO[nIdx];
339
0
    }
340
341
    // Longitude minutes
342
0
    {
343
0
        constexpr int MINUTES_IN_DEGREE = 60;
344
0
        const int nMinutes =
345
0
            static_cast<int>((dfLon - LON_ORIGIN) * MINUTES_IN_DEGREE +
346
0
                             EPSILON) %
347
0
            MINUTES_IN_DEGREE;
348
0
        osRes += CPLSPrintf("%02d", nMinutes);
349
0
    }
350
351
0
    return osRes;
352
0
}
353
354
/************************************************************************/
355
/*               Create_RPFTOC_FrameFileIndexSubsection()               */
356
/************************************************************************/
357
358
static void Create_RPFTOC_FrameFileIndexSubsection(
359
    GDALOffsetPatcher::OffsetPatcher &offsetPatcher,
360
    const std::string &osSecurityCountryCode,
361
    const std::map<ScaleZone, std::vector<FrameDesc>> &oMapScaleZoneToFrames,
362
    const std::map<ScaleZone, MinMaxFrameXY> &oMapScaleZoneToMinMaxFrameXY,
363
    const std::map<std::string, int> &oMapSubdirToIdx)
364
0
{
365
0
    auto poBuffer = offsetPatcher.CreateBuffer(
366
0
        "FrameFileIndexSubsection", /* bEndiannessIsLittle = */ false);
367
0
    CPLAssert(poBuffer);
368
0
    poBuffer->DeclareOffsetAtCurrentPosition(
369
0
        "FRAME_FILE_INDEX_SUBSECTION_LOCATION");
370
371
0
    std::map<ScaleZone, uint16_t> oMapScaleZoneToIdx;
372
0
    for ([[maybe_unused]] const auto &[scaleZone, unused] :
373
0
         oMapScaleZoneToFrames)
374
0
    {
375
0
        if (!cpl::contains(oMapScaleZoneToIdx, scaleZone))
376
0
        {
377
0
            oMapScaleZoneToIdx[scaleZone] =
378
0
                static_cast<uint16_t>(oMapScaleZoneToIdx.size());
379
0
        }
380
0
    }
381
382
0
    for (const auto &[scaleZone, framesDesc] : oMapScaleZoneToFrames)
383
0
    {
384
0
        const auto oIterMapScaleZoneToMinMaxFrameXY =
385
0
            oMapScaleZoneToMinMaxFrameXY.find(scaleZone);
386
0
        CPLAssert(oIterMapScaleZoneToMinMaxFrameXY !=
387
0
                  oMapScaleZoneToMinMaxFrameXY.end());
388
0
        const auto &oMinMaxFrameXY = oIterMapScaleZoneToMinMaxFrameXY->second;
389
390
0
        for (const auto &frameDesc : framesDesc)
391
0
        {
392
0
            const auto oIterMapScaleZoneToIdx =
393
0
                oMapScaleZoneToIdx.find(scaleZone);
394
0
            CPLAssert(oIterMapScaleZoneToIdx != oMapScaleZoneToIdx.end());
395
0
            poBuffer->AppendUInt16(oIterMapScaleZoneToIdx->second);
396
0
            poBuffer->AppendUInt16(
397
0
                static_cast<uint16_t>(frameDesc.nFrameY - oMinMaxFrameXY.MinY));
398
0
            poBuffer->AppendUInt16(
399
0
                static_cast<uint16_t>(frameDesc.nFrameX - oMinMaxFrameXY.MinX));
400
401
0
            const std::string osSubdir =
402
0
                CPLGetPathSafe(frameDesc.osRelativeFilename.c_str());
403
0
            const auto oIterSubdirToIdx = oMapSubdirToIdx.find(osSubdir);
404
0
            CPLAssert(oIterSubdirToIdx != oMapSubdirToIdx.end());
405
0
            poBuffer->AppendUInt32RefForOffset(
406
0
                CPLSPrintf("PATHNAME_RECORD_OFFSET_%d",
407
0
                           oIterSubdirToIdx->second),
408
0
                /* bRelativeToStartOfBuffer = */ true);
409
0
            poBuffer->AppendString(StrPadTruncate(
410
0
                CPLGetFilename(frameDesc.osRelativeFilename.c_str()), 12));
411
0
            const std::string osGeographicLocation =
412
0
                GetGEOREF(frameDesc.dfMinX, frameDesc.dfMinY);
413
0
            CPLAssert(osGeographicLocation.size() == 6);
414
0
            poBuffer->AppendString(StrPadTruncate(osGeographicLocation, 6));
415
0
            poBuffer->AppendString("U");  // FRAME_FILE_SECURITY_CLASSIFICATION
416
0
            poBuffer->AppendString(StrPadTruncate(osSecurityCountryCode, 2));
417
            // FRAME_FILE_SECURITY_RELEASE_MARKING
418
0
            poBuffer->AppendString("  ");
419
0
        }
420
0
    }
421
422
0
    struct SortedDirPrefixes
423
0
    {
424
0
        int nIdx = 0;
425
0
        std::string osSubdir{};
426
0
    };
427
428
0
    std::vector<SortedDirPrefixes> asSortedDirPrefixes;
429
0
    for (const auto &[osSubdir, nIdx] : oMapSubdirToIdx)
430
0
    {
431
0
        SortedDirPrefixes s;
432
0
        s.nIdx = nIdx;
433
0
        s.osSubdir = osSubdir;
434
0
        asSortedDirPrefixes.push_back(std::move(s));
435
0
    }
436
0
    std::sort(asSortedDirPrefixes.begin(), asSortedDirPrefixes.end(),
437
0
              [](const SortedDirPrefixes &a, const SortedDirPrefixes &b)
438
0
              { return a.nIdx < b.nIdx; });
439
440
0
    for (const auto &sortedDirPrefix : asSortedDirPrefixes)
441
0
    {
442
0
        poBuffer->DeclareOffsetAtCurrentPosition(
443
0
            CPLSPrintf("PATHNAME_RECORD_OFFSET_%d", sortedDirPrefix.nIdx));
444
0
        std::string osPath =
445
0
            "./" + CPLString(sortedDirPrefix.osSubdir).replaceAll('\\', '/');
446
0
        if (osPath.back() != '/')
447
0
            osPath += '/';
448
0
        poBuffer->AppendUInt16(static_cast<uint16_t>(osPath.size()));
449
0
        poBuffer->AppendString(osPath);
450
0
    }
451
0
}
452
453
/************************************************************************/
454
/*                         RPCTOCCreateRPFDES()                         */
455
/************************************************************************/
456
457
static bool RPCTOCCreateRPFDES(
458
    VSILFILE *fp, GDALOffsetPatcher::OffsetPatcher &offsetPatcher,
459
    const std::string &osProducer, const std::string &osSecurityCountryCode,
460
    const std::map<ScaleZone, std::vector<FrameDesc>> &oMapScaleZoneToFrames,
461
    const std::map<ScaleZone, MinMaxFrameXY> &oMapScaleZoneToMinMaxFrameXY)
462
0
{
463
0
    (void)oMapScaleZoneToFrames;
464
465
0
    bool bOK = fp->Seek(0, SEEK_END) == 0;
466
467
0
    const char *pszDESHeader = RPFFrameWriteGetDESHeader();
468
0
    bOK &=
469
0
        fp->Write(pszDESHeader, strlen(pszDESHeader)) == strlen(pszDESHeader);
470
471
0
    const auto nOffsetTRESize = fp->Tell() + strlen("RPFDES");
472
    // xxxxx is a placeholder for the TRE size, patched later with the actual
473
    // size.
474
0
    constexpr const char *pszRPFDESTREStart = "RPFDESxxxxx";
475
0
    bOK &= fp->Write(pszRPFDESTREStart, 1, strlen(pszRPFDESTREStart)) ==
476
0
           strlen(pszRPFDESTREStart);
477
478
    // Associate an index to each subdir name used by frames
479
0
    std::map<std::string, int> oMapSubdirToIdx;
480
0
    size_t nCountFrames = 0;
481
482
    // From lowest to highest classification level
483
0
    constexpr const char achClassifications[] = {
484
0
        'U',  // Unclassified
485
0
        'R',  // Restricted
486
0
        'C',  // Confidential
487
0
        'S',  // Secret
488
0
        'T',  // Top Secret
489
0
    };
490
0
    std::map<char, unsigned> oMapClassificationToLevel;
491
0
    for (unsigned i = 0; i < CPL_ARRAYSIZE(achClassifications); ++i)
492
0
        oMapClassificationToLevel[achClassifications[i]] = i;
493
494
0
    unsigned nHighestClassification = 0;
495
496
0
    for ([[maybe_unused]] const auto &[unused, framesDesc] :
497
0
         oMapScaleZoneToFrames)
498
0
    {
499
0
        for (const auto &frameDesc : framesDesc)
500
0
        {
501
0
            const std::string osSubdir =
502
0
                CPLGetPathSafe(frameDesc.osRelativeFilename.c_str());
503
0
            if (!cpl::contains(oMapSubdirToIdx, osSubdir))
504
0
            {
505
0
                oMapSubdirToIdx[osSubdir] =
506
0
                    static_cast<int>(oMapSubdirToIdx.size());
507
0
            }
508
509
0
            const auto oClassificationIter =
510
0
                oMapClassificationToLevel.find(frameDesc.chClassification);
511
0
            if (oClassificationIter == oMapClassificationToLevel.end())
512
0
            {
513
0
                CPLError(CE_Warning, CPLE_AppDefined,
514
0
                         "Unknown classification level '%c' for %s",
515
0
                         frameDesc.chClassification,
516
0
                         frameDesc.osRelativeFilename.c_str());
517
0
            }
518
0
            else
519
0
            {
520
0
                nHighestClassification = std::max(nHighestClassification,
521
0
                                                  oClassificationIter->second);
522
0
            }
523
0
        }
524
0
        nCountFrames += framesDesc.size();
525
0
    }
526
0
    if (oMapSubdirToIdx.size() > std::numeric_limits<uint16_t>::max())
527
0
    {
528
0
        CPLError(CE_Failure, CPLE_AppDefined,
529
0
                 "Too many subdirectories: %u. Only up to %u are allowed",
530
0
                 static_cast<unsigned>(oMapSubdirToIdx.size()),
531
0
                 std::numeric_limits<uint16_t>::max());
532
0
        return false;
533
0
    }
534
535
    // Create RPF sections
536
0
    Create_RPFTOC_LocationComponent(offsetPatcher);
537
0
    Create_RPFTOC_BoundaryRectangleSectionSubheader(
538
0
        offsetPatcher, oMapScaleZoneToMinMaxFrameXY.size());
539
0
    Create_RPFTOC_BoundaryRectangleTable(offsetPatcher, osProducer,
540
0
                                         oMapScaleZoneToMinMaxFrameXY);
541
0
    const char chHighestClassification =
542
0
        achClassifications[nHighestClassification];
543
0
    Create_RPFTOC_FrameFileIndexSectionSubHeader(
544
0
        offsetPatcher, chHighestClassification, nCountFrames,
545
0
        static_cast<uint16_t>(oMapSubdirToIdx.size()));
546
0
    Create_RPFTOC_FrameFileIndexSubsection(
547
0
        offsetPatcher, osSecurityCountryCode, oMapScaleZoneToFrames,
548
0
        oMapScaleZoneToMinMaxFrameXY, oMapSubdirToIdx);
549
550
    // Write RPF sections
551
0
    size_t nTREDataSize = 0;
552
0
    for (const char *pszName :
553
0
         {"LocationComponent", "BoundaryRectangleSectionSubheader",
554
0
          "BoundaryRectangleTable", "FrameFileIndexSectionSubHeader",
555
0
          "FrameFileIndexSubsection"})
556
0
    {
557
0
        const auto poBuffer = offsetPatcher.GetBufferFromName(pszName);
558
0
        CPLAssert(poBuffer);
559
0
        poBuffer->DeclareBufferWrittenAtPosition(fp->Tell());
560
0
        bOK &= fp->Write(poBuffer->GetBuffer().data(),
561
0
                         poBuffer->GetBuffer().size()) ==
562
0
               poBuffer->GetBuffer().size();
563
0
        nTREDataSize += poBuffer->GetBuffer().size();
564
0
    }
565
566
    // Patch the size of the RPFDES TRE data
567
0
    if (nTREDataSize <= 99999)
568
0
    {
569
0
        bOK &= fp->Seek(nOffsetTRESize, SEEK_SET) == 0;
570
0
        const std::string osTRESize =
571
0
            CPLSPrintf("%05d", static_cast<int>(nTREDataSize));
572
0
        bOK &=
573
0
            fp->Write(osTRESize.c_str(), osTRESize.size()) == osTRESize.size();
574
0
    }
575
0
    else
576
0
    {
577
0
        CPLError(CE_Warning, CPLE_AppDefined,
578
0
                 "RPFDES TRE size exceeds 99999 bytes. Some readers might not "
579
0
                 "be able to read the A.TOC file correctly");
580
0
    }
581
582
    // Update LDSH and LD in the NITF Header
583
584
    // NUMI offset is at a fixed offset 360 (unless there is a FSDWNG field)
585
0
    constexpr vsi_l_offset nNumIOffset = 360;
586
0
    constexpr vsi_l_offset nNumGOffset = nNumIOffset + 3;
587
    // the last + 3 is for NUMX field, which is not used
588
0
    constexpr vsi_l_offset nNumTOffset = nNumGOffset + 3 + 3;
589
0
    constexpr vsi_l_offset nNumDESOffset = nNumTOffset + 3;
590
0
    constexpr auto nOffsetLDSH = nNumDESOffset + 3;
591
592
0
    constexpr int iDES = 0;
593
0
    bOK &= fp->Seek(nOffsetLDSH + iDES * 13, SEEK_SET) == 0;
594
0
    bOK &= fp->Write(CPLSPrintf("%04d", static_cast<int>(strlen(pszDESHeader))),
595
0
                     4) == 4;
596
0
    bOK &= fp->Write(
597
0
               CPLSPrintf("%09d", static_cast<int>(nTREDataSize +
598
0
                                                   strlen(pszRPFDESTREStart))),
599
0
               9) == 9;
600
601
    // Update total file length
602
0
    bOK &= fp->Seek(0, SEEK_END) == 0;
603
0
    const uint64_t nFileLen = fp->Tell();
604
0
    CPLString osFileLen = CPLString().Printf("%012" PRIu64, nFileLen);
605
0
    constexpr vsi_l_offset FILE_LENGTH_OFFSET = 342;
606
0
    bOK &= fp->Seek(FILE_LENGTH_OFFSET, SEEK_SET) == 0;
607
0
    bOK &= fp->Write(osFileLen.data(), osFileLen.size()) == osFileLen.size();
608
609
0
    return bOK;
610
0
}
611
612
/************************************************************************/
613
/*                        RPFTOCCollectFrames()                         */
614
/************************************************************************/
615
616
static bool RPFTOCCollectFrames(
617
    VSIDIR *psDir, const std::string &osInputDirectory,
618
    const int nReciprocalScale,
619
    std::map<ScaleZone, std::vector<FrameDesc>> &oMapScaleZoneToFrames,
620
    std::map<ScaleZone, MinMaxFrameXY> &oMapScaleZoneToMinMaxFrameXY)
621
0
{
622
623
0
    while (const VSIDIREntry *psEntry = VSIGetNextDirEntry(psDir))
624
0
    {
625
0
        if (VSI_ISDIR(psEntry->nMode) ||
626
0
            EQUAL(CPLGetFilename(psEntry->pszName), "A.TOC"))
627
0
            continue;
628
0
        const char *const apszAllowedDrivers[] = {"NITF", nullptr};
629
0
        const std::string osFullFilename = CPLFormFilenameSafe(
630
0
            osInputDirectory.c_str(), psEntry->pszName, nullptr);
631
0
        auto poDS = std::unique_ptr<GDALDataset>(GDALDataset::Open(
632
0
            osFullFilename.c_str(), GDAL_OF_RASTER, apszAllowedDrivers));
633
0
        if (!poDS)
634
0
            continue;
635
636
0
        const std::string osFilenamePart =
637
0
            CPLGetFilename(osFullFilename.c_str());
638
0
        if (osFilenamePart.size() != 12)
639
0
        {
640
0
            CPLDebug("RPFTOC", "%s filename is not 12 character long",
641
0
                     osFullFilename.c_str());
642
0
            continue;
643
0
        }
644
645
0
        if (poDS->GetRasterXSize() != CADRG_FRAME_PIXEL_COUNT ||
646
0
            poDS->GetRasterYSize() != CADRG_FRAME_PIXEL_COUNT)
647
0
        {
648
0
            CPLDebug("RPFTOC", "%s has not the dimensions of a CADRG frame",
649
0
                     osFullFilename.c_str());
650
0
            continue;
651
0
        }
652
653
0
        const std::string osDataSeriesCode(osFilenamePart.substr(9, 2));
654
0
        if (!RPFCADRGIsKnownDataSeriesCode(osDataSeriesCode.c_str()))
655
0
        {
656
0
            CPLError(CE_Warning, CPLE_AppDefined,
657
0
                     "Data series code '%s' in %s extension is a unknown CADRG "
658
0
                     "series code",
659
0
                     osDataSeriesCode.c_str(), osFullFilename.c_str());
660
0
        }
661
662
0
        int nThisScale = nReciprocalScale;
663
0
        if (nThisScale == 0)
664
0
        {
665
0
            nThisScale =
666
0
                RPFCADRGGetScaleFromDataSeriesCode(osDataSeriesCode.c_str());
667
0
            if (nThisScale == 0)
668
0
            {
669
0
                CPLError(CE_Failure, CPLE_AppDefined,
670
0
                         "Scale cannot be inferred from filename %s. Specify "
671
0
                         "the 'scale' argument",
672
0
                         osFullFilename.c_str());
673
0
                return false;
674
0
            }
675
0
        }
676
677
0
        const int nZone = RPFCADRGZoneCharToNum(osFilenamePart.back());
678
0
        if (nZone == 0)
679
0
        {
680
0
            CPLError(CE_Failure, CPLE_AppDefined,
681
0
                     "CADRG zone cannot be inferred from last character of "
682
0
                     "filename %s.",
683
0
                     osFullFilename.c_str());
684
0
            return false;
685
0
        }
686
687
0
        OGREnvelope sExtentWGS84;
688
0
        if (poDS->GetExtentWGS84LongLat(&sExtentWGS84) != CE_None)
689
0
        {
690
0
            CPLError(CE_Failure, CPLE_AppDefined,
691
0
                     "Cannot get dataset extent for %s",
692
0
                     osFullFilename.c_str());
693
0
            return false;
694
0
        }
695
696
0
        const auto frameDefinitions =
697
0
            RPFGetCADRGFramesForEnvelope(nZone, nThisScale, poDS.get());
698
0
        if (frameDefinitions.empty())
699
0
        {
700
0
            CPLError(CE_Failure, CPLE_AppDefined,
701
0
                     "Cannot establish CADRG frames intersecting dataset "
702
0
                     "extent for %s",
703
0
                     osFullFilename.c_str());
704
0
            return false;
705
0
        }
706
0
        if (frameDefinitions.size() != 1)
707
0
        {
708
0
            CPLError(CE_Failure, CPLE_AppDefined,
709
0
                     "Extent of file %s does not match a single CADRG frame",
710
0
                     osFullFilename.c_str());
711
0
            return false;
712
0
        }
713
714
0
        const std::string osExpectedFilenameStart =
715
0
            RPFGetCADRGFrameNumberAsString(nZone, nThisScale,
716
0
                                           frameDefinitions[0].nFrameMinX,
717
0
                                           frameDefinitions[0].nFrameMinY);
718
0
        if (!cpl::starts_with(CPLString(osFilenamePart).toupper(),
719
0
                              osExpectedFilenameStart))
720
0
        {
721
0
            CPLError(CE_Warning, CPLE_AppDefined,
722
0
                     "Filename part of %s should begin with %s",
723
0
                     osFullFilename.c_str(), osExpectedFilenameStart.c_str());
724
0
        }
725
726
        // Store needed metadata on the frame
727
0
        FrameDesc desc;
728
0
        desc.nZone = nZone;
729
0
        desc.nReciprocalScale = nThisScale;
730
0
        desc.nFrameX = frameDefinitions[0].nFrameMinX;
731
0
        desc.nFrameY = frameDefinitions[0].nFrameMinY;
732
0
        desc.dfMinX = sExtentWGS84.MinX;
733
0
        desc.dfMinY = sExtentWGS84.MinY;
734
0
        desc.osRelativeFilename = psEntry->pszName;
735
0
        const char *pszClassification = poDS->GetMetadataItem("FCLASS");
736
0
        if (pszClassification)
737
0
            desc.chClassification = pszClassification[0];
738
739
        // Update min and max frame indices for this (scale, zone) pair
740
0
        auto &sMinMaxFrameXY =
741
0
            oMapScaleZoneToMinMaxFrameXY[{nThisScale, nZone}];
742
0
        sMinMaxFrameXY.MinX = std::min(sMinMaxFrameXY.MinX, desc.nFrameX);
743
0
        sMinMaxFrameXY.MinY = std::min(sMinMaxFrameXY.MinY, desc.nFrameY);
744
0
        sMinMaxFrameXY.MaxX = std::max(sMinMaxFrameXY.MaxX, desc.nFrameX);
745
0
        sMinMaxFrameXY.MaxY = std::max(sMinMaxFrameXY.MaxY, desc.nFrameY);
746
747
0
        oMapScaleZoneToFrames[{nThisScale, nZone}].push_back(std::move(desc));
748
0
    }
749
750
    // For each (scale, zone) pair, sort by increasing y and then x
751
    // to have a reproducible output
752
0
    for ([[maybe_unused]] auto &[unused, frameDescs] : oMapScaleZoneToFrames)
753
0
    {
754
0
        std::sort(frameDescs.begin(), frameDescs.end(),
755
0
                  [](const FrameDesc &a, const FrameDesc &b)
756
0
                  {
757
0
                      return a.nFrameY < b.nFrameY ||
758
0
                             (a.nFrameY == b.nFrameY && a.nFrameX < b.nFrameX);
759
0
                  });
760
0
    }
761
762
0
    return true;
763
0
}
764
765
/************************************************************************/
766
/*                            RPFTOCCreate()                            */
767
/************************************************************************/
768
769
bool RPFTOCCreate(const std::string &osInputDirectory,
770
                  const std::string &osOutputFilename,
771
                  const char chIndexClassification, const int nReciprocalScale,
772
                  const std::string &osProducerID,
773
                  const std::string &osProducerName,
774
                  const std::string &osSecurityCountryCode,
775
                  bool bDoNotCreateIfNoFrame)
776
0
{
777
0
    std::unique_ptr<VSIDIR, decltype(&VSICloseDir)> psDir(
778
0
        VSIOpenDir(osInputDirectory.c_str(), -1 /* unlimited recursion */,
779
0
                   nullptr),
780
0
        VSICloseDir);
781
0
    if (!psDir)
782
0
    {
783
0
        CPLError(CE_Failure, CPLE_AppDefined,
784
0
                 "%s is not a directory or cannot be opened",
785
0
                 osInputDirectory.c_str());
786
0
        return false;
787
0
    }
788
789
0
    std::map<ScaleZone, std::vector<FrameDesc>> oMapScaleZoneToFrames;
790
0
    std::map<ScaleZone, MinMaxFrameXY> oMapScaleZoneToMinMaxFrameXY;
791
0
    if (!RPFTOCCollectFrames(psDir.get(), osInputDirectory, nReciprocalScale,
792
0
                             oMapScaleZoneToFrames,
793
0
                             oMapScaleZoneToMinMaxFrameXY))
794
0
    {
795
0
        return false;
796
0
    }
797
798
0
    if (oMapScaleZoneToFrames.empty())
799
0
    {
800
0
        if (bDoNotCreateIfNoFrame)
801
0
        {
802
0
            return true;
803
0
        }
804
0
        else
805
0
        {
806
0
            CPLError(CE_Failure, CPLE_AppDefined, "No CADRG frame found in %s",
807
0
                     osInputDirectory.c_str());
808
0
            return false;
809
0
        }
810
0
    }
811
812
0
    GDALOffsetPatcher::OffsetPatcher offsetPatcher;
813
814
0
    CPLStringList aosOptions;
815
0
    aosOptions.SetNameValue("FHDR", "NITF02.00");
816
0
    aosOptions.SetNameValue("NUMI", "0");
817
0
    aosOptions.SetNameValue("NUMDES", "1");
818
0
    constexpr const char pszLeftPaddedATOC[] = "       A.TOC";
819
0
    static_assert(sizeof(pszLeftPaddedATOC) == 12 + 1);
820
0
    aosOptions.SetNameValue("FCLASS", CPLSPrintf("%c", chIndexClassification));
821
0
    aosOptions.SetNameValue("FDT", "11111111ZJAN26");
822
0
    aosOptions.SetNameValue("FTITLE", pszLeftPaddedATOC);
823
0
    if (!osProducerID.empty())
824
0
        aosOptions.SetNameValue("OSTAID", osProducerID.c_str());
825
0
    if (!osProducerName.empty())
826
0
        aosOptions.SetNameValue("ONAME", osProducerName.c_str());
827
0
    Create_CADRG_RPFHDR(&offsetPatcher, pszLeftPaddedATOC, aosOptions);
828
0
    if (!NITFCreateEx(osOutputFilename.c_str(), /* nPixels = */ 0,
829
0
                      /* nLines = */ 0, /* nBands = */ 0,
830
0
                      /* nBitsPerSample = */ 0, /* PVType = */ nullptr,
831
0
                      aosOptions.List(), /* pnIndex = */ nullptr,
832
0
                      /* pnImageCount = */ nullptr,
833
0
                      /* pnImageOffset = */ nullptr, /* pnICOffset = */ nullptr,
834
0
                      &offsetPatcher))
835
0
    {
836
0
        return false;
837
0
    }
838
839
0
    auto fp = VSIFilesystemHandler::OpenStatic(osOutputFilename.c_str(), "rb+");
840
0
    return fp != nullptr &&
841
0
           RPCTOCCreateRPFDES(fp.get(), offsetPatcher, osProducerID,
842
0
                              osSecurityCountryCode, oMapScaleZoneToFrames,
843
0
                              oMapScaleZoneToMinMaxFrameXY) &&
844
0
           offsetPatcher.Finalize(fp.get()) && fp->Close() == 0;
845
0
}