unit fileformat_core;

{$mode ObjFPC}{$H+}

// -----------------------------------------------------------------------------
//                                2024-09-15
//
//                             File Format Core
//
//
//      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)
//
// single(32), Double(64)  (Extended is possible, but .. see IEEE 754 formats)
//
// -----------------------------------------------------------------------------
// 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
  LCLType,
  Classes, Sysutils, Dialogs;


// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
type
  TFileHeaderCore = packed object  // keyword OBJECT can be used like a RECORD
    // --- mandatory -----------------------------------------------------------
    MagicNumber  : array[0..11] of char;  // MagicNumbers in earlier days where just 4 bytes long ..
    VersionMajor : byte;
    VersionSub   : byte;
    SizeOfHeader : word;  // type [word] limits the possible size of the Header Block
    // SizeOfHeader = Offset to next Block / Chunk
  end;


// Comments:
//
// The HEADER of a good file format has, at minimum, the following elements:
//
// - Identification bytes ("magic number" or ID string)
// - Version number
// - Offset to data
// - .. .. .. .. ..
// - Header checksum (recommended here at the end of the Header)
//
//
// VersionNumber :
// - The major/minor approach would be using two values:
//     VersionMajor  : word or byte;
//     VersionSub    : word or byte;
// - The single number solution:
//     VersionNumber : longword or word;
//
// 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
//
// CRC16         : word;  // or CRC32, not a must, but would be a good design
// The checksum ..
// - may be at the end of the Header, as the last field, or
// - may immediately follow the magic number, and is then applied to
//   everything that follows the checksum and precedes the start of data
//   (as identified by the "offset to data" field).



// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
type
  TIndexBlockCore = packed object  //not mandatory, so not in use in example "TFileFormatCore"
    // --- mandatory -----------------------------------------------------------
    MagicNumber      : array[0..7] of char;  // human readable in HEX Editor ..
    SizeOfIndexBlock : qword;
  end;


// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
type
  TDataBlockHeaderCore = packed object
    // --- mandatory -----------------------------------------------------------
    MagicNumber     : array[0..7] of char;  // human readable in HEX Editor ..
    SizeOfDataBlock : qword;
  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 ?


// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
const
  CONST_EOF = 'EOF|';   // just an example ..


// 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.


// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
type  //https://www.freepascal.org/docs-html/rtl/system/tobject.html
  TFileFormatCore = class( TObject)
    private
      // Only known to the parent class
      //FileName:string;
    protected
      // Known to all classes in the hierarachy
      FileName:string;
      FileHeaderCore      : TFileHeaderCore;
      DataBlockHeaderCore : TDataBlockHeaderCore;  // simple example, so only 1 DataBlock
      procedure iWriteHeader( FileStream:TFileStream);
      procedure iWriteDataBlock( FileStream:TFileStream; Arr:TStringArray); //not: virtual;
    public
      // Known externally by class users
      constructor Create( aFileName:string); reintroduce; // suppresses a compiler warning
      function Save( DataBlockContent:TStringArray):integer;
      function Load( aFileName:string):integer; virtual; //not: abstract;
      // must be implemented in a descendent object, with keyword 'override'
    published
      // Known externally, appear in the Object Inspector at design time
  end;


implementation


constructor TFileFormatCore.Create( aFileName:string); //reintroduce; // suppresses a compiler warning
begin
  //https://forum.lazarus.freepascal.org/index.php?topic=45366.15
  inherited Create;

  FileName:= aFileName;
end;

function TFileFormatCore.Load( aFileName:string):integer; //virtual;
begin
  Result:= -1;
end;

function TFileFormatCore.Save( DataBlockContent:TStringArray):integer;
var
  Stream : TFileStream = NIL;
begin
  result:= -1;
  if length( DataBlockContent) = 0 then exit;

  result:= -2;
  if FileName = '' then exit;
  ShowMessage( 'valid:' +LineEnding +FileName);

  Stream:= TFileStream.Create( FileName, fmCreate);

  // --- write header ---
  iWriteHeader( Stream);

  // --- write data block ---
  iWriteDataBlock( Stream, DataBlockContent);

  // --- write EOF (not mandatory, but useful ..) ---
  Stream.Seek( 0, soFromEnd);
  Stream.WriteBuffer( CONST_EOF, SizeOf( CONST_EOF));

  Stream.Free;
end;

procedure TFileFormatCore.iWriteHeader( FileStream:TFileStream);
begin
  // --- write file header ---
  FileHeaderCore.MagicNumber  := 'CORE Demo 01';  // example "text", 12 chars long, do what you want here ..
  FileHeaderCore.VersionMajor := 1;  // example value
  FileHeaderCore.VersionSub   := 0;  // example value
  FileHeaderCore.SizeOfHeader := SizeOf( FileHeaderCore);

  // we can write this Header "in one shot" because it's a "packed record"
  FileStream.WriteBuffer( FileHeaderCore, SizeOf( FileHeaderCore));
end;

procedure TFileFormatCore.iWriteDataBlock( FileStream:TFileStream; Arr:TStringArray);
var
  i                          : integer;
  StreamPos_StartOfDataBlock : qword;
  StreamPos_EndOfDataBlock   : qword;
begin
  // store Stream Position of StartOfDataBlock:
  StreamPos_StartOfDataBlock:= FileStream.Position;

  // --- write DataBlock header ---
  DataBlockHeaderCore.SizeOfDataBlock:= SizeOf( DataBlockHeaderCore);
  // we can write this Header "in one shot" because it's a "packed record"
  FileStream.WriteBuffer( DataBlockHeaderCore, SizeOf( DataBlockHeaderCore));

  // up to here, the SizeOfDataBlock is "SizeOfDataBlockheader" only.
  // at the end we have to write the correct value to DataBlockHeaderCore.SizeOfDataBlock !


  // --- write data ---
  for i:= 0 to length( Arr) -1 do
    begin
      FileStream.WriteAnsiString( Arr[i]);
    end;

  // now write correct Size Of whole DataBlock:
  //
  // 1)  write Size to DataBlock :
  StreamPos_EndOfDataBlock:= FileStream.Position;
  DataBlockHeaderCore.SizeOfDataBlock:= StreamPos_EndOfDataBlock
                                        -StreamPos_StartOfDataBlock;
  // 2)  write Size to File :
  FileStream.Seek( StreamPos_StartOfDataBlock
                   +SizeOf( DataBlockHeaderCore.MagicNumber),
                   soFromBeginning);
  FileStream.WriteBuffer( DataBlockHeaderCore.SizeOfDataBlock,
                          SizeOf( DataBlockHeaderCore.SizeOfDataBlock));
end;


end.
