I want to use functions of the Winapi.Security.Cryptography.Core_IAsymmetricKeyAlgorithmProvider and it has a parameter of type IBuffer.
i dont know how to use an IBuffer or how to give data to it.
i tried allocationg memory and giving the function a pwide char but the ide already told me this wont work.
I also didnt find anything useful online.
Winapi.Security.Cryptography contains wrappers for
Windows.Security.Cryptography.Core (Windows UWP)
Check this page first:
https://learn.microsoft.com/en-us/uwp/api/windows.security.cryptography.core.asymmetrickeyalgorithmprovider?view=winrt-22621
There is a C# app example that demonstrates how to use this interface.
Details about IBuffer can be found on this page:
https://learn.microsoft.com/en-us/uwp/api/windows.storage.streams.ibuffer?view=winrt-22621
This is probably your first step, if you want encrypt with RSA PKCS1:
uses
System.Win.WinRT,
Winapi.WinRT,
Winapi.Security.Cryptography,
Winapi.Storage.Streams;
const
KeyLength = 512;
var
objAlgProv: Core_IAsymmetricKeyAlgorithmProvider;
data: TBytes;
cleardata, encrypted: IBuffer;
key: Core_ICryptographicKey;
hs: HSTRING;
EncryptedBase64: string;
begin
data := TEncoding.UTF8.GetBytes('clear text');
objAlgProv := TCore_AsymmetricKeyAlgorithmProvider.OpenAlgorithm(TCore_AsymmetricAlgorithmNames.RsaPkcs1);
key := objAlgProv.CreateKeyPair(KeyLength);
cleardata := TCryptographicBuffer.CreateFromByteArray(length(data), #data[0]);
encrypted := TCore_CryptographicEngine.Encrypt(key, cleardata, nil {IV});
hs := TCryptographicBuffer.EncodeToBase64String(encrypted);
EncryptedBase64 := TWindowsString.HStringToString(hs);
end;
If you have further trouble translating the solution into Delphi, show your code an explain the details what you need to know.
Related
I have a client providing an API that dictates the data to send to them must be encrypted with AES, 128-bit key, ECB mode, and PKCS5Padding. I'm trying to use LockBox 3 in Delphi 10.3 Rio and am not getting the same encrypted string as an online test tool they pointed to for verification. It is close, but not quite there.
With lots of reading here about Unicode, PKCS5Padding, and related questions, I've come to the end of what to try. I must admit I don't do very much with encryption and have been reading as much as I can to get my head around this before I ask questions.
There are a couple things I'd like confirmation on:
The difference between a password and a key. I've read that LB3 uses a password to generate a key, but I have specific instructions from the client on how the key is to be generated so I've made my own Base64-encoded key and am calling InitFromStream to initialize it. I believe this takes the place of setting a password, is that right? Or maybe passwords are only used by Asymetric cipers (not Symetric ones, like AES)?
PKCS5Padding: I was worried by something I read on the LB3 Help site that said padding is done intelligently depending on the choice of cipher, chaining mode, etc. So does that mean there's no way to force it to use a specific padding method? I've converted the data to a byte array and implemented by own PKCS5Padding but I think LB3 may still be padding beyond that. (I've tried looking through the code and have not found any evidence this is what it's doing.)
Should I use a different encryption library in Delphi to accomplish this? I've checked out DelphiEncryptionCompendium and DcPCryptV2 but I found LB3 seems to have the most support and I felt it was the easiest to work with, especially in my Unicode version of Delphi. Plus I have used LockBox 2 quite a bit in years past, so I figured it would be more familiar (this turned out not to be the case).
To illustrate what I've tried, I extracted my code from the project to a console application. Perhaps my assumptions above are correct and there's a glaring error in my code or a LB3 parameter I don't understand that someone will point out:
program LB3ConsoleTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.Classes, System.NetEncoding,
uTPLb_Codec, uTPLb_CryptographicLibrary,
uTPLb_StreamUtils, uTPLb_Constants;
var
Codec: TCodec;
CryptographicLibrary: TCryptographicLibrary;
function PKCS5PadStringToBytes(RawData: string; const PadSize: Integer): TBytes;
{ implement our own block padding }
var
DataLen: Integer;
PKCS5PaddingCount: ShortInt;
begin
Result := TEncoding.UTF8.GetBytes(RawData);
DataLen := Length(RawData);
PKCS5PaddingCount := PadSize - DataLen mod PadSize;
if PKCS5PaddingCount = 0 then
PKCS5PaddingCount := PadSize;
Inc(DataLen, PKCS5PaddingCount);
SetLength(Result, DataLen);
FillChar(Result[DataLen - PKCS5PaddingCount], PKCS5PaddingCount, PKCS5PaddingCount);
end;
procedure InitializeAESKey(const AESKey: string);
{ convert the string to a byte array,
use that to initialize a ByteStream,
and call LB3's InitFromStream }
var
AESKeyBytes: TBytes;
AESKeyStream: TBytesStream;
begin
AESKeyBytes := TEncoding.UTF8.GetBytes(AESKey);
AESKeyStream := TBytesStream.Create(AESKeyBytes);
Codec.InitFromStream(AESKeyStream);
end;
const
RawData = '{"invoice_id":"456456000018047","clerk_id":"0023000130234234","trans_amount":1150034534,"cust_code":"19455605000987890641","trans_type":"TYPE1"}';
AESKeyStr = 'CEAA31AD1EE4BDC8';
var
DataBytes: TBytes;
DataStream: TBytesStream;
ResultStream: TBytesStream;
ResultBytes: TBytes;
Base64Encoder: TBase64Encoding;
begin
// create the LockBox3 objects
Codec := TCodec.Create(nil);
CryptographicLibrary := TCryptographicLibrary.Create(nil);
try
// setup LB3 for AES, 128-bit key, ECB
Codec.CryptoLibrary := CryptographicLibrary;
Codec.StreamCipherId := uTPLb_Constants.BlockCipher_ProgId;
Codec.BlockCipherId := Format(uTPLb_Constants.AES_ProgId, [128]);
Codec.ChainModeId := uTPLb_Constants.ECB_ProgId;
// prep the data, the key, and the resulting stream
DataBytes := PKCS5PadStringToBytes(RawData, 8);
DataStream := TBytesStream.Create(DataBytes);
InitializeAESKey(AESKeyStr);
ResultStream := TBytesStream.Create;
// ENCRYPT!
Codec.EncryptStream(DataStream, ResultStream);
// take the result stream, convert it to a byte array
ResultStream.Seek(0, soFromBeginning);
ResultBytes := Stream_to_Bytes(ResultStream);
// convert the byte array to a Base64-encoded string and display
Base64Encoder := TBase64Encoding.Create(0);
Writeln(Base64Encoder.EncodeBytesToString(ResultBytes));
Readln;
finally
Codec.Free;
CryptographicLibrary.Free;
end;
end.
This program generates an encrypted string 216 characters long with only the last 25 different than what the online tool produces.
Why?
AES uses 16-bytes blocks, not 8-bytes.
So you need PKCS7 with 16 bytes padding, not PKCS5 which is fixed to 8 bytes.
Please try
DataBytes := PKCS5PadStringToBytes(RawData, 16);
Also consider changing the chaining mode away from ECB, which is pretty weak, so is to be avoided for any serious work.
I tried to pass a database record from my server-side application to my client-side application. On the client-side I need to store my data into a TStrings collection.
When I pass a multiline field, I receive two separate data items at the client-side, instead of one multiline data item! I've also tried to do that with Unicode UTF8 based commands, but unfortunately the result is same.
Server-side code:
procedure TForm1.IdCmdTCPServer1CommandHandlers0Command(ASender: TIdCommand);
var
myData: TStrings;
begin
myData := TStringList.Create;
myData.Add('12'); // ID
myData.Add('This is a multi line' + #13#10 + 'description.'); // Descriptions
myData.Add('Thom Smith'); // Name
try
ASender.Context.Connection.Socket.Write(myData, True{, TIdTextEncoding.UTF8});
finally
myData.Free;
end;
end;
myData debug-time values on server-side are:
myData[0] = '12'
myData[1] = 'This is a multi line'#$D#$A'description.'
myData[2] = 'Thom Smith'
Client-side code:
procedure TForm1.Button1Click(Sender: TObject);
var
myData: TStrings;
begin
with TIdTCPClient.Create(nil) do
begin
Port := 1717;
Host := 'localhost';
try
Connect;
//IOHandler.DefStringEncoding := TIdTextEncoding.UTF8;
myData := TStringList.Create;
try
SendCmd('greating');
Socket.ReadStrings(myData, -1{, TIdTextEncoding.UTF8});
eID.Text := myData[0]; // ID TEdit
mDes.Text := myData[1]; // Descriptions TMemo
eTelNo.Text := myData[2]; // Name TEdit
finally
myData.Free;
end;
finally
Disconnect;
Free;
end;
end;
end;
myData debug-time valuese on client-side:
myData[0] = '12'
myData1 = 'This is a multi line'
myData[2] = 'description.'
Telnet result:
Actually, myData[2] that should keep 'Thom Smith' was replaced with the second line of the Description field! and there are no items after myData[2]. myData[3] is not accessible any more.
I think this issue is related to Indy's Write or ReadStrings procedures, because it sends ItemCount as 3, but it sends two items (one correct, and next beaked to two items!).
How can I pass a Carriage Return character to the other side without having the Write procedure break myData[1] into two separate lines?
Thanks a lot.
If you want TStrings.Text be oblivious to special characters - you should escape them before sending by net, and un-escape after that. There are a lot of ways of escaping, so choose one that suits you.
function EscapeString:(String): String --- your choice
function DeEscapeString:(String): String --- your choice
procedure SendEscapedStrings(const socket: TIdSocket; const data: TStrings);
var s: string; temp: TStringList;
begin
temp := TStringList.Create;
try
temp.Capacity := data.Count;
for s in data do
temp.Add( EscapeString( s ) );
socket.Write(temp);
finally
temp.Destroy;
end;
end;
procedure ReadDeescapedStrings(const socket: TIdSocket; const data: TStrings);
var s: string; temp: TStringList;
begin
temp := TStringList.Create;
try
Socket.ReadStrings(temp, -1);
data.Clear;
data.Capacity := temp.Count;
for s in temp do
temp.Add( DeEscapeString( s ) );
finally
temp.Destroy;
end;
end;
Now the question is what would you choose for DeEscapeString and EscapeString ? The options are many.
You can choose convert string to base64 before sending and from base64 after reading
You can choose UUEEncode for escapgin and UUEDecode for de-escaping
You can choose yEnc: http://en.wikipedia.org/wiki/YEnc
Or you can choose very simplistic functions StrStringToEscaped and StrEscapedToString from JclString unit of from Jedi CodeLib ( http://jcl.sf.net ):
what kind of escaping
If you ask for suggestion i would suggest not using raw TCP Server. There is well-known and standard HTTP protocol, there are many libraries for Delphi implementing both HTTP server and HTTP client. And in the protocol (and libraries) there are already decided things like ciphering, compressing, languages support, etc. And if somethign goes wrong - you can take any HTTP sniffer and see who is in the wrong- clent or server - with your own eyes. Debugging is much simpler.
If you are just starting, i suggest you looking into HTTP+JSON Synopse mORMot library, maybe it would cover your needs. You can take sample server code from http://robertocschneiders.wordpress.com/2012/11/22/datasnap-analysis-based-on-speed-stability-tests/ for example, or from demos in the lib.
Then, if to arrange around raw TCP server, i'd send compressed data, so it would work better (networks are slower than CPU usually). See http://docwiki.embarcadero.com/CodeExamples/XE5/en/ZLibCompressDecompress_(Delphi).
Sending:
1: Send into network (int32) - TStringList.Count
2: for every string doing
2.1 creating TStringStream from the string[i]
2.2 passing it via TZCompressionStream
2.3 sending (int32) size of compressed data
2.4 sending the data itself
2.5 freeing the temporary streams
Receiving
1: Receive from net (int32) - count of packets
1.1 ResultStringList.Clear; ResultStringList.Capacity := read_count.
2: for every string doing
2.1 creating TBytesStream
2.2 read from net (int32) size of compressed data
2.3 read N bytes from the network into BytesStream
2.4 unpack it via TZDecompressionStream into TStringStream
2.5 ResultStringList.Add( StringStream -> string );
2.6 freeing the temporary streams
Now, if you really don't want ot change almost anything, then JCL escaping would hopefully be enough for you. At least it worked for me, but my task was very different and was not about networks at all. But you can just test them all and see how it works for you.
Don't use the TStrings overload as it seems to use line breaks as separator between strings which does not work if your strings contain line breaks themselves.
You can easily write your own wrapper method to send a list of strings over the wire (take that as pseudocode):
procedure WriteStrings(IOHandler : TIdIOHandler; Strings : TStrings);
var
Str : String;
begin
IOHandler.WriteBufferOpen;
try
IOHandler.Write(Strings.Count);
for Str in Strings do
IOHandler.Write(Str);
finally
IOHandler.WriteBufferClose;
end;
end;
procedure ReadStrings(IOHandler : TIdIOHandler; Strings : TStrings);
var
Count, I : Integer;
begin
Count := IOHandler.ReadInteger;
for I := 1 to Count do
Strings.Add(IOHandler.ReadString);
end;
Our programming dept just spent about a non-mythical man-month tracking down what we think is a bug in a 3rd party component, here's their copyrighted source code:
function TGDIPPicture.GetImageSizes: boolean;
var
multi: TGPImage;
pstm: IStream;
hGlobal: THandle;
pcbWrite: Longint;
begin
result := false;
if Empty then
Exit;
if FDataStream.Size = 0 then
Exit;
hGlobal := GlobalAlloc(GMEM_MOVEABLE, FDataStream.Size);
if (hGlobal = 0) then
raise Exception.Create('Could not allocate memory for image');
try
pstm := nil;
// Create IStream* from global memory
CreateStreamOnHGlobal(hGlobal, TRUE, pstm);
pstm.Write(FDataStream.Memory, FDataStream.Size,#pcbWrite);
multi := TGPImage.Create(pstm);
FWidth := multi.GetWidth;
FHeight := multi.GetHeight;
Result := true;
multi.Free;
finally
GlobalFree(hGlobal);
end;
end;
We found the problem was with TMS's AdvOfficeTabSet. If we added tabs, then it crashed, if we didn't add tabs then it didn't crash. (the crash was one of those un-debuggable app hangs that hits you 10 steps after the real problem).
Following Raymond Chen's advice I replaced GMEM_MOVEABLE with GPTR and it appears to have fixed the problem.
I'm wondering if anyone can tell me if the above code had any legitimate reason for using GMEM_MOVEABLE. AFAIK it's only for the clipboard and it should always be used with GlobalAlloc.
while I was typing this another programmer got an error in the GlobalFree function using my code. So, apparently this doesn't work either. Could really use some help here!
*CreateStreamOnHGlobal is a Windows API function. (which apparently prefers GMEM_MOVEABLE)
*TGPImage is part of TMS's implementation of the GDI+ library.
Jonathan has identified the obvious problem, that being the double free of the HGLOBAL. But as you have found, the use is GMEM_MOVEABLE is correct.
Frankly, the code seems needlessly complex. I suggest you use the built in stream adapter and avoid any GlobalAlloc. To get an IStream you just need to do this:
pstm := TStreamAdapter.Create(FDataStream);
That's it.
I'm using both old good MPGTools and own, simple method of setting ID3 Tag in my MP3 files. But both approaches are too old to support ID3Tag version 2. I'm looking for any solution that would allow my application, written in Delphi 7, to either remove ID3Tag from each file it process or to set it to exactly the same values as ID3Tag version 1 is set.
Currently I'm removing ID3Tagv2 manually, using quick keyboard combination in Winamp.
I don't use v2 or album art or all these "new" addition, so the quickiest way to get rid of ID3Tagv2 (if it exists in particular file) would be all I need.
Of course I've tried to search the Internet using Google, but either I've got bad day or I'm asking wrong question, because all the results I'm getting on above mentioned questions are fake result from search engine stealers like Software Informer etc.
As it happens, one of my projects sitting here that is awaiting completion (about 80%, I'm more a hobbyist when it comes to Delphi and had more pressing stuff come up, then I found a program I was able to download which fit my requirements precisely) is a full ID3 tag editor for MP3 files. While v1 was super-easy, v2 is much harder. You can refer to the standard document for v2.3 here.
But I will confine myself to the points addressed here.
You might want ID3v2 tags depending on the application. My portable MP3 player only accepts v2 tags, which is what pushed me to do the project in the first place.
ID3v2 tags are written at the beginning of files in a variable length manner with variable numbers of tags which may or may not be present. Fortunately, the full length of the data should be in the first record if it's an ID3v2 tagged file. Hence, read the file locate the length of the ID3v2 data, then rewrite the file without the ID3v2 data and the tags are removed. Having the data at the beginning makes this necessary and is indeed a frustration. Anything I do in the future to the code would involve trying to change data in place. Some very dirty code follows, which AFAIR worked, but you will need to clean up if you use (I'm sure some here will be content to point out exactly how I should). But test it well just to be sure. Also be sure to ask if I missed anything from the unit I copied this out of (it's a 19.3KB pas file) that you would need:
type
sarray = array[0..3] of byte;
psarray = ^sarray;
ID3v2Header = packed record
identifier: array[0..2] of char;
major_version: byte;
minor_version: byte;
flags: byte;
size: DWord;
end;
function size_decodeh(insize: DWord): DWord;
{ decodes the size headers only, which does not use bit 7 in each byte,
requires MSB conversion as well }
var
outdval: DWord;
outd, ind: psarray;
tnext2, pnext2: byte;
begin
outdval := 0;
outd := #outdval;
ind := #insize;
tnext2 := ind^[2] shr 1;
pnext2 := ind^[1] shr 2;
outd^[0] := ind^[3] or ((ind^[2] and $01) shl 7);
outd^[1] := tnext2 or ((ind^[1] and $03) shl 6);
outd^[2] := pnext2 or ((ind^[0] and $07) shl 5);
outd^[3] := ind^[0] shr 3;
Result := outdval;
end;
procedure ID3v2_LoadData(filename: string; var memrec: pointer;
var outsize: integer);
{ procedure loads ID3v2 data from "filename". Returns outsize = 0 if
there is no ID3v2 data }
var
infile: file;
v1h: ID3V2Header;
begin
assign(infile, filename);
reset(infile, 1);
// read main header to get id3v2 size
blockread(infile, v1h, sizeof(v1h));
// detect if there is id3v2 data
if v1h.identifier = 'ID3' then
begin
outsize := size_decodeh(v1h.size);
// read ID3v2 header data
getmem(memrec, outsize);
blockread(infile, memrec^, outsize);
Close(infile);
end
else
outsize := 0;
end;
function id3v2_erase(infilestr: string): boolean;
{ erase all ID3v2 data. Data are stored at the beginning of file, so file
must be rewritten }
const
tempfilename = 'TMp#!0X.MP3';
var
memrec: pointer;
outsize, dataread: integer;
IsID3v2: boolean;
databuffer: array[1..32768] of byte;
newfile, origfile: file;
begin
// reuse service routine to get information
Id3v2_loaddata(infilestr, memrec, outsize);
// is there ID3v2 data?
if outsize > 0 then
begin
// need to clean up after the service routine
freemem(memrec);
// get amount of data to erase
outsize := outsize + sizeof(Id3v2Header);
writeln('Data to delete is: ', outsize, ' bytes.');
// now rewrite the file
AssignFile(origfile, infilestr);
reset(origfile, 1);
AssignFile(newfile, tempfilename);
rewrite(newfile, 1);
Seek(origfile, outsize);
repeat
blockread(origfile, databuffer, sizeof(databuffer), dataread);
blockwrite(newfile, databuffer, dataread);
until dataread = 0;
CloseFile(origfile);
CloseFile(newfile);
// rename temp file and delete original
DeleteFile(infilestr);
RenameFile(tempfilename, infilestr);
IsID3v2 := true;
end
else
IsID3v2 := false;
Result := IsID3v2;
end;
Full editing capability that works in most all situations is obviously a tougher hill to climb than that, but all the details are there in that document I linked to. Hopefully this helps you out.
There are few libs that works fine with ID3V2. Back in 2006 I did a big research to find Delphi library that supports most of the Id3V2 specification for Delphi 7.
And I found these 2:
Audio Tools Library (was the best for that moment). I think that it even could read/write tags in Unicode. Here is the unit Id3V2.pas
JVCL has component to work with Id3V2 tags. But it didn't had Unicode support for non-unicode Delphi in 2006.
Btw, if you do not use JVCL yet, it's not worth to install more than 600 components just to get Id3V2 support.
So, take a look at Audio Tools Library.
I had some code before I moved to Unicode and Delphi 2009 that appended some text to a log file a line at a time:
procedure AppendToLogFile(S: string);
// this function adds our log line to our shared log file
// Doing it this way allows Wordpad to open it at the same time.
var F, C1 : dword;
begin
if LogFileName <> '' then begin
F := CreateFileA(Pchar(LogFileName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_ALWAYS, 0, 0);
if F <> 0 then begin
SetFilePointer(F, 0, nil, FILE_END);
S := S + #13#10;
WriteFile(F, Pchar(S)^, Length(S), C1, nil);
CloseHandle(F);
end;
end;
end;
But CreateFileA and WriteFile are binary file handlers and are not appropriate for Unicode.
I need to get something to do the equivalent under Delphi 2009 and be able to handle Unicode.
The reason why I'm opening and writing and then closing the file for each line is simply so that other programs (such as WordPad) can open the file and read it while the log is being written.
I have been experimenting with TFileStream and TextWriter but there is very little documentation on them and few examples.
Specifically, I'm not sure if they're appropriate for this constant opening and closing of the file. Also I'm not sure if they can make the file available for reading while they have it opened for writing.
Does anyone know of a how I can do this in Delphi 2009 or later?
Conclusion:
Ryan's answer was the simplest and the one that led me to my solution. With his solution, you also have to write the BOM and convert the string to UTF8 (as in my comment to his answer) and then that worked just fine.
But then I went one step further and investigated TStreamWriter. That is the equivalent of the .NET function of the same name. It understands Unicode and provides very clean code.
My final code is:
procedure AppendToLogFile(S: string);
// this function adds our log line to our shared log file
// Doing it this way allows Wordpad to open it at the same time.
var F: TStreamWriter;
begin
if LogFileName <> '' then begin
F := TStreamWriter.Create(LogFileName, true, TEncoding.UTF8);
try
F.WriteLine(S);
finally
F.Free;
end;
end;
Finally, the other aspect I discovered is if you are appending a lot of lines (e.g. 1000 or more), then the appending to the file takes longer and longer and it becomes quite inefficient.
So I ended up not recreating and freeing the LogFile each time. Instead I keep it open and then it is very fast. The only thing I can't seem to do is allow viewing of the file with notepad while it is being created.
For logging purposes why use Streams at all?
Why not use TextFiles? Here is a very simple example of one of my logging routines.
procedure LogToFile(Data:string);
var
wLogFile: TextFile;
begin
AssignFile(wLogFile, 'C:\MyTextFile.Log');
{$I-}
if FileExists('C:\MyTextFile.Log') then
Append(wLogFile)
else
ReWrite(wLogFile);
WriteLn(wLogfile, S);
CloseFile(wLogFile);
{$I+}
IOResult; //Used to clear any possible remaining I/O errors
end;
I actually have a fairly extensive logging unit that uses critical sections for thread safety, can optionally be used for internal logging via the OutputDebugString command as well as logging specified sections of code through the use of sectional identifiers.
If anyone is interested I'll gladly share the code unit here.
Char and string are Wide since D2009. Thus you should use CreateFile instead of CreateFileA!
If you werite the string you shoudl use Length( s ) * sizeof( Char ) as the byte length and not only Length( s ). because of the widechar issue. If you want to write ansi chars, you should define s as AnsiString or UTF8String and use sizeof( AnsiChar ) as a multiplier.
Why are you using the Windows API function instead of TFileStream defined in classes.pas?
Try this little function I whipped up just for you.
procedure AppendToLog(filename,line:String);
var
fs:TFileStream;
ansiline:AnsiString;
amode:Integer;
begin
if not FileExists(filename) then
amode := fmCreate
else
amode := fmOpenReadWrite;
fs := TFileStream.Create(filename,{mode}amode);
try
if (amode<>fmCreate) then
fs.Seek(fs.Size,0); {go to the end, append}
ansiline := AnsiString(line)+AnsiChar(#13)+AnsiChar(#10);
fs.WriteBuffer(PAnsiChar(ansiline)^,Length(ansiline));
finally
fs.Free;
end;
Also, try this UTF8 version:
procedure AppendToLogUTF8(filename, line: UnicodeString);
var
fs: TFileStream;
preamble:TBytes;
outpututf8: RawByteString;
amode: Integer;
begin
if not FileExists(filename) then
amode := fmCreate
else
amode := fmOpenReadWrite;
fs := TFileStream.Create(filename, { mode } amode, fmShareDenyWrite);
{ sharing mode allows read during our writes }
try
{internal Char (UTF16) codepoint, to UTF8 encoding conversion:}
outpututf8 := Utf8Encode(line); // this converts UnicodeString to WideString, sadly.
if (amode = fmCreate) then
begin
preamble := TEncoding.UTF8.GetPreamble;
fs.WriteBuffer( PAnsiChar(preamble)^, Length(preamble));
end
else
begin
fs.Seek(fs.Size, 0); { go to the end, append }
end;
outpututf8 := outpututf8 + AnsiChar(#13) + AnsiChar(#10);
fs.WriteBuffer(PAnsiChar(outpututf8)^, Length(outpututf8));
finally
fs.Free;
end;
end;
If you try to use text file or Object Pascal typed/untyped files in a multithreaded application you gonna have a bad time.
No kidding - the (Object) Pascal standard file I/O uses global variables to set file mode and sharing. If your application runs in more than one thread (or fiber if anyone still use them) using standard file operations could result in access violations and unpredictable behavior.
Since one of the main purposes of logging is debugging a multithreaded application, consider using other means of file I/O: Streams and Windows API.
(And yes, I know it is not really an answer to the original question, but I do not wish to log in - therefor I do not have the reputation score to comment on Ryan J. Mills's practically wrong answer.)