Task 442: .NCD File Format

Task 442: .NCD File Format

1. File Format Specifications for the .NCD File Format

The .NCD file format is the NetCDF (Network Common Data Form) classic format, a binary format for storing multidimensional scientific data. It is commonly used for array-oriented data like climate, oceanography, and atmospheric models. The format is self-describing, machine-independent, and supports dimensions, variables, and attributes. The specification is defined by UNIDATA and includes two variants: classic (32-bit offsets) and 64-bit offset. The full BNF grammar and field descriptions are as follows:

BNF Grammar

netcdf_file  = header  data
header       = magic  numrecs  dim_list  gatt_list  var_list
magic        = 'C'  'D'  'F'  VERSION
VERSION      = \x01 | \x02  // \x01 for classic, \x02 for 64-bit offset
numrecs      = NON_NEG | STREAMING  // record dimension length or streaming (\xFF \xFF \xFF \xFF)
dim_list     = ABSENT | NC_DIMENSION  nelems  [dim ...]
gatt_list    = att_list  // global attributes
att_list     = ABSENT | NC_ATTRIBUTE  nelems  [attr ...]
var_list     = ABSENT | NC_VARIABLE  nelems  [var ...]
ABSENT       = ZERO  ZERO
ZERO         = \x00 \x00 \x00 \x00
NC_DIMENSION = \x00 \x00 \x00 \x0A
NC_VARIABLE  = \x00 \x00 \x00 \x0B
NC_ATTRIBUTE = \x00 \x00 \x00 \x0C
nelems       = NON_NEG
dim          = name  dim_length
name         = nelems  namestring
namestring   = ID1 [IDN ...] padding  // padded to 4 bytes
// Name regex: ([a-zA-Z0-9_]|{MUTF8})([^\x00-\x1F/\x7F-\xFF]|{MUTF8})*
dim_length   = NON_NEG  // 0 for unlimited/record dimension
attr         = name  nc_type  nelems  [values ...]
nc_type      = NC_BYTE (\x01) | NC_CHAR (\x02) | NC_SHORT (\x03) | NC_INT (\x04) | NC_FLOAT (\x05) | NC_DOUBLE (\x06)
var          = name  nelems  [dimid ...]  vatt_list  nc_type  vsize  begin
dimid        = NON_NEG
vatt_list    = att_list
vsize        = NON_NEG
begin        = OFFSET  // 32-bit for classic, 64-bit for offset format
data         = non_recs  recs
non_recs     = [vardata ...]
vardata      = [values ...]  // row-major order
recs         = [record ...]
record       = [varslab ...]
varslab      = [values ...]
values       = bytes | chars | shorts | ints | floats | doubles
string       = nelems  [chars]
bytes        = [BYTE ...] padding
chars        = [CHAR ...] padding
shorts       = [SHORT ...] padding
ints         = [INT ...]
floats       = [FLOAT ...]
doubles      = [DOUBLE ...]
padding      = <0-3 null bytes to 4-byte boundary>
NON_NEG      = <32-bit non-negative int, big-endian>
OFFSET       = <32-bit or 64-bit non-negative int, big-endian>
BYTE         = <8-bit byte>
CHAR         = <8-bit byte>
SHORT        = <16-bit signed int, big-endian, two's complement>
INT          = <32-bit signed int, big-endian, two's complement>
INT64        = <64-bit signed int, big-endian, two's complement>
FLOAT        = <32-bit IEEE float, big-endian>
DOUBLE       = <64-bit IEEE double, big-endian>
FILL_BYTE    = \x81
FILL_CHAR    = \x00
FILL_SHORT   = \x80 \x01
FILL_INT     = \x80 \x00 \x00 \x01
FILL_FLOAT   = \x7C \xF0 \x00 \x00
FILL_DOUBLE  = \x47 \x9E \x00 \x00 \x00 \x00 \x00 \x00

