I want to open a 50 MB binary file and read only the last 4 bytes and convert it to string for some purpose.
The only way I found to do it now is using LoadStringFromFile, to load the file entirely on the memory then copy that last 4 bytes, however this method is very slow because the binary file is heavy.
Is there any better way to do it in Inno Setup script?
Update: This is a final working function that I edited from Martin Prikryl's answer
function readlast4byte() : AnsiString;
var
Stream: TFileStream;
Buffer: string;
Count: Integer;
Index: Integer;
begin
Count := 4;
Stream := TFileStream.Create('C:\test.txt', fmOpenRead);
try
Stream.Seek(-Count, soFromEnd);
SetLength(Buffer, 1);
SetLength(Result, Count);
for Index := 1 to Count do
begin
Stream.ReadBuffer(Buffer, 1);
Result[Index] := Chr(Ord(Buffer[1])) ;
end;
finally
Stream.Free;
end;
end;
Update 2: Also this is another great working function that written by TLama and should mark as answer too:
[Code]
#IFNDEF Unicode
#DEFINE CharSize 1
#ELSE
#DEFINE CharSize 2
#ENDIF
type
TSeekOrigin = (
soBeginning,
soCurrent,
soEnd
);
#IFDEF UNICODE
function BufferToAnsi(const Buffer: string): AnsiString;
var
W: Word;
I: Integer;
begin
SetLength(Result, Length(Buffer) * 2);
for I := 1 to Length(Buffer) do
begin
W := Ord(Buffer[I]);
Result[(I * 2)] := Chr(W shr 8); // high byte
Result[(I * 2) - 1] := Chr(Byte(W)); // low byte
end;
end;
#ENDIF
function ReadStringFromFile(
const FileName: string; Origin: TSeekOrigin; Offset, Length: Integer;
var S: AnsiString): Boolean;
var
Buffer: string;
Stream: TFileStream;
begin
Result := True;
try
Stream := TFileStream.Create(FileName, fmOpenRead);
try
Stream.Seek(Offset, Ord(Origin));
SetLength(Buffer, Length div {#CharSize});
Stream.ReadBuffer(Buffer, Length);
#IFNDEF UNICODE
S := Buffer;
#ELSE
S := BufferToAnsi(Buffer);
#ENDIF
finally
Stream.Free;
end;
except
Result := False;
end;
end;
You can use TFileStream support class:
TStream = class(TObject)
function Read(Buffer: String; Count: Longint): Longint;
function Write(Buffer: String; Count: Longint): Longint;
function Seek(Offset: Longint; Origin: Word): Longint;
procedure ReadBuffer(Buffer: String; Count: Longint);
procedure WriteBuffer(Buffer: String; Count: Longint);
function CopyFrom(Source: TStream; Count: Longint): Longint;
property Position: Longint; read write;
property Size: Longint; read write;
end;
THandleStream = class(TStream)
constructor Create(AHandle: Integer);
property Handle: Integer; read;
end;
TFileStream = class(THandleStream)
constructor Create(Filename: String; Mode: Word);
end;
Use the .Seek(-4, soFromEnd) property to seek the read pointer to the desired position.
A complication is that the TStream works with characters not bytes, so you have to convert Unicode strings that your read back to bytes.
When reading just four bytes, it's way easier to read byte by byte, preventing any multibyte conversions:
procedure ReadFileEnd;
var
Stream: TFileStream;
Buffer: string;
Count: Integer;
Index: Integer;
begin
Count := 4;
Stream := TFileStream.Create('my_binary_file.dat', fmOpenRead);
try
Stream.Seek(-Count, soFromEnd);
SetLength(Buffer, 1);
for Index := 1 to Count do
begin
Stream.ReadBuffer(Buffer, 1);
Log(Format('Byte %2.2x: %2.2x', [Index, Ord(Buffer[1])]));
end;
finally
Stream.Free;
end;
end;
Here's a generic alternative from by #TLama that efficiently works for arbitrary large read:
https://pastebin.com/nzGEdXVj
By the way, for me LoadStringFromFile function seems to be efficient enough to load 50 MB file. It takes only 40 ms.
Related
I want to edit a JSON file with Inno Setup for entering the installation path of my programme.
I have also found the post: How to parse a JSON string in Inno Setup? however, I can't cope with it because it talks about Info, User & String, but I only have Info & User.
This is what the entry that needs to be edited looks like:
"game_dirs": [
"Installation path"
]
This is my code:
[Code]
function JSONQueryString(FileName, Section, Key, Default: WideString;
var Value: WideString; var ValueLength: Integer): Boolean;
external 'JSONQueryString#files:jsonconfig.dll stdcall';
function JSONQueryBoolean(FileName, Section, Key: WideString;
Default: Boolean; var Value: Boolean): Boolean;
external 'JSONQueryBoolean#files:jsonconfig.dll stdcall';
function JSONQueryInteger(FileName, Section, Key: WideString;
Default: Int64; var Value: Int64): Boolean;
external 'JSONQueryInteger#files:jsonconfig.dll stdcall';
function JSONWriteString(FileName, Section, Key,
Value: WideString): Boolean;
external 'JSONWriteString#files:jsonconfig.dll stdcall';
function JSONWriteBoolean(FileName, Section, Key: WideString;
Value: Boolean): Boolean;
external 'JSONWriteBoolean#files:jsonconfig.dll stdcall';
function JSONWriteInteger(FileName, Section, Key: WideString;
Value: Int64): Boolean;
external 'JSONWriteInteger#files:jsonconfig.dll stdcall';
function BoolToStr(Value: Boolean): string;
begin
Result := 'True';
if not Value then
Result := 'False';
end;
procedure InitializeWizard;
var
FileName: WideString;
IntValue: Int64;
StrValue: WideString;
StrLength: Integer;
BoolValue: Boolean;
begin
{ set the source JSON config file path }
FileName := '{app}\\PATH\\Config.json';
{ allocate string buffer to enough length }
SetLength(StrValue, 16);
{ set the buffer length value }
StrLength := Length(StrValue);
{ query string value }
if JSONQueryString(FileName, 'game_dirs', '{app}\TEST', 'Default', StrValue,
StrLength)
then
MsgBox('Section_1:Key_1=' + StrValue, mbInformation, MB_OK);
{ query integer value }
if JSONQueryInteger(FileName, 'Section_1', 'Key_2', 0, IntValue) then
MsgBox('Section_1:Key_2=' + IntToStr(IntValue), mbInformation, MB_OK);
{ query boolean value }
end;
The following error message appears during installation:
Runtime error (at 46:224): Could not call proc.
Your game_dirs element is a JSON array. I do not think that JSONConfig.dll supports arrays.
You can use JsonParser library instead. With use of additional functions from my answer to How to parse a JSON string in Inno Setup?, the code can be like:
function FindJsonArray(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Arr: TJsonArray): Boolean;
var
JsonValue: TJsonValue;
begin
Result :=
FindJsonValue(Output, Parent, Key, JsonValue) and
(JsonValue.Kind = JVKArray);
if Result then
begin
Arr := Output.Arrays[JsonValue.Index];
end;
end;
procedure EditGameDirs(FileName: string; GameDir: string);
var
JsonLines: TStringList;
JsonParser: TJsonParser;
JsonRoot: TJsonObject;
GameDirs: TJsonArray;
begin
JsonLines := TStringList.Create;
JsonLines.LoadFromFile(FileName);
if ParseJsonAndLogErrors(JsonParser, JsonLines.Text) then
begin
JsonRoot := GetJsonRoot(JsonParser.Output);
if not FindJsonArray(JsonParser.Output, JsonRoot, 'game_dirs', GameDirs) then
begin
Log('Cannot find game_dirs');
end
else
if (GetArrayLength(GameDirs) <> 1) or
(GameDirs[0].Kind <> JVKString) then
begin
Log('game_dirs does not have one string element');
end
else
begin
Log(Format('Previous game_dirs was [%s]', [
JsonParser.Output.Strings[GameDirs[0].Index]]));
Log(Format('New game_dirs is [%s]', [GameDir]));
JsonParser.Output.Strings[GameDirs[0].Index] := GameDir;
JsonLines.Clear;
PrintJsonParserOutput(JsonParser.Output, JsonLines);
JsonLines.SaveToFile(FileName + '2');
end;
end;
end;
I'd like a function that takes an enum and returns a stringlist (or name:value pairs). It must be possible, the Object Inspector and Code Complete seem to do that. The pseudocode seems simple enough, but the details escape me....
function ParseEnum(e: Enum, S: TStringList): TStringList;
var
i: Integer;
begin
for i := 0 to length(enum) - 1 do
S.Add(GetEnumName(e, i));
Result := S;
end;
if this is a common funciton(all of Enum):
if the Enum is Continuity, as follows code is OK(by rtti)
if not Continuity i can't find a way now
type
TMyEnum = (s, b, c, d, e, f);
implementation
uses
System.Rtti, TypInfo;
procedure ParseEnum<T>(sl: TStrings);
var
rt: TRttiType;
rot: TRttiOrdinalType;
i: Integer;
begin
rt := TRttiContext.Create.GetType(TypeInfo(T));
rot := rt.AsOrdinal;
for i := rot.MinValue to rot.MaxValue do
sl.Add(GetEnumName(TypeInfo(T), i));
end;
procedure TForm1.btn1Click(Sender: TObject);
var
sl: TStringList;
begin
sl := TStringList.Create;
try
ParseEnum<TMyEnum>(sl);
ShowMessage(sl.Text);
finally
sl.Free;
end;
end;
why sl as a param but result: to attent people don't forget to free
How can an array of record be stored in JSON via SuperObject library. For example..
type
TData = record
str: string;
int: Integer;
bool: Boolean;
flt: Double;
end;
var
DataArray: Array[0..100] of TData;
Just use the superobject Marshalling TSuperRTTIContext
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
superobject,
System.SysUtils;
type
TData = record
str : string;
int : Integer;
bool : Boolean;
flt : Double;
end;
TDataArray = Array [0 .. 100] of TData;
procedure Test;
var
DataArray : TDataArray;
so : ISuperObject;
ctx : TSuperRttiContext;
begin
ctx := TSuperRttiContext.Create;
try
so := ctx.AsJson<TDataArray>( DataArray );
finally
ctx.Free;
end;
Writeln( so.AsJson );
end;
begin
try
Test;
except
on E : Exception do
Writeln( E.ClassName, ': ', E.Message );
end;
ReadLn;
end.
Make it a string first.
Your array:
//Array[0] := 'Apple';
//Array[1] := 'Orange';
//Array[2] := 'Banana';
myArrayAsStr := '"MyArray": [{ "1": "' + Array[0] +'", "2": "' + Array[1] +'"}';
Then you can just make it into JSON with SO(myArrayAsStr)
You can always generate your array as string in a different procedure but I think thats the way to do it.
Ill keep checking if there is an easier way ;)
EDIT:
SuperObject also has the following function:
function SA(const Args: array of const): ISuperObject; overload;
You will be able to convert that to a string again and add it in the total JSON string.
I have the following JSON:
{
"Info": {
"User": 2,
"String": "foo"
}
}
Unfortunately TLama's Inno JSON Config library doesn't work with JSON strings but only with json files.
I tried to use JSON string instead of path to json file, but it didn't work.
if JSONQueryInteger('{"Info":{"User":2,"String":"foo"}}', 'Info', 'User', 0, IntValue) then
MsgBox('User=' + IntToStr(IntValue), mbInformation, MB_OK);
I know I could save my JSON to a file and then parse it but it seems kind of messy.
How to parse a JSON string in Inno Setup?
You can use JsonParser library instead. It can parse JSON strings.
It's not as easy to use as JSONConfig.dll – but that's the reason why it is more flexible. Also it's a native Pascal Script code. So, it not only saves you from a temporary .json file, but also from a temporary .dll.
The code can be like:
[Code]
#include "JsonParser.pas"
function GetJsonRoot(Output: TJsonParserOutput): TJsonObject;
begin
Result := Output.Objects[0];
end;
function FindJsonValue(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Value: TJsonValue): Boolean;
var
I: Integer;
begin
for I := 0 to Length(Parent) - 1 do
begin
if Parent[I].Key = Key then
begin
Value := Parent[I].Value;
Result := True;
Exit;
end;
end;
Result := False;
end;
function FindJsonObject(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Object: TJsonObject): Boolean;
var
JsonValue: TJsonValue;
begin
Result :=
FindJsonValue(Output, Parent, Key, JsonValue) and
(JsonValue.Kind = JVKObject);
if Result then
begin
Object := Output.Objects[JsonValue.Index];
end;
end;
function FindJsonNumber(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Number: TJsonNumber): Boolean;
var
JsonValue: TJsonValue;
begin
Result :=
FindJsonValue(Output, Parent, Key, JsonValue) and
(JsonValue.Kind = JVKNumber);
if Result then
begin
Number := Output.Numbers[JsonValue.Index];
end;
end;
function FindJsonString(
Output: TJsonParserOutput; Parent: TJsonObject; Key: TJsonString;
var Str: TJsonString): Boolean;
var
JsonValue: TJsonValue;
begin
Result :=
FindJsonValue(Output, Parent, Key, JsonValue) and
(JsonValue.Kind = JVKString);
if Result then
begin
Str := Output.Strings[JsonValue.Index];
end;
end;
function ParseJsonAndLogErrors(
var JsonParser: TJsonParser; const Source: WideString): Boolean;
var
I: Integer;
begin
ParseJson(JsonParser, Source);
Result := (Length(JsonParser.Output.Errors) = 0);
if not Result then
begin
Log('Error parsing JSON');
for I := 0 to Length(JsonParser.Output.Errors) - 1 do
begin
Log(JsonParser.Output.Errors[I]);
end;
end;
end;
procedure ParseJsonString;
var
Json: string;
JsonParser: TJsonParser;
I: Integer;
JsonRoot, InfoObject: TJsonObject;
UserNumber: TJsonNumber; { = Double }
UserString: TJsonString; { = WideString = string }
begin
Json := '{"Info":{"User":2,"String":"abc"}}';
if ParseJsonAndLogErrors(JsonParser, Json) then
begin
JsonRoot := GetJsonRoot(JsonParser.Output);
if FindJsonObject(JsonParser.Output, JsonRoot, 'Info', InfoObject) and
FindJsonNumber(JsonParser.Output, InfoObject, 'User', UserNumber) and
FindJsonString(JsonParser.Output, InfoObject, 'String', UserString) then
begin
Log(Format('Info:User:%d', [Round(UserNumber)]));
Log(Format('Info:String:%s', [UserString]));
end;
end;
ClearJsonParser(JsonParser);
end;
Another option is to fork the Inno JSON Config library and add support for parsing strings.
How can an array of record be stored in JSON via SuperObject library. For example..
type
TData = record
str: string;
int: Integer;
bool: Boolean;
flt: Double;
end;
var
DataArray: Array[0..100] of TData;
Just use the superobject Marshalling TSuperRTTIContext
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
superobject,
System.SysUtils;
type
TData = record
str : string;
int : Integer;
bool : Boolean;
flt : Double;
end;
TDataArray = Array [0 .. 100] of TData;
procedure Test;
var
DataArray : TDataArray;
so : ISuperObject;
ctx : TSuperRttiContext;
begin
ctx := TSuperRttiContext.Create;
try
so := ctx.AsJson<TDataArray>( DataArray );
finally
ctx.Free;
end;
Writeln( so.AsJson );
end;
begin
try
Test;
except
on E : Exception do
Writeln( E.ClassName, ': ', E.Message );
end;
ReadLn;
end.
Make it a string first.
Your array:
//Array[0] := 'Apple';
//Array[1] := 'Orange';
//Array[2] := 'Banana';
myArrayAsStr := '"MyArray": [{ "1": "' + Array[0] +'", "2": "' + Array[1] +'"}';
Then you can just make it into JSON with SO(myArrayAsStr)
You can always generate your array as string in a different procedure but I think thats the way to do it.
Ill keep checking if there is an easier way ;)
EDIT:
SuperObject also has the following function:
function SA(const Args: array of const): ISuperObject; overload;
You will be able to convert that to a string again and add it in the total JSON string.