I'm trying to use the SysUtils.WrapText() function with a string containing escaped single quotes characters, and I'm getting an unexpected result.
var
Lines : TStrings;
begin
Lines := TStringList.Create;
try
Lines.Text := WrapText('Can''t format message, message file not found', 15);
ShowMessage(Lines.Text);
finally
Lines.Free;
end;
end;
It seems that the function doesn't wrap the string at all, if the string contains an apostrophe character.
I've also tried using the #39 code instead of the single quote char but the problem persists. Furthermore I've checked Lines.Count and it's 1.
I've tried removing the single quote character:
var
Lines : TStrings;
begin
Lines := TStringList.Create;
try
Lines.Text := WrapText('Cant format message, message file not found', 15);
ShowMessage(Lines.Text);
finally
Lines.Free;
end;
end;
And it started wrapping the string as expected:
I'm wondering why is this happening, and how should I do for using the WrapText() function with such strings?
What you describe is intentional behavior.
In Delphi XE and earlier, the WrapText() documentation included this statement:
WrapText does not insert a break into an embedded quoted string (both single quotation marks and double quotation marks are supported).
In Delphi XE2 onwards, that statement is omitted from the documentation, but the behavior is still implemented in the RTL.
I have opened a ticket with Embarcadero about this omission:
RSP-24114: Important clause about embedded quoted strings is missing from WrapText documentation
In 10.3.1 the source includes code for handling quote characters, both double and single quotes, which looks to just ignore text between them. So one solution would be to use an apostrophe that is different from the single quote character. A second would to be to avoid using contractions. The start of the function source:
function WrapText(const Line, BreakStr: string; const BreakChars: TSysCharSet;
MaxCol: Integer): string;
const
QuoteChars = ['''', '"'];
FirstIndex = Low(string);
StrAdjust = 1 - Low(string);
var
...
One option:
Lines.Text := WrapText('Can`t format message, message file not found', 15);
A second option:
Lines.Text := WrapText('Cannot format message, message file not found', 15);
Related
I am using DCPcrypt and SHA512 to hash strings.
I am using the version by Warren Postma https://bitbucket.org/wpostma/dcpcrypt2010
It is working fine. However it failes with german umlauts like ä, ö, ü and probably other unicodes.
I am using the library like this:
function TForm1.genhash(str: string): string;
var
Hash : TDCP_sha512;
Digest: array[0..63] of byte;
i: integer;
s: string;
begin
s:= '';
hash := TDCP_sha512.Create(nil);
if hash<>nil then
begin
try
Hash.Init;
Hash.UpdateStr(str);
Hash.Final(Digest);
for i:= 0 to length(Digest)-1 do
s:= s + IntToHex(Digest[i],2);
finally
hash.free;
end;
end;
Result := s;
end;
When i input the letter ä i expect the output to be:
64868C5784A6004E675BCF405F549369BF607CD3269C0CAC1711E21BA9F40A5ABBF0C7535856E7CF77EA55A072DD04AA89EEA361E95F497AA965309B50587157
I checked it with those sites:
http://hashgenerator.de/
http://passwordsgenerator.net/sha512-hash-generator/
However i get:
1A7F725BD18E062020A646D4639F264891368863160A74DF2BFC069C4DADE04E6FA854A2474166EED0914B922A9D8BE0C89858D437DDD7FBCA5C9C89FC07323A
So my question is:
How can i use the DCPcrypt library to generate hashes for german umlauts? THanks
This must be the single most common mistake that people make with hashing and encryption. These algos operate on binary data, but you are passing text. Something somewhere has got to encode that text as binary. And what encoding should be used. How do you know that your library uses the same as the online tool? You don't.
So, here's a rule for you to follow. Never hash text. Just don't do it. Encode the text as binary using a well-defined, explicitly chosen encoding. And hash that. I suggest you encode as UTF-8 and hash that. So, TEncoding.UTF8.GetBytes(...) is your friend here.
Now, looking at the actual detail here, you are calling this method:
procedure UpdateStr(const Str: RawByteString);
The RawByteString parameter, means that your Unicode text is being converted to an ANSI string, with the default system code page. I'm sure that's not what you intend to happen. Indeed the compiler says this:
[dcc32 Warning] W1058 Implicit string cast with potential data loss from 'string' to 'RawByteString'
So the compiler is telling you that you are doing something wrong. You really must take good heed of compiler messages.
Now, you could call UpdateUnicodeStr instead of UpdateStr. But again, how do you know what encoding is used? It happens to be the native internal encoding, UTF-16LE.
But, let's follow my rule of never encoding text.
{$APPTYPE CONSOLE}
uses
SysUtils, Classes, DCPsha512;
function genhash(str: string): string;
var
Bytes: TBytes;
Hash: TDCP_sha512;
Digest: array[0..63] of byte;
begin
Bytes := TEncoding.UTF8.GetBytes(str); // encode text as UTF-8 bytes
hash := TDCP_sha512.Create(nil);
try
Hash.Init;
Hash.Update(Pointer(Bytes)^, Length(Bytes));
Hash.Final(Digest);
finally
hash.Free;
end;
// convert the digest to a hex hash string
SetLength(Result, Length(Digest)*2);
BinToHex(Digest, PChar(Result), Length(Digest));
end;
begin
Writeln(genhash('ä'));
Readln;
end.
Output
64868C5784A6004E675BCF405F549369BF607CD3269C0CAC1711E21BA9F40A5ABBF0C7535856E7CF77EA55A072DD04AA89EEA361E95F497AA965309B50587157
Note that I simplified the code in some other ways. I removed the local string variable and worked directly with Result. I used BinToHex from the Classes unit to do the digest to hex conversion. I also changed this code:
hash := TDCP_sha512.Create(nil);
if hash<>nil then
....
to remove the if statement which is not needed. If a constructor fails, an exception is raised.
Please follow my rule never to hash text. It will serve you well!
I have an .URL file which contains the following text which contains a German Umlaut character:
[InternetShortcut]
URL=http://edn.embarcadero.com/article/44358
[MyApp]
Notes=Special Test geändert
Icon=default
Title=Bug fix list for RAD Studio XE8
I try to load the text with TMemIniFile:
uses System.IniFiles;
//
procedure TForm1.Button1Click(Sender: TObject);
var
BookmarkIni: TMemIniFile;
begin
// The error occurs here:
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url',
TEncoding.UTF8);
try
// Some code here
finally
BookmarkIni.Free;
end;
end;
This is the error message text from the debugger:
Project MyApp.exe raised exception class EEncodingError with message
'No mapping for the Unicode character exists in the target multi-byte
code page'.
When I remove the word with the German Umlaut character "geändert" from the .URL file then there is NO error.
But that's why I use TMemIniFile, because TIniFile does not work here when the text in the .URL file contains Unicode characters. (There could also be other Unicode characters in the .URL file).
So why I get an exception here in TMemIniFile.Create?
EDIT: Found the culprit: The .URL file is in ANSI format. The error does not happen when the .URL file is in UTF-8 format. But what can I do when the file is in ANSI format?
EDIT2: I've created a workaround which does work BOTH with ANSI and UTF-8 files:
procedure TForm1.Button1Click(Sender: TObject);
var
BookmarkIni: TMemIniFile;
BookmarkIni_: TIniFile;
ThisFileIsAnsi: Boolean;
begin
try
ThisFileIsAnsi := False;
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url',
TEncoding.UTF8);
except
BookmarkIni_ := TIniFile.Create('F:\Bug fix list for RAD Studio XE8.url');
ThisFileIsAnsi := True;
end;
try
// Some code here
finally
if ThisFileIsAnsi then
BookmarkIni_.Free
else
BookmarkIni.Free;
end;
end;
What do you think?
It is not possible, in general, to auto-detect the encoding of a file from its contents.
A clear demonstration of this is given by this article from Raymond Chen: The Notepad file encoding problem, redux. Raymond uses the example of a file containing these two bytes:
D0 AE
Raymond goes on to show that this is a well formed file with the following four encodings: ANSI 1252, UTF-8, UTF-16BE and UTF-16LE.
The take home lesson here is that you have to know the encoding of your file. Either agree it by convention with whoever writes the file. Or enforce the presence of a BOM.
You need to decide on what the encoding of the file is, once and for all. There's no fool proof way to auto-detect this, so you'll have to enforce it from your code that creates these files.
If the creation of this file is outside your control, then you are more or less out of luck. You can try to rely of the BOM (Byte-Order-Mark) at the beginning of the file (which should be there if it is a UTF-8 file). I can't see from the specification of the TMemIniFile what the CREATE constructor without an encoding parameter assumes about the encoding of the file (my guess is that it follows the BOM and if there's no such thing, it assumes ANSI, ie. system codepage).
One thing you can do - if you decide to stick to your current method - is to change your code to:
procedure TForm1.Button1Click(Sender: TObject);
var
BookmarkIni: TCustomIniFile;
begin
// The error occurs here:
try
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url',
TEncoding.UTF8);
except
BookmarkIni := TIniFile.Create('F:\Bug fix list for RAD Studio XE8.url');
end;
try
// Some code here
finally
BookmarkIni.Free;
end;
end;
You don't need two separate variables, as both TIniFile and TMemIniFile (as well as TRegistryIniFile) all have a common ancestor: TCustomIniFile. By declaring your variable as this common ancestor, you can instantiate (create) it as any of the class types that inherit from TCustomIniFile. The actual (run-time) type is determined depending on which construtcor you're calling to create.
But first, you should try to use
BookmarkIni := TMemIniFile.Create('F:\Bug fix list for RAD Studio XE8.url');
ie. without any encoding specified, and see if it works with both ANSI and UTF-8 files.
EDIT: Here's a test program to verify my claim made in the comments:
program Project21;
{$APPTYPE CONSOLE}
uses
IniFiles, System.SysUtils;
const
FileName = 'F:\Bug fix list for RAD Studio XE8.url';
var
TXT : TextFile;
procedure Test;
var
BookmarkIni: TCustomIniFile;
begin
try
BookmarkIni := TMemIniFile.Create(FileName,TEncoding.UTF8);
except
BookmarkIni := TIniFile.Create(FileName);
end;
try
Writeln(BookmarkIni.ReadString('MyApp','Notes','xxx'))
finally
BookmarkIni.Free;
end;
end;
begin
try
AssignFile(TXT,FileName); REWRITE(TXT);
try
WRITELN(TXT,'[InternetShortcut]');
WRITELN(TXT,'URL=http://edn.embarcadero.com/article/44358');
WRITELN(TXT,'[MyApp]');
WRITELN(TXT,'Notes=The German a umlaut consists of the following two ANSI characters: '#$C3#$A4);
WRITELN(TXT,'Icon=default');
WRITELN(TXT,'Title=Bug fix list for RAD Studio XE8');
finally
CloseFile(TXT)
end;
Test;
ReadLn
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
The rule of thumb - to read data (file, stream whatever) correctly you must know the encoding! And the best solution is to let user to choose encoding or force one e.g. utf-8.
Moreover, the information ANSI does make things easier without code page.
A must read - The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
Other approach is to try to detect encoding (like browsers do with sites if no encoding specified). Detecting UTF is relatively easy if BOM exists, but more often is omitted. Take a look Mozilla's universalchardet or chsdet.
For URL-Encoding/Decoding, it has been suggested to use TNetEncoding from Delphi XE7 upwards.
So in Delphi XE8 i use this code for example:
uses
System.NetEncoding;
...
edtEncodedURL.Text := TNetEncoding.URL.Encode('SetFont(''Arial'',15)');
which gives the following result:
SetFont('Arial'%2C15)
However, this does not encode the single quote characters, as you can see from the example above.
Moreover, at W3Schools.com HTML URL Encoding Reference, in the "Try It Yourself" section, the string SetFont('Arial',15) is URL-encoded to:
SetFont%28%27Arial%27%2C15%29
So is there a way to URL-encode also the single quote characters in Delphi XE8?
This seems to work for the purpose asked in the question:
uses
System.NetEncoding,
REST.Utils;
procedure TForm1.btnURLEncodeClick(Sender: TObject);
begin
edtEncodedURL.Text := REST.Utils.URIEncode(edtOriginalURL.Text);
end;
procedure TForm1.btnURLDecodeClick(Sender: TObject);
begin
edtDecodedURL.Text := TNetEncoding.URL.Decode(edtEncodedURL.Text);
end;
I am trying to send commands directly to a Zebra TLP2844 printer. I followed the suggestion made here and my final code came to be as follows:
var
cm: String;
p: TPrinter;
i: integer;
begin
p := Printer;
p.BeginDoc;
for i := 0 to memo1.Lines.Count-2 do
begin
cm := memo1.Lines[i];
if Escape(p.Canvas.Handle,
PASSTHROUGH,
Length(cm),
PAnsiChar(cm),
nil) = 0 then
ShowMessage('Command error: ' + IntToStr(GetLastError));
end;
p.EndDoc;
end;
The content of memo1 is (first line is empty) as purposed here:
N
q609
Q203,26
B26,26,0,UA0,2,2,152,B,"603679025109"
A253,26,0,3,1,1,N,"SKU 6205518 MFG 6354"
A253,56,0,3,1,1,N,"2XIST TROPICAL BEACH"
A253,86,0,3,1,1,N,"STRIPE SQUARE CUT TRUNK"
A253,116,0,3,1,1,N,"BRICK"
A253,146,0,3,1,1,N,"X-LARGE"
P1,1
The commands don't seem to be properly received or interpreted by the printer. I checked that the printer is in Page Mode (EPL2), with the suggested code I am able to open the printer handle. But nothing is printed, only a new line of labels is feeded.
I tried to completely change the commands to something obviously wrong and the behaviour is the same.
What else should I be looking to get things printed?
Most printers that take raw commands require a prefix (starting sequence of characters) and suffix (ending sequence of chars) wrapping each command. I don't know what the prefix and suffix are for the Zebra, but the documentation should tell you.
Just add a pair of constants to define the prefix and suffix, and add them to your command before sending it.
The other issue might be that you're reading the content of your commands from a TMemo, which in Delphi 2009 and higher contains Unicode strings. You're then casting them down to PAnsiChar, which may be truncating the content. Do the conversion ahead of time by defining cm as an AnsiString, and then assigning to it first (as you are) before typecasting to pass to the Escape function. I've done this in my code to illustrate it.
var
cm: AnsiString;
p: TPrinter;
i: integer;
const
ZPrefix = AnsiString('$('); // Replace values for each of these with what
ZSuffix = AnsiString(')$'); // your documentation says you should use
begin
p := Printer;
p.BeginDoc;
for i := 0 to memo1.Lines.Count-2 do
begin
cm := ZPrefix + memo1.Lines[i] + ZSuffix;
if Escape(p.Canvas.Handle,
PASSTHROUGH,
Length(cm),
PAnsiChar(cm),
nil) = 0 then
ShowMessage('Command error: ' + IntToStr(GetLastError));
end;
p.EndDoc;
end;
I program in php which is like C
I can send things to the printer just fine
my code looks like your code the only thing is I am not sure how your programming language handles the newline in php it's \n at the end of each line
if the newline is not there the print job will not print
and if the " are not sent it will not print
your EPL looks fine and should print
there is somewhere on the zebra web site a download where you can send commands to a printer which is hooked up to your computer by USB cable
think it is called Zebra Setup Utilities
I have a string that contains null characters.
I've tried to save it to a file with this code:
myStringList.Text := myString;
myStringList.SaveToFile('c:\myfile');
Unfortunately myStringList.Text is empty if the source string has a null character at the beginning.
I thought only C string were terminated by a null character, and Delphi was always fine with it.
How to save the content of the string to a file?
I think you mean "save a string that has #0 characters in it".
If that's the case, don't try and put it in a TStringList. In fact, don't try to save it as a string at all; just like in C, a NULL character (#0 in Delphi) causes the string to be truncated at times. Use a TFileStream and write it directly as byte content:
var
FS: TFileStream;
begin
FS := TFileStream.Create('C:\MyFile', fmCreate);
try
FS.Write(myString[1], Length(myString) * SizeOf(Char));
finally
FS.Free;
end;
end;
To read it back:
var
FS: TFileStream;
begin
FS := TFileStream.Create('C:\MyFile', fmOpenRead);
try
SetLength(MyString, FS.Size);
FS.Read(MyString[1], FS.Size);
finally
FS.Free;
end;
end;
When you set the Text property of a TStrings object, the new value is parsed as a null-terminated string. Therefore when the code reaches your null character, the parsing stops.
I'm not sure why the Delphi RTL code was designed that way, and its not documented, but that's just how setting the Text property works.
You can avoid this by using the Add method rather than the Text property.
myStringList.Clear;
myStringList.Add(myString);
myStringList.SaveToFile(FileName);
About writing strings to a file in general.. I still see people creating streams or stringlists just to write some stuff to a file, and then destroy the stream or stringlist.
Delphi7 didn't have IOUtuls.pas yet, but you're missing out on that.
There's a handy TFile record with class methods that lets you write text to a file with a single line, without requiring temporary variables:
TFile.WriteAllText('out.txt','hi');
Upgrading makes your life as a Delphi developer a lot easier. This is just a tiny example.