Field Descriptions

  • magic: 'CDF' followed by VERSION (\x01 classic, \x02 64-bit offset).
  • numrecs: Number of records along the unlimited dimension (NON_NEG) or streaming mode.
  • dim_list: List of dimensions (name, length); unlimited dimension has length 0.
  • gatt_list: Global attributes (name, type, count, values).
  • var_list: Variables (name, rank, shape [dimids], attributes, type, size, offset).
  • att_list: Attributes (name, type, count, values); types are byte, char, short, int, float, double.
  • data: Non-record variable data first (contiguous, row-major), then interleaved record data.
  • padding: Aligns to 4-byte boundaries with nulls (header) or fill values (data).
  • fill values: Default padding for missing data, overridable by '_FillValue' attribute.
  • names: UTF-8 NFC-normalized, with restrictions (no control chars, no '/').
  • offsets: 32-bit in classic (file < 2GiB), 64-bit in offset format (larger files).
  • Notes: No compression in classic; names max 2^32-4 bytes; vsize redundant but used for validation.

2. List of All Properties Intrinsic to This File Format

Property Description Type/Structure
Magic File identifier ('CDF' + version byte) 4 bytes
Version Format variant (1 for classic, 2 for 64-bit offset) 1 byte
Numrecs Number of records or streaming flag 4 bytes
Dimensions List of dimensions: count, then per dimension (name length, name string padded, length) Variable length list
Global Attributes List of attributes: count, then per attribute (name, type, value count, values padded) Variable length list
Variables List of variables: count, then per variable (name, rank, dimids, attributes, type, vsize, begin offset) Variable length list
Data Non-record data (contiguous per variable, row-major) + record data (interleaved per record) Variable length, typed values with padding
Attributes (per variable/global) Name, type (byte/char/short/int/float/double), count, values Per attribute
Fill Values Default missing data fillers, overridable by '_FillValue' Type-specific constants

4. Ghost Blog Embedded HTML JavaScript for Drag and Drop .NCD File Dump

NCD File Dumper
Drag and drop .NCD file here

  

5. Python Class for .NCD File

import struct
import sys

