unit fileformat_template;

{$mode ObjFPC}{$H+}

// -----------------------------------------------------------------------------
//                                2024-09-06
//
//                           File Format Template
//
//
//      If you create a binary file format, document what every byte means
//
// -----------------------------------------------------------------------------
// Probably Read this :
//
// Designing File Formats - © 2005 by Andy McFadden :
// https://fadden.com/tech/file-formats.html
//
// -----------------------------------------------------------------------------
// For writing to disk / in FileFormat only use well defined types :
//
// int64,     longint(32),  smallint(16), shortint(8),
// qword(64), longword(32), word(16),     byte(8)
//
//
// In case of using type "record", it is mostly required to use "Packed Record"
// or (does the same) declare {$PackRecords 1} in front of Your record
//    => http://www.delphibasics.co.uk/RTL.php?Name=Packed
// -----------------------------------------------------------------------------

interface

uses
  Classes;



// -----------------------------------------------------------------------------
// The HEADER of a good file format has, at minimum, the following elements:
//
//  Identification bytes ("magic number" or ID string)
//  Header checksum (here or at the end of the Header)
//  Version number
//  Offset to data
//  .. .. .. .. ..
//  End of Header, somehow. Maybe move the Header checksum here ..


// keyword OBJECT can be used like a RECORD :
type
  TFileHeaderExample = packed object
    MagicNumber   : array[0..3] of char;
    CRC16         : word;  // not a must, but would be a good design
    VersionNumber : longword;
    OffsetToData  : word;
    TestCharArray : array[0..7] of char;
    x             : longint;
    c             : char;
    //CRC16         : word;  // instead, probably put CRC to the end of the header ..
  end;

// Comments:
//
// The checksum can immediately follow the magic number, and is applied to
// everything that follows the checksum and precedes the start of data
// (as identified by the "offset to data" field).
//
// Instead the major/minor approach would be using two values:
// VersionMajor  : longword;
// VersionSub    : longword;
//
// Offset To Data tells the application how to skip unrecognized header fields
// It is recommended to measure the offset from the start of the file



// -----------------------------------------------------------------------------
type
  TDataBlockHeaderExample = packed object
    MagicNumber   : array[0..3] of char;
    Length        : longword;
    TestCharArray : array[0..7] of char;
    x             : longint;
    c             : char;
  end;

// Comments:
//
// Having an embedded length will let you detect if the file
// was inadvertently truncated (perhaps while being downloaded from the web),
// and is very important if your data is ever streamed over a network.
//
// Putting a CRC on the file data is valuable for the same reasons as
// putting it on the file header.  The best place for a CRC is actually
// at the end of a chunk of data.  CRC-32 ?




// ---- my main function -------------------------------------------------------

procedure TestWriteToDisk( FileName:string);



implementation


procedure TestWriteHeader( FileStream:TFileStream);
var
  FileHeaderExample : TFileHeaderExample;
begin
  // fill structure with data
  FileHeaderExample.MagicNumber   := 'TEST';
  FileHeaderExample.CRC16         := 1111;  // indeed: this is wrong ..
  FileHeaderExample.VersionNumber := 117;
  FileHeaderExample.OffsetToData  := 1000;  // indeed: this is wrong ..
  FileHeaderExample.TestCharArray := 'Test_+~#';
  FileHeaderExample.x             := 42;
  FileHeaderExample.c             := 'w';

  // OffsetToData and CRC16 need to receive valid data, missing here ..

  FileStream.Write( FileHeaderExample, SizeOf( FileHeaderExample));
end;



procedure TestWriteDataBlock( FileStream:TFileStream);
var
  DataBlockHeaderExample : TDataBlockHeaderExample;
begin
  DataBlockHeaderExample.MagicNumber   := 'DATA';
  DataBlockHeaderExample.Length        := 7;  // indeed: this is wrong ..
  DataBlockHeaderExample.TestCharArray := 'abcdEFGH';
  DataBlockHeaderExample.x             := 2;
  DataBlockHeaderExample.c             := 'c';

  FileStream.Write( DataBlockHeaderExample, SizeOf( DataBlockHeaderExample));
end;


// DataBlockExample .. jet missing



procedure TestWriteEOF( FileStream:TFileStream);
var
  EOF   : array[0..3] of char;
begin
  EOF:= '#EOF';
  FileStream.Write( EOF, SizeOf( EOF));
end;

// End-of-File Marker
//
// File truncation can occur when files are sent over a network or a disk
// has bad blocks.  Your program can detect truncation by:
//
// - Having a full file CRC.  Best for reliability, worst for performance.
// - Having a full file length in the header.  Read until the length is satisfied, not until EOF.  If you're not reading the entire file all at once, then seek to (length-1) and try to read one byte.
// - Add an explicit end marker.  Seek to the end of the file and read it.  The file length can come from the header or from the filesystem (fseek with SEEK_END).
//
// If you're memory-mapping a large file directly into multiple process address spaces, and don't want the performance overhead of a full-file CRC check, an end marker is a trivial way to make sure you've got the whole thing.



procedure TestWriteToDisk( FileName:string);
var
  FileStream : TFileStream = NIL;
begin
  FileStream:= TFileStream.Create( FileName, fmCreate);

  TestWriteHeader( FileStream);
  TestWriteDataBlock( FileStream);
  TestWriteEOF( FileStream);

  FileStream.Free;
end;


end.
