program TestSc14n;
{
  Some tests for the Delphi/FreePascal interface to SC14N
  <https://www.cryptosys.net/sc14n/>
  $Id: TestSc14n.pas $
  $Date: 2023-04-14 11:01 $
  $Revision: 1.0.1 $
  ************************** LICENSE *****************************************
  Copyright (C) 2023 David Ireland, DI Management Services Pty Limited.
  All rights reserved. <www.di-mgt.com.au> <www.cryptosys.net>
  The code in this module is licensed under the terms of the MIT license.
  For a copy, see <http://opensource.org/licenses/MIT>
  ****************************************************************************
}

{$APPTYPE CONSOLE}
{$mode Delphi}
{$ASSERTIONS ON}

uses
  SysUtils, diSc14n;

const
  { WARNING: this is where we expect to find the test files.
    CHANGE THIS TO SUIT }
  TEST_DIR = '.\Test';
  
  CRLF = #13 + #10;


{ Declare forward functions for safer wrapper functions returning strings. }
Function c14nFile2Digest(szInputFile : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString; forward;
Function c14nFile2String(szInputFile : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString; forward;
Function c14nString2String(szInput : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString; forward;
Function c14nString2Digest(szInput : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString; forward;
Function sc14nErrorLookup(nErrCode : LongInt): AnsiString; forward;
Function sc14nLastError(): AnsiString; forward;


// DO SOME TESTS
Procedure do_tests;
var
  n : Integer;
  s : AnsiString;
  buf : AnsiString;
  ch : Char;
  nchars : Integer;
  fname : AnsiString;
  outfile : AnsiString;
  

begin
  WriteLn('Running ' + ExtractFileName(ParamStr(0)) + ' at ' + DateTimeToStr(Now));
   
  // Set current working directory to find test files
  ChDir(TEST_DIR);
  
  // INTERROGATE THE CORE DLL
  // Either return an integer value or fill a pre-dimensioned output string buffer.
  // (You could write wrapper functions for the 'Gen' functions here that output to a string)
  
  WriteLn(CRLF + 'GENERAL:');
  WriteLn('Interrogate the core DLL:');
  WriteLn('Version='+(IntToStr(SC14N_Gen_Version)));

  nchars := SC14N_Gen_CompileTime(NIL, 0);
  WriteLn('CompileTime returns nchars=' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0,nchars));
  nchars := SC14N_Gen_CompileTime(PAnsiChar(buf), nchars);
  WriteLn('CompileTime=' + buf);
  Assert (nchars > 0);

  ch := Chr(SC14N_Gen_LicenceType());
  WriteLn('LicenceType='+ ch);
  
  nchars := SC14N_Gen_ModuleName(NIL, 0, 0);
  WriteLn('ModuleName returns nchars=' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  SC14N_Gen_ModuleName(PAnsiChar(buf), nchars, 0);
  WriteLn('ModuleName=' + Trim(string(buf)));

  nchars := SC14N_Gen_Platform(NIL, 0);
  WriteLn('Platform returns nchars=' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  SC14N_Gen_Platform(PAnsiChar(buf), nchars);
  WriteLn('Platform=' + Trim(string(buf)));

  // Compute C14N of FirstName element using tag
  fname := 'test_bruce_utf8.xml';
  WriteLn('FILE: ' + fname);
  outfile := 'fileout.xml';
  n := C14N_File2File(outfile, fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG);
  WriteLn('C14N_File2File returns '+(IntToStr(n))+' (expecting 0)');
  Assert(0 = n);
  WriteLn('Created output file: ' + outfile);
  
  // Compute digest values the long way...
  // Compute SHA-1 digest of C14N'd data
  nchars := C14N_File2Digest(NIL, 0, fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG);
  WriteLn('C14N_File2Digest returns nchars=' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  C14N_File2Digest(PAnsiChar(buf), nchars, fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG);
  WriteLn('File2Digest(SHA-1)=' + Trim(string(buf)));
  Assert('Qy90ICPAAbnjWl/UpAn37sXS1wU=' = buf);
  
  // Same using SHA-256
  nchars := C14N_File2Digest(NIL, 0, fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG or SC14N_DIG_SHA256);
  WriteLn('C14N_File2Digest returns nchars=' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  C14N_File2Digest(PAnsiChar(buf), nchars, fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG or SC14N_DIG_SHA256);
  WriteLn('File2Digest(SHA-256)=' + Trim(string(buf)));
  Assert('93ljdxd4XQ2k66gOZai6a3bhjVO5t6XNqqBuArFyhc0=' = buf);
  
  // Repeat using wrapper function
  s := c14nFile2Digest(fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG or SC14N_DIG_SHA256);
  WriteLn('c14nFile2Digest(SHA-256) returns [' + s + ']');
  Assert('93ljdxd4XQ2k66gOZai6a3bhjVO5t6XNqqBuArFyhc0=' = s);
  s := c14nFile2Digest(fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG or SC14N_DIG_SHA512);
  WriteLn('c14nFile2Digest(SHA-512) returns ' + CRLF + '[' + s + ']');

  // Extract C14N output from file into a string
  s := c14nFile2String(fname, 'FirstName', '', SC14N_TRAN_SUBSETBYTAG);
  WriteLn('c14nFile2String returns:' + CRLF + s);
  WriteLn('--expecting:' + CRLF + '<FirstName xml:id="F01">BruceƱ</FirstName>');
  
  // String --> String
  s := c14nString2String('<a><b xyz="last" abc="first" /></a>', 'b', '', SC14N_TRAN_SUBSETBYTAG);
  WriteLn('c14nString2String returns [' + s + ']');
  // String --> Digest
  s := c14nString2Digest('<a><b xyz="last" abc="first" /></a>', 'b', '', SC14N_TRAN_SUBSETBYTAG or SC14N_DIG_SHA256);
  WriteLn('c14nString2Digest returns [' + s + ']');
  
  WriteLn('Display some error codes...');
  for n := 0 to 5 do
    begin
		WriteLn('Error(' + IntToStr(n) + ')=' + sc14nErrorLookup(n));
	end;
	
  WriteLn('Cause a deliberate error...');
  n := C14N_File2File(outfile, 'missing.file', 'FirstName', '', SC14N_TRAN_SUBSETBYTAG);
  WriteLn('C14N_File2File(missing.file) returns n=', n);
  WriteLn('Error(' + IntToStr(n) + ')=' + sc14nErrorLookup(n));
  WriteLn('LastError=' + sc14nLastError());
  // An error in a wrapper function will raise an exception
  try
	s := c14nFile2Digest(fname, 'BadName', '', SC14N_TRAN_SUBSETBYTAG);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  
  WriteLn('...END OF deliberate errors.');
  
  WriteLn('SC14N Version=', SC14N_Gen_Version());
  WriteLn(CRLF+'ALL DONE.');

end;

{ WRAPPER FUNCTIONS 
  -- functions that return a string directly in a safe manner.
}

{ Our crude Exception class }
type ESc14nException = Class(Exception);

{ Compute digest value of C14N transformation (file-to-digest).
  @param(szInputFile Name of input file containing XML document to be processed.)
  @param(szNameOrId Tag name or Id to include or omit.)
  @param(szParams InclusiveNamespaces PrefixList parameter for exclusive c14n.)
  @param(nOptions Option flags.)
  @returns(Digest value as base64-encoded string, or an empty string on error.)
  @raises(ESc14nException if the core function fails for any reason.)
  }
Function c14nFile2Digest(szInputFile : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString;
var
  buf : AnsiString;
  nchars : Integer;

begin
  nchars := C14N_File2Digest(NIL, 0, szInputFile, szNameOrId, szParams, nOptions);
  if nchars < 0 then raise ESc14nException.Create('SC14N error: ' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  C14N_File2Digest(PAnsiChar(buf), nchars, szInputFile, szNameOrId, szParams, nOptions);
  Result := buf
end;

{ Perform C14N transformation of XML document (file-to-memory).
  @param(szInputFile Name of input file containing XML document to be processed.)
  @param(szNameOrId Tag name or Id to include or omit.)
  @param(szParams InclusiveNamespaces PrefixList parameter for exclusive c14n.)
  @param(nOptions Option flags.)
  @returns(UTF-8-encoded string containing transformed data, or an empty string on error.)
  @raises(ESc14nException if the core function fails for any reason.)
  }
Function c14nFile2String(szInputFile : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString;
var
  buf : AnsiString;
  nchars : Integer;

begin
  nchars := C14N_File2String(NIL, 0, szInputFile, szNameOrId, szParams, nOptions);
  if nchars < 0 then raise ESc14nException.Create('SC14N error: ' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  C14N_File2String(PAnsiChar(buf), nchars, szInputFile, szNameOrId, szParams, nOptions);
  Result := buf
end;

{ Perform C14N transformation of XML document (string-to-string).
  @param(szInput String containing UTF-8-encoded XML data.)
  @param(szNameOrId Tag name or Id to include or omit.)
  @param(szParams InclusiveNamespaces PrefixList parameter for exclusive c14n.)
  @param(nOptions Option flags.)
  @returns(UTF-8-encoded string containing transformed data, or an empty string on error.)
  @raises(ESc14nException if the core function fails for any reason.)
  }
Function c14nString2String(szInput : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString;
var
  buf : AnsiString;
  nchars : Integer;

begin
  nchars := C14N_String2String(NIL, 0, szInput, Length(szInput), szNameOrId, szParams, nOptions);
  if nchars < 0 then raise ESc14nException.Create('SC14N error: ' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  C14N_String2String(PAnsiChar(buf), nchars, szInput, Length(szInput), szNameOrId, szParams, nOptions);
  Result := buf
end;

{ Compute digest value of C14N transformation of XML document (string-to-digest).
  @param(szInput String containing UTF-8-encoded XML data.)
  @param(szNameOrId Tag name or Id to include or omit.)
  @param(szParams InclusiveNamespaces PrefixList parameter for exclusive c14n.)
  @param(nOptions Option flags.)
  @returns(Digest value as base64-encoded string, or an empty string on error.)
  @raises(ESc14nException if the core function fails for any reason.)
  }
Function c14nString2Digest(szInput : AnsiString; szNameOrId : AnsiString; szParams : AnsiString; nOptions : LongInt): AnsiString;
var
  buf : AnsiString;
  nchars : Integer;

begin
  nchars := C14N_String2Digest(NIL, 0, szInput, Length(szInput), szNameOrId, szParams, nOptions);
  if nchars < 0 then raise ESc14nException.Create('SC14N error: ' + IntToStr(nchars));
  buf := AnsiString(StringOfChar(#0, nchars));
  C14N_String2Digest(PAnsiChar(buf), nchars, szInput, Length(szInput), szNameOrId, szParams, nOptions);
  Result := buf
end;

{ Look up description for error code.
  @param(nErrCode Value of error code to lookup (may be positive or negative).)
  @returns(Error message, or empty string if no corresponding error code.) 
  }
Function sc14nErrorLookup(nErrCode : LongInt): AnsiString;
var
  buf : AnsiString;
  nchars : Integer;

begin
  nchars := SC14N_Err_ErrorLookup(NIL, 0, nErrCode);
  if nchars < 0 then Exit('');
  buf := AnsiString(StringOfChar(#0, nchars));
  SC14N_Err_ErrorLookup(PAnsiChar(buf), nchars, nErrCode);
  Result := buf
end;

{ Retrieve the last error message (if available).
  @returns(String with more information about the last error.) 
  @note(Not all functions set this.)
  }
Function sc14nLastError(): AnsiString;
var
  buf : AnsiString;
  nchars : Integer;

begin
  nchars := SC14N_Err_LastError(NIL, 0);
  if nchars < 0 then Exit('');
  buf := AnsiString(StringOfChar(#0, nchars));
  SC14N_Err_LastError(PAnsiChar(buf), nchars);
  Result := buf
end;


{ MAIN TEST PROCEDURE }
begin
  try
    do_tests;

   except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
   end;
 end.