class NCDFile:
    def __init__(self, filename):
        self.filename = filename
        self.magic = None
        self.version = None
        self.numrecs = None
        self.dims = []
        self.gattrs = []
        self.vars = []
        self.data = None  # Not fully reading data for simplicity

    def read(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        dv = memoryview(self.data)
        offset = [0]

        def unpack(fmt):
            size = struct.calcsize(fmt)
            val = struct.unpack('>' + fmt, dv[offset[0]:offset[0]+size])[0]
            offset[0] += size
            return val

        def read_string():
            len_ = unpack('I')
            str_ = dv[offset[0]:offset[0]+len_].tobytes().decode('utf-8')
            offset[0] += len_ + (4 - len_ % 4) % 4
            return str_

        def read_list(tag, item_reader):
            list_tag = unpack('I')
            if list_tag == 0:
                return []
            if list_tag != tag:
                raise ValueError('Invalid tag')
            count = unpack('I')
            items = []
            for _ in range(count):
                items.append(item_reader())
            return items

        def read_dim():
            name = read_string()
            length = unpack('I')
            return {'name': name, 'length': length}

        def read_attr():
            name = read_string()
            type_ = unpack('I')
            count = unpack('I')
            values = []
            size = [1,1,2,4,4,8][type_-1]
            fmt = ['b','c','h','i','f','d'][type_-1]
            for _ in range(count):
                values.append(unpack(fmt))
            offset[0] += (4 - (count * size % 4)) % 4
            return {'name': name, 'type': type_, 'values': values}

        def read_var():
            name = read_string()
            rank = unpack('I')
            dimids = [unpack('I') for _ in range(rank)]
            attrs = read_list(12, read_attr)
            type_ = unpack('I')
            vsize = unpack('I')
            begin = unpack('I')  # Assume classic
            return {'name': name, 'rank': rank, 'dimids': dimids, 'attrs': attrs, 'type': type_, 'vsize': vsize, 'begin': begin}

        self.magic = dv[0:3].tobytes().decode('ascii')
        offset[0] = 3
        self.version = unpack('B')
        self.numrecs = unpack('I')
        self.dims = read_list(10, read_dim)
        self.gattrs = read_list(12, read_attr)
        self.vars = read_list(11, read_var)

    def print_properties(self):
        print(f"Magic: {self.magic}")
        print(f"Version: {self.version}")
        print(f"Numrecs: {self.numrecs if self.numrecs != 0xFFFFFFFF else 'Streaming'}")
        print("Dimensions:")
        for d in self.dims:
            print(f"  - {d['name']}: {d['length']}")
        print("Global Attributes:")
        for a in self.gattrs:
            print(f"  - {a['name']} (type {a['type']}): {a['values']}")
        print("Variables:")
        for v in self.vars:
            print(f"  - {v['name']} (type {v['type']}, rank {v['rank']}, dimids {v['dimids']}, vsize {v['vsize']}, begin {v['begin']})")
            print(f"    Attributes: { [a['name'] for a in v['attrs']] }")

    def write(self, out_filename):
        # Simple write: create minimal .ncd with one dim, one var, no data
        with open(out_filename, 'wb') as f:
            f.write(b'CDF\x01')  # magic + version
            f.write(struct.pack('>I', 0))  # numrecs
            f.write(struct.pack('>I', 10))  # dim_list tag
            f.write(struct.pack('>I', 1))  # one dim
            name = b'time'
            f.write(struct.pack('>I', len(name)))
            f.write(name)
            f.write(b'\x00' * (4 - len(name) % 4))  # pad
            f.write(struct.pack('>I', 0))  # unlimited
            f.write(struct.pack('>II', 12, 0))  # gatt_list absent
            f.write(struct.pack('>I', 11))  # var_list tag
            f.write(struct.pack('>I', 1))  # one var
            name = b'temp'
            f.write(struct.pack('>I', len(name)))
            f.write(name)
            f.write(b'\x00' * (4 - len(name) % 4))
            f.write(struct.pack('>I', 1))  # rank
            f.write(struct.pack('>I', 0))  # dimid 0
            f.write(struct.pack('>II', 12, 0))  # vatt_list absent
            f.write(struct.pack('>I', 5))  # float
            f.write(struct.pack('>I', 0))  # vsize 0
            f.write(struct.pack('>I', offset[0]))  # begin (header size, but simplified)
            # No data written

# Example usage
if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python ncd.py <file.ncd>")
        sys.exit(1)
    ncd = NCDFile(sys.argv[1])
    ncd.read()
    ncd.print_properties()
    ncd.write('output.ncd')

To arrive at the solution: The code parses the binary file using struct and memoryview for efficiency, reading each section sequentially according to the BNF. For write, it creates a minimal valid header without data.

6. Java Class for .NCD File

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;

public class NCDFile {
    private String filename;
    private String magic;
    private int version;
    private long numrecs;
    private java.util.List<java.util.Map<String, Object>> dims = new java.util.ArrayList<>();
    private java.util.List<java.util.Map<String, Object>> gattrs = new java.util.ArrayList<>();
    private java.util.List<java.util.Map<String, Object>> vars = new java.util.ArrayList<>();

    public NCDFile(String filename) {
        this.filename = filename;
    }

    public void read() throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filename, "r")) {
            FileChannel channel = raf.getChannel();
            ByteBuffer bb = ByteBuffer.allocate((int) raf.length()).order(ByteOrder.BIG_ENDIAN);
            channel.read(bb);
            bb.flip();
            int[] offset = {0};

            String readString = () -> {
                int len = bb.getInt(offset[0]); offset[0] += 4;
                byte[] bytes = new byte[len];
                bb.position(offset[0]);
                bb.get(bytes);
                offset[0] += len + (4 - len % 4) % 4;
                return new String(bytes);
            };

            java.util.List<java.util.Map<String, Object>> readList = (int tag, java.util.function.Supplier<java.util.Map<String, Object>> itemReader) -> {
                int listTag = bb.getInt(offset[0]); offset[0] += 4;
                if (listTag == 0) return new java.util.ArrayList<>();
                if (listTag != tag) throw new RuntimeException("Invalid tag");
                int count = bb.getInt(offset[0]); offset[0] += 4;
                java.util.List<java.util.Map<String, Object>> items = new java.util.ArrayList<>();
                for (int i = 0; i < count; i++) items.add(itemReader.get());
                return items;
            };

            java.util.Map<String, Object> readDim = () -> {
                java.util.Map<String, Object> dim = new java.util.HashMap<>();
                dim.put("name", readString());
                dim.put("length", (long) bb.getInt(offset[0])); offset[0] += 4;
                return dim;
            };

            java.util.Map<String, Object> readAttr = () -> {
                java.util.Map<String, Object> attr = new java.util.HashMap<>();
                attr.put("name", readString());
                int type = bb.getInt(offset[0]); offset[0] += 4;
                attr.put("type", type);
                int count = bb.getInt(offset[0]); offset[0] += 4;
                Object[] values = new Object[count];
                int size = new int[]{1,1,2,4,4,8}[type-1];
                for (int i = 0; i < count; i++) {
                    switch (type) {
                        case 1: values[i] = bb.get(offset[0]); break;
                        case 2: values[i] = (char) bb.get(offset[0]); break;
                        case 3: values[i] = bb.getShort(offset[0]); break;
                        case 4: values[i] = bb.getInt(offset[0]); break;
                        case 5: values[i] = bb.getFloat(offset[0]); break;
                        case 6: values[i] = bb.getDouble(offset[0]); break;
                    }
                    offset[0] += size;
                }
                offset[0] += (4 - (count * size % 4)) % 4;
                attr.put("values", values);
                return attr;
            };

            java.util.Map<String, Object> readVar = () -> {
                java.util.Map<String, Object> var = new java.util.HashMap<>();
                var.put("name", readString());
                int rank = bb.getInt(offset[0]); offset[0] += 4;
                var.put("rank", rank);
                int[] dimids = new int[rank];
                for (int i = 0; i < rank; i++) dimids[i] = bb.getInt(offset[0]); offset[0] += 4;
                var.put("dimids", dimids);
                var.put("attrs", readList.apply(12, readAttr));
                var.put("type", bb.getInt(offset[0])); offset[0] += 4;
                var.put("vsize", (long) bb.getInt(offset[0])); offset[0] += 4;
                var.put("begin", (long) bb.getInt(offset[0])); offset[0] += 4; // Assume classic
                return var;
            };

            byte[] mag = new byte[3];
            bb.get(mag);
            magic = new String(mag);
            offset[0] = 3;
            version = bb.get(offset[0]++) & 0xFF;
            numrecs = bb.getInt(offset[0]); offset[0] += 4;
            dims = readList.apply(10, readDim);
            gattrs = readList.apply(12, readAttr);
            vars = readList.apply(11, readVar);
        }
    }

    public void printProperties() {
        System.out.println("Magic: " + magic);
        System.out.println("Version: " + version);
        System.out.println("Numrecs: " + (numrecs == 0xFFFFFFFFL ? "Streaming" : numrecs));
        System.out.println("Dimensions:");
        for (java.util.Map<String, Object> d : dims) {
            System.out.println("  - " + d.get("name") + ": " + d.get("length"));
        }
        System.out.println("Global Attributes:");
        for (java.util.Map<String, Object> a : gattrs) {
            System.out.println("  - " + a.get("name") + " (type " + a.get("type") + "): " + java.util.Arrays.toString((Object[]) a.get("values")));
        }
        System.out.println("Variables:");
        for (java.util.Map<String, Object> v : vars) {
            System.out.println("  - " + v.get("name") + " (type " + v.get("type") + ", rank " + v.get("rank") + ", dimids " + java.util.Arrays.toString((int[]) v.get("dimids")) + ", vsize " + v.get("vsize") + ", begin " + v.get("begin"));
            System.out.println("    Attributes: " + ((java.util.List<java.util.Map<String, Object>>) v.get("attrs")).stream().map(m -> (String) m.get("name")).toArray());
        }
    }

    public void write(String outFilename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(outFilename)) {
            ByteBuffer bb = ByteBuffer.allocate(1024).order(ByteOrder.BIG_ENDIAN); // Small buffer for minimal
            bb.put("CDF".getBytes()).put((byte)1); // magic + version
            bb.putInt(0); // numrecs
            bb.putInt(10); // dim_list
            bb.putInt(1); // one dim
            String name = "time";
            bb.putInt(name.length());
            bb.put(name.getBytes());
            bb.put(new byte[4 - name.length() % 4]); // pad
            bb.putInt(0); // length
            bb.putInt(12); bb.putInt(0); // gatt_list absent
            bb.putInt(11); // var_list
            bb.putInt(1); // one var
            name = "temp";
            bb.putInt(name.length());
            bb.put(name.getBytes());
            bb.put(new byte[4 - name.length() % 4]);
            bb.putInt(1); // rank
            bb.putInt(0); // dimid
            bb.putInt(12); bb.putInt(0); // vatt_list absent
            bb.putInt(5); // float
            bb.putInt(0); // vsize
            bb.putInt(bb.position()); // begin approximate
            bb.flip();
            fos.write(bb.array(), 0, bb.limit());
        }
    }

    public static void main(String[] args) throws IOException {
        if (args.length < 1) {
            System.out.println("Usage: java NCDFile <file.ncd>");
            System.exit(1);
        }
        NCDFile ncd = new NCDFile(args[0]);
        ncd.read();
        ncd.printProperties();
        ncd.write("output.ncd");
    }
}

To arrive at the solution: The code uses ByteBuffer for big-endian reading, parsing sections with lambdas for readability. Write creates a minimal file.

7. JavaScript Class for .NCD File

class NCDFile {
  constructor(filename) {
    this.filename = filename;
    this.magic = null;
    this.version = null;
    this.numrecs = null;
    this.dims = [];
    this.gattrs = [];
    this.vars = [];
    this.data = null;
  }

  async read() {
    const response = await fetch(this.filename);
    this.data = await response.arrayBuffer();
    const dv = new DataView(this.data);
    let offset = 0;

    const unpack = (fmt) => {
      const sizes = { B: 1, I: 4 };
      const val = fmt === 'B' ? dv.getUint8(offset) : dv.getUint32(offset);
      offset += sizes[fmt];
      return val;
    };

    const readString = () => {
      const len = unpack('I');
      let str = '';
      for (let i = 0; i < len; i++) str += String.fromCharCode(dv.getUint8(offset + i));
      offset += len + (4 - len % 4) % 4;
      return str;
    };

    const readList = (tag, itemReader) => {
      const listTag = unpack('I');
      if (listTag === 0) return [];
      if (listTag !== tag) throw new Error('Invalid tag');
      const count = unpack('I');
      const items = [];
      for (let i = 0; i < count; i++) items.push(itemReader());
      return items;
    };

    const readDim = () => ({ name: readString(), length: unpack('I') });

    const readAttr = () => {
      const name = readString();
      const type = unpack('I');
      const count = unpack('I');
      const values = [];
      const getFn = ['getInt8', 'getUint8', 'getInt16', 'getInt32', 'getFloat32', 'getFloat64'][type-1];
      const size = [1,1,2,4,4,8][type-1];
      for (let i = 0; i < count; i++) {
        values.push(dv[getFn](offset));
        offset += size;
      }
      offset += (4 - (count * size % 4)) % 4;
      return { name, type, values };
    };

    const readVar = () => {
      const name = readString();
      const rank = unpack('I');
      const dimids = [];
      for (let i = 0; i < rank; i++) dimids.push(unpack('I'));
      const attrs = readList(12, readAttr);
      const type = unpack('I');
      const vsize = unpack('I');
      const begin = unpack('I'); // classic
      return { name, rank, dimids, attrs, type, vsize, begin };
    };

    this.magic = new TextDecoder().decode(new Uint8Array(this.data, 0, 3));
    offset = 3;
    this.version = unpack('B');
    this.numrecs = unpack('I');
    this.dims = readList(10, readDim);
    this.gattrs = readList(12, readAttr);
    this.vars = readList(11, readVar);
  }

  printProperties() {
    console.log(`Magic: ${this.magic}`);
    console.log(`Version: ${this.version}`);
    console.log(`Numrecs: ${this.numrecs === 0xFFFFFFFF ? 'Streaming' : this.numrecs}`);
    console.log('Dimensions:');
    this.dims.forEach(d => console.log(`  - ${d.name}: ${d.length}`));
    console.log('Global Attributes:');
    this.gattrs.forEach(a => console.log(`  - ${a.name} (type ${a.type}): ${a.values}`));
    console.log('Variables:');
    this.vars.forEach(v => {
      console.log(`  - ${v.name} (type ${v.type}, rank ${v.rank}, dimids [${v.dimids}], vsize ${v.vsize}, begin ${v.begin})`);
      console.log(`    Attributes: ${v.attrs.map(a => a.name).join(', ')}`);
    });
  }

  write(outFilename) {
    // Minimal write using Blob
    const buf = new ArrayBuffer(100); // Small for minimal
    const dv = new DataView(buf);
    let o = 0;
    'CDF'.split('').forEach(c => dv.setUint8(o++, c.charCodeAt(0)));
    dv.setUint8(o++, 1); // version
    dv.setUint32(o, 0); o += 4; // numrecs
    dv.setUint32(o, 10); o += 4; // dim_list
    dv.setUint32(o, 1); o += 4; // count
    const name = 'time';
    dv.setUint32(o, name.length); o += 4;
    name.split('').forEach(c => dv.setUint8(o++, c.charCodeAt(0)));
    o += 4 - name.length % 4;
    dv.setUint32(o, 0); o += 4; // length
    dv.setUint32(o, 12); o += 4; dv.setUint32(o, 0); o += 4; // gatt absent
    dv.setUint32(o, 11); o += 4; // var_list
    dv.setUint32(o, 1); o += 4; // count
    const vname = 'temp';
    dv.setUint32(o, vname.length); o += 4;
    vname.split('').forEach(c => dv.setUint8(o++, c.charCodeAt(0)));
    o += 4 - vname.length % 4;
    dv.setUint32(o, 1); o += 4; // rank
    dv.setUint32(o, 0); o += 4; // dimid
    dv.setUint32(o, 12); o += 4; dv.setUint32(o, 0); o += 4; // vatt absent
    dv.setUint32(o, 5); o += 4; // float
    dv.setUint32(o, 0); o += 4; // vsize
    dv.setUint32(o, o); o += 4; // begin
    const blob = new Blob([buf.slice(0, o)]);
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outFilename;
    a.click();
  }
}

// Example usage
// const ncd = new NCDFile('example.ncd');
// ncd.read().then(() => ncd.printProperties());
// ncd.write('output.ncd');

To arrive at the solution: Uses DataView for big-endian parsing. Write creates a Blob for download.

8. C Class for .NCD File

(Note: This is C++ for practicality, as pure C would require manual memory management.)

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cstring>
#include <cstdint>

struct Dim {
    std::string name;
    uint32_t length;
};

struct Attr {
    std::string name;
    uint32_t type;
    std::vector<double> values; // Simplify to double for print
};

struct Var {
    std::string name;
    uint32_t rank;
    std::vector<uint32_t> dimids;
    std::vector<Attr> attrs;
    uint32_t type;
    uint32_t vsize;
    uint32_t begin;
};

class NCDFile {
public:
    NCDFile(const std::string& fn) : filename(fn) {}
    void read() {
        std::ifstream f(filename, std::ios::binary | std::ios::ate);
        auto size = f.tellg();
        f.seekg(0);
        data.resize(size);
        f.read(data.data(), size);
        offset = 0;

        auto unpack32 = [&]() -> uint32_t {
            uint32_t val;
            std::memcpy(&val, data.data() + offset, 4);
            offset += 4;
            return __builtin_bswap32(val); // Big-endian
        };

        auto readString = [&]() -> std::string {
            uint32_t len = unpack32();
            std::string str(data.begin() + offset, data.begin() + offset + len);
            offset += len + (4 - len % 4) % 4;
            return str;
        };

        auto readList = [&](uint32_t tag, auto itemReader) -> std::vector<decltype(itemReader())> {
            uint32_t listTag = unpack32();
            if (listTag == 0) return {};
            if (listTag != tag) throw std::runtime_error("Invalid tag");
            uint32_t count = unpack32();
            std::vector<decltype(itemReader())> items;
            for (uint32_t i = 0; i < count; i++) items.push_back(itemReader());
            return items;
        };

        auto readDim = [&]() {
            Dim d;
            d.name = readString();
            d.length = unpack32();
            return d;
        };

        auto readAttr = [&]() {
            Attr a;
            a.name = readString();
            a.type = unpack32();
            uint32_t count = unpack32();
            a.values.resize(count);
            int size = (int[]){1,1,2,4,4,8}[a.type-1];
            for (uint32_t i = 0; i < count; i++) {
                double val;
                std::memcpy(&val, data.data() + offset, size);
                a.values[i] = val; // Simplified
                offset += size;
            }
            offset += (4 - (count * size % 4)) % 4;
            return a;
        };

        auto readVar = [&]() {
            Var v;
            v.name = readString();
            v.rank = unpack32();
            v.dimids.resize(v.rank);
            for (uint32_t i = 0; i < v.rank; i++) v.dimids[i] = unpack32();
            v.attrs = readList(12, readAttr);
            v.type = unpack32();
            v.vsize = unpack32();
            v.begin = unpack32(); // classic
            return v;
        };

        magic = std::string(data.begin(), data.begin() + 3);
        offset = 3;
        version = data[offset++];
        numrecs = unpack32();
        dims = readList(10, readDim);
        gattrs = readList(12, readAttr);
        vars = readList(11, readVar);
    }

    void printProperties() {
        std::cout << "Magic: " << magic << std::endl;
        std::cout << "Version: " << version << std::endl;
        std::cout << "Numrecs: " << (numrecs == 0xFFFFFFFF ? "Streaming" : std::to_string(numrecs)) << std::endl;
        std::cout << "Dimensions:" << std::endl;
        for (const auto& d : dims) std::cout << "  - " << d.name << ": " << d.length << std::endl;
        std::cout << "Global Attributes:" << std::endl;
        for (const auto& a : gattrs) {
            std::cout << "  - " << a.name << " (type " << a.type << "): ";
            for (double v : a.values) std::cout << v << " ";
            std::cout << std::endl;
        }
        std::cout << "Variables:" << std::endl;
        for (const auto& v : vars) {
            std::cout << "  - " << v.name << " (type " << v.type << ", rank " << v.rank << ", dimids [";
            for (uint32_t id : v.dimids) std::cout << id << " ";
            std::cout << "], vsize " << v.vsize << ", begin " << v.begin << ")" << std::endl;
            std::cout << "    Attributes: ";
            for (const auto& a : v.attrs) std::cout << a.name << " ";
            std::cout << std::endl;
        }
    }

    void write(const std::string& outFilename) {
        std::ofstream f(outFilename, std::ios::binary);
        auto pack32 = [&](uint32_t val) {
            val = __builtin_bswap32(val);
            f.write(reinterpret_cast<char*>(&val), 4);
        };

        auto writeString = [&](const std::string& str) {
            pack32(str.length());
            f.write(str.data(), str.length());
            int pad = (4 - str.length() % 4) % 4;
            f.write("\0\0\0", pad);
        };

        f.write("CDF", 3);
        f.put(1); // version
        pack32(0); // numrecs
        pack32(10); // dim_list
        pack32(1); // count
        writeString("time");
        pack32(0); // length
        pack32(12); pack32(0); // gatt absent
        pack32(11); // var_list
        pack32(1); // count
        writeString("temp");
        pack32(1); // rank
        pack32(0); // dimid
        pack32(12); pack32(0); // vatt absent
        pack32(5); // float
        pack32(0); // vsize
        pack32(100); // begin dummy
        // No data
    }

private:
    std::string filename;
    std::string magic;
    uint8_t version;
    uint32_t numrecs;
    std::vector<Dim> dims;
    std::vector<Attr> gattrs;
    std::vector<Var> vars;
    std::vector<char> data;
    size_t offset;
};

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " <file.ncd>" << std::endl;
        return 1;
    }
    NCDFile ncd(argv[1]);
    ncd.read();
    ncd.printProperties();
    ncd.write("output.ncd");
    return 0;
}

To arrive at the solution: Uses vector for buffer, __builtin_bswap32 for big-endian. Write uses ofstream. Simplified value storage as double for print.