delphimvcframework/lib/dmustache/SynTable.pas

18190 lines
609 KiB
ObjectPascal
Raw Normal View History

/// filter/database/cache/buffer/security/search/multithread/OS features
// - as a complement to SynCommons, which tended to increase too much
// - licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynTable;
(*
This file is part of Synopse framework.
2021-04-23 10:11:05 +02:00
Synopse framework. Copyright (C) 2021 Arnaud Bouchez
Synopse Informatique - https://synopse.info
*** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.
The Original Code is Synopse framework.
The Initial Developer of the Original Code is Arnaud Bouchez.
2021-04-23 10:11:05 +02:00
Portions created by the Initial Developer are Copyright (C) 2021
the Initial Developer. All Rights Reserved.
Contributor(s):
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****
A lot of code has moved from SynCommons.pas and mORMot.pas, to reduce the
number of source code lines of those units, and circumvent Delphi 5/6/7
limitations (e.g. internal error PRO-3006)
*)
interface
{$I Synopse.inc} // define HASINLINE CPU32 CPU64
uses
{$ifdef MSWINDOWS}
Windows,
Messages,
{$else}
{$ifdef KYLIX3}
Types,
LibC,
SynKylix,
{$endif KYLIX3}
{$ifdef FPC}
BaseUnix,
Unix,
{$endif FPC}
{$endif MSWINDOWS}
SysUtils,
Classes,
{$ifndef LVCL}
SyncObjs, // for TEvent and TCriticalSection
Contnrs, // for TObjectList
{$endif}
{$ifndef NOVARIANTS}
Variants,
{$endif}
SynCommons;
{ ************ text search and functions ****************** }
type
PMatch = ^TMatch;
TMatchSearchFunction = function(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
/// low-level structure used by IsMatch() for actual glob search
// - you can use this object to prepare a given pattern, e.g. in a loop
// - implemented as a fast brute-force state-machine without any heap allocation
// - some common patterns ('exactmatch', 'startwith*', '*endwith', '*contained*')
// are handled with dedicated code, optionally with case-insensitive search
// - consider using TMatchs (or SetMatchs/TMatchDynArray) if you expect to
// search for several patterns, or even TExprParserMatch for expression search
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TMatch = record
{$else}TMatch = object{$endif}
private
Pattern, Text: PUTF8Char;
P, T, PMax, TMax: PtrInt;
Upper: PNormTable;
State: (sNONE, sABORT, sEND, sLITERAL, sPATTERN, sRANGE, sVALID);
procedure MatchAfterStar;
procedure MatchMain;
public
/// published for proper inlining
Search: TMatchSearchFunction;
/// initialize the internal fields for a given glob search pattern
// - note that the aPattern instance should remain in memory, since it will
// be pointed to by the Pattern private field of this object
procedure Prepare(const aPattern: RawUTF8; aCaseInsensitive, aReuse: boolean); overload;
/// initialize the internal fields for a given glob search pattern
// - note that the aPattern buffer should remain in memory, since it will
// be pointed to by the Pattern private field of this object
procedure Prepare(aPattern: PUTF8Char; aPatternLen: integer;
aCaseInsensitive, aReuse: boolean); overload;
/// initialize low-level internal fields for'*aPattern*' search
// - this method is faster than a regular Prepare('*' + aPattern + '*')
// - warning: the supplied aPattern variable may be modified in-place to be
// filled with some lookup buffer, for length(aPattern) in [2..31] range
procedure PrepareContains(var aPattern: RawUTF8; aCaseInsensitive: boolean); overload;
/// initialize low-level internal fields for a custom search algorithm
procedure PrepareRaw(aPattern: PUTF8Char; aPatternLen: integer;
aSearch: TMatchSearchFunction);
/// returns TRUE if the supplied content matches the prepared glob pattern
// - this method is not thread-safe
function Match(const aText: RawUTF8): boolean; overload;
{$ifdef FPC}inline;{$endif}
/// returns TRUE if the supplied content matches the prepared glob pattern
// - this method is not thread-safe
function Match(aText: PUTF8Char; aTextLen: PtrInt): boolean; overload;
{$ifdef HASINLINE}inline;{$endif}
/// returns TRUE if the supplied content matches the prepared glob pattern
// - this method IS thread-safe, and won't lock
function MatchThreadSafe(const aText: RawUTF8): boolean;
/// returns TRUE if the supplied VCL/LCL content matches the prepared glob pattern
// - this method IS thread-safe, will use stack to UTF-8 temporary conversion
// if possible, and won't lock
function MatchString(const aText: string): boolean;
/// returns TRUE if this search pattern matches another
function Equals(const aAnother{$ifndef DELPHI5OROLDER}: TMatch{$endif}): boolean;
{$ifdef HASINLINE}inline;{$endif}
/// access to the pattern length as stored in PMax + 1
function PatternLength: integer; {$ifdef HASINLINE}inline;{$endif}
/// access to the pattern text as stored in Pattern
function PatternText: PUTF8Char; {$ifdef HASINLINE}inline;{$endif}
end;
/// use SetMatchs() to initialize such an array from a CSV pattern text
TMatchDynArray = array of TMatch;
/// TMatch descendant owning a copy of the Pattern string to avoid GPF issues
TMatchStore = record
/// access to the research criteria
// - defined as a nested record (and not an object) to circumvent Delphi bug
Pattern: TMatch;
/// Pattern.Pattern PUTF8Char will point to this instance
PatternInstance: RawUTF8;
end;
TMatchStoreDynArray = array of TMatchStore;
/// stores several TMatch instances, from a set of glob patterns
TMatchs = class(TSynPersistent)
protected
fMatch: TMatchStoreDynArray;
fMatchCount: integer;
public
/// add once some glob patterns to the internal TMach list
// - aPatterns[] follows the IsMatch() syntax
constructor Create(const aPatterns: TRawUTF8DynArray; CaseInsensitive: Boolean); reintroduce; overload;
/// add once some glob patterns to the internal TMach list
// - aPatterns[] follows the IsMatch() syntax
procedure Subscribe(const aPatterns: TRawUTF8DynArray; CaseInsensitive: Boolean); overload; virtual;
/// add once some glob patterns to the internal TMach list
// - each CSV item in aPatterns follows the IsMatch() syntax
procedure Subscribe(const aPatternsCSV: RawUTF8; CaseInsensitive: Boolean); overload;
/// search patterns in the supplied UTF-8 text
// - returns -1 if no filter has been subscribed
// - returns -2 if there is no match on any previous pattern subscription
// - returns fMatch[] index, i.e. >= 0 number on first matching pattern
// - this method is thread-safe
function Match(const aText: RawUTF8): integer; overload; {$ifdef HASINLINE}inline;{$endif}
/// search patterns in the supplied UTF-8 text buffer
function Match(aText: PUTF8Char; aLen: integer): integer; overload;
/// search patterns in the supplied VCL/LCL text
// - could be used on a TFileName for instance
// - will avoid any memory allocation if aText is small enough
function MatchString(const aText: string): integer;
end;
/// fill the Match[] dynamic array with all glob patterns supplied as CSV
// - returns how many patterns have been set in Match[|]
// - note that the CSVPattern instance should remain in memory, since it will
// be pointed to by the Match[].Pattern private field
function SetMatchs(const CSVPattern: RawUTF8; CaseInsensitive: boolean;
out Match: TMatchDynArray): integer; overload;
/// fill the Match[0..MatchMax] static array with all glob patterns supplied as CSV
// - note that the CSVPattern instance should remain in memory, since it will
// be pointed to by the Match[].Pattern private field
function SetMatchs(CSVPattern: PUTF8Char; CaseInsensitive: boolean;
Match: PMatch; MatchMax: integer): integer; overload;
/// search if one TMach is already registered in the Several[] dynamic array
function MatchExists(const One: TMatch; const Several: TMatchDynArray): boolean;
/// add one TMach if not already registered in the Several[] dynamic array
function MatchAdd(const One: TMatch; var Several: TMatchDynArray): boolean;
/// returns TRUE if Match=nil or if any Match[].Match(Text) is TRUE
function MatchAny(const Match: TMatchDynArray; const Text: RawUTF8): boolean;
/// apply the CSV-supplied glob patterns to an array of RawUTF8
// - any text not maching the pattern will be deleted from the array
procedure FilterMatchs(const CSVPattern: RawUTF8; CaseInsensitive: boolean;
var Values: TRawUTF8DynArray);
/// return TRUE if the supplied content matchs a glob pattern
// - ? Matches any single characer
// - * Matches any contiguous characters
// - [abc] Matches a or b or c at that position
// - [^abc] Matches anything but a or b or c at that position
// - [!abc] Matches anything but a or b or c at that position
// - [a-e] Matches a through e at that position
// - [abcx-z] Matches a or b or c or x or y or or z, as does [a-cx-z]
// - 'ma?ch.*' would match match.exe, mavch.dat, march.on, etc..
// - 'this [e-n]s a [!zy]est' would match 'this is a test', but would not
// match 'this as a test' nor 'this is a zest'
// - consider using TMatch or TMatchs if you expect to reuse the pattern
function IsMatch(const Pattern, Text: RawUTF8; CaseInsensitive: boolean=false): boolean;
/// return TRUE if the supplied content matchs a glob pattern, using VCL strings
// - is a wrapper around IsMatch() with fast UTF-8 conversion
function IsMatchString(const Pattern, Text: string; CaseInsensitive: boolean=false): boolean;
type
/// available pronunciations for our fast Soundex implementation
TSynSoundExPronunciation =
(sndxEnglish, sndxFrench, sndxSpanish, sndxNone);
TSoundExValues = array[0..ord('Z')-ord('B')] of byte;
PSoundExValues = ^TSoundExValues;
PSynSoundEx = ^TSynSoundEx;
/// fast search of a text value, using the Soundex approximation mechanism
// - Soundex is a phonetic algorithm for indexing names by sound,
// as pronounced in a given language. The goal is for homophones to be
// encoded to the same representation so that they can be matched despite
// minor differences in spelling
// - this implementation is very fast and can be used e.g. to parse and search
// in a huge text buffer
// - this version also handles french and spanish pronunciations on request,
// which differs from default Soundex, i.e. English
TSynSoundEx = object
protected
Search, FirstChar: cardinal;
fValues: PSoundExValues;
public
/// prepare for a Soundex search
// - you can specify another language pronunciation than default english
function Prepare(UpperValue: PAnsiChar;
Lang: TSynSoundExPronunciation=sndxEnglish): boolean; overload;
/// prepare for a custom Soundex search
// - you can specify any language pronunciation from raw TSoundExValues array
function Prepare(UpperValue: PAnsiChar; Lang: PSoundExValues): boolean; overload;
/// return true if prepared value is contained in a text buffer
// (UTF-8 encoded), by using the SoundEx comparison algorithm
// - search prepared value at every word beginning in U^
function UTF8(U: PUTF8Char): boolean;
/// return true if prepared value is contained in a ANSI text buffer
// by using the SoundEx comparison algorithm
// - search prepared value at every word beginning in A^
function Ansi(A: PAnsiChar): boolean;
end;
/// Retrieve the Soundex value of a text word, from Ansi buffer
// - Return the soundex value as an easy to use cardinal value, 0 if the
// incoming string contains no valid word
// - if next is defined, its value is set to the end of the encoded word
// (so that you can call again this function to encode a full sentence)
function SoundExAnsi(A: PAnsiChar; next: PPAnsiChar=nil;
Lang: TSynSoundExPronunciation=sndxEnglish): cardinal; overload;
/// Retrieve the Soundex value of a text word, from Ansi buffer
// - Return the soundex value as an easy to use cardinal value, 0 if the
// incoming string contains no valid word
// - if next is defined, its value is set to the end of the encoded word
// (so that you can call again this function to encode a full sentence)
function SoundExAnsi(A: PAnsiChar; next: PPAnsiChar; Lang: PSoundExValues): cardinal; overload;
/// Retrieve the Soundex value of a text word, from UTF-8 buffer
// - Return the soundex value as an easy to use cardinal value, 0 if the
// incoming string contains no valid word
// - if next is defined, its value is set to the end of the encoded word
// (so that you can call again this function to encode a full sentence)
// - very fast: all UTF-8 decoding is handled on the fly
function SoundExUTF8(U: PUTF8Char; next: PPUTF8Char=nil;
Lang: TSynSoundExPronunciation=sndxEnglish): cardinal;
const
/// number of bits to use for each interresting soundex char
// - default is to use 8 bits, i.e. 4 soundex chars, which is the
// standard approach
// - for a more detailled soundex, use 4 bits resolution, which will
// compute up to 7 soundex chars in a cardinal (that's our choice)
SOUNDEX_BITS = 4;
{ ************ filtering and validation classes and functions ************** }
/// convert an IPv4 'x.x.x.x' text into its 32-bit value
// - returns TRUE if the text was a valid IPv4 text, unserialized as 32-bit aValue
// - returns FALSE on parsing error, also setting aValue=0
// - '' or '127.0.0.1' will also return false
function IPToCardinal(P: PUTF8Char; out aValue: cardinal): boolean; overload;
/// convert an IPv4 'x.x.x.x' text into its 32-bit value
// - returns TRUE if the text was a valid IPv4 text, unserialized as 32-bit aValue
// - returns FALSE on parsing error, also setting aValue=0
// - '' or '127.0.0.1' will also return false
function IPToCardinal(const aIP: RawUTF8; out aValue: cardinal): boolean; overload;
{$ifdef HASINLINE}inline;{$endif}
/// convert an IPv4 'x.x.x.x' text into its 32-bit value, 0 or localhost
// - returns <> 0 value if the text was a valid IPv4 text, 0 on parsing error
// - '' or '127.0.0.1' will also return 0
function IPToCardinal(const aIP: RawUTF8): cardinal; overload;
{$ifdef HASINLINE}inline;{$endif}
/// return TRUE if the supplied content is a valid email address
// - follows RFC 822, to validate local-part@domain email format
function IsValidEmail(P: PUTF8Char): boolean;
/// return TRUE if the supplied content is a valid IP v4 address
function IsValidIP4Address(P: PUTF8Char): boolean;
type
TSynFilterOrValidate = class;
TSynFilterOrValidateObjArray = array of TSynFilterOrValidate;
TSynFilterOrValidateObjArrayArray = array of TSynFilterOrValidateObjArray;
/// will define a filter (transformation) or a validation process to be
// applied to a database Record content (typicaly a TSQLRecord)
// - the optional associated parameters are to be supplied JSON-encoded
TSynFilterOrValidate = class
protected
fParameters: RawUTF8;
/// children must override this method in order to parse the JSON-encoded
// parameters, and store it in protected field values
procedure SetParameters(const Value: RawUTF8); virtual;
public
/// add the filter or validation process to a list, checking if not present
// - if an instance with the same class type and parameters is already
// registered, will call aInstance.Free and return the exising instance
// - if there is no similar instance, will add it to the list and return it
function AddOnce(var aObjArray: TSynFilterOrValidateObjArray;
aFreeIfAlreadyThere: boolean=true): TSynFilterOrValidate;
public
/// initialize the filter (transformation) or validation instance
// - most of the time, optional parameters may be specified as JSON,
// possibly with the extended MongoDB syntax
constructor Create(const aParameters: RawUTF8=''); overload; virtual;
/// initialize the filter or validation instance
/// - this overloaded constructor will allow to easily set the parameters
constructor CreateUTF8(const Format: RawUTF8; const Args, Params: array of const); overload;
/// the optional associated parameters, supplied as JSON-encoded
property Parameters: RawUTF8 read fParameters write SetParameters;
end;
/// will define a validation to be applied to a Record (typicaly a TSQLRecord)
// field content
// - a typical usage is to validate an email or IP adress e.g.
// - the optional associated parameters are to be supplied JSON-encoded
TSynValidate = class(TSynFilterOrValidate)
public
/// perform the validation action to the specified value
// - the value is expected by be UTF-8 text, as generated by
// TPropInfo.GetValue e.g.
// - if the validation failed, must return FALSE and put some message in
// ErrorMsg (translated into the current language: you could e.g. use
// a resourcestring and a SysUtils.Format() call for automatic translation
// via the mORMoti18n unit - you can leave ErrorMsg='' to trigger a
// generic error message from clas name ('"Validate email" rule failed'
// for TSynValidateEmail class e.g.)
// - if the validation passed, will return TRUE
function Process(FieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean;
virtual; abstract;
end;
/// points to a TSynValidate variable
// - used e.g. as optional parameter to TSQLRecord.Validate/FilterAndValidate
PSynValidate = ^TSynValidate;
/// IP v4 address validation to be applied to a Record field content
// (typicaly a TSQLRecord)
// - this versions expect no parameter
TSynValidateIPAddress = class(TSynValidate)
protected
public
/// perform the IP Address validation action to the specified value
function Process(aFieldIndex: integer; const Value: RawUTF8;
var ErrorMsg: string): boolean; override;
end;
/// IP address validation to be applied to a Record field content
// (typicaly a TSQLRecord)
// - optional JSON encoded parameters are "AllowedTLD" or "ForbiddenTLD",
// expecting a CSV lis of Top-Level-Domain (TLD) names, e.g.
// $ '{"AllowedTLD":"com,org,net","ForbiddenTLD":"fr"}'
// $ '{AnyTLD:true,ForbiddenDomains:"mailinator.com,yopmail.com"}'
// - this will process a validation according to RFC 822 (calling the
// IsValidEmail() function) then will check for the TLD to be in one of
// the Top-Level domains ('.com' and such) or a two-char country, and
// then will check the TLD according to AllowedTLD and ForbiddenTLD
TSynValidateEmail = class(TSynValidate)
private
fAllowedTLD: RawUTF8;
fForbiddenTLD: RawUTF8;
fForbiddenDomains: RawUTF8;
fAnyTLD: boolean;
protected
/// decode all published properties from their JSON representation
procedure SetParameters(const Value: RawUTF8); override;
public
/// perform the Email Address validation action to the specified value
// - call IsValidEmail() function and check for the supplied TLD
function Process(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean; override;
/// allow any TLD to be allowed, even if not a generic TLD (.com,.net ...)
// - this may be mandatory since already over 1,300 new gTLD names or
// "strings" could become available in the next few years: there is a
// growing list of new gTLDs available at
// @http://newgtlds.icann.org/en/program-status/delegated-strings
// - the only restriction is that it should be ascii characters
property AnyTLD: boolean read fAnyTLD write fAnyTLD;
/// a CSV list of allowed TLD
// - if accessed directly, should be set as lower case values
// - e.g. 'com,org,net'
property AllowedTLD: RawUTF8 read fAllowedTLD write fAllowedTLD;
/// a CSV list of forbidden TLD
// - if accessed directly, should be set as lower case values
// - e.g. 'fr'
property ForbiddenTLD: RawUTF8 read fForbiddenTLD write fForbiddenTLD;
/// a CSV list of forbidden domain names
// - if accessed directly, should be set as lower case values
// - not only the TLD, but whole domains like 'cracks.ru,hotmail.com' or such
property ForbiddenDomains: RawUTF8 read fForbiddenDomains write fForbiddenDomains;
end;
/// glob case-sensitive pattern validation of a Record field content
// - parameter is NOT JSON encoded, but is some basic TMatch glob pattern
// - ? Matches any single characer
// - * Matches any contiguous characters
// - [abc] Matches a or b or c at that position
// - [^abc] Matches anything but a or b or c at that position
// - [!abc] Matches anything but a or b or c at that position
// - [a-e] Matches a through e at that position
// - [abcx-z] Matches a or b or c or x or y or or z, as does [a-cx-z]
// - 'ma?ch.*' would match match.exe, mavch.dat, march.on, etc..
// - 'this [e-n]s a [!zy]est' would match 'this is a test', but would not
// match 'this as a test' nor 'this is a zest'
// - pattern check IS case sensitive (TSynValidatePatternI is not)
// - this class is not as complete as PCRE regex for example,
// but code overhead is very small, and speed good enough in practice
TSynValidatePattern = class(TSynValidate)
protected
fMatch: TMatch;
procedure SetParameters(const Value: RawUTF8); override;
public
/// perform the pattern validation to the specified value
// - pattern can be e.g. '[0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
// - this method will implement both TSynValidatePattern and
// TSynValidatePatternI, checking the current class
function Process(aFieldIndex: integer; const Value: RawUTF8;
var ErrorMsg: string): boolean; override;
end;
/// glob case-insensitive pattern validation of a text field content
// (typicaly a TSQLRecord)
// - parameter is NOT JSON encoded, but is some basic TMatch glob pattern
// - same as TSynValidatePattern, but is NOT case sensitive
TSynValidatePatternI = class(TSynValidatePattern);
/// text validation to ensure that to any text field would not be ''
TSynValidateNonVoidText = class(TSynValidate)
public
/// perform the non void text validation action to the specified value
function Process(aFieldIndex: integer; const Value: RawUTF8;
var ErrorMsg: string): boolean; override;
end;
TSynValidateTextProps = array[0..15] of cardinal;
{$M+} // to have existing RTTI for published properties
/// text validation to be applied to any Record field content
// - default MinLength value is 1, MaxLength is maxInt: so a blank
// TSynValidateText.Create('') is the same as TSynValidateNonVoidText
// - MinAlphaCount, MinDigitCount, MinPunctCount, MinLowerCount and
// MinUpperCount allow you to specify the minimal count of respectively
// alphabetical [a-zA-Z], digit [0-9], punctuation [_!;.,/:?%$="#@(){}+-*],
// lower case or upper case characters
// - expects optional JSON parameters of the allowed text length range as
// $ '{"MinLength":5,"MaxLength":10,"MinAlphaCount":1,"MinDigitCount":1,
// $ "MinPunctCount":1,"MinLowerCount":1,"MinUpperCount":1}
TSynValidateText = class(TSynValidate)
private
/// used to store all associated validation properties by index
fProps: TSynValidateTextProps;
fUTF8Length: boolean;
protected
/// use sInvalidTextChar resourcestring to create a translated error message
procedure SetErrorMsg(fPropsIndex, InvalidTextIndex, MainIndex: integer;
var result: string);
/// decode "MinLength", "MaxLength", and other parameters into fProps[]
procedure SetParameters(const Value: RawUTF8); override;
public
/// perform the text length validation action to the specified value
function Process(aFieldIndex: integer; const Value: RawUTF8;
var ErrorMsg: string): boolean; override;
published
/// Minimal length value allowed for the text content
// - the length is calculated with UTF-16 Unicode codepoints, unless
// UTF8Length has been set to TRUE so that the UTF-8 byte count is checked
// - default is 1, i.e. a void text will not pass the validation
property MinLength: cardinal read fProps[0] write fProps[0];
/// Maximal length value allowed for the text content
// - the length is calculated with UTF-16 Unicode codepoints, unless
// UTF8Length has been set to TRUE so that the UTF-8 byte count is checked
// - default is maxInt, i.e. no maximum length is set
property MaxLength: cardinal read fProps[1] write fProps[1];
/// Minimal alphabetical character [a-zA-Z] count
// - default is 0, i.e. no minimum set
property MinAlphaCount: cardinal read fProps[2] write fProps[2];
/// Maximal alphabetical character [a-zA-Z] count
// - default is maxInt, i.e. no Maximum set
property MaxAlphaCount: cardinal read fProps[10] write fProps[10];
/// Minimal digit character [0-9] count
// - default is 0, i.e. no minimum set
property MinDigitCount: cardinal read fProps[3] write fProps[3];
/// Maximal digit character [0-9] count
// - default is maxInt, i.e. no Maximum set
property MaxDigitCount: cardinal read fProps[11] write fProps[11];
/// Minimal punctuation sign [_!;.,/:?%$="#@(){}+-*] count
// - default is 0, i.e. no minimum set
property MinPunctCount: cardinal read fProps[4] write fProps[4];
/// Maximal punctuation sign [_!;.,/:?%$="#@(){}+-*] count
// - default is maxInt, i.e. no Maximum set
property MaxPunctCount: cardinal read fProps[12] write fProps[12];
/// Minimal alphabetical lower case character [a-z] count
// - default is 0, i.e. no minimum set
property MinLowerCount: cardinal read fProps[5] write fProps[5];
/// Maximal alphabetical lower case character [a-z] count
// - default is maxInt, i.e. no Maximum set
property MaxLowerCount: cardinal read fProps[13] write fProps[13];
/// Minimal alphabetical upper case character [A-Z] count
// - default is 0, i.e. no minimum set
property MinUpperCount: cardinal read fProps[6] write fProps[6];
/// Maximal alphabetical upper case character [A-Z] count
// - default is maxInt, i.e. no Maximum set
property MaxUpperCount: cardinal read fProps[14] write fProps[14];
/// Minimal space count inside the value text
// - default is 0, i.e. any space number allowed
property MinSpaceCount: cardinal read fProps[7] write fProps[7];
/// Maximal space count inside the value text
// - default is maxInt, i.e. any space number allowed
property MaxSpaceCount: cardinal read fProps[15] write fProps[15];
/// Maximal space count allowed on the Left side
// - default is maxInt, i.e. any Left space allowed
property MaxLeftTrimCount: cardinal read fProps[8] write fProps[8];
/// Maximal space count allowed on the Right side
// - default is maxInt, i.e. any Right space allowed
property MaxRightTrimCount: cardinal read fProps[9] write fProps[9];
/// defines if lengths parameters expects UTF-8 or UTF-16 codepoints number
// - with default FALSE, the length is calculated with UTF-16 Unicode
// codepoints - MaxLength may not match the UCS4 glyphs number, in case of
// UTF-16 surrogates
// - you can set this property to TRUE so that the UTF-8 byte count would
// be used for truncation againts the MaxLength parameter
property UTF8Length: boolean read fUTF8Length write fUTF8Length;
end;
{$M-}
/// strong password validation for a Record field content (typicaly a TSQLRecord)
// - the following parameters are set by default to
// $ '{"MinLength":5,"MaxLength":20,"MinAlphaCount":1,"MinDigitCount":1,
// $ "MinPunctCount":1,"MinLowerCount":1,"MinUpperCount":1,"MaxSpaceCount":0}'
// - you can specify some JSON encoded parameters to change this default
// values, which will validate the text field only if it contains from 5 to 10
// characters, with at least one digit, one upper case letter, one lower case
// letter, and one ponctuation sign, with no space allowed inside
TSynValidatePassWord = class(TSynValidateText)
protected
/// set password specific parameters
procedure SetParameters(const Value: RawUTF8); override;
end;
{ C++Builder doesn't support array elements as properties (RSP-12595).
For now, simply exclude the relevant classes from C++Builder. }
{$NODEFINE TSynValidateTextProps}
{$NODEFINE TSynValidateText }
{$NODEFINE TSynValidatePassWord }
/// will define a transformation to be applied to a Record field content
// (typicaly a TSQLRecord)
// - here "filter" means that content would be transformed according to a
// set of defined rules
// - a typical usage is to convert to lower or upper case, or
// trim any time or date value in a TDateTime field
// - the optional associated parameters are to be supplied JSON-encoded
TSynFilter = class(TSynFilterOrValidate)
protected
public
/// perform the transformation to the specified value
// - the value is converted into UTF-8 text, as expected by
// TPropInfo.GetValue / TPropInfo.SetValue e.g.
procedure Process(aFieldIndex: integer; var Value: RawUTF8); virtual; abstract;
end;
/// class-refrence type (metaclass) for a TSynFilter or a TSynValidate
TSynFilterOrValidateClass = class of TSynFilterOrValidate;
/// class-reference type (metaclass) of a record filter (transformation)
TSynFilterClass = class of TSynFilter;
/// convert the value into ASCII Upper Case characters
// - UpperCase conversion is made for ASCII-7 only, i.e. 'a'..'z' characters
// - this version expects no parameter
TSynFilterUpperCase = class(TSynFilter)
public
/// perform the case conversion to the specified value
procedure Process(aFieldIndex: integer; var Value: RawUTF8); override;
end;
/// convert the value into WinAnsi Upper Case characters
// - UpperCase conversion is made for all latin characters in the WinAnsi
// code page only, e.g. 'e' acute will be converted to 'E'
// - this version expects no parameter
TSynFilterUpperCaseU = class(TSynFilter)
public
/// perform the case conversion to the specified value
procedure Process(aFieldIndex: integer; var Value: RawUTF8); override;
end;
/// convert the value into ASCII Lower Case characters
// - LowerCase conversion is made for ASCII-7 only, i.e. 'A'..'Z' characters
// - this version expects no parameter
TSynFilterLowerCase = class(TSynFilter)
public
/// perform the case conversion to the specified value
procedure Process(aFieldIndex: integer; var Value: RawUTF8); override;
end;
/// convert the value into WinAnsi Lower Case characters
// - LowerCase conversion is made for all latin characters in the WinAnsi
// code page only, e.g. 'E' acute will be converted to 'e'
// - this version expects no parameter
TSynFilterLowerCaseU = class(TSynFilter)
public
/// perform the case conversion to the specified value
procedure Process(aFieldIndex: integer; var Value: RawUTF8); override;
end;
/// trim any space character left or right to the value
// - this versions expect no parameter
TSynFilterTrim = class(TSynFilter)
public
/// perform the space triming conversion to the specified value
procedure Process(aFieldIndex: integer; var Value: RawUTF8); override;
end;
/// truncate a text above a given maximum length
// - expects optional JSON parameters of the allowed text length range as
// $ '{MaxLength":10}
TSynFilterTruncate = class(TSynFilter)
protected
fMaxLength: cardinal;
fUTF8Length: boolean;
/// decode the MaxLength: and UTF8Length: parameters
procedure SetParameters(const Value: RawUTF8); override;
public
/// perform the length truncation of the specified value
procedure Process(aFieldIndex: integer; var Value: RawUTF8); override;
/// Maximal length value allowed for the text content
// - the length is calculated with UTF-16 Unicode codepoints, unless
// UTF8Length has been set to TRUE so that the UTF-8 byte count is checked
// - default is 0, i.e. no maximum length is forced
property MaxLength: cardinal read fMaxLength write fMaxLength;
/// defines if MaxLength is stored as UTF-8 or UTF-16 codepoints number
// - with default FALSE, the length is calculated with UTF-16 Unicode
// codepoints - MaxLength may not match the UCS4 glyphs number, in case of
// UTF-16 surrogates
// - you can set this property to TRUE so that the UTF-8 byte count would
// be used for truncation againts the MaxLength parameter
property UTF8Length: boolean read fUTF8Length write fUTF8Length;
end;
2021-04-23 10:11:05 +02:00
resourcestring
sInvalidIPAddress = '"%s" is an invalid IP v4 address';
sInvalidEmailAddress = '"%s" is an invalid email address';
sInvalidPattern = '"%s" does not match the expected pattern';
sCharacter01n = 'character,character,characters';
sInvalidTextLengthMin = 'Expect at least %d %s';
sInvalidTextLengthMax = 'Expect up to %d %s';
sInvalidTextChar = 'Expect at least %d %s %s,Expect up to %d %s %s,'+
'alphabetical,digital,punctuation,lowercase,uppercase,space,'+
'Too much spaces on the left,Too much spaces on the right';
sValidationFailed = '"%s" rule failed';
sValidationFieldVoid = 'An unique key field must not be void';
sValidationFieldDuplicate = 'Value already used for this unique key field';
{ ************ Database types and classes ************************** }
type
/// handled field/parameter/column types for abstract database access
// - this will map JSON-compatible low-level database-level access types, not
// high-level Delphi types as TSQLFieldType defined in mORMot.pas
// - it does not map either all potential types as defined in DB.pas (which
// are there for compatibility with old RDBMS, and are not abstract enough)
// - those types can be mapped to standard SQLite3 generic types, i.e.
// NULL, INTEGER, REAL, TEXT, BLOB (with the addition of a ftCurrency and
// ftDate type, for better support of most DB engines)
// see @http://www.sqlite.org/datatype3.html
// - the only string type handled here uses UTF-8 encoding (implemented
// using our RawUTF8 type), for cross-Delphi true Unicode process
TSQLDBFieldType =
(ftUnknown, ftNull, ftInt64, ftDouble, ftCurrency, ftDate, ftUTF8, ftBlob);
/// set of field/parameter/column types for abstract database access
TSQLDBFieldTypes = set of TSQLDBFieldType;
/// array of field/parameter/column types for abstract database access
TSQLDBFieldTypeDynArray = array of TSQLDBFieldType;
/// array of field/parameter/column types for abstract database access
// - this array as a fixed size, ready to handle up to MAX_SQLFIELDS items
TSQLDBFieldTypeArray = array[0..MAX_SQLFIELDS-1] of TSQLDBFieldType;
2021-04-23 10:11:05 +02:00
PSQLDBFieldTypeArray = ^TSQLDBFieldTypeArray;
/// how TSQLVar may be processed
// - by default, ftDate will use seconds resolution unless svoDateWithMS is set
TSQLVarOption = (svoDateWithMS);
/// defines how TSQLVar may be processed
TSQLVarOptions = set of TSQLVarOption;
/// memory structure used for database values by reference storage
// - used mainly by SynDB, mORMot, mORMotDB and mORMotSQLite3 units
// - defines only TSQLDBFieldType data types (similar to those handled by
// SQLite3, with the addition of ftCurrency and ftDate)
// - cleaner/lighter dedicated type than TValue or variant/TVarData, strong
// enough to be marshalled as JSON content
// - variable-length data (e.g. UTF-8 text or binary BLOB) are never stored
// within this record, but VText/VBlob will point to an external (temporary)
// memory buffer
// - date/time is stored as ISO-8601 text (with milliseconds if svoDateWithMS
// option is set and the database supports it), and currency as double or BCD
// in most databases
TSQLVar = record
/// how this value should be processed
Options: TSQLVarOptions;
/// the type of the value stored
case VType: TSQLDBFieldType of
ftInt64: (
VInt64: Int64);
ftDouble: (
VDouble: double);
ftDate: (
VDateTime: TDateTime);
ftCurrency: (
VCurrency: Currency);
ftUTF8: (
VText: PUTF8Char);
ftBlob: (
VBlob: pointer;
VBlobLen: Integer)
end;
/// dynamic array of database values by reference storage
TSQLVarDynArray = array of TSQLVar;
/// used to store bit set for all available fields in a Table
// - with current MAX_SQLFIELDS value, 64 bits uses 8 bytes of memory
// - see also IsZero() and IsEqual() functions
// - you can also use ALL_FIELDS as defined in mORMot.pas
TSQLFieldBits = set of 0..MAX_SQLFIELDS-1;
/// used to store a field index in a Table
// - note that -1 is commonly used for the ID/RowID field so the values should
// be signed
// - even if ShortInt (-128..127) may have been enough, we define a 16 bit
// safe unsigned integer to let the source compile with Delphi 5
TSQLFieldIndex = SmallInt; // -32768..32767
/// used to store field indexes in a Table
// - same as TSQLFieldBits, but allowing to store the proper order
TSQLFieldIndexDynArray = array of TSQLFieldIndex;
/// points to a bit set used for all available fields in a Table
PSQLFieldBits = ^TSQLFieldBits;
/// generic parameter types, as recognized by SQLParamContent() and
// ExtractInlineParameters() functions
TSQLParamType = (sptUnknown, sptInteger, sptFloat, sptText, sptBlob, sptDateTime);
/// array of parameter types, as recognized by SQLParamContent() and
// ExtractInlineParameters() functions
TSQLParamTypeDynArray = array of TSQLParamType;
/// simple writer to a Stream, specialized for the JSON format and SQL export
// - i.e. define some property/method helpers to export SQL resultset as JSON
// - see mORMot.pas for proper class serialization via TJSONSerializer.WriteObject
TJSONWriter = class(TTextWriterWithEcho)
protected
/// used to store output format
fExpand: boolean;
/// used to store output format for TSQLRecord.GetJSONValues()
fWithID: boolean;
/// used to store field for TSQLRecord.GetJSONValues()
fFields: TSQLFieldIndexDynArray;
/// if not Expanded format, contains the Stream position of the first
// useful Row of data; i.e. ',val11' position in:
// & { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
fStartDataPosition: integer;
public
/// used internally to store column names and count for AddColumns
ColNames: TRawUTF8DynArray;
/// the data will be written to the specified Stream
// - if no Stream is supplied, a temporary memory stream will be created
// (it's faster to supply one, e.g. any TSQLRest.TempMemoryStream)
constructor Create(aStream: TStream; Expand, withID: boolean;
const Fields: TSQLFieldBits; aBufSize: integer=8192); overload;
/// the data will be written to the specified Stream
// - if no Stream is supplied, a temporary memory stream will be created
// (it's faster to supply one, e.g. any TSQLRest.TempMemoryStream)
constructor Create(aStream: TStream; Expand, withID: boolean;
2021-04-23 10:11:05 +02:00
const Fields: TSQLFieldIndexDynArray=nil; aBufSize: integer=8192;
aStackBuffer: PTextWriterStackBuffer=nil); overload;
/// rewind the Stream position and write void JSON object
procedure CancelAllVoid;
/// write or init field names for appropriate JSON Expand later use
// - ColNames[] must have been initialized before calling this procedure
// - if aKnownRowsCount is not null, a "rowCount":... item will be added
// to the generated JSON stream (for faster unserialization of huge content)
procedure AddColumns(aKnownRowsCount: integer=0);
/// allow to change on the fly an expanded format column layout
// - by definition, a non expanded format will raise a ESynException
// - caller should then set ColNames[] and run AddColumns()
procedure ChangeExpandedFields(aWithID: boolean; const aFields: TSQLFieldIndexDynArray); overload;
/// end the serialized JSON object
// - cancel last ','
// - close the JSON object ']' or ']}'
// - write non expanded postlog (,"rowcount":...), if needed
// - flush the internal buffer content if aFlushFinal=true
procedure EndJSONObject(aKnownRowsCount,aRowsCount: integer; aFlushFinal: boolean=true);
{$ifdef HASINLINE}inline;{$endif}
/// the first data row is erased from the content
// - only works if the associated storage stream is TMemoryStream
// - expect not Expanded format
procedure TrimFirstRow;
/// is set to TRUE in case of Expanded format
property Expand: boolean read fExpand write fExpand;
/// is set to TRUE if the ID field must be appended to the resulting JSON
// - this field is used only by TSQLRecord.GetJSONValues
// - this field is ignored by TSQLTable.GetJSONValues
property WithID: boolean read fWithID;
/// Read-Only access to the field bits set for each column to be stored
property Fields: TSQLFieldIndexDynArray read fFields;
/// if not Expanded format, contains the Stream position of the first
// useful Row of data; i.e. ',val11' position in:
// & { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
property StartDataPosition: integer read fStartDataPosition;
end;
/// returns TRUE if no bit inside this TSQLFieldBits is set
// - is optimized for 64, 128, 192 and 256 max bits count (i.e. MAX_SQLFIELDS)
// - will work also with any other value
function IsZero(const Fields: TSQLFieldBits): boolean; overload;
{$ifdef HASINLINE}inline;{$endif}
/// fast comparison of two TSQLFieldBits values
// - is optimized for 64, 128, 192 and 256 max bits count (i.e. MAX_SQLFIELDS)
// - will work also with any other value
function IsEqual(const A,B: TSQLFieldBits): boolean; overload;
{$ifdef HASINLINE}inline;{$endif}
/// fast initialize a TSQLFieldBits with 0
// - is optimized for 64, 128, 192 and 256 max bits count (i.e. MAX_SQLFIELDS)
// - will work also with any other value
procedure FillZero(var Fields: TSQLFieldBits); overload;
{$ifdef HASINLINE}inline;{$endif}
/// convert a TSQLFieldBits set of bits into an array of integers
procedure FieldBitsToIndex(const Fields: TSQLFieldBits;
var Index: TSQLFieldIndexDynArray; MaxLength: integer=MAX_SQLFIELDS;
IndexStart: integer=0); overload;
/// convert a TSQLFieldBits set of bits into an array of integers
function FieldBitsToIndex(const Fields: TSQLFieldBits;
MaxLength: integer=MAX_SQLFIELDS): TSQLFieldIndexDynArray; overload;
{$ifdef HASINLINE}inline;{$endif}
/// add a field index to an array of field indexes
// - returns the index in Indexes[] of the newly appended Field value
function AddFieldIndex(var Indexes: TSQLFieldIndexDynArray; Field: integer): integer;
/// convert an array of field indexes into a TSQLFieldBits set of bits
procedure FieldIndexToBits(const Index: TSQLFieldIndexDynArray; var Fields: TSQLFieldBits); overload;
// search a field index in an array of field indexes
// - returns the index in Indexes[] of the given Field value, -1 if not found
function SearchFieldIndex(var Indexes: TSQLFieldIndexDynArray; Field: integer): integer;
/// convert an array of field indexes into a TSQLFieldBits set of bits
function FieldIndexToBits(const Index: TSQLFieldIndexDynArray): TSQLFieldBits; overload;
{$ifdef HASINLINE}inline;{$endif}
/// returns the stored size of a TSQLVar database value
// - only returns VBlobLen / StrLen(VText) size, 0 otherwise
function SQLVarLength(const Value: TSQLVar): integer;
{$ifndef NOVARIANTS}
/// convert any Variant into a database value
// - ftBlob kind won't be handled by this function
// - complex variant types would be converted into ftUTF8 JSON object/array
procedure VariantToSQLVar(const Input: variant; var temp: RawByteString;
var Output: TSQLVar);
/// guess the correct TSQLDBFieldType from a variant type
2021-04-23 10:11:05 +02:00
function VariantVTypeToSQLDBFieldType(VType: cardinal): TSQLDBFieldType;
/// guess the correct TSQLDBFieldType from a variant value
function VariantTypeToSQLDBFieldType(const V: Variant): TSQLDBFieldType;
{$ifdef HASINLINE}inline;{$endif}
/// guess the correct TSQLDBFieldType from the UTF-8 representation of a value
function TextToSQLDBFieldType(json: PUTF8Char): TSQLDBFieldType;
type
/// define a variant published property as a nullable integer
// - either a varNull or a varInt64 value will be stored in the variant
// - either a NULL or an INTEGER value will be stored in the database
// - the property should be defined as such:
// ! property Int: TNullableInteger read fInt write fInt;
TNullableInteger = type variant;
/// define a variant published property as a nullable boolean
// - either a varNull or a varBoolean value will be stored in the variant
// - either a NULL or a 0/1 INTEGER value will be stored in the database
// - the property should be defined as such:
// ! property Bool: TNullableBoolean read fBool write fBool;
TNullableBoolean = type variant;
/// define a variant published property as a nullable floating point value
// - either a varNull or a varDouble value will be stored in the variant
// - either a NULL or a FLOAT value will be stored in the database
// - the property should be defined as such:
// ! property Flt: TNullableFloat read fFlt write fFlt;
TNullableFloat = type variant;
/// define a variant published property as a nullable decimal value
// - either a varNull or a varCurrency value will be stored in the variant
// - either a NULL or a FLOAT value will be stored in the database
// - the property should be defined as such:
// ! property Cur: TNullableCurrency read fCur write fCur;
TNullableCurrency = type variant;
/// define a variant published property as a nullable date/time value
// - either a varNull or a varDate value will be stored in the variant
// - either a NULL or a ISO-8601 TEXT value will be stored in the database
// - the property should be defined as such:
// ! property Dat: TNullableDateTime read fDat write fDat;
TNullableDateTime = type variant;
/// define a variant published property as a nullable timestamp value
// - either a varNull or a varInt64 value will be stored in the variant
// - either a NULL or a TTimeLog INTEGER value will be stored in the database
// - the property should be defined as such:
// ! property Tim: TNullableTimrency read fTim write fTim;
TNullableTimeLog = type variant;
/// define a variant published property as a nullable UTF-8 encoded text
// - either a varNull or varString (RawUTF8) will be stored in the variant
// - either a NULL or a TEXT value will be stored in the database
// - the property should be defined as such:
// ! property Txt: TNullableUTF8Text read fTxt write fTxt;
// or for a fixed-width VARCHAR (in external databases), here of 32 max chars:
// ! property Txt: TNullableUTF8Text index 32 read fTxt write fTxt;
// - warning: prior to Delphi 2009, since the variant will be stored as
// RawUTF8 internally, you should not use directly the field value as a
// VCL string=AnsiString like string(aField) but use VariantToString(aField)
TNullableUTF8Text = type variant;
var
/// a nullable integer value containing null
NullableIntegerNull: TNullableInteger absolute NullVarData;
/// a nullable boolean value containing null
NullableBooleanNull: TNullableBoolean absolute NullVarData;
/// a nullable float value containing null
NullableFloatNull: TNullableFloat absolute NullVarData;
/// a nullable currency value containing null
NullableCurrencyNull: TNullableCurrency absolute NullVarData;
/// a nullable TDateTime value containing null
NullableDateTimeNull: TNullableDateTime absolute NullVarData;
/// a nullable TTimeLog value containing null
NullableTimeLogNull: TNullableTimeLog absolute NullVarData;
/// a nullable UTF-8 encoded text value containing null
NullableUTF8TextNull: TNullableUTF8Text absolute NullVarData;
/// creates a nullable integer value from a supplied constant
// - FPC does not allow direct assignment to a TNullableInteger = type variant
// variable: use this function to circumvent it
function NullableInteger(const Value: Int64): TNullableInteger;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableInteger = type variant variable: use this
// function to circumvent those limitations
function NullableIntegerIsEmptyOrNull(const V: TNullableInteger): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableInteger is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the Integer value
function NullableIntegerToValue(const V: TNullableInteger; out Value: Int64): Boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableInteger is null, or return its value
// - returns 0 if V is null or empty, or the stored Integer value
function NullableIntegerToValue(const V: TNullableInteger): Int64;
overload; {$ifdef HASINLINE}inline;{$endif}
/// creates a nullable Boolean value from a supplied constant
// - FPC does not allow direct assignment to a TNullableBoolean = type variant
// variable: use this function to circumvent it
function NullableBoolean(Value: boolean): TNullableBoolean;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableBoolean = type variant variant: use this
// function to circumvent those limitations
function NullableBooleanIsEmptyOrNull(const V: TNullableBoolean): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableBoolean is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the Boolean value
function NullableBooleanToValue(const V: TNullableBoolean; out Value: Boolean): Boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableBoolean is null, or return its value
// - returns false if V is null or empty, or the stored Boolean value
function NullableBooleanToValue(const V: TNullableBoolean): Boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// creates a nullable floating-point value from a supplied constant
// - FPC does not allow direct assignment to a TNullableFloat = type variant
// variable: use this function to circumvent it
function NullableFloat(const Value: double): TNullableFloat;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableFloat = type variant variable: use this
// function to circumvent those limitations
function NullableFloatIsEmptyOrNull(const V: TNullableFloat): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableFloat is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the Float value
function NullableFloatToValue(const V: TNullableFloat; out Value: double): boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableFloat is null, or return its value
// - returns 0 if V is null or empty, or the stored Float value
function NullableFloatToValue(const V: TNullableFloat): double;
overload; {$ifdef HASINLINE}inline;{$endif}
/// creates a nullable Currency value from a supplied constant
// - FPC does not allow direct assignment to a TNullableCurrency = type variant
// variable: use this function to circumvent it
function NullableCurrency(const Value: currency): TNullableCurrency;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableCurrency = type variant variable: use this
// function to circumvent those limitations
function NullableCurrencyIsEmptyOrNull(const V: TNullableCurrency): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableCurrency is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the Currency value
function NullableCurrencyToValue(const V: TNullableCurrency; out Value: currency): boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableCurrency is null, or return its value
// - returns 0 if V is null or empty, or the stored Currency value
function NullableCurrencyToValue(const V: TNullableCurrency): currency;
overload; {$ifdef HASINLINE}inline;{$endif}
/// creates a nullable TDateTime value from a supplied constant
// - FPC does not allow direct assignment to a TNullableDateTime = type variant
// variable: use this function to circumvent it
function NullableDateTime(const Value: TDateTime): TNullableDateTime;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableDateTime = type variant variable: use this
// function to circumvent those limitations
function NullableDateTimeIsEmptyOrNull(const V: TNullableDateTime): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableDateTime is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the DateTime value
function NullableDateTimeToValue(const V: TNullableDateTime; out Value: TDateTime): boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableDateTime is null, or return its value
// - returns 0 if V is null or empty, or the stored DateTime value
function NullableDateTimeToValue(const V: TNullableDateTime): TDateTime;
overload; {$ifdef HASINLINE}inline;{$endif}
/// creates a nullable TTimeLog value from a supplied constant
// - FPC does not allow direct assignment to a TNullableTimeLog = type variant
// variable: use this function to circumvent it
function NullableTimeLog(const Value: TTimeLog): TNullableTimeLog;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableTimeLog = type variant variable: use this
// function to circumvent those limitations
function NullableTimeLogIsEmptyOrNull(const V: TNullableTimeLog): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableTimeLog is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the TimeLog value
function NullableTimeLogToValue(const V: TNullableTimeLog; out Value: TTimeLog): boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableTimeLog is null, or return its value
// - returns 0 if V is null or empty, or the stored TimeLog value
function NullableTimeLogToValue(const V: TNullableTimeLog): TTimeLog;
overload; {$ifdef HASINLINE}inline;{$endif}
/// creates a nullable UTF-8 encoded text value from a supplied constant
// - FPC does not allow direct assignment to a TNullableUTF8 = type variant
// variable: use this function to circumvent it
function NullableUTF8Text(const Value: RawUTF8): TNullableUTF8Text;
{$ifdef HASINLINE}inline;{$endif}
/// same as VarIsEmpty(V) or VarIsEmpty(V), but faster
// - FPC VarIsNull() seems buggy with varByRef variants, and does not allow
// direct transtyping from a TNullableUTF8Text = type variant variable: use this
// function to circumvent those limitations
function NullableUTF8TextIsEmptyOrNull(const V: TNullableUTF8Text): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableUTF8Text is null, or return its value
// - returns FALSE if V is null or empty, or TRUE and set the UTF8Text value
function NullableUTF8TextToValue(const V: TNullableUTF8Text; out Value: RawUTF8): boolean;
overload; {$ifdef HASINLINE}inline;{$endif}
/// check if a TNullableUTF8Text is null, or return its value
// - returns '' if V is null or empty, or the stored UTF8-encoded text value
function NullableUTF8TextToValue(const V: TNullableUTF8Text): RawUTF8;
overload; {$ifdef HASINLINE}inline;{$endif}
{$endif NOVARIANTS}
/// convert a date to a ISO-8601 string format for SQL '?' inlined parameters
// - will return the date encoded as '\uFFF1YYYY-MM-DD' - therefore
// ':("\uFFF12012-05-04"):' pattern will be recognized as a sftDateTime
// inline parameter in SQLParamContent() / ExtractInlineParameters() functions
// (JSON_SQLDATE_MAGIC will be used as prefix to create '\uFFF1...' pattern)
// - to be used e.g. as in:
// ! aRec.CreateAndFillPrepare(Client,'Datum=?',[DateToSQL(EncodeDate(2012,5,4))]);
function DateToSQL(Date: TDateTime): RawUTF8; overload;
/// convert a date to a ISO-8601 string format for SQL '?' inlined parameters
// - will return the date encoded as '\uFFF1YYYY-MM-DD' - therefore
// ':("\uFFF12012-05-04"):' pattern will be recognized as a sftDateTime
// inline parameter in SQLParamContent() / ExtractInlineParameters() functions
// (JSON_SQLDATE_MAGIC will be used as prefix to create '\uFFF1...' pattern)
// - to be used e.g. as in:
// ! aRec.CreateAndFillPrepare(Client,'Datum=?',[DateToSQL(2012,5,4)]);
function DateToSQL(Year,Month,Day: cardinal): RawUTF8; overload;
/// convert a date/time to a ISO-8601 string format for SQL '?' inlined parameters
// - if DT=0, returns ''
// - if DT contains only a date, returns the date encoded as '\uFFF1YYYY-MM-DD'
// - if DT contains only a time, returns the time encoded as '\uFFF1Thh:mm:ss'
// - otherwise, returns the ISO-8601 date and time encoded as '\uFFF1YYYY-MM-DDThh:mm:ss'
// (JSON_SQLDATE_MAGIC will be used as prefix to create '\uFFF1...' pattern)
// - if WithMS is TRUE, will append '.sss' for milliseconds resolution
// - to be used e.g. as in:
// ! aRec.CreateAndFillPrepare(Client,'Datum<=?',[DateTimeToSQL(Now)]);
// - see TimeLogToSQL() if you are using TTimeLog/TModTime/TCreateTime values
function DateTimeToSQL(DT: TDateTime; WithMS: boolean=false): RawUTF8;
/// decode a SQL '?' inlined parameter (i.e. with JSON_SQLDATE_MAGIC prefix)
// - as generated by DateToSQL/DateTimeToSQL/TimeLogToSQL functions
function SQLToDateTime(const ParamValueWithMagic: RawUTF8): TDateTime;
/// convert a TTimeLog value into a ISO-8601 string format for SQL '?' inlined
// parameters
// - handle TTimeLog bit-encoded Int64 format
// - follows the same pattern as DateToSQL or DateTimeToSQL functions, i.e.
// will return the date or time encoded as '\uFFF1YYYY-MM-DDThh:mm:ss' -
// therefore ':("\uFFF12012-05-04T20:12:13"):' pattern will be recognized as a
// sftDateTime inline parameter in SQLParamContent() / ExtractInlineParameters()
// (JSON_SQLDATE_MAGIC will be used as prefix to create '\uFFF1...' pattern)
// - to be used e.g. as in:
// ! aRec.CreateAndFillPrepare(Client,'Datum<=?',[TimeLogToSQL(TimeLogNow)]);
function TimeLogToSQL(const Timestamp: TTimeLog): RawUTF8;
/// convert a Iso8601 encoded string into a ISO-8601 string format for SQL
// '?' inlined parameters
// - follows the same pattern as DateToSQL or DateTimeToSQL functions, i.e.
// will return the date or time encoded as '\uFFF1YYYY-MM-DDThh:mm:ss' -
// therefore ':("\uFFF12012-05-04T20:12:13"):' pattern will be recognized as a
// sftDateTime inline parameter in SQLParamContent() / ExtractInlineParameters()
// (JSON_SQLDATE_MAGIC will be used as prefix to create '\uFFF1...' pattern)
// - in practice, just append the JSON_SQLDATE_MAGIC prefix to the supplied text
function Iso8601ToSQL(const S: RawByteString): RawUTF8;
/// guess the content type of an UTF-8 SQL value, in :(....): format
// - will be used e.g. by ExtractInlineParameters() to un-inline a SQL statement
// - sftInteger is returned for an INTEGER value, e.g. :(1234):
// - sftFloat is returned for any floating point value (i.e. some digits
// separated by a '.' character), e.g. :(12.34): or :(12E-34):
// - sftUTF8Text is returned for :("text"): or :('text'):, with double quoting
// inside the value
// - sftBlob will be recognized from the ':("\uFFF0base64encodedbinary"):'
// pattern, and return raw binary (for direct blob parameter assignment)
// - sftDateTime will be recognized from ':(\uFFF1"2012-05-04"):' pattern,
// i.e. JSON_SQLDATE_MAGIC-prefixed string as returned by DateToSQL() or
// DateTimeToSQL() functions
// - sftUnknown is returned on invalid content, or if wasNull is set to TRUE
// - if ParamValue is not nil, the pointing RawUTF8 string is set with the
// value inside :(...): without double quoting in case of sftUTF8Text
// - wasNull is set to TRUE if P was ':(null):' and ParamType is sftUnknwown
function SQLParamContent(P: PUTF8Char; out ParamType: TSQLParamType; out ParamValue: RawUTF8;
out wasNull: boolean): PUTF8Char;
/// this function will extract inlined :(1234): parameters into Types[]/Values[]
// - will return the generic SQL statement with ? place holders for inlined
// parameters and setting Values with SQLParamContent() decoded content
// - will set maxParam=0 in case of no inlined parameters
// - recognized types are sptInteger, sptFloat, sptDateTime ('\uFFF1...'),
// sptUTF8Text and sptBlob ('\uFFF0...')
// - sptUnknown is returned on invalid content
function ExtractInlineParameters(const SQL: RawUTF8;
var Types: TSQLParamTypeDynArray; var Values: TRawUTF8DynArray;
var maxParam: integer; var Nulls: TSQLFieldBits): RawUTF8;
/// returns a 64-bit value as inlined ':(1234):' text
function InlineParameter(ID: Int64): shortstring; overload;
/// returns a string value as inlined ':("value"):' text
function InlineParameter(const value: RawUTF8): RawUTF8; overload;
type
/// SQL Query comparison operators
// - used e.g. by CompareOperator() functions in SynTable.pas or vt_BestIndex()
// in mORMotSQLite3.pas
TCompareOperator = (
soEqualTo,
soNotEqualTo,
soLessThan,
soLessThanOrEqualTo,
soGreaterThan,
soGreaterThanOrEqualTo,
soBeginWith,
soContains,
soSoundsLikeEnglish,
soSoundsLikeFrench,
soSoundsLikeSpanish);
const
/// convert identified field types into high-level ORM types
// - as will be implemented in unit mORMot.pas
SQLDBFIELDTYPE_TO_DELPHITYPE: array[TSQLDBFieldType] of RawUTF8 = (
'???','???', 'Int64', 'Double', 'Currency', 'TDateTime', 'RawUTF8', 'TSQLRawBlob');
{ ************ low-level buffer processing functions ************************* }
type
/// safe decoding of a TFileBufferWriter content
// - similar to TFileBufferReader, but faster and only for in-memory buffer
// - is also safer, since will check for reaching end of buffer
// - raise a EFastReader exception on decoding error (e.g. if a buffer
// overflow may occur) or call OnErrorOverflow/OnErrorData event handlers
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TFastReader = record
{$else}TFastReader = object{$endif}
public
/// the current position in the memory
P: PAnsiChar;
/// the last position in the buffer
Last: PAnsiChar;
/// use this event to customize the ErrorOverflow process
OnErrorOverflow: procedure of object;
/// use this event to customize the ErrorData process
OnErrorData: procedure(const fmt: RawUTF8; const args: array of const) of object;
/// some opaque value, which may be a version number to define the binary layout
Tag: PtrInt;
/// initialize the reader from a memory block
procedure Init(Buffer: pointer; Len: integer); overload;
/// initialize the reader from a RawByteString content
procedure Init(const Buffer: RawByteString); overload;
/// raise a EFastReader with an "overflow" error message
procedure ErrorOverflow;
/// raise a EFastReader with an "incorrect data" error message
procedure ErrorData(const fmt: RawUTF8; const args: array of const);
/// read the next 32-bit signed value from the buffer
function VarInt32: integer; {$ifdef HASINLINE}inline;{$endif}
/// read the next 32-bit unsigned value from the buffer
function VarUInt32: cardinal;
/// try to read the next 32-bit signed value from the buffer
// - don't change the current position
function PeekVarInt32(out value: PtrInt): boolean; {$ifdef HASINLINE}inline;{$endif}
/// try to read the next 32-bit unsigned value from the buffer
// - don't change the current position
function PeekVarUInt32(out value: PtrUInt): boolean;
/// read the next 32-bit unsigned value from the buffer
// - this version won't call ErrorOverflow, but return false on error
// - returns true on read success
function VarUInt32Safe(out Value: cardinal): boolean;
/// read the next 64-bit signed value from the buffer
function VarInt64: Int64; {$ifdef HASINLINE}inline;{$endif}
/// read the next 64-bit unsigned value from the buffer
function VarUInt64: QWord;
/// read the next RawUTF8 value from the buffer
function VarUTF8: RawUTF8; overload;
/// read the next RawUTF8 value from the buffer
procedure VarUTF8(out result: RawUTF8); overload;
/// read the next RawUTF8 value from the buffer
// - this version won't call ErrorOverflow, but return false on error
// - returns true on read success
function VarUTF8Safe(out Value: RawUTF8): boolean;
/// read the next RawByteString value from the buffer
function VarString: RawByteString; {$ifdef HASINLINE}inline;{$endif}
/// read the next pointer and length value from the buffer
procedure VarBlob(out result: TValueResult); overload; {$ifdef HASINLINE}inline;{$endif}
/// read the next pointer and length value from the buffer
function VarBlob: TValueResult; overload; {$ifdef HASINLINE}inline;{$endif}
/// read the next ShortString value from the buffer
function VarShortString: shortstring; {$ifdef HASINLINE}inline;{$endif}
/// fast ignore the next VarUInt32/VarInt32/VarUInt64/VarInt64 value
// - don't raise any exception, so caller could check explicitly for any EOF
procedure VarNextInt; overload; {$ifdef HASINLINE}inline;{$endif}
/// fast ignore the next count VarUInt32/VarInt32/VarUInt64/VarInt64 values
// - don't raise any exception, so caller could check explicitly for any EOF
procedure VarNextInt(count: integer); overload;
/// read the next byte from the buffer
function NextByte: byte; {$ifdef HASINLINE}inline;{$endif}
/// read the next byte from the buffer, checking
function NextByteSafe(dest: pointer): boolean; {$ifdef HASINLINE}inline;{$endif}
/// read the next 4 bytes from the buffer as a 32-bit unsigned value
function Next4: cardinal; {$ifdef HASINLINE}inline;{$endif}
/// read the next 8 bytes from the buffer as a 64-bit unsigned value
function Next8: Qword; {$ifdef HASINLINE}inline;{$endif}
/// consumes the next byte from the buffer, if matches a given value
function NextByteEquals(Value: byte): boolean; {$ifdef HASINLINE}inline;{$endif}
/// returns the current position, and move ahead the specified bytes
function Next(DataLen: PtrInt): pointer; {$ifdef HASINLINE}inline;{$endif}
/// returns the current position, and move ahead the specified bytes
function NextSafe(out Data: Pointer; DataLen: PtrInt): boolean; {$ifdef HASINLINE}inline;{$endif}
{$ifndef NOVARIANTS}
/// read the next variant from the buffer
// - is a wrapper around VariantLoad()
procedure NextVariant(var Value: variant; CustomVariantOptions: PDocVariantOptions);
/// read the JSON-serialized TDocVariant from the buffer
// - matches TFileBufferWriter.WriteDocVariantData format
procedure NextDocVariantData(out Value: variant; CustomVariantOptions: PDocVariantOptions);
{$endif NOVARIANTS}
/// copy data from the current position, and move ahead the specified bytes
procedure Copy(out Dest; DataLen: PtrInt); {$ifdef HASINLINE}inline;{$endif}
/// copy data from the current position, and move ahead the specified bytes
// - this version won't call ErrorOverflow, but return false on error
// - returns true on read success
function CopySafe(out Dest; DataLen: PtrInt): boolean;
/// apply TDynArray.LoadFrom on the buffer
// - will unserialize a previously appended dynamic array, e.g. as
// ! aWriter.WriteDynArray(DA);
procedure Read(var DA: TDynArray; NoCheckHash: boolean=false);
/// retrieved cardinal values encoded with TFileBufferWriter.WriteVarUInt32Array
// - only supports wkUInt32, wkVarInt32, wkVarUInt32 kind of encoding
function ReadVarUInt32Array(var Values: TIntegerDynArray): PtrInt;
/// retrieve some TAlgoCompress buffer, appended via Write()
// - BufferOffset could be set to reserve some bytes before the uncompressed buffer
function ReadCompressed(Load: TAlgoCompressLoad=aclNormal; BufferOffset: integer=0): RawByteString;
/// returns TRUE if the current position is the end of the input stream
function EOF: boolean; {$ifdef HASINLINE}inline;{$endif}
/// returns remaining length (difference between Last and P)
function RemainingLength: PtrUInt; {$ifdef HASINLINE}inline;{$endif}
end;
/// implements a stack-based writable storage of binary content
// - memory allocation is performed via a TSynTempBuffer
TSynTempWriter = object
public
/// the current writable position in tmp.buf
pos: PAnsiChar;
/// initialize a new temporary buffer of a given number of bytes
// - if maxsize is left to its 0 default value, the default stack-allocated
// memory size is used, i.e. 4 KB
procedure Init(maxsize: integer=0);
/// finalize the temporary storage
procedure Done;
/// append some binary to the internal buffer
// - will raise an ESynException in case of potential overflow
procedure wr(const val; len: PtrInt);
/// append some shortstring as binary to the internal buffer
procedure wrss(const str: shortstring); {$ifdef HASINLINE}inline;{$endif}
/// append some string as binary to the internal buffer
procedure wrs(const str: RawByteString); {$ifdef HASINLINE}inline;{$endif}
/// append some 8-bit value as binary to the internal buffer
procedure wrb(b: byte); {$ifdef HASINLINE}inline;{$endif}
/// append some 16-bit value as binary to the internal buffer
procedure wrw(w: word); {$ifdef HASINLINE}inline;{$endif}
/// append some 32-bit value as binary to the internal buffer
procedure wrint(int: integer); {$ifdef HASINLINE}inline;{$endif}
/// append some 32-bit/64-bit pointer value as binary to the internal buffer
procedure wrptr(ptr: pointer); {$ifdef HASINLINE}inline;{$endif}
/// append some 32-bit/64-bit integer as binary to the internal buffer
procedure wrptrint(int: PtrInt); {$ifdef HASINLINE}inline;{$endif}
/// append some fixed-value bytes as binary to the internal buffer
// - returns a pointer to the first byte of the added memory chunk
function wrfillchar(count: integer; value: byte): PAnsiChar;
/// returns the current offset position in the internal buffer
function Position: PtrInt; {$ifdef HASINLINE}inline;{$endif}
/// returns the buffer as a RawByteString instance
function AsBinary: RawByteString;
/// returns the buffer as a RawUTF8 instance
procedure AsUTF8(var result: RawUTF8);
protected
tmp: TSynTempBuffer;
end;
/// available kind of integer array storage, corresponding to the data layout
// - wkUInt32 will write the content as "plain" 4 bytes binary (this is the
// prefered way if the integers can be negative)
// - wkVarUInt32 will write the content using our 32-bit variable-length integer
// encoding
// - wkVarInt32 will write the content using our 32-bit variable-length integer
// encoding and the by-two complement (0=0,1=1,2=-1,3=2,4=-2...)
// - wkSorted will write an increasing array of integers, handling the special
// case of a difference of similar value (e.g. 1) between two values - note
// that this encoding is efficient only if the difference is main < 253
// - wkOffsetU and wkOffsetI will write the difference between two successive
// values, handling constant difference (Unsigned or Integer) in an optimized manner
// - wkFakeMarker won't be used by WriteVarUInt32Array, but to notify a
// custom encoding
TFileBufferWriterKind = (wkUInt32, wkVarUInt32, wkVarInt32, wkSorted,
wkOffsetU, wkOffsetI, wkFakeMarker);
/// this class can be used to speed up writing to a file
// - big speed up if data is written in small blocks
// - also handle optimized storage of any dynamic array of Integer/Int64/RawUTF8
// - use TFileBufferReader or TFastReader for decoding of the stored binary
TFileBufferWriter = class
private
fPos: PtrInt;
fBufLen: PtrInt;
fStream: TStream;
fTotalWritten: Int64;
fInternalStream: boolean;
fTag: PtrInt;
fBuffer: PByteArray;
fBufInternal: RawByteString;
procedure InternalFlush;
public
/// initialize the buffer, and specify a file handle to use for writing
// - use an internal buffer of the specified size
constructor Create(aFile: THandle; BufLen: integer=65536); overload;
/// initialize the buffer, and specify a TStream to use for writing
// - use an internal buffer of the specified size
constructor Create(aStream: TStream; BufLen: integer=65536); overload;
/// initialize the buffer, and specify a file to use for writing
// - use an internal buffer of the specified size
// - would replace any existing file by default, unless Append is TRUE
constructor Create(const aFileName: TFileName; BufLen: integer=65536;
Append: boolean=false); overload;
/// initialize the buffer, using an internal TStream instance
// - parameter could be e.g. THeapMemoryStream or TRawByteStringStream
// - use Flush then TMemoryStream(Stream) to retrieve its content, or
// TRawByteStringStream(Stream).DataString
constructor Create(aClass: TStreamClass; BufLen: integer=4096); overload;
/// initialize with a specified buffer and TStream class
// - use a specified external buffer (which may be allocated on stack),
// to avoid a memory allocation
constructor Create(aStream: TStream; aTempBuf: pointer; aTempLen: integer); overload;
/// initialize with a specified buffer
// - use a specified external buffer (which may be allocated on stack),
// to avoid a memory allocation
// - aStream parameter could be e.g. THeapMemoryStream or TRawByteStringStream
constructor Create(aClass: TStreamClass; aTempBuf: pointer; aTempLen: integer); overload;
/// release internal TStream (after AssignToHandle call)
// - warning: an explicit call to Flush is needed to write the data pending
// in internal buffer
destructor Destroy; override;
/// append some data at the current position
procedure Write(Data: pointer; DataLen: PtrInt); overload;
/// append 1 byte of data at the current position
procedure Write1(Data: Byte); {$ifdef HASINLINE}inline;{$endif}
/// append 2 bytes of data at the current position
procedure Write2(Data: Word); {$ifdef HASINLINE}inline;{$endif}
/// append 4 bytes of data at the current position
procedure Write4(Data: integer); {$ifdef HASINLINE}inline;{$endif}
/// append 4 bytes of data, encoded as BigEndian, at the current position
procedure Write4BigEndian(Data: integer); {$ifdef HASINLINE}inline;{$endif}
/// append 8 bytes of data at the current position
procedure Write8(const Data8Bytes); {$ifdef HASINLINE}inline;{$endif}
/// append the same byte a given number of occurences at the current position
procedure WriteN(Data: Byte; Count: integer);
/// append some UTF-8 encoded text at the current position
// - will write the string length (as VarUInt32), then the string content, as expected
// by the FromVarString() function
procedure Write(const Text: RawByteString); overload; {$ifdef HASINLINE}inline;{$endif}
/// append some UTF-8 encoded text at the current position
// - will write the string length (as VarUInt32), then the string content
procedure WriteShort(const Text: ShortString);
/// append some content at the current position
// - will write the binary data, without any length prefix
procedure WriteBinary(const Data: RawByteString);
{$ifndef NOVARIANTS}
/// append some variant value at the current position
// - matches FromVarVariant() and VariantSave/VariantLoad format
procedure Write(const Value: variant); overload;
/// append some TDocVariant value at the current position, as JSON string
// - matches TFastReader.NextDocVariantData format
procedure WriteDocVariantData(const Value: variant);
{$endif}
/// append some record at the current position, with binary serialization
// - will use the binary serialization as for:
// ! aWriter.WriteBinary(RecordSave(Rec,RecTypeInfo));
// but writing directly into the buffer, if possible
procedure WriteRecord(const Rec; RecTypeInfo: pointer);
/// append some dynamic array at the current position
// - will use the binary serialization as for:
// ! aWriter.WriteBinary(DA.SaveTo);
// but writing directly into the buffer, if possible
procedure WriteDynArray(const DA: TDynArray);
/// append "New[0..Len-1] xor Old[0..Len-1]" bytes
// - as used e.g. by ZeroCompressXor/TSynBloomFilterDiff.SaveTo
procedure WriteXor(New,Old: PAnsiChar; Len: PtrInt; crc: PCardinal=nil);
/// append a cardinal value using 32-bit variable-length integer encoding
procedure WriteVarUInt32(Value: PtrUInt);
/// append an integer value using 32-bit variable-length integer encoding of
// the by-two complement of the given value
procedure WriteVarInt32(Value: PtrInt);
/// append an integer value using 64-bit variable-length integer encoding of
// the by-two complement of the given value
procedure WriteVarInt64(Value: Int64);
/// append an unsigned integer value using 64-bit variable-length encoding
procedure WriteVarUInt64(Value: QWord);
/// append cardinal values (NONE must be negative!) using 32-bit
// variable-length integer encoding or other specialized algorithm,
// depending on the data layout
procedure WriteVarUInt32Array(const Values: TIntegerDynArray; ValuesCount: integer;
DataLayout: TFileBufferWriterKind);
/// append cardinal values (NONE must be negative!) using 32-bit
// variable-length integer encoding or other specialized algorithm,
// depending on the data layout
procedure WriteVarUInt32Values(Values: PIntegerArray; ValuesCount: integer;
DataLayout: TFileBufferWriterKind);
/// append UInt64 values using 64-bit variable length integer encoding
// - if Offset is TRUE, then it will store the difference between
// two values using 64-bit variable-length integer encoding (in this case,
// a fixed-sized record storage is also handled separately)
// - could be decoded later on via TFileBufferReader.ReadVarUInt64Array
procedure WriteVarUInt64DynArray(const Values: TInt64DynArray;
ValuesCount: integer; Offset: Boolean);
/// append the RawUTF8 dynamic array
// - handled the fixed size strings array case in a very efficient way
procedure WriteRawUTF8DynArray(const Values: TRawUTF8DynArray; ValuesCount: integer);
/// append a RawUTF8 array of values, from its low-level memory pointer
// - handled the fixed size strings array case in a very efficient way
procedure WriteRawUTF8Array(Values: PPtrUIntArray; ValuesCount: integer);
/// append the RawUTF8List content
// - if StoreObjectsAsVarUInt32 is TRUE, all Objects[] properties will be
// stored as VarUInt32
procedure WriteRawUTF8List(List: TRawUTF8List; StoreObjectsAsVarUInt32: Boolean=false);
/// append a TStream content
// - is StreamSize is left as -1, the Stream.Size is used
// - the size of the content is stored in the resulting stream
procedure WriteStream(aStream: TCustomMemoryStream; aStreamSize: Integer=-1);
/// allows to write directly to a memory buffer
// - caller should specify the maximum possible number of bytes to be written
// - then write the data to the returned pointer, and call DirectWriteFlush
function DirectWritePrepare(len: PtrInt; out tmp: RawByteString): PAnsiChar;
/// finalize a direct write to a memory buffer
// - by specifying the number of bytes written to the buffer
procedure DirectWriteFlush(len: PtrInt; const tmp: RawByteString);
/// write any pending data in the internal buffer to the file
// - after a Flush, it's possible to call FileSeek64(aFile,....)
// - returns the number of bytes written between two FLush method calls
function Flush: Int64;
/// write any pending data, then call algo.Compress() on the buffer
// - expect the instance to have been created via
// ! TFileBufferWriter.Create(TRawByteStringStream)
// - if algo is left to its default nil, will use global AlgoSynLZ
// - features direct compression from internal buffer, if stream was not used
// - BufferOffset could be set to reserve some bytes before the compressed buffer
function FlushAndCompress(nocompression: boolean=false; algo: TAlgoCompress=nil;
BufferOffset: integer=0): RawByteString;
/// rewind the Stream to the position when Create() was called
// - note that this does not clear the Stream content itself, just
// move back its writing position to its initial place
procedure CancelAll; virtual;
/// the associated writing stream
property Stream: TStream read fStream;
/// get the byte count written since last Flush
property TotalWritten: Int64 read fTotalWritten;
/// simple property used to store some integer content
property Tag: PtrInt read fTag write fTag;
end;
PFileBufferReader = ^TFileBufferReader;
/// this structure can be used to speed up reading from a file
// - use internaly memory mapped files for a file up to 2 GB (Windows has
// problems with memory mapped files bigger than this size limit - at least
// with 32-bit executables) - but sometimes, Windows fails to allocate
// more than 512 MB for a memory map, because it does lack of contiguous
// memory space: in this case, we fall back on direct file reading
// - maximum handled file size has no limit (but will use slower direct
// file reading)
// - can handle sophisticated storage layout of TFileBufferWriter for
// dynamic arrays of Integer/Int64/RawUTF8
// - is defined as an object or as a record, due to a bug
// in Delphi 2009/2010 compiler (at least): this structure is not initialized
// if defined as an object on the stack, but will be as a record :(
TFileBufferReader = object
protected
fCurrentPos: PtrUInt;
fMap: TMemoryMap;
/// get Isize + buffer from current memory map or fBufTemp into (P,PEnd)
procedure ReadChunk(out P, PEnd: PByte; var BufTemp: RawByteString);
public
/// initialize the buffer, and specify a file to use for reading
// - will try to map the whole file content in memory
// - if memory mapping failed, or aFileNotMapped is true, methods
// will use default slower file API
procedure Open(aFile: THandle; aFileNotMapped: boolean=false);
/// initialize the buffer from an already existing memory block
// - may be e.g. a resource or a TMemoryStream
procedure OpenFrom(aBuffer: pointer; aBufferSize: PtrUInt); overload;
/// initialize the buffer from an already existing memory block
procedure OpenFrom(const aBuffer: RawByteString); overload;
/// initialize the buffer from an already existing Stream
// - accept either TFileStream or TCustomMemoryStream kind of stream
function OpenFrom(Stream: TStream): boolean; overload;
/// close all internal mapped files
// - call Open() again to use the Read() methods
procedure Close;
{$ifndef CPU64}
/// change the current reading position, from the beginning of the file
// - returns TRUE if success, or FALSE if Offset is out of range
function Seek(Offset: Int64): boolean; overload;
{$endif}
/// change the current reading position, from the beginning of the file
// - returns TRUE if success, or FALSE if Offset is out of range
function Seek(Offset: PtrInt): boolean; overload;
/// raise an exception in case of invalid content
procedure ErrorInvalidContent;
/// read some bytes from the given reading position
// - returns the number of bytes which was read
// - if Data is nil, it won't read content but will forward reading position
function Read(Data: pointer; DataLen: PtrInt): integer; overload;
/// read some UTF-8 encoded text at the current position
// - returns the resulting text length, in bytes
function Read(out Text: RawUTF8): integer; overload;
/// read some buffer texgt at the current position
// - returns the resulting text length, in bytes
function Read(out Text: RawByteString): integer; overload;
/// read some UTF-8 encoded text at the current position
// - returns the resulting text
function ReadRawUTF8: RawUTF8; {$ifdef HASINLINE}inline;{$endif}
/// read one byte
// - if reached end of file, don't raise any error, but returns 0
function ReadByte: PtrUInt; {$ifdef HASINLINE}inline;{$endif}
/// read one cardinal, which was written as fixed length
// - if reached end of file, don't raise any error, but returns 0
function ReadCardinal: cardinal;
/// read one cardinal value encoded using our 32-bit variable-length integer
function ReadVarUInt32: PtrUInt;
/// read one integer value encoded using our 32-bit variable-length integer,
// and the by-two complement
function ReadVarInt32: PtrInt;
/// read one UInt64 value encoded using our 64-bit variable-length integer
function ReadVarUInt64: QWord;
/// read one Int64 value encoded using our 64-bit variable-length integer
function ReadVarInt64: Int64;
/// retrieved cardinal values encoded with TFileBufferWriter.WriteVarUInt32Array
// - returns the number of items read into Values[] (may differ from
// length(Values), which will be resized, so could be void before calling)
// - if the returned integer is negative, it is -Count, and testifies from
// wkFakeMarker and the content should be retrieved by the caller
function ReadVarUInt32Array(var Values: TIntegerDynArray): PtrInt;
/// retrieved Int64 values encoded with TFileBufferWriter.WriteVarUInt64DynArray
// - returns the number of items read into Values[] (may differ from length(Values))
function ReadVarUInt64Array(var Values: TInt64DynArray): PtrInt;
/// retrieved RawUTF8 values encoded with TFileBufferWriter.WriteRawUTF8DynArray
// - returns the number of items read into Values[] (may differ from length(Values))
function ReadVarRawUTF8DynArray(var Values: TRawUTF8DynArray): PtrInt;
/// retrieve the RawUTF8List content encoded with TFileBufferWriter.WriteRawUTF8List
// - if StoreObjectsAsVarUInt32 was TRUE, all Objects[] properties will be
// retrieved as VarUInt32
function ReadRawUTF8List(List: TRawUTF8List): boolean;
/// retrieve a pointer to the current position, for a given data length
// - if the data is available in the current memory mapped file, it
// will just return a pointer to it
// - otherwise (i.e. if the data is split between to 1GB memory map buffers),
// data will be copied into the temporary aTempData buffer before retrieval
function ReadPointer(DataLen: PtrUInt; var aTempData: RawByteString): pointer;
/// create a TMemoryStream instance from the current position
// - the content size is either specified by DataLen>=0, either available at
// the current position, as saved by TFileBufferWriter.WriteStream method
// - if this content fit in the current 1GB memory map buffer, a
// TSynMemoryStream instance is returned, with no data copy (faster)
// - if this content is not already mapped in memory, a separate memory map
// will be created (the returned instance is a TSynMemoryStreamMapped)
function ReadStream(DataLen: PtrInt=-1): TCustomMemoryStream;
/// retrieve the current in-memory pointer
// - if file was not memory-mapped, returns nil
// - if DataLen>0, will increment the current in-memory position
function CurrentMemory(DataLen: PtrUInt=0; PEnd: PPAnsiChar=nil): pointer;
/// retrieve the current in-memory position
// - if file was not memory-mapped, returns -1
function CurrentPosition: integer; {$ifdef HASINLINE}inline;{$endif}
/// read-only access to the global file size
function FileSize: Int64; {$ifdef HASINLINE}inline;{$endif}
/// read-only access to the global mapped buffer binary
function MappedBuffer: PAnsiChar; {$ifdef HASINLINE}inline;{$endif}
end;
/// implements a thread-safe Bloom Filter storage
// - a "Bloom Filter" is a space-efficient probabilistic data structure,
// that is used to test whether an element is a member of a set. False positive
// matches are possible, but false negatives are not. Elements can be added to
// the set, but not removed. Typical use cases are to avoid unecessary
// slow disk or network access if possible, when a lot of items are involved.
// - memory use is very low, when compared to storage of all values: fewer
// than 10 bits per element are required for a 1% false positive probability,
// independent of the size or number of elements in the set - for instance,
// storing 10,000,000 items presence with 1% of false positive ratio
// would consume only 11.5 MB of memory, using 7 hash functions
// - use Insert() methods to add an item to the internal bits array, and
// Reset() to clear all bits array, if needed
// - MayExist() function would check if the supplied item was probably set
// - SaveTo() and LoadFrom() methods allow transmission of the bits array,
// for a disk/database storage or transmission over a network
// - internally, several (hardware-accelerated) crc32c hash functions will be
// used, with some random seed values, to simulate several hashing functions
// - Insert/MayExist/Reset methods are thread-safe
TSynBloomFilter = class(TSynPersistentLock)
private
fSize: cardinal;
fFalsePositivePercent: double;
fBits: cardinal;
fHashFunctions: cardinal;
fInserted: cardinal;
fStore: RawByteString;
function GetInserted: cardinal;
public
/// initialize the internal bits storage for a given number of items
// - by default, internal bits array size will be guess from a 1 % false
// positive rate - but you may specify another value, to reduce memory use
// - this constructor would compute and initialize Bits and HashFunctions
// corresponding to the expected false positive ratio
constructor Create(aSize: integer; aFalsePositivePercent: double = 1); reintroduce; overload;
/// initialize the internal bits storage from a SaveTo() binary buffer
// - this constructor will initialize the internal bits array calling LoadFrom()
constructor Create(const aSaved: RawByteString; aMagic: cardinal=$B1003F11); reintroduce; overload;
/// add an item in the internal bits array storage
// - this method is thread-safe
procedure Insert(const aValue: RawByteString); overload;
/// add an item in the internal bits array storage
// - this method is thread-safe
procedure Insert(aValue: pointer; aValueLen: integer); overload; virtual;
/// clear the internal bits array storage
// - you may call this method after some time, if some items may have
// been removed, to reduce false positives
// - this method is thread-safe
procedure Reset; virtual;
/// returns TRUE if the supplied items was probably set via Insert()
// - some false positive may occur, but not much than FalsePositivePercent
// - this method is thread-safe
function MayExist(const aValue: RawByteString): boolean; overload;
/// returns TRUE if the supplied items was probably set via Insert()
// - some false positive may occur, but not much than FalsePositivePercent
// - this method is thread-safe
function MayExist(aValue: pointer; aValueLen: integer): boolean; overload;
/// store the internal bits array into a binary buffer
// - may be used to transmit or store the state of a dataset, avoiding
// to recompute all Insert() at program startup, or to synchronize
// networks nodes information and reduce the number of remote requests
function SaveTo(aMagic: cardinal=$B1003F11): RawByteString; overload;
/// store the internal bits array into a binary buffer
// - may be used to transmit or store the state of a dataset, avoiding
// to recompute all Insert() at program startup, or to synchronize
// networks nodes information and reduce the number of remote requests
procedure SaveTo(aDest: TFileBufferWriter; aMagic: cardinal=$B1003F11); overload;
/// read the internal bits array from a binary buffer
// - as previously serialized by the SaveTo method
// - may be used to transmit or store the state of a dataset
function LoadFrom(const aSaved: RawByteString; aMagic: cardinal=$B1003F11): boolean; overload;
/// read the internal bits array from a binary buffer
// - as previously serialized by the SaveTo method
// - may be used to transmit or store the state of a dataset
function LoadFrom(P: PByte; PLen: integer; aMagic: cardinal=$B1003F11): boolean; overload; virtual;
published
/// maximum number of items which are expected to be inserted
property Size: cardinal read fSize;
/// expected percentage (1..100) of false positive results for MayExists()
property FalsePositivePercent: double read fFalsePositivePercent;
/// number of bits stored in the internal bits array
property Bits: cardinal read fBits;
/// how many hash functions would be applied for each Insert()
property HashFunctions: cardinal read fHashFunctions;
/// how many times the Insert() method has been called
property Inserted: cardinal read GetInserted;
end;
/// implements a thread-safe differential Bloom Filter storage
// - this inherited class is able to compute incremental serialization of
// its internal bits array, to reduce network use
// - an obfuscated revision counter is used to identify storage history
TSynBloomFilterDiff = class(TSynBloomFilter)
protected
fRevision: Int64;
fSnapShotAfterMinutes: cardinal;
fSnapshotAfterInsertCount: cardinal;
fSnapshotTimestamp: Int64;
fSnapshotInsertCount: cardinal;
fKnownRevision: Int64;
fKnownStore: RawByteString;
public
/// add an item in the internal bits array storage
// - this overloaded thread-safe method would compute fRevision
procedure Insert(aValue: pointer; aValueLen: integer); override;
/// clear the internal bits array storage
// - this overloaded thread-safe method would reset fRevision
procedure Reset; override;
/// store the internal bits array into an incremental binary buffer
// - here the difference from a previous SaveToDiff revision will be computed
// - if aKnownRevision is outdated (e.g. if equals 0), the whole bits array
// would be returned, and around 10 bits per item would be transmitted
// (for 1% false positive ratio)
// - incremental retrieval would then return around 10 bytes per newly added
// item since the last snapshot reference state (with 1% ratio, i.e. 7 hash
// functions)
function SaveToDiff(const aKnownRevision: Int64): RawByteString;
/// use the current internal bits array state as known revision
// - is done the first time SaveToDiff() is called, then after 1/32th of
// the filter size has been inserted (see SnapshotAfterInsertCount property),
// or after SnapShotAfterMinutes property timeout period
procedure DiffSnapshot;
/// retrieve the revision number from an incremental binary buffer
// - returns 0 if the supplied binary buffer does not match this bloom filter
function DiffKnownRevision(const aDiff: RawByteString): Int64;
/// read the internal bits array from an incremental binary buffer
// - as previously serialized by the SaveToDiff() method
// - may be used to transmit or store the state of a dataset
// - returns false if the supplied content is incorrect, e.g. if the known
// revision is deprecated
function LoadFromDiff(const aDiff: RawByteString): boolean;
/// the opaque revision number of this internal storage
// - is in fact the Unix timestamp shifted by 31 bits, and an incremental
// counter: this pattern will allow consistent IDs over several ServPanels
property Revision: Int64 read fRevision;
/// after how many Insert() the internal bits array storage should be
// promoted as known revision
// - equals Size div 32 by default
property SnapshotAfterInsertCount: cardinal read fSnapshotAfterInsertCount
write fSnapshotAfterInsertCount;
/// after how many time the internal bits array storage should be
// promoted as known revision
// - equals 30 minutes by default
property SnapShotAfterMinutes: cardinal read fSnapShotAfterMinutes
write fSnapShotAfterMinutes;
end;
/// RLE compression of a memory buffer containing mostly zeros
// - will store the number of consecutive zeros instead of plain zero bytes
// - used for spare bit sets, e.g. TSynBloomFilter serialization
// - will also compute the crc32c of the supplied content
// - use ZeroDecompress() to expand the compressed result
// - resulting content would be at most 14 bytes bigger than the input
// - you may use this function before SynLZ compression
procedure ZeroCompress(P: PAnsiChar; Len: integer; Dest: TFileBufferWriter);
/// RLE uncompression of a memory buffer containing mostly zeros
// - returns Dest='' if P^ is not a valid ZeroCompress() function result
// - used for spare bit sets, e.g. TSynBloomFilter serialization
// - will also check the crc32c of the supplied content
procedure ZeroDecompress(P: PByte; Len: integer; {$ifdef FPC}var{$else}out{$endif} Dest: RawByteString);
/// RLE compression of XORed memory buffers resulting in mostly zeros
// - will perform ZeroCompress(Dest^ := New^ xor Old^) without any temporary
// memory allocation
// - is used e.g. by TSynBloomFilterDiff.SaveToDiff() in incremental mode
// - will also compute the crc32c of the supplied content
procedure ZeroCompressXor(New,Old: PAnsiChar; Len: cardinal; Dest: TFileBufferWriter);
/// RLE uncompression and ORing of a memory buffer containing mostly zeros
// - will perform Dest^ := Dest^ or ZeroDecompress(P^) without any temporary
// memory allocation
// - is used e.g. by TSynBloomFilterDiff.LoadFromDiff() in incremental mode
// - returns false if P^ is not a valid ZeroCompress/ZeroCompressXor() result
// - will also check the crc32c of the supplied content
function ZeroDecompressOr(P,Dest: PAnsiChar; Len,DestLen: integer): boolean;
const
/// normal pattern search depth for DeltaCompress()
// - gives good results on most content
DELTA_LEVEL_FAST = 100;
/// brutal pattern search depth for DeltaCompress()
// - may become very slow, with minor benefit, on huge content
DELTA_LEVEL_BEST = 500;
/// 2MB as internal chunks/window default size for DeltaCompress()
// - will use up to 9 MB of RAM during DeltaCompress() - none in DeltaExtract()
DELTA_BUF_DEFAULT = 2 shl 20;
/// compute difference of two binary buffers
// - returns '=' for equal buffers, or an optimized binary delta
// - DeltaExtract() could be used later on to compute New from Old + Delta
function DeltaCompress(const New, Old: RawByteString;
Level: integer=DELTA_LEVEL_FAST; BufSize: integer=DELTA_BUF_DEFAULT): RawByteString; overload;
/// compute difference of two binary buffers
// - returns '=' for equal buffers, or an optimized binary delta
// - DeltaExtract() could be used later on to compute New from Old
function DeltaCompress(New, Old: PAnsiChar; NewSize, OldSize: integer;
Level: integer=DELTA_LEVEL_FAST; BufSize: integer=DELTA_BUF_DEFAULT): RawByteString; overload;
/// compute difference of two binary buffers
// - returns '=' for equal buffers, or an optimized binary delta
// - DeltaExtract() could be used later on to compute New from Old + Delta
// - caller should call Freemem(Delta) once finished with the output buffer
function DeltaCompress(New, Old: PAnsiChar; NewSize, OldSize: integer;
out Delta: PAnsiChar; Level: integer=DELTA_LEVEL_FAST; BufSize: integer=DELTA_BUF_DEFAULT): integer; overload;
type
/// result of function DeltaExtract()
TDeltaError = (
dsSuccess, dsCrcCopy, dsCrcComp, dsCrcBegin, dsCrcEnd, dsCrcExtract, dsFlag, dsLen);
/// returns how many bytes a DeltaCompress() result will expand to
function DeltaExtractSize(const Delta: RawByteString): integer; overload;
/// returns how many bytes a DeltaCompress() result will expand to
function DeltaExtractSize(Delta: PAnsiChar): integer; overload;
/// apply the delta binary as computed by DeltaCompress()
// - decompression don't use any RAM, will perform crc32c check, and is very fast
// - return dsSuccess if was uncompressed to aOutUpd as expected
function DeltaExtract(const Delta,Old: RawByteString; out New: RawByteString): TDeltaError; overload;
/// low-level apply the delta binary as computed by DeltaCompress()
// - New should already be allocated with DeltaExtractSize(Delta) bytes
// - as such, expect Delta, Old and New to be <> nil, and Delta <> '='
// - return dsSuccess if was uncompressed to aOutUpd as expected
function DeltaExtract(Delta,Old,New: PAnsiChar): TDeltaError; overload;
function ToText(err: TDeltaError): PShortString; overload;
{ ************ high-level storage classes ************************* }
type
/// implement a cache of some key/value pairs, e.g. to improve reading speed
// - used e.g. by TSQLDataBase for caching the SELECT statements results in an
// internal JSON format (which is faster than a query to the SQLite3 engine)
// - internally make use of an efficient hashing algorithm for fast response
// (i.e. TSynNameValue will use the TDynArrayHashed wrapper mechanism)
// - this class is thread-safe if you use properly the associated Safe lock
TSynCache = class(TSynPersistentLock)
protected
fFindLastKey: RawUTF8;
fNameValue: TSynNameValue;
fRamUsed: cardinal;
fMaxRamUsed: cardinal;
fTimeoutSeconds: cardinal;
fTimeoutTix: cardinal;
procedure ResetIfNeeded;
public
/// initialize the internal storage
// - aMaxCacheRamUsed can set the maximum RAM to be used for values, in bytes
// (default is 16 MB), after which the cache is flushed
// - by default, key search is done case-insensitively, but you can specify
// another option here
// - by default, there is no timeout period, but you may specify a number of
// seconds of inactivity (i.e. no Add call) after which the cache is flushed
constructor Create(aMaxCacheRamUsed: cardinal=16 shl 20;
aCaseSensitive: boolean=false; aTimeoutSeconds: cardinal=0); reintroduce;
/// find a Key in the cache entries
// - return '' if nothing found: you may call Add() just after to insert
// the expected value in the cache
// - return the associated Value otherwise, and the associated integer tag
// if aResultTag address is supplied
// - this method is not thread-safe, unless you call Safe.Lock before
// calling Find(), and Safe.Unlock after calling Add()
function Find(const aKey: RawUTF8; aResultTag: PPtrInt=nil): RawUTF8;
/// add a Key and its associated value (and tag) to the cache entries
// - you MUST always call Find() with the associated Key first
// - this method is not thread-safe, unless you call Safe.Lock before
// calling Find(), and Safe.Unlock after calling Add()
procedure Add(const aValue: RawUTF8; aTag: PtrInt);
/// add a Key/Value pair in the cache entries
// - returns true if aKey was not existing yet, and aValue has been stored
// - returns false if aKey did already exist in the internal cache, and
// its entry has been updated with the supplied aValue/aTag
// - this method is thread-safe, using the Safe locker of this instance
function AddOrUpdate(const aKey, aValue: RawUTF8; aTag: PtrInt): boolean;
/// called after a write access to the database to flush the cache
// - set Count to 0
// - release all cache memory
// - returns TRUE if was flushed, i.e. if there was something in cache
// - this method is thread-safe, using the Safe locker of this instance
function Reset: boolean;
/// number of entries in the cache
function Count: integer;
/// access to the internal locker, for thread-safe process
// - Find/Add methods calls should be protected as such:
// ! cache.Safe.Lock;
// ! try
// ! ... cache.Find/cache.Add ...
// ! finally
// ! cache.Safe.Unlock;
// ! end;
property Safe: PSynLocker read fSafe;
/// the current global size of Values in RAM cache, in bytes
property RamUsed: cardinal read fRamUsed;
/// the maximum RAM to be used for values, in bytes
// - the cache is flushed when ValueSize reaches this limit
// - default is 16 MB (16 shl 20)
property MaxRamUsed: cardinal read fMaxRamUsed;
/// after how many seconds betwen Add() calls the cache should be flushed
// - equals 0 by default, meaning no time out
property TimeoutSeconds: cardinal read fTimeoutSeconds;
end;
/// thread-safe FIFO (First-In-First-Out) in-order queue of records
// - uses internally a dynamic array storage, with a sliding algorithm
// (more efficient than the FPC or Delphi TQueue)
TSynQueue = class(TSynPersistentLock)
protected
fValues: TDynArray;
fValueVar: pointer;
fCount, fFirst, fLast: integer;
fWaitPopFlags: set of (wpfDestroying);
fWaitPopCounter: integer;
procedure InternalGrow;
function InternalDestroying(incPopCounter: integer): boolean;
function InternalWaitDone(endtix: Int64; const idle: TThreadMethod): boolean;
public
/// initialize the queue storage
// - aTypeInfo should be a dynamic array TypeInfo() RTTI pointer, which
// would store the values within this TSynQueue instance
constructor Create(aTypeInfo: pointer); reintroduce; virtual;
/// finalize the storage
// - would release all internal stored values, and call WaitPopFinalize
destructor Destroy; override;
/// store one item into the queue
// - this method is thread-safe, since it will lock the instance
procedure Push(const aValue);
/// extract one item from the queue, as FIFO (First-In-First-Out)
// - returns true if aValue has been filled with a pending item, which
// is removed from the queue (use Peek if you don't want to remove it)
// - returns false if the queue is empty
// - this method is thread-safe, since it will lock the instance
function Pop(out aValue): boolean;
/// extract one matching item from the queue, as FIFO (First-In-First-Out)
// - the current pending item is compared with aAnother value
function PopEquals(aAnother: pointer; aCompare: TDynArraySortCompare; out aValue): boolean;
/// lookup one item from the queue, as FIFO (First-In-First-Out)
// - returns true if aValue has been filled with a pending item, without
// removing it from the queue (as Pop method does)
// - returns false if the queue is empty
// - this method is thread-safe, since it will lock the instance
function Peek(out aValue): boolean;
/// waiting extract of one item from the queue, as FIFO (First-In-First-Out)
// - returns true if aValue has been filled with a pending item within the
// specified aTimeoutMS time
// - returns false if nothing was pushed into the queue in time, or if
// WaitPopFinalize has been called
// - aWhenIdle could be assigned e.g. to VCL/LCL Application.ProcessMessages
// - you can optionally compare the pending item before returning it (could
// be used e.g. when several threads are putting items into the queue)
// - this method is thread-safe, but will lock the instance only if needed
function WaitPop(aTimeoutMS: integer; const aWhenIdle: TThreadMethod; out aValue;
aCompared: pointer=nil; aCompare: TDynArraySortCompare=nil): boolean;
/// waiting lookup of one item from the queue, as FIFO (First-In-First-Out)
// - returns a pointer to a pending item within the specified aTimeoutMS
// time - the Safe.Lock is still there, so that caller could check its content,
// then call Pop() if it is the expected one, and eventually always call Safe.Unlock
// - returns nil if nothing was pushed into the queue in time
// - this method is thread-safe, but will lock the instance only if needed
function WaitPeekLocked(aTimeoutMS: integer; const aWhenIdle: TThreadMethod): pointer;
/// ensure any pending or future WaitPop() returns immediately as false
// - is always called by Destroy destructor
// - could be also called e.g. from an UI OnClose event to avoid any lock
// - this method is thread-safe, but will lock the instance only if needed
procedure WaitPopFinalize(aTimeoutMS: integer=100);
/// delete all items currently stored in this queue, and void its capacity
// - this method is thread-safe, since it will lock the instance
procedure Clear;
/// initialize a dynamic array with the stored queue items
// - aDynArrayValues should be a variable defined as aTypeInfo from Create
// - you can retrieve an optional TDynArray wrapper, e.g. for binary or JSON
// persistence
// - this method is thread-safe, and will make a copy of the queue data
procedure Save(out aDynArrayValues; aDynArray: PDynArray=nil);
/// returns how many items are currently stored in this queue
// - this method is thread-safe
function Count: Integer;
/// returns how much slots is currently reserved in memory
// - the queue has an optimized auto-sizing algorithm, you can use this
// method to return its current capacity
// - this method is thread-safe
function Capacity: integer;
/// returns true if there are some items currently pending in the queue
// - slightly faster than checking Count=0, and much faster than Pop or Peek
function Pending: boolean;
end;
/// maintain a thread-safe sorted list of TSynPersistentLock objects
// - will use fast O(log(n)) binary search for efficient search - it is
// a lighter alternative to TObjectListHashedAbstract/TObjectListPropertyHashed
// if hashing has a performance cost (e.g. if there are a few items, or
// deletion occurs regularly)
// - in practice, insertion becomes slower after around 100,000 items stored
// - expect to store only TSynPersistentLock inherited items, so that
// the process is explicitly thread-safe
// - inherited classes should override the Compare and NewItem abstract methods
TObjectListSorted = class(TSynPersistentLock)
protected
fCount: integer;
fObjArray: TSynPersistentLockDynArray;
function FastLocate(const Value; out Index: Integer): boolean;
procedure InsertNew(Item: TSynPersistentLock; Index: integer);
// override those methods for actual implementation
function Compare(Item: TSynPersistentLock; const Value): integer; virtual; abstract;
function NewItem(const Value): TSynPersistentLock; virtual; abstract;
public
/// finalize the list
destructor Destroy; override;
/// search a given TSynPersistentLock instance from a value
// - if returns not nil, caller should make result.Safe.UnLock once finished
// - will use the TObjectListSortedCompare function for the search
function FindLocked(const Value): pointer;
/// search or add a given TSynPersistentLock instance from a value
// - if returns not nil, caller should make result.Safe.UnLock once finished
// - added is TRUE if a new void item has just been created
// - will use the TObjectListSortedCompare function for the search
function FindOrAddLocked(const Value; out added: boolean): pointer;
/// remove a given TSynPersistentLock instance from a value
function Delete(const Value): boolean;
/// how many items are actually stored
property Count: Integer read fCount;
/// low-level access to the stored items
// - warning: use should be protected by Lock.Enter/Lock.Leave
property ObjArray: TSynPersistentLockDynArray read fObjArray;
end;
/// abstract high-level handling of (SynLZ-)compressed persisted storage
// - LoadFromReader/SaveToWriter abstract methods should be overriden
// with proper binary persistence implementation
TSynPersistentStore = class(TSynPersistentLock)
protected
fName: RawUTF8;
fReader: TFastReader;
fReaderTemp: PRawByteString;
fLoadFromLastUncompressed, fSaveToLastUncompressed: integer;
fLoadFromLastAlgo: TAlgoCompress;
2021-04-23 10:11:05 +02:00
/// low-level virtual methods implementing the persistence
procedure LoadFromReader; virtual;
procedure SaveToWriter(aWriter: TFileBufferWriter); virtual;
public
/// initialize a void storage with the supplied name
constructor Create(const aName: RawUTF8); reintroduce; overload; virtual;
/// initialize a storage from a SaveTo persisted buffer
// - raise a EFastReader exception on decoding error
constructor CreateFrom(const aBuffer: RawByteString;
aLoad: TAlgoCompressLoad = aclNormal);
/// initialize a storage from a SaveTo persisted buffer
// - raise a EFastReader exception on decoding error
constructor CreateFromBuffer(aBuffer: pointer; aBufferLen: integer;
aLoad: TAlgoCompressLoad = aclNormal);
/// initialize a storage from a SaveTo persisted buffer
// - raise a EFastReader exception on decoding error
constructor CreateFromFile(const aFileName: TFileName;
aLoad: TAlgoCompressLoad = aclNormal);
/// fill the storage from a SaveTo persisted buffer
// - actually call the LoadFromReader() virtual method for persistence
// - raise a EFastReader exception on decoding error
procedure LoadFrom(const aBuffer: RawByteString;
aLoad: TAlgoCompressLoad = aclNormal); overload;
/// initialize the storage from a SaveTo persisted buffer
// - actually call the LoadFromReader() virtual method for persistence
// - raise a EFastReader exception on decoding error
procedure LoadFrom(aBuffer: pointer; aBufferLen: integer;
aLoad: TAlgoCompressLoad = aclNormal); overload; virtual;
/// initialize the storage from a SaveToFile content
// - actually call the LoadFromReader() virtual method for persistence
// - returns false if the file is not found, true if the file was loaded
// without any problem, or raise a EFastReader exception on decoding error
function LoadFromFile(const aFileName: TFileName;
aLoad: TAlgoCompressLoad = aclNormal): boolean;
/// persist the content as a SynLZ-compressed binary blob
// - to be retrieved later on via LoadFrom method
// - actually call the SaveToWriter() protected virtual method for persistence
// - you can specify ForcedAlgo if you want to override the default AlgoSynLZ
// - BufferOffset could be set to reserve some bytes before the compressed buffer
procedure SaveTo(out aBuffer: RawByteString; nocompression: boolean=false;
BufLen: integer=65536; ForcedAlgo: TAlgoCompress=nil; BufferOffset: integer=0); overload; virtual;
/// persist the content as a SynLZ-compressed binary blob
// - just an overloaded wrapper
function SaveTo(nocompression: boolean=false; BufLen: integer=65536;
ForcedAlgo: TAlgoCompress=nil; BufferOffset: integer=0): RawByteString; overload;
{$ifdef HASINLINE}inline;{$endif}
/// persist the content as a SynLZ-compressed binary file
// - to be retrieved later on via LoadFromFile method
// - returns the number of bytes of the resulting file
// - actually call the SaveTo method for persistence
function SaveToFile(const aFileName: TFileName; nocompression: boolean=false;
BufLen: integer=65536; ForcedAlgo: TAlgoCompress=nil): PtrUInt;
/// one optional text associated with this storage
// - you can define this field as published to serialize its value in log/JSON
property Name: RawUTF8 read fName;
/// after a LoadFrom(), contains the uncompressed data size read
property LoadFromLastUncompressed: integer read fLoadFromLastUncompressed;
/// after a SaveTo(), contains the uncompressed data size written
property SaveToLastUncompressed: integer read fSaveToLastUncompressed;
end;
/// implement binary persistence and JSON serialization (not deserialization)
TSynPersistentStoreJson = class(TSynPersistentStore)
protected
// append "name" -> inherited should add properties to the JSON object
procedure AddJSON(W: TTextWriter); virtual;
public
/// serialize this instance as a JSON object
function SaveToJSON(reformat: TTextWriterJSONFormat = jsonCompact): RawUTF8;
end;
type
/// item as stored in a TRawByteStringGroup instance
TRawByteStringGroupValue = record
Position: integer;
Value: RawByteString;
end;
PRawByteStringGroupValue = ^TRawByteStringGroupValue;
/// items as stored in a TRawByteStringGroup instance
TRawByteStringGroupValueDynArray = array of TRawByteStringGroupValue;
/// store several RawByteString content with optional concatenation
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TRawByteStringGroup = record
{$else}TRawByteStringGroup = object{$endif}
public
/// actual list storing the data
Values: TRawByteStringGroupValueDynArray;
/// how many items are currently stored in Values[]
Count: integer;
/// the current size of data stored in Values[]
Position: integer;
/// naive but efficient cache for Find()
LastFind: integer;
/// add a new item to Values[]
procedure Add(const aItem: RawByteString); overload;
/// add a new item to Values[]
procedure Add(aItem: pointer; aItemLen: integer); overload;
{$ifndef DELPHI5OROLDER}
/// add another TRawByteStringGroup to Values[]
procedure Add(const aAnother: TRawByteStringGroup); overload;
/// low-level method to abort the latest Add() call
// - warning: will work only once, if an Add() has actually been just called:
// otherwise, the behavior is unexpected, and may wrongly truncate data
procedure RemoveLastAdd;
/// compare two TRawByteStringGroup instance stored text
function Equals(const aAnother: TRawByteStringGroup): boolean;
{$endif DELPHI5OROLDER}
/// clear any stored information
procedure Clear;
/// append stored information into another RawByteString, and clear content
procedure AppendTextAndClear(var aDest: RawByteString);
// compact the Values[] array into a single item
// - is also used by AsText to compute a single RawByteString
procedure Compact;
/// return all content as a single RawByteString
// - will also compact the Values[] array into a single item (which is returned)
function AsText: RawByteString;
/// return all content as a single TByteDynArray
function AsBytes: TByteDynArray;
/// save all content into a TTextWriter instance
procedure Write(W: TTextWriter; Escape: TTextWriterKind=twJSONEscape); overload;
/// save all content into a TFileBufferWriter instance
procedure WriteBinary(W: TFileBufferWriter); overload;
/// save all content as a string into a TFileBufferWriter instance
// - storing the length as WriteVarUInt32() prefix
procedure WriteString(W: TFileBufferWriter);
/// add another TRawByteStringGroup previously serialized via WriteString()
procedure AddFromReader(var aReader: TFastReader);
/// returns a pointer to Values[] containing a given position
// - returns nil if not found
function Find(aPosition: integer): PRawByteStringGroupValue; overload;
/// returns a pointer to Values[].Value containing a given position and length
// - returns nil if not found
function Find(aPosition, aLength: integer): pointer; overload;
/// returns the text at a given position in Values[]
// - text should be in a single Values[] entry
procedure FindAsText(aPosition, aLength: integer; out aText: RawByteString); overload;
{$ifdef HASINLINE}inline;{$endif}
/// returns the text at a given position in Values[]
// - text should be in a single Values[] entry
function FindAsText(aPosition, aLength: integer): RawByteString; overload;
{$ifdef HASINLINE}inline;{$endif}
{$ifndef NOVARIANTS}
/// returns the text at a given position in Values[]
// - text should be in a single Values[] entry
// - explicitly returns null if the supplied text was not found
procedure FindAsVariant(aPosition, aLength: integer; out aDest: variant);
{$ifdef HASINLINE}inline;{$endif}
{$endif}
/// append the text at a given position in Values[], JSON escaped by default
// - text should be in a single Values[] entry
procedure FindWrite(aPosition, aLength: integer; W: TTextWriter;
Escape: TTextWriterKind=twJSONEscape; TrailingCharsToIgnore: integer=0);
{$ifdef HASINLINE}inline;{$endif}
/// append the blob at a given position in Values[], base-64 encoded
// - text should be in a single Values[] entry
procedure FindWriteBase64(aPosition, aLength: integer; W: TTextWriter;
withMagic: boolean); {$ifdef HASINLINE}inline;{$endif}
/// copy the text at a given position in Values[]
// - text should be in a single Values[] entry
procedure FindMove(aPosition, aLength: integer; aDest: pointer);
end;
/// pointer reference to a TRawByteStringGroup
PRawByteStringGroup = ^TRawByteStringGroup;
/// simple stack-allocated type for handling a non-void type names list
// - Delphi "object" is buggy on stack -> also defined as record with methods
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TPropNameList = record
{$else}TPropNameList = object{$endif}
public
/// the actual names storage
Values: TRawUTF8DynArray;
/// how many items are currently in Values[]
Count: Integer;
/// initialize the list
// - set Count := 0
procedure Init; {$ifdef HASINLINE}inline;{$endif}
/// search for a Value within Values[0..Count-1] using IdemPropNameU()
function FindPropName(const Value: RawUTF8): Integer; {$ifdef HASINLINE}inline;{$endif}
/// if Value is in Values[0..Count-1] using IdemPropNameU() returns FALSE
// - otherwise, returns TRUE and add Value to Values[]
// - any Value='' is rejected
function AddPropName(const Value: RawUTF8): Boolean;
end;
{ ************ Security and Identifier classes ************************** }
type
/// 64-bit integer unique identifier, as computed by TSynUniqueIdentifierGenerator
// - they are increasing over time (so are much easier to store/shard/balance
// than UUID/GUID), and contain generation time and a 16-bit process ID
// - mapped by TSynUniqueIdentifierBits memory structure
// - may be used on client side for something similar to a MongoDB ObjectID,
// but compatible with TSQLRecord.ID: TID properties
TSynUniqueIdentifier = type Int64;
/// 16-bit unique process identifier, used to compute TSynUniqueIdentifier
// - each TSynUniqueIdentifierGenerator instance is expected to have
// its own unique process identifier, stored as a 16 bit integer 1..65535 value
TSynUniqueIdentifierProcess = type word;
{$A-}
/// map 64-bit integer unique identifier internal memory structure
// - as stored in TSynUniqueIdentifier = Int64 values, and computed by
// TSynUniqueIdentifierGenerator
// - bits 0..14 map a 15-bit increasing counter (collision-free)
// - bits 15..30 map a 16-bit process identifier
// - bits 31..63 map a 33-bit UTC time, encoded as seconds since Unix epoch
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TSynUniqueIdentifierBits = record
{$else}TSynUniqueIdentifierBits = object{$endif}
public
/// the actual 64-bit storage value
// - in practice, only first 63 bits are used
Value: TSynUniqueIdentifier;
/// 15-bit counter (0..32767), starting with a random value
function Counter: word;
{$ifdef HASINLINE}inline;{$endif}
/// 16-bit unique process identifier
// - as specified to TSynUniqueIdentifierGenerator constructor
function ProcessID: TSynUniqueIdentifierProcess;
{$ifdef HASINLINE}inline;{$endif}
/// low-endian 4-byte value representing the seconds since the Unix epoch
// - time is expressed in Coordinated Universal Time (UTC), not local time
// - it uses in fact a 33-bit resolution, so is "Year 2038" bug-free
function CreateTimeUnix: TUnixTime;
{$ifdef HASINLINE}inline;{$endif}
/// fill this unique identifier structure from its TSynUniqueIdentifier value
// - is just a wrapper around PInt64(@self)^
procedure From(const AID: TSynUniqueIdentifier);
{$ifdef HASINLINE}inline;{$endif}
{$ifndef NOVARIANTS}
/// convert this identifier as an explicit TDocVariant JSON object
// - returns e.g.
// ! {"Created":"2016-04-19T15:27:58","Identifier":1,"Counter":1,
// ! "Value":3137644716930138113,"Hex":"2B8B273F00008001"}
function AsVariant: variant; {$ifdef HASINLINE}inline;{$endif}
/// convert this identifier to an explicit TDocVariant JSON object
// - returns e.g.
// ! {"Created":"2016-04-19T15:27:58","Identifier":1,"Counter":1,
// ! "Value":3137644716930138113,"Hex":"2B8B273F00008001"}
procedure ToVariant(out result: variant);
{$endif NOVARIANTS}
/// extract the UTC generation timestamp from the identifier as TDateTime
// - time is expressed in Coordinated Universal Time (UTC), not local time
function CreateDateTime: TDateTime;
{$ifdef HASINLINE}inline;{$endif}
/// extract the UTC generation timestamp from the identifier
// - time is expressed in Coordinated Universal Time (UTC), not local time
function CreateTimeLog: TTimeLog;
{$ifndef DELPHI5OROLDER}
/// compare two Identifiers
function Equal(const Another: TSynUniqueIdentifierBits): boolean;
{$ifdef HASINLINE}inline;{$endif}
{$endif DELPHI5OROLDER}
/// convert the identifier into a 16 chars hexadecimal string
function ToHexa: RawUTF8;
{$ifdef HASINLINE}inline;{$endif}
/// fill this unique identifier back from a 16 chars hexadecimal string
// - returns TRUE if the supplied hexadecimal is on the expected format
// - returns FALSE if the supplied text is invalid
function FromHexa(const hexa: RawUTF8): boolean;
/// fill this unique identifier with a fake value corresponding to a given
// timestamp
// - may be used e.g. to limit database queries on a particular time range
// - bits 0..30 would be 0, i.e. would set Counter = 0 and ProcessID = 0
procedure FromDateTime(const aDateTime: TDateTime);
/// fill this unique identifier with a fake value corresponding to a given
// timestamp
// - may be used e.g. to limit database queries on a particular time range
// - bits 0..30 would be 0, i.e. would set Counter = 0 and ProcessID = 0
procedure FromUnixTime(const aUnixTime: TUnixTime);
end;
{$A+}
/// points to a 64-bit integer identifier, as computed by TSynUniqueIdentifierGenerator
// - may be used to access the identifier internals, from its stored
// Int64 or TSynUniqueIdentifier value
PSynUniqueIdentifierBits = ^TSynUniqueIdentifierBits;
/// a 24 chars cyphered hexadecimal string, mapping a TSynUniqueIdentifier
// - has handled by TSynUniqueIdentifierGenerator.ToObfuscated/FromObfuscated
TSynUniqueIdentifierObfuscated = type RawUTF8;
/// thread-safe 64-bit integer unique identifier computation
// - may be used on client side for something similar to a MongoDB ObjectID,
// but compatible with TSQLRecord.ID: TID properties, since it will contain
// a 63-bit unsigned integer, following our ORM expectations
// - each identifier would contain a 16-bit process identifier, which is
// supplied by the application, and should be unique for this process at a
// given time
// - identifiers may be obfuscated as hexadecimal text, using both encryption
// and digital signature
TSynUniqueIdentifierGenerator = class(TSynPersistent)
protected
fUnixCreateTime: cardinal;
fLatestCounterOverflowUnixCreateTime: cardinal;
fIdentifier: TSynUniqueIdentifierProcess;
fIdentifierShifted: cardinal;
fLastCounter: cardinal;
fCrypto: array[0..7] of cardinal; // only fCrypto[6..7] are used in practice
fCryptoCRC: cardinal;
fSafe: TSynLocker;
function GetComputedCount: Int64;
public
/// initialize the generator for the given 16-bit process identifier
// - you can supply an obfuscation key, which should be shared for the
// whole system, so that you may use FromObfuscated/ToObfuscated methods
constructor Create(aIdentifier: TSynUniqueIdentifierProcess;
const aSharedObfuscationKey: RawUTF8=''); reintroduce;
/// finalize the generator structure
destructor Destroy; override;
/// return a new unique ID
// - this method is very optimized, and would use very little CPU
procedure ComputeNew(out result: TSynUniqueIdentifierBits); overload;
/// return a new unique ID, type-casted to an Int64
function ComputeNew: Int64; overload;
{$ifdef HASINLINE}inline;{$endif}
/// return an unique ID matching this generator pattern, at a given timestamp
// - may be used e.g. to limit database queries on a particular time range
procedure ComputeFromDateTime(const aDateTime: TDateTime; out result: TSynUniqueIdentifierBits);
/// return an unique ID matching this generator pattern, at a given timestamp
// - may be used e.g. to limit database queries on a particular time range
procedure ComputeFromUnixTime(const aUnixTime: TUnixTime; out result: TSynUniqueIdentifierBits);
/// map a TSynUniqueIdentifier as 24 chars cyphered hexadecimal text
// - cyphering includes simple key-based encryption and a CRC-32 digital signature
function ToObfuscated(const aIdentifier: TSynUniqueIdentifier): TSynUniqueIdentifierObfuscated;
/// retrieve a TSynUniqueIdentifier from 24 chars cyphered hexadecimal text
// - any file extension (e.g. '.jpeg') would be first deleted from the
// supplied obfuscated text
// - returns true if the supplied obfuscated text has the expected layout
// and a valid digital signature
// - returns false if the supplied obfuscated text is invalid
function FromObfuscated(const aObfuscated: TSynUniqueIdentifierObfuscated;
out aIdentifier: TSynUniqueIdentifier): boolean;
/// some 32-bit value, derivated from aSharedObfuscationKey as supplied
// to the class constructor
// - FromObfuscated and ToObfuscated methods will validate their hexadecimal
// content with this value to secure the associated CRC
// - may be used e.g. as system-depending salt
property CryptoCRC: cardinal read fCryptoCRC;
/// direct access to the associated mutex
property Safe: TSynLocker read fSafe;
published
/// the process identifier, associated with this generator
property Identifier: TSynUniqueIdentifierProcess read fIdentifier;
/// how many times ComputeNew method has been called
property ComputedCount: Int64 read GetComputedCount;
end;
type
/// abstract TSynPersistent class allowing safe storage of a password
// - the associated Password, e.g. for storage or transmission encryption
// will be persisted encrypted with a private key (which can be customized)
// - if default simple symmetric encryption is not enough, you may define
// a custom TSynPersistentWithPasswordUserCrypt callback, e.g. to
// SynCrypto's CryptDataForCurrentUser, for hardened password storage
// - a published property should be defined as such in inherited class:
// ! property PasswordPropertyName: RawUTF8 read fPassword write fPassword;
// - use the PassWordPlain property to access to its uncyphered value
TSynPersistentWithPassword = class(TSynPersistent)
protected
fPassWord: RawUTF8;
fKey: cardinal;
function GetKey: cardinal; {$ifdef HASINLINE}inline;{$endif}
function GetPassWordPlain: RawUTF8;
function GetPassWordPlainInternal(AppSecret: RawUTF8): RawUTF8;
procedure SetPassWordPlain(const Value: RawUTF8);
public
/// finalize the instance
destructor Destroy; override;
/// this class method could be used to compute the encrypted password,
// ready to be stored as JSON, according to a given private key
class function ComputePassword(const PlainPassword: RawUTF8;
CustomKey: cardinal=0): RawUTF8; overload;
/// this class method could be used to compute the encrypted password from
// a binary digest, ready to be stored as JSON, according to a given private key
// - just a wrapper around ComputePassword(BinToBase64URI())
class function ComputePassword(PlainPassword: pointer; PlainPasswordLen: integer;
CustomKey: cardinal=0): RawUTF8; overload;
/// this class method could be used to decrypt a password, stored as JSON,
// according to a given private key
// - may trigger a ESynException if the password was stored using a custom
// TSynPersistentWithPasswordUserCrypt callback, and the current user
// doesn't match the expected user stored in the field
class function ComputePlainPassword(const CypheredPassword: RawUTF8;
CustomKey: cardinal=0; const AppSecret: RawUTF8=''): RawUTF8;
/// low-level function used to identify if a given field is a Password
// - this method is used e.g. by TJSONSerializer.WriteObject to identify the
// password field, since its published name is set by the inherited classes
function GetPasswordFieldAddress: pointer; {$ifdef HASINLINE}inline;{$endif}
/// the private key used to cypher the password storage on serialization
// - application can override the default 0 value at runtime
property Key: cardinal read GetKey write fKey;
/// access to the associated unencrypted Password value
// - read may trigger a ESynException if the password was stored using a
// custom TSynPersistentWithPasswordUserCrypt callback, and the current user
// doesn't match the expected user stored in the field
property PasswordPlain: RawUTF8 read GetPassWordPlain write SetPassWordPlain;
end;
var
/// function prototype to customize TSynPersistent class password storage
// - is called when 'user1:base64pass1,user2:base64pass2' layout is found,
// and the current user logged on the system is user1 or user2
// - you should not call this low-level method, but assign e.g. from SynCrypto:
// $ TSynPersistentWithPasswordUserCrypt := CryptDataForCurrentUser;
TSynPersistentWithPasswordUserCrypt:
function(const Data,AppServer: RawByteString; Encrypt: boolean): RawByteString;
type
/// could be used to store a credential pair, as user name and password
// - password will be stored with TSynPersistentWithPassword encryption
TSynUserPassword = class(TSynPersistentWithPassword)
protected
fUserName: RawUTF8;
published
/// the associated user name
property UserName: RawUTF8 read FUserName write FUserName;
/// the associated encrypted password
// - use the PasswordPlain public property to access to the uncrypted password
property Password: RawUTF8 read FPassword write FPassword;
end;
/// handle safe storage of any connection properties
// - would be used by SynDB.pas to serialize TSQLDBConnectionProperties, or
// by mORMot.pas to serialize TSQLRest instances
// - the password will be stored as Base64, after a simple encryption as
// defined by TSynPersistentWithPassword
// - typical content could be:
// $ {
// $ "Kind": "TSQLDBSQLite3ConnectionProperties",
// $ "ServerName": "server",
// $ "DatabaseName": "",
// $ "User": "",
// $ "Password": "PtvlPA=="
// $ }
// - the "Kind" value will be used to let the corresponding TSQLRest or
// TSQLDBConnectionProperties NewInstance*() class methods create the
// actual instance, from its class name
TSynConnectionDefinition = class(TSynPersistentWithPassword)
protected
fKind: string;
fServerName: RawUTF8;
fDatabaseName: RawUTF8;
fUser: RawUTF8;
public
/// unserialize the database definition from JSON
// - as previously serialized with the SaveToJSON method
// - you can specify a custom Key used for password encryption, if the
// default value is not safe enough for you
// - this method won't use JSONToObject() so avoid any dependency to mORMot.pas
constructor CreateFromJSON(const JSON: RawUTF8; Key: cardinal=0); virtual;
/// serialize the database definition as JSON
// - this method won't use ObjectToJSON() so avoid any dependency to mORMot.pas
function SaveToJSON: RawUTF8; virtual;
published
/// the class name implementing the connection or TSQLRest instance
// - will be used to instantiate the expected class type
property Kind: string read fKind write fKind;
/// the associated server name (or file, for SQLite3) to be connected to
property ServerName: RawUTF8 read fServerName write fServerName;
/// the associated database name (if any), or additional options
property DatabaseName: RawUTF8 read fDatabaseName write fDatabaseName;
/// the associated User Identifier (if any)
property User: RawUTF8 read fUser write fUser;
/// the associated Password, e.g. for storage or transmission encryption
// - will be persisted encrypted with a private key
// - use the PassWordPlain property to access to its uncyphered value
property Password: RawUTF8 read fPassword write fPassword;
end;
type
/// class-reference type (metaclass) of an authentication class
TSynAuthenticationClass = class of TSynAuthenticationAbstract;
/// abstract authentication class, implementing safe token/challenge security
// and a list of active sessions
// - do not use this class, but plain TSynAuthentication
TSynAuthenticationAbstract = class
protected
fSessions: TIntegerDynArray;
fSessionsCount: Integer;
fSessionGenerator: integer;
fTokenSeed: Int64;
fSafe: TSynLocker;
function ComputeCredential(previous: boolean; const UserName,PassWord: RawUTF8): cardinal; virtual;
function GetPassword(const UserName: RawUTF8; out Password: RawUTF8): boolean; virtual; abstract;
function GetUsersCount: integer; virtual; abstract;
2021-04-23 10:11:05 +02:00
// check the given Hash challenge, against stored credentials
function CheckCredentials(const UserName: RaWUTF8; Hash: cardinal): boolean; virtual;
public
/// initialize the authentication scheme
constructor Create;
/// finalize the authentation
destructor Destroy; override;
/// register one credential for a given user
// - this abstract method will raise an exception: inherited classes should
// implement them as expected
procedure AuthenticateUser(const aName, aPassword: RawUTF8); virtual;
/// unregister one credential for a given user
// - this abstract method will raise an exception: inherited classes should
// implement them as expected
procedure DisauthenticateUser(const aName: RawUTF8); virtual;
/// create a new session
// - should return 0 on authentication error, or an integer session ID
// - this method will check the User name and password, and create a new session
function CreateSession(const User: RawUTF8; Hash: cardinal): integer; virtual;
/// check if the session exists in the internal list
function SessionExists(aID: integer): boolean;
/// delete a session
procedure RemoveSession(aID: integer);
/// returns the current identification token
// - to be sent to the client for its authentication challenge
function CurrentToken: Int64;
/// the number of current opened sessions
property SessionsCount: integer read fSessionsCount;
/// the number of registered users
property UsersCount: integer read GetUsersCount;
/// to be used to compute a Hash on the client sude, for a given Token
// - the token should have been retrieved from the server, and the client
// should compute and return this hash value, to perform the authentication
// challenge and create the session
// - internal algorithm is not cryptographic secure, but fast and safe
class function ComputeHash(Token: Int64; const UserName,PassWord: RawUTF8): cardinal; virtual;
end;
/// simple authentication class, implementing safe token/challenge security
// - maintain a list of user / name credential pairs, and a list of sessions
// - is not meant to handle authorization, just plain user access validation
// - used e.g. by TSQLDBConnection.RemoteProcessMessage (on server side) and
// TSQLDBProxyConnectionPropertiesAbstract (on client side) in SynDB.pas
TSynAuthentication = class(TSynAuthenticationAbstract)
protected
fCredentials: TSynNameValue; // store user/password pairs
function GetPassword(const UserName: RawUTF8; out Password: RawUTF8): boolean; override;
function GetUsersCount: integer; override;
public
/// initialize the authentication scheme
// - you can optionally register one user credential
constructor Create(const aUserName: RawUTF8=''; const aPassword: RawUTF8=''); reintroduce;
/// register one credential for a given user
procedure AuthenticateUser(const aName, aPassword: RawUTF8); override;
/// unregister one credential for a given user
procedure DisauthenticateUser(const aName: RawUTF8); override;
end;
type
/// optimized thread-safe storage of a list of IP v4 adresses
// - can be used e.g. as white-list or black-list of clients
// - will maintain internally a sorted list of 32-bit integers for fast lookup
// - with optional binary persistence
TIPBan = class(TSynPersistentStore)
protected
fIP4: TIntegerDynArray;
fCount: integer;
procedure LoadFromReader; override;
procedure SaveToWriter(aWriter: TFileBufferWriter); override;
public
/// register one IP to the list
function Add(const aIP: RawUTF8): boolean;
/// unregister one IP to the list
function Delete(const aIP: RawUTF8): boolean;
/// returns true if the IP is in the list
function Exists(const aIP: RawUTF8): boolean;
/// creates a TDynArray wrapper around the stored list of values
// - could be used e.g. for binary persistence
// - warning: caller should make Safe.Unlock when finished
function DynArrayLocked: TDynArray;
/// low-level access to the internal IPv4 list
// - 32-bit unsigned values are sorted, for fast O(log(n)) binary search
property IP4: TIntegerDynArray read fIP4;
published
/// how many IPs are currently banned
property Count: integer read fCount;
end;
{ ************ Expression Search Engine ************************** }
type
/// exception type used by TExprParser
EExprParser = class(ESynException);
/// identify an expression search engine node type, as used by TExprParser
TExprNodeType = (entWord, entNot, entOr, entAnd);
/// results returned by TExprParserAbstract.Parse method
TExprParserResult = (
eprSuccess, eprNoExpression,
eprMissingParenthesis, eprTooManyParenthesis, eprMissingFinalWord,
eprInvalidExpression, eprUnknownVariable, eprUnsupportedOperator,
eprInvalidConstantOrVariable);
TParserAbstract = class;
/// stores an expression search engine node, as used by TExprParser
TExprNode = class(TSynPersistent)
protected
fNext: TExprNode;
fNodeType: TExprNodeType;
function Append(node: TExprNode): boolean;
public
/// initialize a node for the search engine
constructor Create(nodeType: TExprNodeType); reintroduce;
/// recursively destroys the linked list of nodes (i.e. Next)
destructor Destroy; override;
/// browse all nodes until Next = nil
function Last: TExprNode;
/// points to the next node in the parsed tree
property Next: TExprNode read fNext;
/// what is actually stored in this node
property NodeType: TExprNodeType read fNodeType;
end;
/// abstract class to handle word search, as used by TExprParser
TExprNodeWordAbstract = class(TExprNode)
protected
fOwner: TParserAbstract;
fWord: RawUTF8;
/// should be set from actual data before TExprParser.Found is called
fFound: boolean;
function ParseWord: TExprParserResult; virtual; abstract;
public
/// you should override this virtual constructor for proper initialization
constructor Create(aOwner: TParserAbstract; const aWord: RawUTF8); reintroduce; virtual;
end;
/// class-reference type (metaclass) for a TExprNode
// - allow to customize the actual searching process for entWord
TExprNodeWordClass = class of TExprNodeWordAbstract;
/// parent class of TExprParserAbstract
TParserAbstract = class(TSynPersistent)
protected
fExpression, fCurrentWord, fAndWord, fOrWord, fNotWord: RawUTF8;
fCurrent: PUTF8Char;
fCurrentError: TExprParserResult;
fFirstNode: TExprNode;
fWordClass: TExprNodeWordClass;
fWords: array of TExprNodeWordAbstract;
fWordCount: integer;
fNoWordIsAnd: boolean;
fFoundStack: array[byte] of boolean; // simple stack-based virtual machine
procedure ParseNextCurrentWord; virtual; abstract;
function ParseExpr: TExprNode;
function ParseFactor: TExprNode;
function ParseTerm: TExprNode;
procedure Clear; virtual;
// override this method to initialize fWordClass and fAnd/Or/NotWord
procedure Initialize; virtual; abstract;
/// perform the expression search over TExprNodeWord.fFound flags
// - warning: caller should check that fFirstNode<>nil (e.g. WordCount>0)
function Execute: boolean; {$ifdef HASINLINE}inline;{$endif}
public
/// initialize an expression parser
constructor Create; override;
/// finalize the expression parser
destructor Destroy; override;
/// initialize the parser from a given text expression
function Parse(const aExpression: RawUTF8): TExprParserResult;
/// try this parser class on a given text expression
// - returns '' on success, or an explicit error message (e.g.
// 'Missing parenthesis')
class function ParseError(const aExpression: RawUTF8): RawUTF8;
/// the associated text expression used to define the search
property Expression: RawUTF8 read fExpression;
/// how many words did appear in the search expression
property WordCount: integer read fWordCount;
end;
/// abstract class to parse a text expression into nodes
// - you should inherit this class to provide actual text search
// - searched expressions can use parenthesis and &=AND -=WITHOUT +=OR operators,
// e.g. '((w1 & w2) - w3) + w4' means ((w1 and w2) without w3) or w4
// - no operator is handled like a AND, e.g. 'w1 w2' = 'w1 & w2'
TExprParserAbstract = class(TParserAbstract)
protected
procedure ParseNextCurrentWord; override;
// may be overriden to provide custom words escaping (e.g. handle quotes)
procedure ParseNextWord; virtual;
procedure Initialize; override;
end;
/// search expression engine using TMatch for the actual word searches
TExprParserMatch = class(TExprParserAbstract)
protected
fCaseSensitive: boolean;
fMatchedLastSet: integer;
procedure Initialize; override;
public
/// initialize the search engine
constructor Create(aCaseSensitive: boolean = true); reintroduce;
/// returns TRUE if the expression is within the text buffer
function Search(aText: PUTF8Char; aTextLen: PtrInt): boolean; overload;
/// returns TRUE if the expression is within the text buffer
function Search(const aText: RawUTF8): boolean; overload; {$ifdef HASINLINE}inline;{$endif}
end;
const
/// may be used when overriding TExprParserAbstract.ParseNextWord method
PARSER_STOPCHAR = ['&', '+', '-', '(', ')'];
function ToText(r: TExprParserResult): PShortString; overload;
function ToUTF8(r: TExprParserResult): RawUTF8; overload;
{ ************ Multi-Threading classes ************************** }
type
/// internal item definition, used by TPendingTaskList storage
TPendingTaskListItem = packed record
/// the task should be executed when TPendingTaskList.GetTimestamp reaches
// this value
Timestamp: Int64;
/// the associated task, stored by representation as raw binary
Task: RawByteString;
end;
/// internal list definition, used by TPendingTaskList storage
TPendingTaskListItemDynArray = array of TPendingTaskListItem;
/// handle a list of tasks, stored as RawByteString, with a time stamp
// - internal time stamps would be GetTickCount64 by default, so have a
// resolution of about 16 ms under Windows
// - you can add tasks to the internal list, to be executed after a given
// delay, using a post/peek like algorithm
// - execution delays are not expected to be accurate, but are best guess,
// according to NextTask call
// - this implementation is thread-safe, thanks to the Safe internal locker
TPendingTaskList = class(TSynPersistentLock)
protected
fCount: Integer;
fTask: TPendingTaskListItemDynArray;
fTasks: TDynArray;
function GetCount: integer;
function GetTimestamp: Int64; virtual;
public
/// initialize the list memory and resources
constructor Create; override;
/// append a task, specifying a delay in milliseconds from current time
procedure AddTask(aMilliSecondsDelayFromNow: integer; const aTask: RawByteString); virtual;
/// append several tasks, specifying a delay in milliseconds between tasks
// - first supplied delay would be computed from the current time, then
// it would specify how much time to wait between the next supplied task
procedure AddTasks(const aMilliSecondsDelays: array of integer;
const aTasks: array of RawByteString);
/// retrieve the next pending task
// - returns '' if there is no scheduled task available at the current time
// - returns the next stack as defined corresponding to its specified delay
function NextPendingTask: RawByteString; virtual;
/// flush all pending tasks
procedure Clear; virtual;
/// access to the internal TPendingTaskListItem.Timestamp stored value
// - corresponding to the current time
// - default implementation is to return GetTickCount64, with a 16 ms
// typical resolution under Windows
property Timestamp: Int64 read GetTimestamp;
/// how many pending tasks are currently defined
property Count: integer read GetCount;
/// direct low-level access to the internal task list
// - warning: this dynamic array length is the list capacity: use Count
// property to retrieve the exact number of stored items
// - use Safe.Lock/TryLock with a try ... finally Safe.Unlock block for
// thread-safe access to this array
// - items are stored in increasing Timestamp, i.e. the first item is
// the next one which would be returned by the NextPendingTask method
property Task: TPendingTaskListItemDynArray read fTask;
end;
{$ifndef LVCL} // LVCL does not implement TEvent
type
{$M+}
TSynBackgroundThreadAbstract = class;
TSynBackgroundThreadEvent = class;
{$M-}
/// idle method called by TSynBackgroundThreadAbstract in the caller thread
// during remote blocking process in a background thread
// - typical use is to run Application.ProcessMessages, e.g. for
// TSQLRestClientURI.URI() to provide a responsive UI even in case of slow
// blocking remote access
// - provide the time elapsed (in milliseconds) from the request start (can be
// used e.g. to popup a temporary message to wait)
// - is call once with ElapsedMS=0 at request start
// - is call once with ElapsedMS=-1 at request ending
// - see TLoginForm.OnIdleProcess and OnIdleProcessForm in mORMotUILogin.pas
TOnIdleSynBackgroundThread = procedure(Sender: TSynBackgroundThreadAbstract;
ElapsedMS: Integer) of object;
/// event prototype used e.g. by TSynBackgroundThreadAbstract callbacks
// - a similar signature is defined in SynCrtSock and LVCL.Classes
TNotifyThreadEvent = procedure(Sender: TThread) of object;
/// abstract TThread with its own execution content
// - you should not use this class directly, but use either
// TSynBackgroundThreadMethodAbstract / TSynBackgroundThreadEvent /
// TSynBackgroundThreadMethod and provide a much more convenient callback
TSynBackgroundThreadAbstract = class(TThread)
protected
fProcessEvent: TEvent;
fOnBeforeExecute: TNotifyThreadEvent;
fOnAfterExecute: TNotifyThreadEvent;
fThreadName: RawUTF8;
fExecute: (exCreated,exRun,exFinished);
fExecuteLoopPause: boolean;
procedure SetExecuteLoopPause(dopause: boolean);
/// where the main process takes place
procedure Execute; override;
procedure ExecuteLoop; virtual; abstract;
public
/// initialize the thread
// - you could define some callbacks to nest the thread execution, e.g.
// assigned to TSQLRestServer.BeginCurrentThread/EndCurrentThread, or
// at least set OnAfterExecute to TSynLogFamily.OnThreadEnded
constructor Create(const aThreadName: RawUTF8; OnBeforeExecute: TNotifyThreadEvent=nil;
OnAfterExecute: TNotifyThreadEvent=nil; CreateSuspended: boolean=false); reintroduce;
/// release used resources
destructor Destroy; override;
{$ifndef HASTTHREADSTART}
/// method to be called to start the thread
// - Resume is deprecated in the newest RTL, since some OS - e.g. Linux -
// do not implement this pause/resume feature; we define here this method
// for older versions of Delphi
procedure Start;
{$endif}
{$ifdef HASTTHREADTERMINATESET}
/// properly terminate the thread
// - called by TThread.Terminate
procedure TerminatedSet; override;
{$else}
/// properly terminate the thread
// - called by reintroduced Terminate
procedure TerminatedSet; virtual;
/// reintroduced to call TeminatedSet
procedure Terminate; reintroduce;
{$endif}
/// wait for Execute/ExecuteLoop to be ended (i.e. fExecute<>exRun)
procedure WaitForNotExecuting(maxMS: integer=500);
/// temporary stop the execution of ExecuteLoop, until set back to false
// - may be used e.g. by TSynBackgroundTimer to delay the process of
// background tasks
property Pause: boolean read fExecuteLoopPause write SetExecuteLoopPause;
/// access to the low-level associated event used to notify task execution
// to the background thread
// - you may call ProcessEvent.SetEvent to trigger the internal process loop
property ProcessEvent: TEvent read fProcessEvent;
/// defined as public since may be used to terminate the processing methods
property Terminated;
end;
/// state machine status of the TSynBackgroundThreadAbstract process
TSynBackgroundThreadProcessStep = (
flagIdle, flagStarted, flagFinished, flagDestroying);
/// state machine statuses of the TSynBackgroundThreadAbstract process
TSynBackgroundThreadProcessSteps = set of TSynBackgroundThreadProcessStep;
/// abstract TThread able to run a method in its own execution content
// - typical use is a background thread for processing data or remote access,
// while the UI will be still responsive by running OnIdle event in loop: see
// e.g. how TSQLRestClientURI.OnIdle handle this in mORMot.pas unit
// - you should not use this class directly, but inherit from it and override
// the Process method, or use either TSynBackgroundThreadEvent /
// TSynBackgroundThreadMethod and provide a much more convenient callback
TSynBackgroundThreadMethodAbstract = class(TSynBackgroundThreadAbstract)
protected
fCallerEvent: TEvent;
fParam: pointer;
fCallerThreadID: TThreadID;
fBackgroundException: Exception;
fOnIdle: TOnIdleSynBackgroundThread;
fOnBeforeProcess: TNotifyThreadEvent;
fOnAfterProcess: TNotifyThreadEvent;
fPendingProcessFlag: TSynBackgroundThreadProcessStep;
fPendingProcessLock: TSynLocker;
procedure ExecuteLoop; override;
function OnIdleProcessNotify(start: Int64): integer;
function GetOnIdleBackgroundThreadActive: boolean;
function GetPendingProcess: TSynBackgroundThreadProcessStep;
procedure SetPendingProcess(State: TSynBackgroundThreadProcessStep);
// returns flagIdle if acquired, flagDestroying if terminated
function AcquireThread: TSynBackgroundThreadProcessStep;
procedure WaitForFinished(start: Int64; const onmainthreadidle: TNotifyEvent);
/// called by Execute method when fProcessParams<>nil and fEvent is notified
procedure Process; virtual; abstract;
public
/// initialize the thread
// - if aOnIdle is not set (i.e. equals nil), it will simply wait for
// the background process to finish until RunAndWait() will return
// - you could define some callbacks to nest the thread execution, e.g.
// assigned to TSQLRestServer.BeginCurrentThread/EndCurrentThread
constructor Create(aOnIdle: TOnIdleSynBackgroundThread;
const aThreadName: RawUTF8; OnBeforeExecute: TNotifyThreadEvent=nil;
OnAfterExecute: TNotifyThreadEvent=nil); reintroduce;
/// finalize the thread
destructor Destroy; override;
/// launch Process abstract method asynchronously in the background thread
// - wait until process is finished, calling OnIdle() callback in
// the meanwhile
// - any exception raised in background thread will be translated in the
// caller thread
// - returns false if self is not set, or if called from the same thread
// as it is currently processing (to avoid race condition from OnIdle()
// callback)
// - returns true when the background process is finished
// - OpaqueParam will be used to specify a thread-safe content for the
// background process
// - this method is thread-safe, that is it will wait for any started process
// already launch by another thread: you may call this method from any
// thread, even if its main purpose is to be called from the main UI thread
function RunAndWait(OpaqueParam: pointer): boolean;
/// set a callback event to be executed in loop during remote blocking
// process, e.g. to refresh the UI during a somewhat long request
// - you can assign a callback to this property, calling for instance
// Application.ProcessMessages, to execute the remote request in a
// background thread, but let the UI still be reactive: the
// TLoginForm.OnIdleProcess and OnIdleProcessForm methods of
// mORMotUILogin.pas will match this property expectations
// - if OnIdle is not set (i.e. equals nil), it will simply wait for
// the background process to finish until RunAndWait() will return
property OnIdle: TOnIdleSynBackgroundThread read fOnIdle write fOnIdle;
/// TRUE if the background thread is active, and OnIdle event is called
// during process
// - to be used e.g. to ensure no re-entrance from User Interface messages
property OnIdleBackgroundThreadActive: Boolean read GetOnIdleBackgroundThreadActive;
/// optional callback event triggered in Execute before each Process
property OnBeforeProcess: TNotifyThreadEvent read fOnBeforeProcess write fOnBeforeProcess;
/// optional callback event triggered in Execute after each Process
property OnAfterProcess: TNotifyThreadEvent read fOnAfterProcess write fOnAfterProcess;
end;
/// background process method called by TSynBackgroundThreadEvent
// - will supply the OpaqueParam parameter as provided to RunAndWait()
// method when the Process virtual method will be executed
TOnProcessSynBackgroundThread = procedure(Sender: TSynBackgroundThreadEvent;
ProcessOpaqueParam: pointer) of object;
/// allow background thread process of a method callback
TSynBackgroundThreadEvent = class(TSynBackgroundThreadMethodAbstract)
protected
fOnProcess: TOnProcessSynBackgroundThread;
/// just call the OnProcess handler
procedure Process; override;
public
/// initialize the thread
// - if aOnIdle is not set (i.e. equals nil), it will simply wait for
// the background process to finish until RunAndWait() will return
constructor Create(aOnProcess: TOnProcessSynBackgroundThread;
aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUTF8); reintroduce;
/// provide a method handler to be execute in the background thread
// - triggered by RunAndWait() method - which will wait until finished
// - the OpaqueParam as specified to RunAndWait() will be supplied here
property OnProcess: TOnProcessSynBackgroundThread read fOnProcess write fOnProcess;
end;
/// allow background thread process of a variable TThreadMethod callback
TSynBackgroundThreadMethod = class(TSynBackgroundThreadMethodAbstract)
protected
/// just call the TThreadMethod, as supplied to RunAndWait()
procedure Process; override;
public
/// run once the supplied TThreadMethod callback
// - use this method, and not the inherited RunAndWait()
procedure RunAndWait(Method: TThreadMethod); reintroduce;
end;
/// background process procedure called by TSynBackgroundThreadProcedure
// - will supply the OpaqueParam parameter as provided to RunAndWait()
// method when the Process virtual method will be executed
TOnProcessSynBackgroundThreadProc = procedure(ProcessOpaqueParam: pointer);
/// allow background thread process of a procedure callback
TSynBackgroundThreadProcedure = class(TSynBackgroundThreadMethodAbstract)
protected
fOnProcess: TOnProcessSynBackgroundThreadProc;
/// just call the OnProcess handler
procedure Process; override;
public
/// initialize the thread
// - if aOnIdle is not set (i.e. equals nil), it will simply wait for
// the background process to finish until RunAndWait() will return
constructor Create(aOnProcess: TOnProcessSynBackgroundThreadProc;
aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUTF8); reintroduce;
/// provide a procedure handler to be execute in the background thread
// - triggered by RunAndWait() method - which will wait until finished
// - the OpaqueParam as specified to RunAndWait() will be supplied here
property OnProcess: TOnProcessSynBackgroundThreadProc read fOnProcess write fOnProcess;
end;
/// an exception which would be raised by TSynParallelProcess
ESynParallelProcess = class(ESynException);
/// callback implementing some parallelized process for TSynParallelProcess
// - if 0<=IndexStart<=IndexStop, it should execute some process
TSynParallelProcessMethod = procedure(IndexStart, IndexStop: integer) of object;
/// thread executing process for TSynParallelProcess
TSynParallelProcessThread = class(TSynBackgroundThreadMethodAbstract)
protected
fMethod: TSynParallelProcessMethod;
fIndexStart, fIndexStop: integer;
procedure Start(Method: TSynParallelProcessMethod; IndexStart,IndexStop: integer);
/// executes fMethod(fIndexStart,fIndexStop)
procedure Process; override;
public
end;
/// allow parallel execution of an index-based process in a thread pool
// - will create its own thread pool, then execute any method by spliting the
// work into each thread
TSynParallelProcess = class(TSynPersistentLock)
protected
fThreadName: RawUTF8;
fPool: array of TSynParallelProcessThread;
fThreadPoolCount: integer;
fParallelRunCount: integer;
public
/// initialize the thread pool
// - you could define some callbacks to nest the thread execution, e.g.
// assigned to TSQLRestServer.BeginCurrentThread/EndCurrentThread
// - up to MaxThreadPoolCount=32 threads could be setup (you may allow a
// bigger value, but interrest of this thread pool is to have its process
// saturating each CPU core)
// - if ThreadPoolCount is 0, no thread would be created, and process
// would take place in the current thread
constructor Create(ThreadPoolCount: integer; const ThreadName: RawUTF8;
OnBeforeExecute: TNotifyThreadEvent=nil; OnAfterExecute: TNotifyThreadEvent=nil;
MaxThreadPoolCount: integer = 32); reintroduce; virtual;
/// finalize the thread pool
destructor Destroy; override;
/// run a method in parallel, and wait for the execution to finish
// - will split Method[0..MethodCount-1] execution over the threads
// - in case of any exception during process, an ESynParallelProcess
// exception would be raised by this method
// - if OnMainThreadIdle is set, the current thread (which is expected to be
// e.g. the main UI thread) won't process anything, but call this event
// during waiting for the background threads
procedure ParallelRunAndWait(const Method: TSynParallelProcessMethod;
MethodCount: integer; const OnMainThreadIdle: TNotifyEvent = nil);
published
/// how many threads have been activated
property ParallelRunCount: integer read fParallelRunCount;
/// how many threads are currently in this instance thread pool
property ThreadPoolCount: integer read fThreadPoolCount;
/// some text identifier, used to distinguish each owned thread
property ThreadName: RawUTF8 read fThreadName;
end;
TSynBackgroundThreadProcess = class;
/// event callback executed periodically by TSynBackgroundThreadProcess
// - Event is wrTimeout after the OnProcessMS waiting period
// - Event is wrSignaled if ProcessEvent.SetEvent has been called
TOnSynBackgroundThreadProcess = procedure(Sender: TSynBackgroundThreadProcess;
Event: TWaitResult) of object;
/// TThread able to run a method at a given periodic pace
TSynBackgroundThreadProcess = class(TSynBackgroundThreadAbstract)
protected
fOnProcess: TOnSynBackgroundThreadProcess;
fOnException: TNotifyEvent;
fOnProcessMS: cardinal;
fStats: TSynMonitor;
procedure ExecuteLoop; override;
public
/// initialize the thread for a periodic task processing
// - aOnProcess would be called when ProcessEvent.SetEvent is called or
// aOnProcessMS milliseconds period was elapse since last process
// - if aOnProcessMS is 0, will wait until ProcessEvent.SetEvent is called
// - you could define some callbacks to nest the thread execution, e.g.
// assigned to TSQLRestServer.BeginCurrentThread/EndCurrentThread
constructor Create(const aThreadName: RawUTF8;
aOnProcess: TOnSynBackgroundThreadProcess; aOnProcessMS: cardinal;
aOnBeforeExecute: TNotifyThreadEvent=nil;
aOnAfterExecute: TNotifyThreadEvent=nil;
aStats: TSynMonitorClass=nil; CreateSuspended: boolean=false); reintroduce; virtual;
/// finalize the thread
destructor Destroy; override;
/// access to the implementation event of the periodic task
property OnProcess: TOnSynBackgroundThreadProcess read fOnProcess;
/// event callback executed when OnProcess did raise an exception
// - supplied Sender parameter is the raised Exception instance
property OnException: TNotifyEvent read fOnException write fOnException;
published
/// access to the delay, in milliseconds, of the periodic task processing
property OnProcessMS: cardinal read fOnProcessMS write fOnProcessMS;
/// processing statistics
// - may be nil if aStats was nil in the class constructor
property Stats: TSynMonitor read fStats;
end;
TSynBackgroundTimer = class;
/// event callback executed periodically by TSynBackgroundThreadProcess
// - Event is wrTimeout after the OnProcessMS waiting period
// - Event is wrSignaled if ProcessEvent.SetEvent has been called
// - Msg is '' if there is no pending message in this task FIFO
// - Msg is set for each pending message in this task FIFO
TOnSynBackgroundTimerProcess = procedure(Sender: TSynBackgroundTimer;
Event: TWaitResult; const Msg: RawUTF8) of object;
/// used by TSynBackgroundTimer internal registration list
TSynBackgroundTimerTask = record
OnProcess: TOnSynBackgroundTimerProcess;
Secs: cardinal;
NextTix: Int64;
FIFO: TRawUTF8DynArray;
end;
/// stores TSynBackgroundTimer internal registration list
TSynBackgroundTimerTaskDynArray = array of TSynBackgroundTimerTask;
/// TThread able to run one or several tasks at a periodic pace in a
// background thread
// - as used e.g. by TSQLRest.TimerEnable/TimerDisable methods, via the
// inherited TSQLRestBackgroundTimer
// - each process can have its own FIFO of text messages
// - if you expect to update some GUI, you should rather use a TTimer
// component (with a period of e.g. 200ms), since TSynBackgroundTimer will
// use its own separated thread
TSynBackgroundTimer = class(TSynBackgroundThreadProcess)
protected
fTask: TSynBackgroundTimerTaskDynArray;
fTasks: TDynArray;
fTaskLock: TSynLocker;
procedure EverySecond(Sender: TSynBackgroundThreadProcess; Event: TWaitResult);
function Find(const aProcess: TMethod): integer;
function Add(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsg: RawUTF8; aExecuteNow: boolean): boolean;
public
/// initialize the thread for a periodic task processing
// - you could define some callbacks to nest the thread execution, e.g.
// assigned to TSQLRestServer.BeginCurrentThread/EndCurrentThread, as
// made by TSQLRestBackgroundTimer.Create
constructor Create(const aThreadName: RawUTF8;
aOnBeforeExecute: TNotifyThreadEvent=nil; aOnAfterExecute: TNotifyThreadEvent=nil;
aStats: TSynMonitorClass=nil); reintroduce; virtual;
/// finalize the thread
destructor Destroy; override;
/// define a process method for a task running on a periodic number of seconds
// - for background process on a mORMot service, consider using TSQLRest
// TimerEnable/TimerDisable methods, and its associated BackgroundTimer thread
procedure Enable(aOnProcess: TOnSynBackgroundTimerProcess; aOnProcessSecs: cardinal);
/// undefine a task running on a periodic number of seconds
// - aOnProcess should have been registered by a previous call to Enable() method
// - returns true on success, false if the supplied task was not registered
// - for background process on a mORMot service, consider using TSQLRestServer
// TimerEnable/TimerDisable methods, and their TSynBackgroundTimer thread
function Disable(aOnProcess: TOnSynBackgroundTimerProcess): boolean;
/// add a message to be processed during the next execution of a task
// - supplied message will be added to the internal FIFO list associated
// with aOnProcess, then supplied to as aMsg parameter for each call
// - if aExecuteNow is true, won't wait for the next aOnProcessSecs occurence
// - aOnProcess should have been registered by a previous call to Enable() method
// - returns true on success, false if the supplied task was not registered
function EnQueue(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsg: RawUTF8; aExecuteNow: boolean=false): boolean; overload;
/// add a message to be processed during the next execution of a task
// - supplied message will be added to the internal FIFO list associated
// with aOnProcess, then supplied to as aMsg parameter for each call
// - if aExecuteNow is true, won't wait for the next aOnProcessSecs occurence
// - aOnProcess should have been registered by a previous call to Enable() method
// - returns true on success, false if the supplied task was not registered
function EnQueue(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsgFmt: RawUTF8; const Args: array of const; aExecuteNow: boolean=false): boolean; overload;
/// remove a message from the processing list
// - supplied message will be searched in the internal FIFO list associated
// with aOnProcess, then removed from the list if found
// - aOnProcess should have been registered by a previous call to Enable() method
// - returns true on success, false if the supplied message was not registered
function DeQueue(aOnProcess: TOnSynBackgroundTimerProcess; const aMsg: RawUTF8): boolean;
/// execute a task without waiting for the next aOnProcessSecs occurence
// - aOnProcess should have been registered by a previous call to Enable() method
// - returns true on success, false if the supplied task was not registered
function ExecuteNow(aOnProcess: TOnSynBackgroundTimerProcess): boolean;
/// returns true if there is currenly one task processed
function Processing: boolean;
/// wait until no background task is processed
procedure WaitUntilNotProcessing(timeoutsecs: integer=10);
/// low-level access to the internal task list
property Task: TSynBackgroundTimerTaskDynArray read fTask;
/// low-level access to the internal task mutex
property TaskLock: TSynLocker read fTaskLock;
end;
/// the current state of a TBlockingProcess instance
TBlockingEvent = (evNone,evWaiting,evTimeOut,evRaised);
{$M+}
/// a semaphore used to wait for some process to be finished
// - used e.g. by TBlockingCallback in mORMot.pas
// - once created, process would block via a WaitFor call, which would be
// released when NotifyFinished is called by the process background thread
TBlockingProcess = class(TEvent)
protected
fTimeOutMs: integer;
fEvent: TBlockingEvent;
fSafe: PSynLocker;
fOwnedSafe: boolean;
procedure ResetInternal; virtual; // override to reset associated params
public
/// initialize the semaphore instance
// - specify a time out millliseconds period after which blocking execution
// should be handled as failure (if 0 is set, default 3000 would be used)
// - an associated mutex shall be supplied
constructor Create(aTimeOutMs: integer; aSafe: PSynLocker); reintroduce; overload; virtual;
/// initialize the semaphore instance
// - specify a time out millliseconds period after which blocking execution
// should be handled as failure (if 0 is set, default 3000 would be used)
// - an associated mutex would be created and owned by this instance
constructor Create(aTimeOutMs: integer); reintroduce; overload; virtual;
/// finalize the instance
destructor Destroy; override;
/// called to wait for NotifyFinished() to be called, or trigger timeout
// - returns the final state of the process, i.e. evRaised or evTimeOut
function WaitFor: TBlockingEvent; reintroduce; overload; virtual;
/// called to wait for NotifyFinished() to be called, or trigger timeout
// - returns the final state of the process, i.e. evRaised or evTimeOut
function WaitFor(TimeOutMS: integer): TBlockingEvent; reintroduce; overload;
/// should be called by the background process when it is finished
// - the caller would then let its WaitFor method return
// - returns TRUE on success (i.e. status was not evRaised or evTimeout)
// - if the instance is already locked (e.g. when retrieved from
// TBlockingProcessPool.FromCallLocked), you may set alreadyLocked=TRUE
function NotifyFinished(alreadyLocked: boolean=false): boolean; virtual;
/// just a wrapper to reset the internal Event state to evNone
// - may be used to re-use the same TBlockingProcess instance, after
// a successfull WaitFor/NotifyFinished process
// - returns TRUE on success (i.e. status was not evWaiting), setting
// the current state to evNone, and the Call property to 0
// - if there is a WaitFor currently in progress, returns FALSE
function Reset: boolean; virtual;
/// just a wrapper around fSafe^.Lock
procedure Lock;
/// just a wrapper around fSafe^.Unlock
procedure Unlock;
published
/// the current state of process
// - use Reset method to re-use this instance after a WaitFor process
property Event: TBlockingEvent read fEvent;
/// the time out period, in ms, as defined at constructor level
property TimeOutMs: integer read fTimeOutMS;
end;
{$M-}
/// used to identify each TBlockingProcessPool call
// - allow to match a given TBlockingProcessPoolItem semaphore
TBlockingProcessPoolCall = type integer;
/// a semaphore used in the TBlockingProcessPool
// - such semaphore have a Call field to identify each execution
TBlockingProcessPoolItem = class(TBlockingProcess)
protected
fCall: TBlockingProcessPoolCall;
procedure ResetInternal; override;
published
/// an unique identifier, when owned by a TBlockingProcessPool
// - Reset would restore this field to its 0 default value
property Call: TBlockingProcessPoolCall read fCall;
end;
/// class-reference type (metaclass) of a TBlockingProcess
TBlockingProcessPoolItemClass = class of TBlockingProcessPoolItem;
/// manage a pool of TBlockingProcessPoolItem instances
// - each call will be identified via a TBlockingProcessPoolCall unique value
// - to be used to emulate e.g. blocking execution from an asynchronous
// event-driven DDD process
// - it would also allow to re-use TEvent system resources
TBlockingProcessPool = class(TSynPersistent)
protected
fClass: TBlockingProcessPoolItemClass;
2021-04-23 10:11:05 +02:00
fPool: TSynObjectListLocked;
fCallCounter: TBlockingProcessPoolCall; // set TBlockingProcessPoolItem.Call
public
/// initialize the pool, for a given implementation class
constructor Create(aClass: TBlockingProcessPoolItemClass=nil); reintroduce;
/// finalize the pool
// - would also force all pending WaitFor to trigger a evTimeOut
destructor Destroy; override;
/// book a TBlockingProcess from the internal pool
// - returns nil on error (e.g. the instance is destroying)
// - or returns the blocking process instance corresponding to this call;
// its Call property would identify the call for the asynchronous callback,
// then after WaitFor, the Reset method should be run to release the mutex
// for the pool
function NewProcess(aTimeOutMs: integer): TBlockingProcessPoolItem; virtual;
/// retrieve a TBlockingProcess from its call identifier
// - may be used e.g. from the callback of the asynchronous process
// to set some additional parameters to the inherited TBlockingProcess,
// then call NotifyFinished to release the caller WaitFor
// - if leavelocked is TRUE, the returned instance would be locked: caller
// should execute result.Unlock or NotifyFinished(true) after use
function FromCall(call: TBlockingProcessPoolCall;
locked: boolean=false): TBlockingProcessPoolItem; virtual;
end;
/// allow to fix TEvent.WaitFor() method for Kylix
// - under Windows or with FPC, will call original TEvent.WaitFor() method
function FixedWaitFor(Event: TEvent; Timeout: LongWord): TWaitResult;
/// allow to fix TEvent.WaitFor(Event,INFINITE) method for Kylix
// - under Windows or with FPC, will call original TEvent.WaitFor() method
procedure FixedWaitForever(Event: TEvent);
{$endif LVCL} // LVCL does not implement TEvent
{ ************ Operating System types and classes ************************** }
type
/// store CPU and RAM usage for a given process
// - as used by TSystemUse class
TSystemUseData = packed record
/// when the data has been sampled
Timestamp: TDateTime;
/// percent of current Kernel-space CPU usage for this process
Kernel: single;
/// percent of current User-space CPU usage for this process
User: single;
/// how many KB of working memory are used by this process
WorkKB: cardinal;
/// how many KB of virtual memory are used by this process
VirtualKB: cardinal;
end;
/// store CPU and RAM usage history for a given process
// - as returned by TSystemUse.History
TSystemUseDataDynArray = array of TSystemUseData;
/// low-level structure used to compute process memory and CPU usage
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TProcessInfo = record
{$else}TProcessInfo = object {$endif}
private
{$ifdef MSWINDOWS}
fSysPrevIdle, fSysPrevKernel, fSysPrevUser,
fDiffIdle, fDiffKernel, fDiffUser, fDiffTotal: Int64;
{$endif}
public
/// initialize the system/process resource tracking
function Init: boolean;
/// to be called before PerSystem() or PerProcess() iteration
function Start: boolean;
/// percent of current Idle/Kernel/User CPU usage for all processes
function PerSystem(out Idle,Kernel,User: currency): boolean;
/// retrieve CPU and RAM usage for a given process
function PerProcess(PID: cardinal; Now: PDateTime; out Data: TSystemUseData;
var PrevKernel, PrevUser: Int64): boolean;
end;
/// event handler which may be executed by TSystemUse.BackgroundExecute
// - called just after the measurement of each process CPU and RAM consumption
// - run from the background thread, so should not directly make VCL calls,
// unless BackgroundExecute is run from a VCL timer
TOnSystemUseMeasured = procedure(ProcessID: integer; const Data: TSystemUseData) of object;
/// internal storage of CPU and RAM usage for one process
TSystemUseProcess = record
ID: integer;
Data: TSystemUseDataDynArray;
PrevKernel: Int64;
PrevUser: Int64;
end;
/// internal storage of CPU and RAM usage for a set of processes
TSystemUseProcessDynArray = array of TSystemUseProcess;
/// monitor CPU and RAM usage of one or several processes
// - you should execute BackgroundExecute on a regular pace (e.g. every second)
// to gather low-level CPU and RAM information for the given set of processes
// - is able to keep an history of latest sample values
// - use Current class function to access a process-wide instance
TSystemUse = class(TSynPersistentLock)
protected
fProcess: TSystemUseProcessDynArray;
fProcesses: TDynArray;
fDataIndex: integer;
fProcessInfo: TProcessInfo;
fHistoryDepth: integer;
fOnMeasured: TOnSystemUseMeasured;
fTimer: TSynBackgroundTimer;
fUnsubscribeProcessOnAccessError: boolean;
function ProcessIndex(aProcessID: integer): integer;
public
/// a TSynBackgroundThreadProcess compatible event
// - matches TOnSynBackgroundTimerProcess callback signature
// - to be supplied e.g. to a TSynBackgroundTimer.Enable method so that it
// will run every few seconds and retrieve the CPU and RAM use
procedure BackgroundExecute(Sender: TSynBackgroundTimer;
Event: TWaitResult; const Msg: RawUTF8);
/// a VCL's TTimer.OnTimer compatible event
// - to be run every few seconds and retrieve the CPU and RAM use:
// ! tmrSystemUse.Interval := 10000; // every 10 seconds
// ! tmrSystemUse.OnTimer := TSystemUse.Current.OnTimerExecute;
procedure OnTimerExecute(Sender: TObject);
/// track the CPU and RAM usage of the supplied set of Process ID
// - any aProcessID[]=0 will be replaced by the current process ID
// - you can specify the number of sample values for the History() method
// - you should then execute the BackgroundExecute method of this instance
// in a VCL timer or from a TSynBackgroundTimer.Enable() registration
constructor Create(const aProcessID: array of integer;
aHistoryDepth: integer=60); reintroduce; overload; virtual;
/// track the CPU and RAM usage of the current process
// - you can specify the number of sample values for the History() method
// - you should then execute the BackgroundExecute method of this instance
// in a VCL timer or from a TSynBackgroundTimer.Enable() registration
constructor Create(aHistoryDepth: integer=60); reintroduce; overload; virtual;
/// add a Process ID to the internal tracking list
procedure Subscribe(aProcessID: integer);
/// remove a Process ID from the internal tracking list
function Unsubscribe(aProcessID: integer): boolean;
/// returns the total (Kernel+User) CPU usage percent of the supplied process
// - aProcessID=0 will return information from the current process
// - returns -1 if the Process ID was not registered via Create/Subscribe
function Percent(aProcessID: integer=0): single; overload;
/// returns the Kernel-space CPU usage percent of the supplied process
// - aProcessID=0 will return information from the current process
// - returns -1 if the Process ID was not registered via Create/Subscribe
function PercentKernel(aProcessID: integer=0): single; overload;
/// returns the User-space CPU usage percent of the supplied process
// - aProcessID=0 will return information from the current process
// - returns -1 if the Process ID was not registered via Create/Subscribe
function PercentUser(aProcessID: integer=0): single; overload;
/// returns the total (Work+Paged) RAM use of the supplied process, in KB
// - aProcessID=0 will return information from the current process
// - returns 0 if the Process ID was not registered via Create/Subscribe
function KB(aProcessID: integer=0): cardinal; overload;
/// percent of current Idle/Kernel/User CPU usage for all processes
function PercentSystem(out Idle,Kernel,User: currency): boolean;
/// returns the detailed CPU and RAM usage percent of the supplied process
// - aProcessID=0 will return information from the current process
// - returns -1 if the Process ID was not registered via Create/Subscribe
function Data(out aData: TSystemUseData; aProcessID: integer=0): boolean; overload;
/// returns the detailed CPU and RAM usage percent of the supplied process
// - aProcessID=0 will return information from the current process
// - returns Timestamp=0 if the Process ID was not registered via Create/Subscribe
function Data(aProcessID: integer=0): TSystemUseData; overload;
/// returns total (Kernel+User) CPU usage percent history of the supplied process
// - aProcessID=0 will return information from the current process
// - returns nil if the Process ID was not registered via Create/Subscribe
// - returns the sample values as an array, starting from the last to the oldest
// - you can customize the maximum depth, with aDepth < HistoryDepth
function History(aProcessID: integer=0; aDepth: integer=0): TSingleDynArray; overload;
/// returns total (Kernel+User) CPU usage percent history of the supplied
// process, as a string of two digits values
// - aProcessID=0 will return information from the current process
// - returns '' if the Process ID was not registered via Create/Subscribe
// - you can customize the maximum depth, with aDepth < HistoryDepth
// - the memory history (in MB) can be optionally returned in aDestMemoryMB
function HistoryText(aProcessID: integer=0; aDepth: integer=0;
aDestMemoryMB: PRawUTF8=nil): RawUTF8;
{$ifndef NOVARIANTS}
/// returns total (Kernel+User) CPU usage percent history of the supplied process
// - aProcessID=0 will return information from the current process
// - returns null if the Process ID was not registered via Create/Subscribe
// - returns the sample values as a TDocVariant array, starting from the
// last to the oldest, with two digits precision (as currency values)
// - you can customize the maximum depth, with aDepth < HistoryDepth
function HistoryVariant(aProcessID: integer=0; aDepth: integer=0): variant;
{$endif}
/// access to a global instance, corresponding to the current process
// - its HistoryDepth will be of 60 items
class function Current(aCreateIfNone: boolean=true): TSystemUse;
/// returns detailed CPU and RAM usage history of the supplied process
// - aProcessID=0 will return information from the current process
// - returns nil if the Process ID was not registered via Create/Subscribe
// - returns the sample values as an array, starting from the last to the oldest
// - you can customize the maximum depth, with aDepth < HistoryDepth
function HistoryData(aProcessID: integer=0; aDepth: integer=0): TSystemUseDataDynArray; overload;
/// if any unexisting (e.g. closed/killed) process should be unregistered
// - e.g. if OpenProcess() API call fails
property UnsubscribeProcessOnAccessError: boolean
read fUnsubscribeProcessOnAccessError write fUnsubscribeProcessOnAccessError;
/// how many items are stored internally, and returned by the History() method
property HistoryDepth: integer read fHistoryDepth;
/// executed when TSystemUse.BackgroundExecute finished its measurement
property OnMeasured: TOnSystemUseMeasured read fOnMeasured write fOnMeasured;
/// low-level access to the associated timer running BackgroundExecute
// - equals nil if has been associated to no timer
property Timer: TSynBackgroundTimer read fTimer write fTimer;
end;
/// stores information about a disk partition
TDiskPartition = packed record
/// the name of this partition
// - is the Volume name under Windows, or the Device name under POSIX
name: RawUTF8;
/// where this partition has been mounted
// - e.g. 'C:' or '/home'
// - you can use GetDiskInfo(mounted) to retrieve current space information
mounted: TFileName;
/// total size (in bytes) of this partition
size: QWord;
end;
/// stores information about several disk partitions
TDiskPartitions = array of TDiskPartition;
/// value object able to gather information about the current system memory
TSynMonitorMemory = class(TSynPersistent)
protected
FAllocatedUsed: TSynMonitorOneSize;
FAllocatedReserved: TSynMonitorOneSize;
FMemoryLoadPercent: integer;
FPhysicalMemoryFree: TSynMonitorOneSize;
FVirtualMemoryFree: TSynMonitorOneSize;
FPagingFileTotal: TSynMonitorOneSize;
FPhysicalMemoryTotal: TSynMonitorOneSize;
FVirtualMemoryTotal: TSynMonitorOneSize;
FPagingFileFree: TSynMonitorOneSize;
fLastMemoryInfoRetrievedTix: cardinal;
procedure RetrieveMemoryInfo; virtual;
function GetAllocatedUsed: TSynMonitorOneSize;
function GetAllocatedReserved: TSynMonitorOneSize;
function GetMemoryLoadPercent: integer;
function GetPagingFileFree: TSynMonitorOneSize;
function GetPagingFileTotal: TSynMonitorOneSize;
function GetPhysicalMemoryFree: TSynMonitorOneSize;
function GetPhysicalMemoryTotal: TSynMonitorOneSize;
function GetVirtualMemoryFree: TSynMonitorOneSize;
function GetVirtualMemoryTotal: TSynMonitorOneSize;
public
/// initialize the class, and its nested TSynMonitorOneSize instances
constructor Create(aTextNoSpace: boolean); reintroduce;
/// finalize the class, and its nested TSynMonitorOneSize instances
destructor Destroy; override;
/// some text corresponding to current 'free/total' memory information
// - returns e.g. '10.3 GB / 15.6 GB'
class function FreeAsText(nospace: boolean=false): ShortString;
/// how many physical memory is currently installed, as text (e.g. '32 GB');
class function PhysicalAsText(nospace: boolean=false): TShort16;
/// returns a JSON object with the current system memory information
// - numbers would be given in KB (Bytes shl 10)
class function ToJSON: RawUTF8;
{$ifndef NOVARIANTS}
/// fill a TDocVariant with the current system memory information
// - numbers would be given in KB (Bytes shl 10)
class function ToVariant: variant;
{$endif}
published
/// Total of allocated memory used by the program
property AllocatedUsed: TSynMonitorOneSize read GetAllocatedUsed;
/// Total of allocated memory reserved by the program
property AllocatedReserved: TSynMonitorOneSize read GetAllocatedReserved;
/// Percent of memory in use for the system
property MemoryLoadPercent: integer read GetMemoryLoadPercent;
/// Total of physical memory for the system
property PhysicalMemoryTotal: TSynMonitorOneSize read GetPhysicalMemoryTotal;
/// Free of physical memory for the system
property PhysicalMemoryFree: TSynMonitorOneSize read GetPhysicalMemoryFree;
/// Total of paging file for the system
property PagingFileTotal: TSynMonitorOneSize read GetPagingFileTotal;
/// Free of paging file for the system
property PagingFileFree: TSynMonitorOneSize read GetPagingFileFree;
{$ifdef MSWINDOWS}
/// Total of virtual memory for the system
// - property not defined under Linux, since not applying to this OS
property VirtualMemoryTotal: TSynMonitorOneSize read GetVirtualMemoryTotal;
/// Free of virtual memory for the system
// - property not defined under Linux, since not applying to this OS
property VirtualMemoryFree: TSynMonitorOneSize read GetVirtualMemoryFree;
{$endif}
end;
/// value object able to gather information about a system drive
TSynMonitorDisk = class(TSynPersistent)
protected
fName: TFileName;
{$ifdef MSWINDOWS}
fVolumeName: TFileName;
{$endif}
fAvailableSize: TSynMonitorOneSize;
fFreeSize: TSynMonitorOneSize;
fTotalSize: TSynMonitorOneSize;
fLastDiskInfoRetrievedTix: cardinal;
procedure RetrieveDiskInfo; virtual;
function GetName: TFileName;
function GetAvailable: TSynMonitorOneSize;
function GetFree: TSynMonitorOneSize;
function GetTotal: TSynMonitorOneSize;
public
/// initialize the class, and its nested TSynMonitorOneSize instances
constructor Create; override;
/// finalize the class, and its nested TSynMonitorOneSize instances
destructor Destroy; override;
/// some text corresponding to current 'free/total' disk information
// - could return e.g. 'D: 64.4 GB / 213.4 GB'
class function FreeAsText: RawUTF8;
published
/// the disk name
property Name: TFileName read GetName;
{$ifdef MSWINDOWS}
/// the volume name (only available on Windows)
property VolumeName: TFileName read fVolumeName write fVolumeName;
/// space currently available on this disk for the current user
// - may be less then FreeSize, if user quotas are specified (only taken
// into account under Windows)
property AvailableSize: TSynMonitorOneSize read GetAvailable;
{$endif MSWINDOWS}
/// free space currently available on this disk
property FreeSize: TSynMonitorOneSize read GetFree;
/// total space
property TotalSize: TSynMonitorOneSize read GetTotal;
end;
/// hold low-level information about current memory usage
// - as filled by GetMemoryInfo()
TMemoryInfo = record
memtotal, memfree, filetotal, filefree, vmtotal, vmfree,
allocreserved, allocused: QWord;
percent: integer;
end;
type
{$A-}
/// used to store Time Zone bias in TSynTimeZone
// - map how low-level information is stored in the Windows Registry
TTimeZoneInfo = record
Bias: integer;
bias_std: integer;
bias_dlt: integer;
change_time_std: TSynSystemTime;
change_time_dlt: TSynSystemTime;
end;
PTimeZoneInfo = ^TTimeZoneInfo;
/// text identifier of a Time Zone, following Microsoft Windows naming
TTimeZoneID = type RawUTF8;
/// used to store Time Zone information for a single area in TSynTimeZone
// - Delphi "object" is buggy on stack -> also defined as record with methods
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TTimeZoneData = record
{$else}TTimeZoneData = object{$endif}
public
id: TTimeZoneID;
display: RawUTF8;
tzi: TTimeZoneInfo;
dyn: array of packed record
year: integer;
tzi: TTimeZoneInfo;
end;
function GetTziFor(year: integer): PTimeZoneInfo;
end;
/// used to store the Time Zone information of a TSynTimeZone class
TTimeZoneDataDynArray = array of TTimeZoneData;
{$A+}
/// handle cross-platform time conversions, following Microsoft time zones
// - is able to retrieve accurate information from the Windows registry,
// or from a binary compressed file on other platforms (which should have been
// saved from a Windows system first)
// - each time zone will be idendified by its TzId string, as defined by
// Microsoft for its Windows Operating system
TSynTimeZone = class
protected
fZone: TTimeZoneDataDynArray;
fZones: TDynArrayHashed;
fLastZone: TTimeZoneID;
fLastIndex: integer;
fIds: TStringList;
fDisplays: TStringList;
public
/// will retrieve the default shared TSynTimeZone instance
// - locally created via the CreateDefault constructor
// - this is the usual entry point for time zone process, calling e.g.
// $ aLocalTime := TSynTimeZone.Default.NowToLocal(aTimeZoneID);
class function Default: TSynTimeZone;
/// initialize the internal storage
// - but no data is available, until Load* methods are called
constructor Create;
/// retrieve the time zones from Windows registry, or from a local file
// - under Linux, the file should be located with the executable, renamed
// with a .tz extension - may have been created via SaveToFile(''), or
// from a 'TSynTimeZone' bound resource
// "dummy" parameter exists only to disambiguate constructors for C++
constructor CreateDefault(dummy: integer=0);
/// finalize the instance
destructor Destroy; override;
{$ifdef MSWINDOWS}
/// read time zone information from the Windows registry
procedure LoadFromRegistry;
{$endif MSWINDOWS}
/// read time zone information from a compressed file
// - if no file name is supplied, a ExecutableName.tz file would be used
procedure LoadFromFile(const FileName: TFileName='');
/// read time zone information from a compressed memory buffer
procedure LoadFromBuffer(const Buffer: RawByteString);
/// read time zone information from a 'TSynTimeZone' resource
// - the resource should contain the SaveToBuffer compressed binary content
// - is no resource matching the TSynTimeZone class name and ResType=10
// do exist, nothing would be loaded
// - the resource could be created as such, from a Windows system:
// ! TSynTimeZone.Default.SaveToFile('TSynTimeZone.data');
// then compile the resource as expected, with a brcc32 .rc entry:
// ! TSynTimeZone 10 "TSynTimeZone.data"
// - you can specify a library (dll) resource instance handle, if needed
procedure LoadFromResource(Instance: THandle=0);
/// write then time zone information into a compressed file
// - if no file name is supplied, a ExecutableName.tz file would be created
procedure SaveToFile(const FileName: TFileName);
/// write then time zone information into a compressed memory buffer
function SaveToBuffer: RawByteString;
/// retrieve the time bias (in minutes) for a given date/time on a TzId
function GetBiasForDateTime(const Value: TDateTime; const TzId: TTimeZoneID;
out Bias: integer; out HaveDaylight: boolean): boolean;
/// retrieve the display text corresponding to a TzId
// - returns '' if the supplied TzId is not recognized
function GetDisplay(const TzId: TTimeZoneID): RawUTF8;
/// compute the UTC date/time corrected for a given TzId
function UtcToLocal(const UtcDateTime: TDateTime; const TzId: TTimeZoneID): TDateTime;
/// compute the current date/time corrected for a given TzId
function NowToLocal(const TzId: TTimeZoneID): TDateTime;
/// compute the UTC date/time for a given local TzId value
// - by definition, a local time may correspond to two UTC times, during the
// time biais period, so the returned value is informative only, and any
// stored value should be following UTC
function LocalToUtc(const LocalDateTime: TDateTime; const TzID: TTimeZoneID): TDateTime;
/// direct access to the low-level time zone information
property Zone: TTimeZoneDataDynArray read fZone;
/// direct access to the wrapper over the time zone information array
property Zones: TDynArrayHashed read fZones;
/// returns a TStringList of all TzID values
// - could be used to fill any VCL component to select the time zone
// - order in Ids[] array follows the Zone[].id information
function Ids: TStrings;
/// returns a TStringList of all Display text values
// - could be used to fill any VCL component to select the time zone
// - order in Displays[] array follows the Zone[].display information
function Displays: TStrings;
end;
/// retrieve low-level information about all mounted disk partitions of the system
// - returned partitions array is sorted by "mounted" ascending order
function GetDiskPartitions: TDiskPartitions;
/// retrieve low-level information about all mounted disk partitions as text
// - returns e.g. under Linux
// '/ /dev/sda3 (19 GB), /boot /dev/sda2 (486.8 MB), /home /dev/sda4 (0.9 TB)'
// or under Windows 'C:\ System (115 GB), D:\ Data (99.3 GB)'
// - uses internally a cache unless nocache is true
// - includes the free space if withfreespace is true - e.g. '(80 GB / 115 GB)'
function GetDiskPartitionsText(nocache: boolean=false;
withfreespace: boolean=false; nospace: boolean=false): RawUTF8;
/// returns a JSON object containing basic information about the computer
// - including Host, User, CPU, OS, freemem, freedisk...
function SystemInfoJson: RawUTF8;
{$ifdef MSWINDOWS}
/// a wrapper around EnumProcesses() PsAPI call
function EnumAllProcesses(out Count: Cardinal): TCardinalDynArray;
/// a wrapper around QueryFullProcessImageNameW/GetModuleFileNameEx PsAPI call
function EnumProcessName(PID: Cardinal): RawUTF8;
{$endif MSWINDOWS}
/// retrieve low-level information about current memory usage
// - as used by TSynMonitorMemory
// - under BSD, only memtotal/memfree/percent are properly returned
// - allocreserved and allocused are set only if withalloc is TRUE
function GetMemoryInfo(out info: TMemoryInfo; withalloc: boolean): boolean;
/// retrieve low-level information about a given disk partition
// - as used by TSynMonitorDisk and GetDiskPartitionsText()
// - only under Windows the Quotas are applied separately to aAvailableBytes
// in respect to global aFreeBytes
function GetDiskInfo(var aDriveFolderOrFile: TFileName;
out aAvailableBytes, aFreeBytes, aTotalBytes: QWord
{$ifdef MSWINDOWS}; aVolumeName: PFileName = nil{$endif}): boolean;
2021-04-23 10:11:05 +02:00
{ ************ Markup (e.g. HTML or Emoji) process ******************** }
type
/// tune AddHtmlEscapeWiki/AddHtmlEscapeMarkdown wrapper functions process
// - heHtmlEscape will escape any HTML special chars, e.g. & into &amp;
// - heEmojiToUTF8 will convert any Emoji text into UTF-8 Unicode character,
// recognizing e.g. :joy: or :) in the text
TTextWriterHTMLEscape = set of (
heHtmlEscape, heEmojiToUTF8);
/// convert some wiki-like text into proper HTML
// - convert all #13#10 into <p>...</p>, *..* into <em>..</em>, +..+ into
// <strong>..</strong>, `..` into <code>..</code>, and http://... as
// <a href=http://...>
// - escape any HTML special chars, and Emoji tags as specified with esc
procedure AddHtmlEscapeWiki(W: TTextWriter; P: PUTF8Char;
esc: TTextWriterHTMLEscape=[heHtmlEscape,heEmojiToUTF8]);
/// convert minimal Markdown text into proper HTML
// - see https://enterprise.github.com/downloads/en/markdown-cheatsheet.pdf
// - convert all #13#10 into <p>...</p>, *..* into <em>..</em>, **..** into
// <strong>..</strong>, `...` into <code>...</code>, backslash espaces \\
// \* \_ and so on, [title](http://...) and detect plain http:// as
// <a href=...>
// - create unordered lists from trailing * + - chars, blockquotes from
// trailing > char, and code line from 4 initial spaces
// - as with default Markdown, won't escape HTML special chars (i.e. you can
// write plain HTML in the supplied text) unless esc is set otherwise
// - only inline-style links and images are supported yet (not reference-style);
// tables aren't supported either
procedure AddHtmlEscapeMarkdown(W: TTextWriter; P: PUTF8Char;
esc: TTextWriterHTMLEscape=[heEmojiToUTF8]);
/// escape some wiki-marked text into HTML
// - just a wrapper around AddHtmlEscapeWiki() process
function HtmlEscapeWiki(const wiki: RawUTF8; esc: TTextWriterHTMLEscape=[heHtmlEscape,heEmojiToUTF8]): RawUTF8;
/// escape some Markdown-marked text into HTML
// - just a wrapper around AddHtmlEscapeMarkdown() process
function HtmlEscapeMarkdown(const md: RawUTF8; esc: TTextWriterHTMLEscape=[heEmojiToUTF8]): RawUTF8;
type
/// map the first Unicode page of Emojis, from U+1F600 to U+1F64F
// - naming comes from github/Markdown :identifiers:
TEmoji = (eNone,
eGrinning, eGrin, eJoy, eSmiley, eSmile, eSweat_smile,
eLaughing, eInnocent, eSmiling_imp, eWink, eBlush, eYum, eRelieved,
eHeart_eyes, eSunglasses, eSmirk, eNeutral_face, eExpressionless,
eUnamused, eSweat, ePensive,eConfused, eConfounded, eKissing,
eKissing_heart, eKissing_smiling_eyes, eKissing_closed_eyes,
eStuck_out_tongue, eStuck_out_tongue_winking_eye,
eStuck_out_tongue_closed_eyes, eDisappointed, eWorried, eAngry,
ePout, eCry, ePersevere, eTriumph, eDisappointed_relieved, eFrowning,
eAnguished, eFearful, eWeary, eSleepy, eTired_face, eGrimacing, eSob,
eOpen_mouth, eHushed, eCold_sweat, eScream, eAstonished, eFlushed,
eSleeping, eDizzy_face, eNo_mouth, eMask, eSmile_cat, eJoy_cat, eSmiley_cat,
eHeart_eyes_cat, eSmirk_cat, eKissing_cat, ePouting_cat, eCrying_cat_face,
eScream_cat, eSlightly_frowning_face, eSlightly_smiling_face,
eUpside_down_face, eRoll_eyes, eNo_good, oOk_woman, eBow, eSee_no_evil,
eHear_no_evil, eSpeak_no_evil, eRaising_hand, eRaised_hands,
eFrowning_woman, ePerson_with_pouting_face, ePray);
var
/// github/Markdown compatible text of Emojis
// - e.g. 'grinning' or 'person_with_pouting_face'
EMOJI_TEXT: array[TEmoji] of RawUTF8;
/// github/Markdown compatible tag of Emojis, including trailing and ending :
// - e.g. ':grinning:' or ':person_with_pouting_face:'
EMOJI_TAG: array[TEmoji] of RawUTF8;
/// the Unicode character matching a given Emoji, after UTF-8 encoding
EMOJI_UTF8: array[TEmoji] of RawUTF8;
/// low-level access to TEmoji RTTI - used when inlining EmojiFromText()
EMOJI_RTTI: PShortString;
/// to recognize simple :) :( :| :/ :D :o :p :s characters as smilleys
EMOJI_AFTERDOTS: array['('..'|'] of TEmoji;
/// recognize github/Markdown compatible text of Emojis
// - for instance 'sunglasses' text buffer will return eSunglasses
// - returns eNone if no case-insensitive match was found
function EmojiFromText(P: PUTF8Char; len: PtrInt): TEmoji;
{$ifdef HASINLINE}inline;{$endif}
/// low-level parser of github/Markdown compatible text of Emojis
// - supplied P^ should point to ':'
// - will append the recognized UTF-8 Emoji if P contains e.g. :joy: or :)
// - will append ':' if no Emoji text is recognized, and return eNone
// - will try both EMOJI_AFTERDOTS[] and EMOJI_RTTI[] reference set
// - if W is nil, won't append anything, but just return the recognized TEmoji
function EmojiParseDots(var P: PUTF8Char; W: TTextWriter=nil): TEmoji;
/// low-level conversion of UTF-8 Emoji sequences into github/Markdown :identifiers:
procedure EmojiToDots(P: PUTF8Char; W: TTextWriter); overload;
/// conversion of UTF-8 Emoji sequences into github/Markdown :identifiers:
function EmojiToDots(const text: RawUTF8): RawUTF8; overload;
/// low-level conversion of github/Markdown :identifiers: into UTF-8 Emoji sequences
procedure EmojiFromDots(P: PUTF8Char; W: TTextWriter); overload;
/// conversion of github/Markdown :identifiers: into UTF-8 Emoji sequences
function EmojiFromDots(const text: RawUTF8): RawUTF8; overload;
{ ************ Command Line and Console process ************************** }
type
/// available console colors (under Windows at least)
TConsoleColor = (
ccBlack, ccBlue, ccGreen, ccCyan, ccRed, ccMagenta, ccBrown, ccLightGray,
ccDarkGray, ccLightBlue, ccLightGreen, ccLightCyan, ccLightRed, ccLightMagenta,
ccYellow, ccWhite);
{$ifdef FPC}{$ifdef Linux}
var
stdoutIsTTY: boolean;
{$endif}{$endif}
/// change the console text writing color
// - you should call this procedure to initialize StdOut global variable, if
// you manually initialized the Windows console, e.g. via the following code:
// ! AllocConsole;
// ! TextColor(ccLightGray); // initialize internal console context
procedure TextColor(Color: TConsoleColor);
/// write some text to the console using a given color
procedure ConsoleWrite(const Text: RawUTF8; Color: TConsoleColor=ccLightGray;
NoLineFeed: boolean=false; NoColor: boolean=false); overload;
/// write some text to the console using a given color
procedure ConsoleWrite(const Fmt: RawUTF8; const Args: array of const;
Color: TConsoleColor=ccLightGray; NoLineFeed: boolean=false); overload;
/// change the console text background color
procedure TextBackground(Color: TConsoleColor);
/// will wait for the ENTER key to be pressed, processing Synchronize() pending
// notifications, and the internal Windows Message loop (on this OS)
// - to be used e.g. for proper work of console applications with interface-based
// service implemented as optExecInMainThread
procedure ConsoleWaitForEnterKey;
/// read all available content from stdin
// - could be used to retrieve some file piped to the command line
// - the content is not converted, so will follow the encoding used for storage
function ConsoleReadBody: RawByteString;
{$ifdef MSWINDOWS}
/// low-level access to the keyboard state of a given key
function ConsoleKeyPressed(ExpectedKey: Word): Boolean;
{$endif}
/// direct conversion of a UTF-8 encoded string into a console OEM-encoded String
// - under Windows, will use the CP_OEMCP encoding
// - under Linux, will expect the console to be defined with UTF-8 encoding
function Utf8ToConsole(const S: RawUTF8): RawByteString;
{$ifndef MSWINDOWS}{$ifdef HASINLINE}inline;{$endif}{$endif}
/// direct conversion of a VCL string into a console OEM-encoded String
// - under Windows, will use the CP_OEMCP encoding
// - under Linux, will expect the console to be defined with UTF-8 encoding
function StringToConsole(const S: string): RawByteString;
{$ifndef MSWINDOWS}{$ifdef HASINLINE}inline;{$endif}{$endif}
/// could be used in the main program block of a console application to
// handle unexpected fatal exceptions
// - typical use may be:
// !begin
// ! try
// ! ... // main console process
// ! except
// ! on E: Exception do
// ! ConsoleShowFatalException(E);
// ! end;
// !end.
procedure ConsoleShowFatalException(E: Exception; WaitForEnterKey: boolean=true);
var
/// low-level handle used for console writing
// - may be overriden when console is redirected
// - is initialized when TextColor() is called
StdOut: THandle;
{$ifndef NOVARIANTS}
type
/// an interface to process the command line switches over a console
// - as implemented e.g. by TCommandLine class
// - can implement any process, optionally with console interactivity
ICommandLine = interface
['{77AB427C-1025-488B-8E04-3E62C8100E62}']
/// returns a command line switch value as UTF-8 text
// - you can specify a prompt text, when asking for any missing switch
function AsUTF8(const Switch, Default: RawUTF8; const Prompt: string): RawUTF8;
/// returns a command line switch value as VCL string text
// - you can specify a prompt text, when asking for any missing switch
function AsString(const Switch: RawUTF8; const Default, Prompt: string): string;
/// returns a command line switch value as integer
// - you can specify a prompt text, when asking for any missing switch
function AsInt(const Switch: RawUTF8; Default: Int64; const Prompt: string): Int64;
/// returns a command line switch ISO-8601 value as date value
// - here dates are expected to be encoded with ISO-8601, i.e. YYYY-MM-DD
// - you can specify a prompt text, when asking for any missing switch
function AsDate(const Switch: RawUTF8; Default: TDateTime; const Prompt: string): TDateTime;
/// returns a command line switch value as enumeration ordinal
// - RTTI will be used to check for the enumeration text, or plain integer
// value will be returned as ordinal value
// - you can specify a prompt text, when asking for any missing switch
function AsEnum(const Switch, Default: RawUTF8; TypeInfo: pointer;
const Prompt: string): integer;
/// returns all command line values as an array of UTF-8 text
// - i.e. won't interpret the various switches in the input parameters
// - as created e.g. by TCommandLine.CreateAsArray constructor
function AsArray: TRawUTF8DynArray;
/// serialize all recognized switches as UTF-8 JSON text
function AsJSON(Format: TTextWriterJSONFormat): RawUTF8;
/// equals TRUE if the -noprompt switch has been supplied
// - may be used to force pure execution without console interaction,
// e.g. when run from another process
function NoPrompt: boolean;
/// change the console text color
// - do nothing if NoPrompt is TRUE
procedure TextColor(Color: TConsoleColor);
/// write some console text, with an optional color
// - will output the text even if NoPrompt is TRUE
procedure Text(const Fmt: RawUTF8; const Args: array of const;
Color: TConsoleColor=ccLightGray);
end;
/// a class to process the command line switches, with console interactivity
// - is able to redirect all Text() output to an internal UTF-8 storage,
// in addition or instead of the console (to be used e.g. from a GUI)
// - implements ICommandLine interface
TCommandLine = class(TInterfacedObjectWithCustomCreate, ICommandLine)
private
fValues: TDocVariantData;
fNoPrompt: boolean;
fNoConsole: boolean;
fLines: TRawUTF8DynArray;
procedure SetNoConsole(value: boolean);
public
/// initialize the internal storage from the command line
// - will parse "-switch1 value1 -switch2 value2" layout
// - stand-alone "-switch1 -switch2 value2" will a create switch1=true value
constructor Create; overload; override;
/// initialize the internal storage from the command line
// - will set paramstr(firstParam)..paramstr(paramcount) in fValues as array
// - may be used e.g. for "val1 val2 val3" command line layout
constructor CreateAsArray(firstParam: integer);
/// initialize the internal storage with some ready-to-use switches
// - will also set the NoPrompt option, and set the supplied NoConsole value
// - may be used e.g. from a graphical interface instead of console mode
constructor Create(const switches: variant;
aNoConsole: boolean=true); reintroduce; overload;
/// initialize the internal storage with some ready-to-use name/value pairs
// - will also set the NoPrompt option, and set the supplied NoConsole value
// - may be used e.g. from a graphical interface instead of console mode
constructor Create(const NameValuePairs: array of const;
aNoConsole: boolean=true); reintroduce; overload;
/// returns a command line switch value as UTF-8 text
// - you can specify a prompt text, when asking for any missing switch
function AsUTF8(const Switch, Default: RawUTF8; const Prompt: string): RawUTF8;
/// returns a command line switch value as VCL string text
// - you can specify a prompt text, when asking for any missing switch
function AsString(const Switch: RawUTF8; const Default, Prompt: string): string;
/// returns a command line switch value as integer
// - you can specify a prompt text, when asking for any missing switch
function AsInt(const Switch: RawUTF8; Default: Int64; const Prompt: string): Int64;
/// returns a command line switch ISO-8601 value as date value
// - here dates are expected to be encoded with ISO-8601, i.e. YYYY-MM-DD
// - you can specify a prompt text, when asking for any missing switch
function AsDate(const Switch: RawUTF8; Default: TDateTime; const Prompt: string): TDateTime;
/// returns a command line switch value as enumeration ordinal
// - RTTI will be used to check for the enumeration text, or plain integer
// value will be returned as ordinal value
// - you can specify a prompt text, when asking for any missing switch
function AsEnum(const Switch, Default: RawUTF8; TypeInfo: pointer;
const Prompt: string): integer;
/// returns all command line values as an array of UTF-8 text
// - i.e. won't interpret the various switches in the input parameters
// - as created e.g. by TCommandLine.CreateAsArray constructor
function AsArray: TRawUTF8DynArray;
/// serialize all recognized switches as UTF-8 JSON text
function AsJSON(Format: TTextWriterJSONFormat): RawUTF8;
/// equals TRUE if the -noprompt switch has been supplied
// - may be used to force pure execution without console interaction,
// e.g. when run from another process
function NoPrompt: boolean;
/// change the console text color
// - do nothing if NoPrompt is TRUE
procedure TextColor(Color: TConsoleColor);
/// write some console text, with an optional color
// - will output the text even if NoPrompt=TRUE, but not if NoConsole=TRUE
// - will append the text to the internal storage, available from ConsoleText
procedure Text(const Fmt: RawUTF8; const Args: array of const;
Color: TConsoleColor=ccLightGray);
/// low-level access to the internal switches storage
property Values: TDocVariantData read fValues;
/// if Text() should be redirected to ConsoleText internal storage
// - and don't write anything to the console
// - should be associated with NoProperty = TRUE property
property NoConsole: boolean read fNoConsole write SetNoConsole;
/// low-level access to the internal UTF-8 console lines storage
property ConsoleLines: TRawUTF8DynArray read fLines;
/// returns the UTF-8 text as inserted by Text() calls
// - line feeds will be included to the ConsoleLines[] values
function ConsoleText(const LineFeed: RawUTF8=sLineBreak): RawUTF8;
end;
{$endif NOVARIANTS}
{ ************ TSynTable types and classes ************************** }
{$define SORTCOMPAREMETHOD}
{ if defined, the field content comparison will use a method instead of fixed
functions - could be mandatory for tftArray field kind }
type
/// exception raised by all TSynTable related code
ETableDataException = class(ESynException);
/// the available types for any TSynTable field property
// - this is used in our so-called SBF compact binary format
// (similar to BSON or Protocol Buffers)
// - those types are used for both storage and JSON conversion
// - basic types are similar to SQLite3, i.e. Int64/Double/UTF-8/Blob
// - storage can be of fixed size, or of variable length
// - you can specify to use WinAnsi encoding instead of UTF-8 for string storage
// (it can use less space on disk than UTF-8 encoding)
// - BLOB fields can be either internal (i.e. handled by TSynTable like a
// RawByteString text storage), either external (i.e. must be stored in a dedicated
// storage structure - e.g. another TSynBigTable instance)
TSynTableFieldType =
(// unknown or not defined field type
tftUnknown,
// some fixed-size field value
tftBoolean, tftUInt8, tftUInt16, tftUInt24, tftInt32, tftInt64,
tftCurrency, tftDouble,
// some variable-size field value
tftVarUInt32, tftVarInt32, tftVarUInt64,
// text storage
tftWinAnsi, tftUTF8,
// BLOB fields
tftBlobInternal, tftBlobExternal,
// other variable-size field value
tftVarInt64);
/// set of available field types for TSynTable
TSynTableFieldTypes = set of TSynTableFieldType;
/// available option types for a field property
// - tfoIndex is set if an index must be created for this field
// - tfoUnique is set if field values must be unique (if set, the tfoIndex
// will be always forced)
// - tfoCaseInsensitive can be set to make no difference between 'a' and 'A'
// (by default, comparison is case-sensitive) - this option has an effect
// not only if tfoIndex or tfoUnique is set, but also for iterating search
TSynTableFieldOption = (
tfoIndex, tfoUnique, tfoCaseInsensitive);
/// set of option types for a field
TSynTableFieldOptions = set of TSynTableFieldOption;
/// used to store bit set for all available fiels in a Table
// - with current format, maximum field count is 64
TSynTableFieldBits = set of 0..63;
/// an custom RawByteString type used to store internaly a data in
// our SBF compact binary format
TSBFString = type RawByteString;
/// function prototype used to retrieve the index of a specified property name
// - 'ID' is handled separately: here must be available only the custom fields
TSynTableFieldIndex = function(const PropName: RawUTF8): integer of object;
/// the recognized operators for a TSynTableStatement where clause
TSynTableStatementOperator = (
opEqualTo,
opNotEqualTo,
opLessThan,
opLessThanOrEqualTo,
opGreaterThan,
opGreaterThanOrEqualTo,
opIn,
opIsNull,
opIsNotNull,
opLike,
opContains,
opFunction);
TSynTableFieldProperties = class;
/// one recognized SELECT expression for TSynTableStatement
TSynTableStatementSelect = record
/// the column SELECTed for the SQL statement, in the expected order
// - contains 0 for ID/RowID, or the RTTI field index + 1
Field: integer;
/// an optional integer to be added
// - recognized from .. +123 .. -123 patterns in the select
ToBeAdded: integer;
/// the optional column alias, e.g. 'MaxID' for 'max(id) as MaxID'
Alias: RawUTF8;
/// the optional function applied to the SELECTed column
// - e.g. Max(RowID) would store 'Max' and SelectField[0]=0
// - but Count( * ) would store 'Count' and SelectField[0]=0, and
// set FunctionIsCountStart = TRUE
FunctionName: RawUTF8;
/// if the function needs a special process
// - e.g. funcCountStar for the special Count( * ) expression or
// funcDistinct, funcMax for distinct(...)/max(...) aggregation
FunctionKnown: (funcNone, funcCountStar, funcDistinct, funcMax);
/// MongoDB-like sub field e.g. 'mainfield.subfield1.subfield2'
// - still identifying 'mainfield' in Field index, and setting
// SubField='.subfield1.subfield2'
SubField: RawUTF8;
end;
/// the recognized SELECT expressions for TSynTableStatement
TSynTableStatementSelectDynArray = array of TSynTableStatementSelect;
/// one recognized WHERE expression for TSynTableStatement
TSynTableStatementWhere = record
/// any '(' before the actual expression
ParenthesisBefore: RawUTF8;
/// any ')' after the actual expression
ParenthesisAfter: RawUTF8;
/// expressions are evaluated as AND unless this field is set to TRUE
JoinedOR: boolean;
/// if this expression is preceded by a NOT modifier
NotClause: boolean;
/// the index of the field used for the WHERE expression
// - WhereField=0 for ID, 1 for field # 0, 2 for field #1,
// and so on... (i.e. WhereField = RTTI field index +1)
Field: integer;
/// MongoDB-like sub field e.g. 'mainfield.subfield1.subfield2'
// - still identifying 'mainfield' in Field index, and setting
// SubField='.subfield1.subfield2'
SubField: RawUTF8;
/// the operator of the WHERE expression
Operator: TSynTableStatementOperator;
/// the SQL function name associated to a Field and Value
// - e.g. 'INTEGERDYNARRAYCONTAINS' and Field=0 for
// IntegerDynArrayContains(RowID,10) and ValueInteger=10
// - Value does not contain anything
FunctionName: RawUTF8;
/// the value used for the WHERE expression
Value: RawUTF8;
/// the raw value SQL buffer used for the WHERE expression
ValueSQL: PUTF8Char;
/// the raw value SQL buffer length used for the WHERE expression
ValueSQLLen: integer;
/// an integer representation of WhereValue (used for ID check e.g.)
ValueInteger: integer;
/// used to fast compare with SBF binary compact formatted data
ValueSBF: TSBFString;
{$ifndef NOVARIANTS}
/// the value used for the WHERE expression, encoded as Variant
// - may be a TDocVariant for the IN operator
ValueVariant: variant;
{$endif}
end;
/// the recognized WHERE expressions for TSynTableStatement
TSynTableStatementWhereDynArray = array of TSynTableStatementWhere;
/// used to parse a SELECT SQL statement, following the SQlite3 syntax
// - handle basic REST commands, i.e. a SELECT over a single table (no JOIN)
// with its WHERE clause, and result column aliases
// - handle also aggregate functions like "SELECT Count( * ) FROM TableName"
// - will also parse any LIMIT, OFFSET, ORDER BY, GROUP BY statement clause
TSynTableStatement = class
protected
fSQLStatement: RawUTF8;
fSelect: TSynTableStatementSelectDynArray;
fSelectFunctionCount: integer;
fTableName: RawUTF8;
fWhere: TSynTableStatementWhereDynArray;
fOrderByField: TSQLFieldIndexDynArray;
fGroupByField: TSQLFieldIndexDynArray;
fWhereHasParenthesis, fHasSelectSubFields, fWhereHasSubFields: boolean;
fOrderByDesc: boolean;
fLimit: integer;
fOffset: integer;
fWriter: TJSONWriter;
public
/// parse the given SELECT SQL statement and retrieve the corresponding
// parameters into this class read-only properties
// - the supplied GetFieldIndex() method is used to populate the
// SelectedFields and Where[].Field properties
// - SimpleFieldsBits is used for '*' field names
// - SQLStatement is left '' if the SQL statement is not correct
// - if SQLStatement is set, the caller must check for TableName to match
// the expected value, then use the Where[] to retrieve the content
// - if FieldProp is set, then the Where[].ValueSBF property is initialized
// with the SBF equivalence of the Where[].Value
constructor Create(const SQL: RawUTF8; GetFieldIndex: TSynTableFieldIndex;
SimpleFieldsBits: TSQLFieldBits=[0..MAX_SQLFIELDS-1];
FieldProp: TSynTableFieldProperties=nil);
/// compute the SELECT column bits from the SelectFields array
// - optionally set Select[].SubField into SubFields[Select[].Field]
// (e.g. to include specific fields from MongoDB embedded document)
procedure SelectFieldBits(var Fields: TSQLFieldBits; var withID: boolean;
SubFields: PRawUTF8Array=nil);
/// the SELECT SQL statement parsed
// - equals '' if the parsing failed
property SQLStatement: RawUTF8 read fSQLStatement;
/// the column SELECTed for the SQL statement, in the expected order
property Select: TSynTableStatementSelectDynArray read fSelect;
/// if the SELECTed expression of this SQL statement have any function defined
property SelectFunctionCount: integer read fSelectFunctionCount;
/// the retrieved table name
property TableName: RawUTF8 read fTableName;
/// if any Select[].SubField was actually set
property HasSelectSubFields: boolean read fHasSelectSubFields;
/// the WHERE clause of this SQL statement
property Where: TSynTableStatementWhereDynArray read fWhere;
/// if the WHERE clause contains any ( ) parenthesis expression
property WhereHasParenthesis: boolean read fWhereHasParenthesis;
/// if the WHERE clause contains any Where[].SubField
property WhereHasSubFields: boolean read fWhereHasSubFields;
/// recognize an GROUP BY clause with one or several fields
// - here 0 = ID, otherwise RTTI field index +1
property GroupByField: TSQLFieldIndexDynArray read fGroupByField;
/// recognize an ORDER BY clause with one or several fields
// - here 0 = ID, otherwise RTTI field index +1
property OrderByField: TSQLFieldIndexDynArray read fOrderByField;
/// false for default ASC order, true for DESC attribute
property OrderByDesc: boolean read fOrderByDesc;
/// the number specified by the optional LIMIT ... clause
// - set to 0 by default (meaning no LIMIT clause)
property Limit: integer read fLimit;
/// the number specified by the optional OFFSET ... clause
// - set to 0 by default (meaning no OFFSET clause)
property Offset: integer read fOffset;
/// optional associated writer
property Writer: TJSONWriter read fWriter write fWriter;
end;
/// function prototype used to retrieve the RECORD data of a specified Index
// - the index is not the per-ID index, but the "physical" index, i.e. the
// index value used to retrieve data from low-level (and faster) method
// - should return nil if Index is out of range
// - caller must provide a temporary storage buffer to be used optionally
TSynTableGetRecordData = function(
Index: integer; var aTempData: RawByteString): pointer of object;
TSynTable = class;
{$ifdef SORTCOMPAREMETHOD}
/// internal value used by TSynTableFieldProperties.SortCompare() method to
// avoid stack allocation
TSortCompareTmp = record
PB1, PB2: PByte;
L1,L2: integer;
end;
{$endif}
/// store the type properties of a given field / database column
TSynTableFieldProperties = class
protected
/// used during OrderedIndexSort to prevent stack usage
SortPivot: pointer;
{$ifdef SORTCOMPAREMETHOD}
/// internal value used by SortCompare() method to avoid stack allocation
SortCompareTmp: TSortCompareTmp;
{$endif}
/// these two temporary buffers are used to call TSynTableGetRecordData
DataTemp1, DataTemp2: RawByteString;
/// the associated table which own this field property
Owner: TSynTable;
/// the global size of a default field value, as encoded
// in our SBF compact binary format
fDefaultFieldLength: integer;
/// a default field data, as encoded in our SBF compact binary format
fDefaultFieldData: TSBFString;
/// last >=0 value returned by the last OrderedIndexFindAdd() call
fOrderedIndexFindAdd: integer;
/// used for internal QuickSort of OrderedIndex[]
// - call SortCompare() for sorting the items
procedure OrderedIndexSort(L,R: PtrInt);
/// retrieve an index from OrderedIndex[] of the given value
// - call SortCompare() to compare to the reference value
function OrderedIndexFind(Value: pointer): PtrInt;
/// retrieve an index where a Value must be added into OrderedIndex[]
// - call SortCompare() to compare to the reference value
// - returns -1 if Value is there, or the index where to insert
// - the returned value (if >= 0) will be stored in fOrderedIndexFindAdd
function OrderedIndexFindAdd(Value: pointer): PtrInt;
/// set OrderedIndexReverse[OrderedIndex[aOrderedIndex]] := aOrderedIndex;
procedure OrderedIndexReverseSet(aOrderedIndex: integer);
public
/// the field name
Name: RawUTF8;
/// kind of field (defines both value type and storage to be used)
FieldType: TSynTableFieldType;
/// the fixed-length size, or -1 for a varInt, -2 for a variable string
FieldSize: integer;
/// options of this field
Options: TSynTableFieldOptions;
/// contains the offset of this field, in case of fixed-length field
// - normally, fixed-length fields are stored in the beginning of the record
// storage: in this case, a value >= 0 will point to the position of the
// field value of this field
// - if the value is < 0, its absolute will be the field number to be counted
// after TSynTable.fFieldVariableOffset (-1 for first item)
Offset: integer;
/// number of the field in the table (starting at 0)
FieldNumber: integer;
/// if allocated, contains the storage indexes of every item, in sorted order
// - only available if tfoIndex is in Options
// - the index is not the per-ID index, but the "physical" index, i.e. the
// index value used to retrieve data from low-level (and faster) method
OrderedIndex: TIntegerDynArray;
/// if allocated, contains the reverse storage index of OrderedIndex
// - i.e. OrderedIndexReverse[OrderedIndex[i]] := i;
// - used to speed up the record update procedure with huge number of
// records
OrderedIndexReverse: TIntegerDynArray;
/// number of items in OrderedIndex[]
// - is set to 0 when the content has been modified (mark force recreate)
OrderedIndexCount: integer;
/// if set to TRUE after an OrderedIndex[] refresh but with not sorting
// - OrderedIndexSort(0,OrderedIndexCount-1) must be called before using
// the OrderedIndex[] array
// - you should call OrderedIndexRefresh method to ensure it is sorted
OrderedIndexNotSorted: boolean;
/// all TSynValidate instances registered per each field
2021-04-23 10:11:05 +02:00
Filters: TSynObjectList;
/// all TSynValidate instances registered per each field
2021-04-23 10:11:05 +02:00
Validates: TSynObjectList;
/// low-level binary comparison used by IDSort and TSynTable.IterateJSONValues
// - P1 and P2 must point to the values encoded in our SBF compact binary format
{$ifdef SORTCOMPAREMETHOD}
function SortCompare(P1,P2: PUTF8Char): PtrInt;
{$else}
SortCompare: TUTF8Compare;
{$endif}
/// read entry from a specified file reader
constructor CreateFrom(var RD: TFileBufferReader);
/// release associated memory and objects
destructor Destroy; override;
/// save entry to a specified file writer
procedure SaveTo(WR: TFileBufferWriter);
/// decode the value from our SBF compact binary format into UTF-8 JSON
// - returns the next FieldBuffer value
function GetJSON(FieldBuffer: pointer; W: TTextWriter): pointer;
/// decode the value from our SBF compact binary format into UTF-8 text
// - this method does not check for FieldBuffer to be not nil -> caller
// should check this explicitely
function GetValue(FieldBuffer: pointer): RawUTF8;
/// decode the value from a record buffer into an Boolean
// - will call Owner.GetData to retrieve then decode the field SBF content
function GetBoolean(RecordBuffer: pointer): Boolean;
{$ifdef HASINLINE}inline;{$endif}
/// decode the value from a record buffer into an integer
// - will call Owner.GetData to retrieve then decode the field SBF content
function GetInteger(RecordBuffer: pointer): Integer;
/// decode the value from a record buffer into an Int64
// - will call Owner.GetData to retrieve then decode the field SBF content
function GetInt64(RecordBuffer: pointer): Int64;
/// decode the value from a record buffer into an floating-point value
// - will call Owner.GetData to retrieve then decode the field SBF content
function GetDouble(RecordBuffer: pointer): Double;
/// decode the value from a record buffer into an currency value
// - will call Owner.GetData to retrieve then decode the field SBF content
function GetCurrency(RecordBuffer: pointer): Currency;
/// decode the value from a record buffer into a RawUTF8 string
// - will call Owner.GetData to retrieve then decode the field SBF content
function GetRawUTF8(RecordBuffer: pointer): RawUTF8;
{$ifndef NOVARIANTS}
/// decode the value from our SBF compact binary format into a Variant
function GetVariant(FieldBuffer: pointer): Variant; overload;
{$ifdef HASINLINE}inline;{$endif}
/// decode the value from our SBF compact binary format into a Variant
procedure GetVariant(FieldBuffer: pointer; var result: Variant); overload;
{$endif}
/// retrieve the binary length (in bytes) of some SBF compact binary format
function GetLength(FieldBuffer: pointer): Integer;
{$ifdef HASINLINE}inline;{$endif}
/// create some SBF compact binary format from a Delphi binary value
// - will return '' if the field type doesn't match a boolean
function SBF(const Value: Boolean): TSBFString; overload;
/// create some SBF compact binary format from a Delphi binary value
// - will encode any byte, word, integer, cardinal, Int64 value
// - will return '' if the field type doesn't match an integer
function SBF(const Value: Int64): TSBFString; overload;
/// create some SBF compact binary format from a Delphi binary value
// - will encode any byte, word, integer, cardinal value
// - will return '' if the field type doesn't match an integer
function SBF(const Value: Integer): TSBFString; overload;
/// create some SBF compact binary format from a Delphi binary value
// - will return '' if the field type doesn't match a currency
// - we can't use SBF() method name because of Currency/Double ambiguity
function SBFCurr(const Value: Currency): TSBFString;
/// create some SBF compact binary format from a Delphi binary value
// - will return '' if the field type doesn't match a floating-point
// - we can't use SBF() method name because of Currency/Double ambiguity
function SBFFloat(const Value: Double): TSBFString;
/// create some SBF compact binary format from a Delphi binary value
// - expect a RawUTF8 string: will be converted to WinAnsiString
// before storage, for tftWinAnsi
// - will return '' if the field type doesn't match a string
function SBF(const Value: RawUTF8): TSBFString; overload;
/// create some SBF compact binary format from a BLOB memory buffer
// - will return '' if the field type doesn't match tftBlobInternal
function SBF(Value: pointer; ValueLen: integer): TSBFString; overload;
/// convert any UTF-8 encoded value into our SBF compact binary format
// - can be used e.g. from a WHERE clause, for fast comparison in
// TSynTableStatement.WhereValue content using OrderedIndex[]
// - is the reverse of GetValue/GetRawUTF8 methods above
function SBFFromRawUTF8(const aValue: RawUTF8): TSBFString;
{$ifndef NOVARIANTS}
/// create some SBF compact binary format from a Variant value
function SBF(const Value: Variant): TSBFString; overload;
{$endif}
/// will update then sort the array of indexes used for the field index
// - the OrderedIndex[] array is first refreshed according to the
// aOldIndex, aNewIndex parameters: aOldIndex=-1 for Add, aNewIndex=-1 for
// Delete, or both >= 0 for update
// - call with both indexes = -1 will sort the existing OrderedIndex[] array
// - GetData property must have been set with a method returning a pointer
// to the field data for a given index (this index is not the per-ID index,
// but the "physical" index, i.e. the index value used to retrieve data
// from low-level (and fast) GetData method)
// - aOldRecordData and aNewRecordData can be specified in order to guess
// if the field data has really been modified (speed up the update a lot
// to only sort indexed fields if its content has been really modified)
// - returns FALSE if any parameter is invalid
function OrderedIndexUpdate(aOldIndex, aNewIndex: integer;
aOldRecordData, aNewRecordData: pointer): boolean;
/// retrieve one or more "physical" indexes matching a WHERE Statement
// - is faster than O(1) GetIteraring(), because will use O(log(n)) binary
// search using the OrderedIndex[] array
// - returns the resulting indexes as a a sorted list in MatchIndex/MatchIndexCount
// - if the indexes are already present in the list, won't duplicate them
// - WhereSBFValue must be a valid SBF formated field buffer content
// - the Limit parameter is similar to the SQL LIMIT clause: if greater than 0,
// an upper bound on the number of rows returned is placed (e.g. set Limit=1
// to only retrieve the first match)
// - GetData property must have been set with a method returning a pointer
// to the field data for a given index (this index is not the per-ID index,
// but the "physical" index, i.e. the index value used to retrieve data
// from low-level (and fast) GetData method)
// - in this method, indexes are not the per-ID indexes, but the "physical"
// indexes, i.e. each index value used to retrieve data from low-level
// (and fast) GetData method
function OrderedIndexMatch(WhereSBFValue: pointer;
var MatchIndex: TIntegerDynArray; var MatchIndexCount: integer;
Limit: Integer=0): Boolean;
/// will force refresh the OrderedIndex[] array
// - to be called e.g. if OrderedIndexNotSorted = TRUE, if you want to
// access to the OrderedIndex[] array
procedure OrderedIndexRefresh;
/// register a custom filter or validation rule to the class for this field
// - this will be used by Filter() and Validate() methods
// - will return the specified associated TSynFilterOrValidate instance
// - a TSynValidateTableUniqueField is always added by
// TSynTable.AfterFieldModif if tfoUnique is set in Options
function AddFilterOrValidate(aFilter: TSynFilterOrValidate): TSynFilterOrValidate;
/// check the registered constraints
// - returns '' on success
// - returns an error message e.g. if a tftUnique constraint failed
// - RecordIndex=-1 in case of adding, or the physical index of the updated record
function Validate(RecordBuffer: pointer; RecordIndex: integer): string;
/// some default SBF compact binary format content
property SBFDefault: TSBFString read fDefaultFieldData;
end;
{$ifndef DELPHI5OROLDER}
/// a pointer to structure used to store a TSynTable record
PSynTableData = ^TSynTableData;
{$A-} { packet object not allowed since Delphi 2009 :( }
/// used to store a TSynTable record using our SBF compact binary format
// - this object can be created on the stack
// - it is mapped into a variant TVarData, to be retrieved by the
// TSynTable.Data method - but direct allocation of a TSynTableData on the
// stack is faster (due to the Variant overhead)
// - is defined either as an object either as a record, due to a bug
// in Delphi 2009/2010 compiler (at least): this structure is not initialized
// if defined as an object on the stack, but will be as a record :(
2021-04-23 10:11:05 +02:00
{$ifdef USERECORDWITHMETHODS}TSynTableData = record
{$else}TSynTableData = object {$endif UNICODE}
private
VType: cardinal; // defined as cardinal not as word for proper aligment
VID: integer;
VTable: TSynTable;
VValue: TSBFString;
{$ifndef NOVARIANTS}
function GetFieldVarData(FieldName: PUTF8Char; FieldNameLen: PtrInt; var Value: TVarData): boolean;
procedure GetFieldVariant(const FieldName: RawUTF8; var result: Variant);
2021-04-23 10:11:05 +02:00
function GetField(const FieldName: RawUTF8): Variant;
procedure SetField(const FieldName: RawUTF8; const Value: Variant);
{$endif}
/// raise an exception if VTable=nil
procedure CheckVTableInitialized;
{$ifdef HASINLINE}inline;{$endif}
public
/// initialize a record data content for a specified table
// - a void content is set
procedure Init(aTable: TSynTable; aID: Integer=0); overload; {$ifdef HASINLINE}inline;{$endif}
/// initialize a record data content for a specified table
// - the specified SBF content is store inside this TSynTableData
procedure Init(aTable: TSynTable; aID: Integer; RecordBuffer: pointer;
RecordBufferLen: integer); overload;
/// the associated record ID
property ID: integer read VID write VID;
/// the associated TSynTable instance
property Table: TSynTable read VTable write VTable;
/// the record content, SBF compact binary format encoded
property SBF: TSBFString read VValue;
{$ifndef NOVARIANTS}
/// set or retrieve a field value from a variant data
2021-04-23 10:11:05 +02:00
property Field[const FieldName: RawUTF8]: Variant read GetField write SetField;
/// get a field value for a specified field
// - this method is faster than Field[], because it won't look for the field name
2021-04-23 10:11:05 +02:00
function GetFieldValue(aField: TSynTableFieldProperties): Variant;
/// set a field value for a specified field
// - this method is faster than Field[], because it won't look for the field name
2021-04-23 10:11:05 +02:00
procedure SetFieldValue(aField: TSynTableFieldProperties; const Value: Variant);
{$ifdef HASINLINE}inline;{$endif}
{$endif}
/// set a field value for a specified field, from SBF-encoded data
// - this method is faster than the other, because it won't look for the field
// name nor make any variant conversion
procedure SetFieldSBFValue(aField: TSynTableFieldProperties; const Value: TSBFString);
/// get a field value for a specified field, into SBF-encoded data
// - this method is faster than the other, because it won't look for the field
// name nor make any variant conversion
function GetFieldSBFValue(aField: TSynTableFieldProperties): TSBFString;
/// filter the SBF buffer record content with all registered filters
// - all field values are filtered in-place, following our SBF compact
// binary format encoding for this record
procedure FilterSBFValue; {$ifdef HASINLINE}inline;{$endif}
/// check the registered constraints according to a record SBF buffer
// - returns '' on success
// - returns an error message e.g. if a tftUnique constraint failed
// - RecordIndex=-1 in case of adding, or the physical index of the updated record
function ValidateSBFValue(RecordIndex: integer): string;
end;
{$A+} { packet object not allowed since Delphi 2009 :( }
{$endif DELPHI5OROLDER}
PUpdateFieldEvent = ^TUpdateFieldEvent;
/// an opaque structure used for TSynTable.UpdateFieldEvent method
TUpdateFieldEvent = record
/// the number of record added
Count: integer;
/// the list of IDs added
// - this list is already in increasing order, because GetIterating was
// called with the ioID order
IDs: TIntegerDynArray;
/// the offset of every record added
// - follows the IDs[] order
Offsets64: TInt64DynArray;
/// previous indexes: NewIndexs[oldIndex] := newIndex
NewIndexs: TIntegerDynArray;
/// the list of existing field in the previous data
AvailableFields: TSQLFieldBits;
/// where to write the updated data
WR: TFileBufferWriter;
end;
/// will define a validation to be applied to a TSynTableFieldProperties field
// - a typical usage is to validate a value to be unique in the table
// (implemented in the TSynValidateTableUniqueField class)
// - the optional associated parameters are to be supplied JSON-encoded
// - ProcessField and ProcessRecordIndex properties will be filled before
// Process method call by TSynTableFieldProperties.Validate()
TSynValidateTable = class(TSynValidate)
protected
fProcessField: TSynTableFieldProperties;
fProcessRecordIndex: integer;
public
/// the associated TSQLRest instance
// - this value is filled by TSynTableFieldProperties.Validate with its
// self value to be used for the validation
// - it can be used in the overridden Process method
property ProcessField: TSynTableFieldProperties read fProcessField write fProcessField;
/// the associated record index (in case of update)
// - is set to -1 in case of adding, or the physical index of the updated record
// - this value is filled by TSynTableFieldProperties.Validate
// - it can be used in the overridden Process method
property ProcessRecordIndex: integer read fProcessRecordIndex write fProcessRecordIndex;
end;
/// will define a validation for a TSynTableFieldProperties Unique field
// - implement constraints check e.g. if tfoUnique is set in Options
// - it will check that the field value is not void
// - it will check that the field value is not a duplicate
TSynValidateTableUniqueField = class(TSynValidateTable)
public
/// perform the unique field validation action to the specified value
// - duplication value check will use the ProcessField and
// ProcessRecordIndex properties, which will be filled before call by
// TSynTableFieldProperties.Validate()
// - aFieldIndex parameter is not used here, since we have already the
// ProcessField property set
// - here the Value is expected to be UTF-8 text, as converted from our SBF
// compact binary format via e.g. TSynTableFieldProperties.GetValue /
// GetRawUTF8: this is mandatory to have the validation rule fit with other
// TSynValidateTable classes
function Process(aFieldIndex: integer; const Value: RawUTF8; var ErrorMsg: string): boolean; override;
end;
/// store the description of a table with records, to implement a Database
// - can be used with several storage engines, for instance TSynBigTableRecord
// - each record can have up to 64 fields
// - a mandatory ID field must be handled by the storage engine itself
// - will handle the storage of records into our SBF compact binary format, in
// which fixed-length fields are stored leftmost side, then variable-length
// fields follow
TSynTable = class
protected
fTableName: RawUTF8;
/// list of TSynTableFieldProperties instances
fField: TObjectList;
/// offset of the first variable length value field
fFieldVariableOffset: PtrUInt;
/// index of the first variable length value field
// - equals -1 if no variable length field exists
fFieldVariableIndex: integer;
/// bit is set for a tftWinAnsi, tftUTF8 or tftBlobInternal kind of field
// - these kind of field are encoded as a VarInt length, then the data
fFieldIsVarString: TSynTableFieldBits;
/// bit is set for a tftBlobExternal kind of field e.g.
fFieldIsExternal: TSynTableFieldBits;
/// event used for proper data retrieval of a given record buffer
fGetRecordData: TSynTableGetRecordData;
/// the global size of a default value, as encoded in our SBF compact binary format
fDefaultRecordLength: integer;
/// a default record data, as encoded in our SBF compact binary format
fDefaultRecordData: TSBFString;
/// list of TSynTableFieldProperties added via all AddField() call
fAddedField: TList;
/// true if any field has a tfoUnique option set
fFieldHasUniqueIndexes: boolean;
function GetFieldType(Index: integer): TSynTableFieldProperties;
function GetFieldCount: integer;
function GetFieldFromName(const aName: RawUTF8): TSynTableFieldProperties;
function GetFieldFromNameLen(aName: PUTF8Char; aNameLen: integer): TSynTableFieldProperties;
/// following method matchs the TSynTableFieldIndex event type
function GetFieldIndexFromName(const aName: RawUTF8): integer; overload;
function GetFieldIndexFromNameLen(aName: PUTF8Char; aNameLen: integer): integer; overload;
/// refresh Offset,FieldNumber,FieldSize and fFieldVariableIndex,fFieldVariableOffset
procedure AfterFieldModif;
public
/// create a table definition instance
constructor Create(const aTableName: RawUTF8);
/// create a table definition instance from a specified file reader
procedure LoadFrom(var RD: TFileBufferReader);
/// release used memory
destructor Destroy; override;
/// save field properties to a specified file writer
procedure SaveTo(WR: TFileBufferWriter);
/// retrieve to the corresponding data address of a given field
function GetData(RecordBuffer: PUTF8Char; Field: TSynTableFieldProperties): pointer;
/// add a field description to the table
// - warning: the class responsible of the storage itself must process the
// data already stored when a field is created, e.g. in
// TSynBigTableRecord.AddFieldUpdate method
// - physical order does not necessary follow the AddField() call order:
// for better performance, it will try to store fixed-sized record first,
// multiple of 4 bytes first (access is faster if dat is 4 byte aligned),
// then variable-length after fixed-sized fields; in all case, a field
// indexed will be put first
function AddField(const aName: RawUTF8; aType: TSynTableFieldType;
aOptions: TSynTableFieldOptions=[]): TSynTableFieldProperties;
/// update a record content
// - return the updated record data, in our SBF compact binary format
// - if NewFieldData is not specified, a default 0 or '' value is appended
// - if NewFieldData is set, it must match the field value kind
// - warning: this method will update result in-place, so RecordBuffer MUST
// be <> pointer(result) or data corruption may occur
procedure UpdateFieldData(RecordBuffer: PUTF8Char; RecordBufferLen,
FieldIndex: integer; var result: TSBFString; const NewFieldData: TSBFString='');
/// update a record content after any AddfieldUpdate, to refresh the data
// - AvailableFields must contain the list of existing fields in the previous data
function UpdateFieldRecord(RecordBuffer: PUTF8Char; var AvailableFields: TSQLFieldBits): TSBFString;
/// this Event is to be called for all data records (via a GetIterating method)
// after any AddfieldUpdate, to refresh the data
// - Opaque is in fact a pointer to a TUpdateFieldEvent record, and will contain
// all parameters set by TSynBigTableRecord.AddFieldUpdate, including a
// TFileBufferWriter instance to use to write the recreated data
// - it will work with either any newly added field, handly also field data
// order change in SBF record (e.g. when a fixed-sized field has been added
// on a record containing variable-length fields)
function UpdateFieldEvent(Sender: TObject; Opaque: pointer; ID, Index: integer;
Data: pointer; DataLen: integer): boolean;
/// event which must be called by the storage engine when some values are modified
// - if aOldIndex and aNewIndex are both >= 0, the corresponding aOldIndex
// will be replaced by aNewIndex value (i.e. called in case of a data Update)
// - if aOldIndex is -1 and aNewIndex is >= 0, aNewIndex refers to a just
// created item (i.e. called in case of a data Add)
// - if aOldIndex is >= 0 and aNewIndex is -1, aNewIndex refers to a just
// deleted item (i.e. called in case of a data Delete)
// - will update then sort all existing TSynTableFieldProperties.OrderedIndex
// values
// - the GetDataBuffer protected virtual method must have been overridden to
// properly return the record data for a given "physical/stored" index
// - aOldRecordData and aNewRecordData can be specified in order to guess
// if the field data has really been modified (speed up the update a lot
// to only sort indexed fields if its content has been really modified)
procedure FieldIndexModify(aOldIndex, aNewIndex: integer;
aOldRecordData, aNewRecordData: pointer);
/// return the total length of the given record buffer, encoded in our SBF
// compact binary format
function DataLength(RecordBuffer: pointer): integer;
{$ifndef NOVARIANTS}
/// create a Variant able to access any field content via late binding
// - i.e. you can use Var.Name to access the 'Name' field of record Var
// - if you leave ID and RecordBuffer void, a void record is created
function Data(aID: integer=0; RecordBuffer: pointer=nil;
RecordBufferLen: Integer=0): Variant; overload;
{$endif NOVARIANTS}
/// return a default content for ALL record fields
// - uses our SBF compact binary format
property DefaultRecordData: TSBFString read fDefaultRecordData;
/// list of TSynTableFieldProperties added via all AddField() call
// - this list will allow TSynBigTableRecord.AddFieldUpdate to refresh
// the data on disk according to the new field configuration
property AddedField: TList read fAddedField write fAddedField;
/// offset of the first variable length value field
property FieldVariableOffset: PtrUInt read fFieldVariableOffset;
public
{$ifndef DELPHI5OROLDER}
/// create a TJSONWriter, ready to be filled with GetJSONValues(W) below
// - will initialize all TJSONWriter.ColNames[] values according to the
// specified Fields index list, and initialize the JSON content
function CreateJSONWriter(JSON: TStream; Expand, withID: boolean;
const Fields: TSQLFieldIndexDynArray): TJSONWriter; overload;
/// create a TJSONWriter, ready to be filled with GetJSONValues(W) below
// - will initialize all TJSONWriter.ColNames[] values according to the
// specified Fields bit set, and initialize the JSON content
function CreateJSONWriter(JSON: TStream; Expand, withID: boolean;
const Fields: TSQLFieldBits): TJSONWriter; overload;
/// return the UTF-8 encoded JSON objects for the values contained
// in the specified RecordBuffer encoded in our SBF compact binary format,
// according to the Expand/WithID/Fields parameters of W
// - if W.Expand is true, JSON data is an object, for direct use with any Ajax or .NET client:
// ! {"col1":val11,"col2":"val12"}
//- if W.Expand is false, JSON data is serialized (as used in TSQLTableJSON)
// ! { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
// - only fields with a bit set in W.Fields will be appended
// - if W.WithID is true, then the first ID field value is included
procedure GetJSONValues(aID: integer; RecordBuffer: PUTF8Char; W: TJSONWriter);
/// can be used to retrieve all values matching a preparated TSynTableStatement
// - this method matchs the TSynBigTableIterateEvent callback definition
// - Sender will be the TSynBigTable instance, and Opaque will point to a
// TSynTableStatement instance (with all fields initialized, including Writer)
function IterateJSONValues(Sender: TObject; Opaque: pointer; ID: integer;
Data: pointer; DataLen: integer): boolean;
{$endif DELPHI5OROLDER}
/// check the registered constraints according to a record SBF buffer
// - returns '' on success
// - returns an error message e.g. if a tftUnique constraint failed
// - RecordIndex=-1 in case of adding, or the physical index of the updated record
function Validate(RecordBuffer: pointer; RecordIndex: integer): string;
/// filter the SBF buffer record content with all registered filters
// - all field values are filtered in-place, following our SBF compact
// binary format encoding for this record
procedure Filter(var RecordBuffer: TSBFString);
/// event used for proper data retrieval of a given record buffer, according
// to the physical/storage index value (not per-ID index)
// - if not set, field indexes won't work
// - will be mapped e.g. to TSynBigTable.GetPointerFromPhysicalIndex
property GetRecordData: TSynTableGetRecordData read fGetRecordData write fGetRecordData;
public
/// the internal Table name used to identify it (e.g. from JSON or SQL)
// - similar to the SQL Table name
property TableName: RawUTF8 read fTableName write fTableName;
/// number of fields in this table
property FieldCount: integer read GetFieldCount;
/// retrieve the properties of a given field
// - returns nil if the specified Index is out of range
property Field[Index: integer]: TSynTableFieldProperties read GetFieldType;
/// retrieve the properties of a given field
// - returns nil if the specified Index is out of range
property FieldFromName[const aName: RawUTF8]: TSynTableFieldProperties read GetFieldFromName; default;
/// retrieve the index of a given field
// - returns -1 if the specified Index is out of range
property FieldIndexFromName[const aName: RawUTF8]: integer read GetFieldIndexFromName;
/// read-only access to the Field list
property FieldList: TObjectList read fField;
/// true if any field has a tfoUnique option set
property HasUniqueIndexes: boolean read fFieldHasUniqueIndexes;
end;
{$ifndef NOVARIANTS}
/// a custom variant type used to have direct access to a record content
// - use TSynTable.Data method to retrieve such a Variant
// - this variant will store internaly a SBF compact binary format
// representation of the record content
// - uses internally a TSynTableData object
TSynTableVariantType = class(TSynInvokeableVariantType)
protected
function IntGet(var Dest: TVarData; const Instance: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override;
function IntSet(const Instance, Value: TVarData; Name: PAnsiChar; NameLen: PtrInt): boolean; override;
public
/// retrieve the SBF compact binary format representation of a record content
class function ToSBF(const V: Variant): TSBFString;
/// retrieve the ID value associated to a record content
class function ToID(const V: Variant): integer;
/// retrieve the TSynTable instance associated to a record content
class function ToTable(const V: Variant): TSynTable;
/// clear the content
procedure Clear(var V: TVarData); override;
/// copy two record content
procedure Copy(var Dest: TVarData; const Source: TVarData;
const Indirect: Boolean); override;
end;
2021-04-23 10:11:05 +02:00
/// initialize TSynTableVariantType if needed, and return the correspongind VType
function SynTableVariantVarType: cardinal;
{$endif NOVARIANTS}
const
/// used by TSynTableStatement.WhereField for "SELECT .. FROM TableName WHERE ID=?"
SYNTABLESTATEMENTWHEREID = 0;
/// low-level integer comparison according to a specified operator
// - SBF must point to the values encoded in our SBF compact binary format
// - Value must contain the plain integer value
// - Value can be a Currency accessed via a PInt64
// - will work only for tftBoolean, tftUInt8, tftUInt16, tftUInt24,
// tftInt32, tftInt64 and tftCurrency field types
// - will handle only soEqualTo...soGreaterThanOrEqualTo operators
// - if SBFEnd is not nil, it will test for all values until SBF>=SBFEnd
// (can be used for tftArray)
// - returns true if both values match, or false otherwise
function CompareOperator(FieldType: TSynTableFieldType; SBF, SBFEnd: PUTF8Char;
Value: Int64; Oper: TCompareOperator): boolean; overload;
/// low-level floating-point comparison according to a specified operator
// - SBF must point to the values encoded in our SBF compact binary format
// - Value must contain the plain floating-point value
// - will work only for tftDouble field type
// - will handle only soEqualTo...soGreaterThanOrEqualTo operators
// - if SBFEnd is not nil, it will test for all values until SBF>=SBFEnd
// (can be used for tftArray)
// - returns true if both values match, or false otherwise
function CompareOperator(SBF, SBFEnd: PUTF8Char;
Value: double; Oper: TCompareOperator): boolean; overload;
/// low-level text comparison according to a specified operator
// - SBF must point to the values encoded in our SBF compact binary format
// - Value must contain the plain text value, in the same encoding (either
// WinAnsi either UTF-8, as FieldType defined for the SBF value)
// - will work only for tftWinAnsi and tftUTF8 field types
// - will handle all kind of operators - including soBeginWith, soContains and
// soSoundsLike* - but soSoundsLike* won't make use of the CaseSensitive parameter
// - for soSoundsLikeEnglish, soSoundsLikeFrench and soSoundsLikeSpanish
// operators, Value is not a real PUTF8Char but a prepared PSynSoundEx
// - if SBFEnd is not nil, it will test for all values until SBF>=SBFEnd
// (can be used for tftArray)
// - returns true if both values match, or false otherwise
function CompareOperator(FieldType: TSynTableFieldType; SBF, SBFEnd: PUTF8Char;
Value: PUTF8Char; ValueLen: integer; Oper: TCompareOperator;
CaseSensitive: boolean): boolean; overload;
/// convert any AnsiString content into our SBF compact binary format storage
procedure ToSBFStr(const Value: RawByteString; out Result: TSBFString);
implementation
2021-04-23 10:11:05 +02:00
{$ifdef WITH_FASTMM4STATS}
uses
FastMM4; // override OS information by actual FastMM4 status
{$endif WITH_FASTMM4STATS}
{$ifdef FPCLINUX}
uses
termio,
{$ifdef BSD}
ctypes,
sysctl,
{$else}
Linux,
{$endif BSD}
SynFPCLinux;
{$endif FPCLINUX}
{ ************ TSynTable generic types and classes ************************** }
{$ifndef NOVARIANTS}
{ TSynTableVariantType }
var
SynTableVariantType: TCustomVariantType = nil;
2021-04-23 10:11:05 +02:00
function SynTableVariantVarType: cardinal;
begin
if SynTableVariantType=nil then
SynTableVariantType := SynRegisterCustomVariantType(TSynTableVariantType);
result := SynTableVariantType.VarType;
end;
procedure TSynTableVariantType.Clear(var V: TVarData);
begin
//Assert(V.VType=SynTableVariantType.VarType);
TSynTableData(V).VValue := ''; // clean memory release
PPtrUInt(@V)^ := 0; // will set V.VType := varEmpty
end;
procedure TSynTableVariantType.Copy(var Dest: TVarData;
const Source: TVarData; const Indirect: Boolean);
begin
//Assert(Source.VType=SynTableVariantType.VarType);
inherited Copy(Dest,Source,Indirect); // copy VType+VID+VTable
if not Indirect then
with TSynTableData(Dest) do begin
PtrInt(VValue) := 0; // avoid GPF
VValue := TSynTableData(Source).VValue; // copy by reference
end;
end;
function TSynTableVariantType.IntGet(var Dest: TVarData; const Instance: TVarData;
Name: PAnsiChar; NameLen: PtrInt): boolean;
begin
result:= TSynTableData(Instance).GetFieldVarData(pointer(Name),NameLen,Dest);
end;
function TSynTableVariantType.IntSet(const Instance, Value: TVarData;
Name: PAnsiChar; NameLen: PtrInt): boolean;
var aName: RawUTF8;
begin
FastSetString(aName,Name,NameLen);
2021-04-23 10:11:05 +02:00
TSynTableData(Instance).SetField(aName,Variant(Value));
result := true;
end;
class function TSynTableVariantType.ToID(const V: Variant): integer;
var Data: TSynTableData absolute V;
begin
if Data.VType<>SynTableVariantType.VarType then
result := 0 else
result := Data.VID;
end;
class function TSynTableVariantType.ToSBF(const V: Variant): TSBFString;
var Data: TSynTableData absolute V;
begin
if Data.VType<>SynTableVariantType.VarType then
result := '' else
result := Data.VValue;
end;
class function TSynTableVariantType.ToTable(const V: Variant): TSynTable;
var Data: TSynTableData absolute V;
begin
if Data.VType<>SynTableVariantType.VarType then
result := nil else
result := Data.VTable;
end;
{$endif NOVARIANTS}
{ TSynTable }
{$ifdef CPUX86}
2021-04-23 10:11:05 +02:00
function SortQWord(const A,B: QWord): integer; {$ifdef FPC} nostackframe; assembler; {$endif}
asm // Delphi x86 compiler is not efficient, and oldest even incorrect
mov ecx, [eax]
mov eax, [eax + 4]
cmp eax, [edx + 4]
jnz @nz
cmp ecx, [edx]
jz @0
@nz: jnb @p
or eax, -1
ret
@0: xor eax, eax
ret
@p: mov eax, 1
end;
2021-04-23 10:11:05 +02:00
function SortInt64(const A,B: Int64): integer; {$ifdef FPC} nostackframe; assembler; {$endif}
asm // Delphi x86 compiler is not efficient at compiling below code
mov ecx, [eax]
mov eax, [eax + 4]
cmp eax, [edx + 4]
jnz @nz
cmp ecx, [edx]
jz @0
jnb @p
@n: or eax, -1
ret
@0: xor eax, eax
ret
@nz: jl @n
@p: mov eax, 1
end;
{$endif}
{$ifndef SORTCOMPAREMETHOD}
function SortU8(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
result := PByte(P1)^-PByte(P2)^;
exit;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortU16(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
result := PWord(P1)^-PWord(P2)^;
exit;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortI32(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
result := PInteger(P1)^-PInteger(P2)^;
exit;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortDouble(P1,P2: PUTF8Char): PtrInt;
var V: Double;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
V := unaligned(PDouble(P1)^)-unaligned(PDouble(P2)^);
if V<0 then
result := -1 else
if V=0 then
result := 0 else
result := 1;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortU24(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
result := PtrInt(PWord(P1)^)+PtrInt(P1[2])shl 16
-PtrInt(PWord(P2)^)-PtrInt(P2[2]) shl 16;
exit;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarUInt32(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
result := FromVarUInt32(PByte(P1))-FromVarUInt32(PByte(P2));
exit;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarInt32(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
result := FromVarInt32(PByte(P1))-FromVarInt32(PByte(P2));
exit;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
{$ifdef CPU64} // PtrInt = Int64 -> so direct substraction works
function SortI64(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
result := PInt64(P1)^-PInt64(P2)^ else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarUInt64(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
result := FromVarUInt64(PByte(P1))-FromVarUInt64(PByte(P2)) else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarInt64(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
result := FromVarInt64(PByte(P1))-FromVarInt64(PByte(P2)) else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
{$else}
{$ifdef CPUX86} // circumvent comparison slowness (and QWord bug)
function SortI64(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
result := SortInt64(PInt64(P1)^,PInt64(P2)^) else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarUInt64(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
result := SortQWord(FromVarUInt64(PByte(P1)),FromVarUInt64(PByte(P2))) else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarInt64(P1,P2: PUTF8Char): PtrInt;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
result := SortInt64(FromVarInt64(PByte(P1)),FromVarInt64(PByte(P2))) else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
{$else}
function SortI64(P1,P2: PUTF8Char): PtrInt;
var V: Int64;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
V := PInt64(P1)^-PInt64(P2)^;
if V<0 then
result := -1 else
if V>0 then
result := 1 else
result := 0;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarUInt64(P1,P2: PUTF8Char): PtrInt;
var V1,V2: QWord;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
V1 := FromVarUInt64(PByte(P1));
V2 := FromVarUInt64(PByte(P2));
if V1>V2 then
result := 1 else
if V1=V2 then
result := 0 else
result := -1;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortVarInt64(P1,P2: PUTF8Char): PtrInt;
var V1,V2: Int64;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
V1 := FromVarInt64(PByte(P1));
V2 := FromVarInt64(PByte(P2));
if V1>V2 then
result := 1 else
if V1=V2 then
result := 0 else
result := -1;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
{$endif CPUX86}
{$endif CPU64}
function SortStr(P1,P2: PUTF8Char): PtrInt;
var L1, L2, L, i: PtrInt;
PB1, PB2: PByte;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
if PtrInt(P1^)<=$7F then begin
L1 := PtrInt(P1^);
inc(P1);
end else begin
PB1 := pointer(P1);
L1 := FromVarUInt32High(PB1);
P1 := pointer(PB1);
end;
if PtrInt(P2^)<=$7F then begin
L2 := PtrInt(P2^);
inc(P2);
end else begin
PB2 := pointer(P2);
L2 := FromVarUInt32High(PB2);
P2 := pointer(PB2);
end;
L := L1;
if L2>L then
L := L2;
for i := 0 to L-1 do begin
result := PtrInt(P1[i])-PtrInt(P2[i]);
if Result<>0 then
exit;
end;
result := L1-L2;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
function SortIStr(P1,P2: PUTF8Char): PtrInt;
var L1, L2, L, i: PtrInt;
PB1, PB2: PByte;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then begin
if PtrInt(P1^)<=$7F then begin
L1 := PtrInt(P1^);
inc(P1);
end else begin
PB1 := pointer(P1);
L1 := FromVarUInt32High(PB1);
P1 := pointer(PB1);
end;
if PtrInt(P2^)<=$7F then begin
L2 := PtrInt(P2^);
inc(P2);
end else begin
PB2 := pointer(P2);
L2 := FromVarUInt32High(PB2);
P2 := pointer(PB2);
end;
if L2>L1 then
L := L2 else
L := L1;
for i := 0 to L-1 do // NormToUpperAnsi7 works for both WinAnsi & UTF-8
if NormToUpperAnsi7[P1[i]]<>NormToUpperAnsi7[P2[i]] then begin
result := PtrInt(P1[i])-PtrInt(P2[i]);
exit;
end;
result := L1-L2;
end else
result := 1 else // P2=nil
result := -1 else // P1=nil
result := 0; // P1=P2
end;
const
FIELD_SORT: array[TSynTableFieldType] of TUTF8Compare = (
nil, // tftUnknown,
SortU8, SortU8, SortU16, SortU24, SortI32, SortI64,
// tftBoolean,tftUInt8,tftUInt16,tftUInt24,tftInt32,tftInt64,
SortI64, SortDouble, SortVarUInt32,SortVarInt32,SortVarUInt64,
// tftCurrency,tftDouble, tftVarUInt32, tftVarInt32,tftVarUInt64,
SortStr, SortStr, SortStr, nil, SortVarInt64);
// tftWinAnsi,tftUTF8, tftBlobInternal,tftBlobExternal,tftVarInt64);
{$endif SORTCOMPAREMETHOD}
const
FIELD_FIXEDSIZE: array[TSynTableFieldType] of Integer = (
0, // tftUnknown,
1, 1, 2, 3, 4, 8, 8, 8,
// tftBoolean, tftUInt8, tftUInt16, tftUInt24, tftInt32, tftInt64, tftCurrency, tftDouble
-1, -1, -1, // tftVarUInt32, tftVarInt32, tftVarUInt64 have -1 as size
-2, -2, -2, // tftWinAnsi, tftUTF8, tftBlobInternal have -2 as size
-3, // tftBlobExternal has -3 as size
-1); //tftVarInt64
// note: boolean is not in this set, because it can be 'true' or 'false'
FIELD_INTEGER: TSynTableFieldTypes = [
tftUInt8, tftUInt16, tftUInt24, tftInt32, tftInt64,
tftVarUInt32, tftVarInt32, tftVarUInt64, tftVarInt64];
function TSynTable.AddField(const aName: RawUTF8;
aType: TSynTableFieldType; aOptions: TSynTableFieldOptions): TSynTableFieldProperties;
var aSize: Integer;
begin
result := nil;
aSize := FIELD_FIXEDSIZE[aType];
if (self=nil) or (aSize=0) or IsRowID(pointer(aName)) or
not PropNameValid(pointer(aName)) or (GetFieldFromName(aName)<>nil) then
exit;
result := TSynTableFieldProperties.Create;
if fAddedField=nil then
fAddedField := TList.Create;
fAddedField.Add(result);
result.Name := aName;
result.FieldType := aType;
if tfoUnique in aOptions then
Include(aOptions,tfoIndex); // create an index for faster Unique field
if aSize=-3 then // external field has no index available
aOptions := aOptions-[tfoIndex,tfoUnique];
result.Options := aOptions;
if aSize>0 then begin
// fixed-size field should be inserted left-side of the stream
if (tfoIndex in aOptions) or (aSize and 3=0) then begin
// indexed field or size is alignment friendly: put left side
if not ((tfoIndex in aOptions) and (aSize and 3=0)) then
// indexed+aligned field -> set first, otherwise at variable or not indexed
while result.FieldNumber<fField.Count do
with TSynTableFieldProperties(fField.List[result.FieldNumber]) do
if (Offset<0) or not (tfoIndex in Options) then
break else
inc(result.FieldNumber);
end else
// not indexed field: insert after previous fixed-sized fields
if fFieldVariableIndex>=0 then
result.FieldNumber := fFieldVariableIndex else
result.FieldNumber := fField.Count;
fField.Insert(result.FieldNumber,result);
end else begin
if (tfoIndex in aOptions) and (fFieldVariableIndex>=0) then begin
// indexed field should be added left side (faster access for sort)
result.FieldNumber := fFieldVariableIndex;
while result.FieldNumber<fField.Count do
with TSynTableFieldProperties(fField.List[result.FieldNumber]) do
if not (tfoIndex in Options) then
break else
inc(result.FieldNumber);
fField.Insert(result.FieldNumber,result);
end else
// not indexed field: just add at the end of the field list
result.FieldNumber := fField.Add(result);
end;
if tfoUnique in aOptions then begin
fFieldHasUniqueIndexes := true;
result.AddFilterOrValidate(TSynValidateTableUniqueField.Create);
end;
AfterFieldModif; // set Offset,FieldNumber,FieldSize fFieldVariableIndex/Offset
end;
procedure TSynTable.UpdateFieldData(RecordBuffer: PUTF8Char; RecordBufferLen,
FieldIndex: integer; var result: TSBFString; const NewFieldData: TSBFString);
var NewSize, DestOffset, OldSize: integer;
F: TSynTableFieldProperties;
NewData, Dest: PAnsiChar;
begin
if (self<>nil) and ((RecordBuffer=nil) or (RecordBufferLen=0)) then begin
// no data yet -> use default
RecordBuffer := pointer(fDefaultRecordData);
RecordBufferLen := fDefaultRecordLength;
end;
if RecordBuffer=pointer(result) then
// update content code below will fail -> please correct calling code
raise ETableDataException.CreateUTF8('In-place call of %.UpdateFieldData',[self]);
if (self=nil) or (cardinal(FieldIndex)>=cardinal(fField.Count)) then begin
SetString(result,PAnsiChar(RecordBuffer),RecordBufferLen);
exit;
end;
F := TSynTableFieldProperties(fField.List[FieldIndex]);
NewSize := length(NewFieldData);
if NewSize=0 then begin
// no NewFieldData specified -> use default field data to be inserted
NewData := pointer(F.fDefaultFieldData);
NewSize := F.fDefaultFieldLength;
end else
NewData := pointer(NewFieldData);
Dest := GetData(RecordBuffer,F);
DestOffset := Dest-RecordBuffer;
// update content
OldSize := F.GetLength(Dest);
dec(RecordBufferLen,OldSize);
SetString(Result,nil,RecordBufferLen+NewSize);
MoveFast(RecordBuffer^,PByteArray(result)[0],DestOffset);
MoveFast(NewData^,PByteArray(result)[DestOffset],NewSize);
MoveFast(Dest[OldSize],PByteArray(result)[DestOffset+NewSize],RecordBufferLen-DestOffset);
end;
constructor TSynTable.Create(const aTableName: RawUTF8);
begin
if not PropNameValid(pointer(aTableName)) then
raise ETableDataException.CreateUTF8('Invalid %.Create(%)',[self,aTableName]);
fTableName := aTableName;
fField := TObjectList.Create;
fFieldVariableIndex := -1;
end;
procedure TSynTable.LoadFrom(var RD: TFileBufferReader);
var n, i: integer;
aTableName: RawUTF8;
begin
fField.Clear;
RD.Read(aTableName);
if not PropNameValid(pointer(aTableName)) then
RD.ErrorInvalidContent;
fTableName := aTableName;
n := RD.ReadVarUInt32;
if cardinal(n)>=MAX_SQLFIELDS then
RD.ErrorInvalidContent;
for i := 0 to n-1 do
fField.Add(TSynTableFieldProperties.CreateFrom(RD));
AfterFieldModif;
end;
destructor TSynTable.Destroy;
begin
fField.Free;
fAddedField.Free;
inherited;
end;
function TSynTable.GetFieldCount: integer;
begin
if self=nil then
result := 0 else
result := fField.Count;
end;
function TSynTable.GetFieldFromName(const aName: RawUTF8): TSynTableFieldProperties;
begin
result := GetFieldFromNameLen(pointer(aName),length(aName));
end;
function TSynTable.GetFieldFromNameLen(aName: PUTF8Char; aNameLen: integer): TSynTableFieldProperties;
var p: ^TSynTableFieldProperties;
i: integer;
begin
if self<>nil then begin
p := pointer(fField.List);
for i := 1 to fField.Count do
if IdemPropNameU(p^.Name,aName,aNameLen) then begin
result := p^;
exit;
end else
inc(p);
end;
result := nil;
end;
function TSynTable.GetFieldIndexFromName(const aName: RawUTF8): integer;
begin
result := GetFieldIndexFromNameLen(pointer(aName),length(aName));
end;
function TSynTable.GetFieldIndexFromNameLen(aName: PUTF8Char; aNameLen: integer): integer;
var p: ^TSynTableFieldProperties;
begin
if self<>nil then begin
p := pointer(fField.List);
for result := 0 to fField.Count-1 do
if IdemPropNameU(p^.Name,aName,aNameLen) then
exit else
inc(p);
end;
result := -1;
end;
function TSynTable.GetFieldType(Index: integer): TSynTableFieldProperties;
begin
if (self=nil) or (cardinal(Index)>=cardinal(fField.Count)) then
result := nil else // avoid GPF
result := fField.List[Index];
end;
{$ifndef DELPHI5OROLDER}
function TSynTable.CreateJSONWriter(JSON: TStream; Expand, withID: boolean;
const Fields: TSQLFieldBits): TJSONWriter;
begin
result := CreateJSONWriter(JSON,Expand,withID,FieldBitsToIndex(Fields,fField.Count));
end;
function TSynTable.CreateJSONWriter(JSON: TStream; Expand, withID: boolean;
const Fields: TSQLFieldIndexDynArray): TJSONWriter;
var i,nf,n: integer;
begin
if (self=nil) or ((Fields=nil) and not withID) then begin
result := nil; // no data to retrieve
exit;
end;
result := TJSONWriter.Create(JSON,Expand,withID,Fields);
// set col names
if withID then
n := 1 else
n := 0;
nf := length(Fields);
SetLength(result.ColNames,nf+n);
if withID then
result.ColNames[0] := 'ID';
for i := 0 to nf-1 do
result.ColNames[i+n] := TSynTableFieldProperties(fField.List[Fields[i]]).Name;
result.AddColumns; // write or init field names for appropriate JSON Expand
end;
procedure TSynTable.GetJSONValues(aID: integer; RecordBuffer: PUTF8Char;
W: TJSONWriter);
var i,n: integer;
buf: array[0..MAX_SQLFIELDS-1] of PUTF8Char;
begin
if (self=nil) or (RecordBuffer=nil) or (W=nil) then
exit; // avoid GPF
if W.Expand then begin
W.Add('{');
if W.WithID then
W.AddString(W.ColNames[0]);
end;
if W.WithID then begin
W.Add(aID);
W.Add(',');
n := 1;
end else
n := 0;
for i := 0 to fField.Count-1 do begin
buf[i] := RecordBuffer;
inc(RecordBuffer,TSynTableFieldProperties(fField.List[i]).GetLength(RecordBuffer));
end;
for i := 0 to length(W.Fields)-1 do begin
if W.Expand then begin
W.AddString(W.ColNames[n]); // '"'+ColNames[]+'":'
inc(n);
end;
TSynTableFieldProperties(fField.List[W.Fields[i]]).GetJSON(buf[i],W);
W.Add(',');
end;
W.CancelLastComma; // cancel last ','
if W.Expand then
W.Add('}');
end;
function TSynTable.IterateJSONValues(Sender: TObject; Opaque: pointer;
ID: integer; Data: pointer; DataLen: integer): boolean;
var Statement: TSynTableStatement absolute Opaque;
F: TSynTableFieldProperties;
nWhere,fIndex: cardinal;
begin // note: we should have handled -2 (=COUNT) case already
nWhere := length(Statement.Where);
if (self=nil) or (Statement=nil) or (Data=nil) or
(Statement.Select=nil) or (nWhere>1) or
((nWhere=1)and(Statement.Where[0].ValueSBF='')) then begin
result := false;
exit;
end;
result := true;
if nWhere=1 then begin // Where=nil -> all rows
fIndex := Statement.Where[0].Field;
if fIndex=SYNTABLESTATEMENTWHEREID then begin
if ID<>Statement.Where[0].ValueInteger then
exit;
end else begin
dec(fIndex); // 0 is ID, 1 for field # 0, 2 for field #1, and so on...
if fIndex<cardinal(fField.Count) then begin
F := TSynTableFieldProperties(fField.List[fIndex]);
if F.SortCompare(GetData(Data,F),pointer(Statement.Where[0].ValueSBF))<>0 then
exit;
end;
end;
end;
GetJSONValues(ID,Data,Statement.Writer);
end;
{$endif DELPHI5OROLDER}
function TSynTable.GetData(RecordBuffer: PUTF8Char; Field: TSynTableFieldProperties): pointer;
var i: integer;
PB: PByte;
begin
if Field.Offset>=0 then
result := RecordBuffer+Field.Offset else begin
result := RecordBuffer+fFieldVariableOffset;
for i := fFieldVariableIndex to Field.FieldNumber-1 do
if i in fFieldIsVarString then begin
// inlined result := GotoNextVarString(result);
if PByte(result)^<=$7f then
inc(PByte(result),PByte(result)^+1) else begin
PB := result;
inc(PByte(result),FromVarUInt32High(PB)+PtrUInt(PB)-PtrUInt(result));
end;
end else
if not (i in fFieldIsExternal) then begin
// inlined result := GotoNextVarInt(result)
while PByte(result)^>$7f do inc(PByte(result));
inc(PByte(result));
end;
end;
end;
procedure TSynTable.SaveTo(WR: TFileBufferWriter);
var i: Integer;
begin
WR.Write(fTableName);
WR.WriteVarUInt32(fField.Count);
for i := 0 to fField.Count-1 do
TSynTableFieldProperties(fField.List[i]).SaveTo(WR);
end;
procedure TSynTable.AfterFieldModif;
var i, Offs: integer;
begin
PInt64(@fFieldIsVarString)^ := 0;
PInt64(@fFieldIsExternal)^ := 0;
fFieldVariableIndex := -1;
fDefaultRecordLength := 0;
fFieldHasUniqueIndexes := false;
Offs := 0;
for i := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[i]) do begin
FieldNumber := i;
{$ifndef SORTCOMPAREMETHOD}
SortCompare := FIELD_SORT[FieldType];
{$endif}
Owner := self;
FieldSize := FIELD_FIXEDSIZE[FieldType];
if FieldSize>=0 then begin
//assert(Offs>=0);
Offset := Offs;
inc(Offs,FieldSize);
inc(fDefaultRecordLength,FieldSize);
fDefaultFieldLength := FieldSize;
end else begin
if FieldSize=-3 then
Include(fFieldIsExternal,i) else begin
fDefaultFieldLength := 1;
inc(fDefaultRecordLength);
if FieldSize=-2 then
Include(fFieldIsVarString,i);
{$ifndef SORTCOMPAREMETHOD}
if (FieldType in [tftWinAnsi,tftUTF8]) and
(tfoCaseInsensitive in Options) then
SortCompare := SortIStr; // works for both WinAnsi and UTF-8 encodings
{$endif}
end;
// we need the Offset even for tftBlobExternal (FieldSize=-3)
if fFieldVariableIndex<0 then begin
fFieldVariableIndex := i;
fFieldVariableOffset := Offs;
Offs := -1;
end;
Offset := Offs;
dec(Offs);
end;
SetLength(fDefaultFieldData,fDefaultFieldLength);
FillcharFast(pointer(fDefaultFieldData)^,fDefaultFieldLength,0);
end;
SetLength(fDefaultRecordData,fDefaultRecordLength);
FillcharFast(pointer(fDefaultRecordData)^,fDefaultRecordLength,0);
end;
procedure TSynTable.FieldIndexModify(aOldIndex, aNewIndex: integer;
aOldRecordData, aNewRecordData: pointer);
var F: integer;
begin
for F := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[F]) do
if tfoIndex in Options then
OrderedIndexUpdate(aOldIndex,aNewIndex,aOldRecordData,aNewRecordData);
end;
procedure TSynTable.Filter(var RecordBuffer: TSBFString);
var Old, New: RawUTF8;
NewRecord: TSBFString; // UpdateFieldData update result in-place
F, i: integer;
begin
for F := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[F]) do
if Filters<>nil then begin
Old := GetRawUTF8(pointer(RecordBuffer));
New := Old;
for i := 0 to Filters.Count-1 do
TSynFilter(Filters.List[i]).Process(F,New);
if Old<>New then begin
// value was changed -> store modified
UpdateFieldData(pointer(RecordBuffer),length(RecordBuffer),F,
NewRecord,SBFFromRawUTF8(New));
RecordBuffer := NewRecord;
end;
end;
end;
{$ifndef NOVARIANTS}
function TSynTable.Data(aID: integer; RecordBuffer: pointer; RecordBufferLen: Integer): Variant;
var data: TSynTableData absolute result;
begin
2021-04-23 10:11:05 +02:00
VarClear(result);
data.VType := SynTableVariantVarType;
data.VID := aID;
data.VTable := self;
pointer(data.VValue) := nil; // avoid GPF
if RecordBuffer=nil then
data.VValue := DefaultRecordData else begin
if RecordBufferLen=0 then
RecordBufferLen := DataLength(RecordBuffer);
SetString(data.VValue,PAnsiChar(RecordBuffer),RecordBufferLen);
end;
end;
{$endif NOVARIANTS}
function TSynTable.DataLength(RecordBuffer: pointer): integer;
var F: Integer;
PC: PUTF8Char;
begin
if (Self<>nil) and (RecordBuffer<>nil) then begin
PC := RecordBuffer;
for F := 0 to fField.Count-1 do
inc(PC,TSynTableFieldProperties(fField.List[F]).GetLength(PC));
result := PC-RecordBuffer;
end else
result := 0;
end;
function TSynTable.UpdateFieldEvent(Sender: TObject; Opaque: pointer;
ID, Index: integer; Data: pointer; DataLen: integer): boolean;
var Added: PUpdateFieldEvent absolute Opaque;
F, aSize: integer;
begin // in practice, this data processing is very fast (thanks to WR speed)
with Added^ do begin
result := Count<length(IDs);
if not result then
exit;
for F := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[F]) do
if F in AvailableFields then begin
// add previous field content: will handle any field offset change in record
aSize := Getlength(Data);
WR.Write(Data,aSize);
inc(PByte(Data),aSize);
end else
// add default field content for a newly added field
WR.Write(Pointer(fDefaultFieldData),fDefaultFieldLength);
if WR.TotalWritten>1 shl 30 then
raise ETableDataException.CreateUTF8('%: File size too big (>1GB)',[self]) else
Offsets64[Count] := WR.TotalWritten;
IDs[Count] := ID;
NewIndexs[Index] := Count;
inc(Count);
end;
end;
function TSynTable.UpdateFieldRecord(RecordBuffer: PUTF8Char;
var AvailableFields: TSQLFieldBits): TSBFString;
var Lens: array[0..MAX_SQLFIELDS-1] of Integer;
F, Len, TotalLen: integer;
P: PUTF8Char;
Dest: PByte;
begin
// retrieve all field buffer lengths, to speed up record content creation
TotalLen := 0;
P := RecordBuffer;
for F := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[F]) do
if F in AvailableFields then begin
Len := GetLength(P);
inc(P,Len);
inc(TotalLen,Len);
Lens[F] := Len;
end else
inc(TotalLen,fDefaultFieldLength);
// create new record content
P := RecordBuffer;
SetString(Result,nil,TotalLen);
Dest := pointer(Result);
for F := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[F]) do
if F in AvailableFields then begin
Len := Lens[F];
MoveFast(P^,Dest^,Len);
inc(P,Len);
inc(Dest,Len);
end else begin
FillcharFast(Dest^,fDefaultFieldLength,0);
inc(Dest,fDefaultFieldLength);
end;
//Assert(PtrUInt(Dest)-PtrUInt(result)=PtrUInt(TotalLen));
end;
function TSynTable.Validate(RecordBuffer: pointer; RecordIndex: integer): string;
var F: integer;
begin
result := '';
for F := 0 to fField.Count-1 do
with TSynTableFieldProperties(fField.List[F]) do
if Validates<>nil then begin
result := Validate(RecordBuffer,RecordIndex);
if result<>'' then
exit;
end;
end;
{ TSynTableFieldProperties }
constructor TSynTableFieldProperties.CreateFrom(var RD: TFileBufferReader);
begin
fOrderedIndexFindAdd := -1;
RD.Read(Name);
if not PropNameValid(pointer(Name)) then
RD.ErrorInvalidContent;
RD.Read(@FieldType,SizeOf(FieldType));
RD.Read(@Options,SizeOf(Options));
if (FieldType>high(FieldType)) then
RD.ErrorInvalidContent;
OrderedIndexCount := RD.ReadVarUInt32Array(OrderedIndex);
if OrderedIndexCount>0 then begin
if tfoIndex in Options then begin
//assert(OrderedIndexReverse=nil);
OrderedIndexReverseSet(-1); // compute whole OrderedIndexReverse[] array
end else
RD.ErrorInvalidContent;
end;
// we allow a void OrderedIndex[] array from disk
end;
destructor TSynTableFieldProperties.Destroy;
begin
Filters.Free;
Validates.Free;
inherited;
end;
function TSynTableFieldProperties.GetJSON(FieldBuffer: pointer;
W: TTextWriter): pointer;
var len: integer;
tmp: RawUTF8;
begin
case FieldType of
// fixed-sized field value
tftBoolean:
W.Add(PBoolean(FieldBuffer)^);
tftUInt8:
W.Add(PByte(FieldBuffer)^);
tftUInt16:
W.Add(PWord(FieldBuffer)^);
tftUInt24:
// PInteger()^ and $ffffff -> possible GPF on Memory Mapped file
W.Add(PWord(FieldBuffer)^+integer(PByteArray(FieldBuffer)^[2])shl 16);
tftInt32:
W.Add(PInteger(FieldBuffer)^);
tftInt64:
W.Add(PInt64(FieldBuffer)^);
tftCurrency:
W.AddCurr64(PInt64(FieldBuffer)^);
tftDouble:
W.AddDouble(unaligned(PDouble(FieldBuffer)^));
// some variable-size field value
tftVarUInt32:
W.Add(FromVarUInt32(PByte(FieldBuffer)));
tftVarInt32:
W.Add(FromVarInt32(PByte(FieldBuffer)));
tftVarUInt64:
W.AddQ(FromVarUInt64(PByte(FieldBuffer)));
tftVarInt64:
W.Add(FromVarInt64(PByte(FieldBuffer)));
// text storage - WinAnsi could use less space than UTF-8
tftWinAnsi, tftUTF8: begin
W.Add('"');
len := FromVarUInt32(PByte(FieldBuffer));
if len>0 then
if FieldType=tftUTF8 then
W.AddJSONEscape(PAnsiChar(FieldBuffer),len) else begin
SetLength(tmp,len*3); // in-place decoding and appending
W.AddJSONEscape(pointer(tmp),WinAnsiBufferToUtf8(pointer(tmp),PAnsiChar(FieldBuffer),len)-pointer(tmp));
end;
W.Add('"');
result := PAnsiChar(FieldBuffer)+len;
exit;
end;
tftBlobInternal: begin
W.AddShort('"X''');
len := FromVarUInt32(PByte(FieldBuffer));
W.AddBinToHex(PByte(FieldBuffer),len);
W.Add('''','"');
end;
tftBlobExternal:
; // BLOB fields are not handled here, but must be directly accessed
end;
result := PAnsiChar(FieldBuffer)+FieldSize; // // tftWinAnsi,tftUTF8 already done
end;
function TSynTableFieldProperties.GetLength(FieldBuffer: pointer): Integer;
var PB: PByte;
begin
if FieldSize>=0 then
result := FieldSize else
case FieldSize of
-1: begin // variable-length data
result := 0;
while PByteArray(FieldBuffer)^[result]>$7f do inc(result);
inc(result);
end;
-2: begin // tftWinAnsi, tftUTF8, tftBlobInternal records
result := PByte(FieldBuffer)^;
if result<=$7F then
inc(Result) else begin
PB := FieldBuffer;
result := FromVarUInt32High(PB)+PtrUInt(PB)-PtrUInt(FieldBuffer);
end;
end;
else
result := 0; // tftBlobExternal is not stored in FieldBuffer
end;
end;
{$ifndef NOVARIANTS}
function TSynTableFieldProperties.GetVariant(FieldBuffer: pointer): Variant;
begin
GetVariant(FieldBuffer,result);
end;
procedure TSynTableFieldProperties.GetVariant(FieldBuffer: pointer; var result: Variant);
var len: integer;
PB: PByte absolute FieldBuffer;
PA: PAnsiChar absolute FieldBuffer;
PU: PUTF8Char absolute FieldBuffer;
tmp: RawByteString;
2021-04-23 10:11:05 +02:00
{$ifndef HASVARUSTRING}
WS: SynUnicode;
{$endif}
begin
case FieldType of
// fixed-sized field value
tftBoolean:
result := PBoolean(FieldBuffer)^;
tftUInt8:
result := PB^;
tftUInt16:
result := PWord(FieldBuffer)^;
tftUInt24:
// PInteger()^ and $ffffff -> possible GPF on Memory Mapped file
result := PWord(FieldBuffer)^+integer(PByteArray(FieldBuffer)^[2])shl 16;
tftInt32:
result := PInteger(FieldBuffer)^;
tftInt64:
result := PInt64(FieldBuffer)^;
tftCurrency:
result := PCurrency(FieldBuffer)^;
tftDouble:
result := unaligned(PDouble(FieldBuffer)^);
// some variable-size field value
tftVarUInt32:
result := FromVarUInt32(PB);
tftVarInt32:
result := FromVarInt32(PB);
tftVarUInt64:
result := FromVarUInt64(PB);
tftVarInt64:
result := FromVarInt64(PB);
// text storage - WinAnsi could use less space than UTF-8
tftWinAnsi: begin
len := FromVarUInt32(PB);
if len>0 then
2021-04-23 10:11:05 +02:00
{$ifdef HASVARUSTRING}
result := WinAnsiToUnicodeString(PA,len)
{$else}
result := CurrentAnsiConvert.AnsiToAnsi(WinAnsiConvert,PA,len)
{$endif} else
result := '';
end;
tftUTF8: begin
len := FromVarUInt32(PB);
if len>0 then
2021-04-23 10:11:05 +02:00
{$ifdef HASVARUSTRING}
result := UTF8DecodeToUnicodeString(PU,len)
{$else} begin
UTF8ToSynUnicode(PU,len,WS);
result := WS;
end
{$endif} else
result := '';
end;
tftBlobInternal: begin
len := FromVarUInt32(PB);
SetString(tmp,PA,len);
result := tmp; // return internal BLOB content as string
end
else
result := ''; // tftBlobExternal fields e.g. must be directly accessed
end;
end;
{$endif}
{$ifdef ISDELPHI20062007}
{$WARNINGS OFF} // circument Delphi 2007 false positive warning
{$endif}
function TSynTableFieldProperties.GetValue(FieldBuffer: pointer): RawUTF8;
var len: integer;
PB: PByte absolute FieldBuffer;
PC: PAnsiChar absolute FieldBuffer;
begin
result := '';
case FieldType of
// fixed-sized field value
tftBoolean:
result := BOOL_UTF8[PBoolean(FieldBuffer)^];
tftUInt8:
UInt32ToUtf8(PB^,result);
tftUInt16:
UInt32ToUtf8(PWord(FieldBuffer)^,result);
tftUInt24:
// PInteger()^ and $ffffff -> possible GPF on Memory Mapped file
UInt32ToUtf8(PWord(FieldBuffer)^+integer(PByteArray(FieldBuffer)^[2])shl 16,result);
tftInt32:
Int32ToUtf8(PInteger(FieldBuffer)^,result);
tftInt64:
Int64ToUtf8(PInt64(FieldBuffer)^,result);
tftCurrency:
Curr64ToStr(PInt64(FieldBuffer)^,result);
tftDouble:
2021-04-23 10:11:05 +02:00
DoubleToStr(unaligned(PDouble(FieldBuffer)^),result);
// some variable-size field value
tftVarUInt32:
UInt32ToUtf8(FromVarUInt32(PB),result);
tftVarInt32:
Int32ToUtf8(FromVarInt32(PB),result);
tftVarUInt64:
UInt64ToUtf8(FromVarUInt64(PB),result);
tftVarInt64:
Int64ToUtf8(FromVarInt64(PB),result);
// text storage - WinAnsi could use less space than UTF-8
tftWinAnsi, tftUTF8, tftBlobInternal: begin
len := FromVarUInt32(PB);
if len>0 then
if FieldType<>tftWinAnsi then
SetString(result,PC,len) else
result := WinAnsiConvert.AnsiBufferToRawUTF8(PC,len);
end;
// tftBlobExternal fields e.g. must be directly accessed
end;
end;
{$ifdef ISDELPHI20062007}
{$WARNINGS ON} // circument Delphi 2007 false positive warning
{$endif}
procedure TSynTableFieldProperties.OrderedIndexReverseSet(aOrderedIndex: integer);
var nrev, ndx, n: PtrInt;
begin
n := length(OrderedIndex);
nrev := length(OrderedIndexReverse);
if nrev=0 then
if n=0 then
exit else begin
// void OrderedIndexReverse[]
nrev := MaxInteger(OrderedIndex,OrderedIndexCount,n)+1;
SetLength(OrderedIndexReverse,nrev);
FillcharFast(OrderedIndexReverse[0],nrev*4,255); // all to -1
Reverse(OrderedIndex,OrderedIndexCount,pointer(OrderedIndexReverse));
end;
if PtrUInt(aOrderedIndex)>=PtrUInt(OrderedIndexCount) then
exit; // e.g. CreateFrom() will call OrderedIndexReverseSet(-1)
if nrev<n then begin
SetLength(OrderedIndexReverse,n); // resize if needed
nrev := n;
end;
ndx := OrderedIndex[aOrderedIndex];
if ndx>=nrev then
SetLength(OrderedIndexReverse,ndx+256) else
OrderedIndexReverse[ndx] := aOrderedIndex;
end;
procedure TSynTableFieldProperties.OrderedIndexSort(L, R: PtrInt);
var I, J, P: PtrInt;
TmpI, TmpJ: integer;
begin
if (L<R) and Assigned(Owner.GetRecordData) then
repeat
I := L; J := R;
P := (L + R) shr 1;
repeat
with Owner do begin
SortPivot := GetData(GetRecordData(OrderedIndex[P],DataTemp1),self);
while SortCompare(GetData(GetRecordData(OrderedIndex[I],DataTemp2),self),
SortPivot)<0 do inc(I);
while SortCompare(GetData(GetRecordData(OrderedIndex[J],DataTemp2),self),
SortPivot)>0 do dec(J);
end;
if I <= J then begin
if I < J then begin
TmpJ := OrderedIndex[J];
TmpI := OrderedIndex[I];
OrderedIndex[J] := TmpI;
OrderedIndex[I] := TmpJ;
// keep OrderedIndexReverse[OrderedIndex[i]]=i
OrderedIndexReverse[TmpJ] := I;
OrderedIndexReverse[TmpI] := J;
end;
if P = I then P := J else if P = J then P := I;
inc(I); dec(J);
end;
until I > J;
if J - L < R - I then begin // use recursion only for smaller range
if L < J then
OrderedIndexSort(L, J);
L := I;
end else begin
if I < R then
OrderedIndexSort(I, R);
R := J;
end;
until L >= R;
end;
procedure TSynTableFieldProperties.OrderedIndexRefresh;
begin
if (self=nil) or not OrderedIndexNotSorted then
exit; // already sorted
OrderedIndexSort(0,OrderedIndexCount-1);
OrderedIndexNotSorted := false;
end;
function TSynTableFieldProperties.OrderedIndexFind(Value: pointer): PtrInt;
var L,R: PtrInt;
cmp: PtrInt;
begin
if OrderedIndexNotSorted then
OrderedIndexRefresh;
L := 0;
R := OrderedIndexCount-1;
with Owner do
if (R>=0) and Assigned(GetRecordData) then
repeat
result := (L + R) shr 1;
cmp := SortCompare(GetData(GetRecordData(OrderedIndex[result],DataTemp1),self),Value);
if cmp=0 then
exit;
if cmp<0 then
L := result + 1 else
R := result - 1;
until (L > R);
result := -1
end;
function TSynTableFieldProperties.OrderedIndexFindAdd(Value: pointer): PtrInt;
var L,R,i: PtrInt;
cmp: PtrInt;
begin
if OrderedIndexNotSorted then
OrderedIndexRefresh;
R := OrderedIndexCount-1;
if R<0 then
result := 0 else
with Owner do begin
fOrderedIndexFindAdd := -1;
L := 0;
result := -1; // return -1 if found
repeat
i := (L + R) shr 1;
cmp := SortCompare(GetData(GetRecordData(OrderedIndex[i],DataTemp1),self),Value);
if cmp=0 then
exit;
if cmp<0 then
L := i + 1 else
R := i - 1;
until (L > R);
while (i>=0) and
(SortCompare(GetData(GetRecordData(OrderedIndex[i],DataTemp1),self),Value)>=0) do
dec(i);
result := i+1; // return the index where to insert
end;
fOrderedIndexFindAdd := result; // store inserting index for OrderedIndexUpdate
end;
function TSynTableFieldProperties.OrderedIndexMatch(WhereSBFValue: pointer;
var MatchIndex: TIntegerDynArray; var MatchIndexCount: integer; Limit: Integer=0): Boolean;
var i, L,R: PtrInt;
begin
result := false;
if (self=nil) or (WhereSBFValue=nil) or not Assigned(Owner.GetRecordData) or
(OrderedIndex=nil) or not (tfoIndex in Options) then
exit;
i := OrderedIndexFind(WhereSBFValue);
if i<0 then
exit; // WHERE value not found
if (tfoUnique in Options) or (Limit=1) then begin
// unique index: direct fastest O(log(n)) binary search
AddSortedInteger(MatchIndex,MatchIndexCount,OrderedIndex[i]);
// AddSortedInteger() will fail if OrderedIndex[i] already exists
end else
with Owner do begin
// multiple index matches possible: add matching range
L := i;
repeat
dec(L);
until (L<0) or (SortCompare(GetData(GetRecordData(
OrderedIndex[L],DataTemp1),self),WhereSBFValue)<>0);
R := i;
repeat
inc(R);
until (R>=OrderedIndexCount) or
(SortCompare(GetData(GetRecordData(OrderedIndex[R],DataTemp1),self),WhereSBFValue)<>0);
if Limit=0 then
Limit := MaxInt; // no LIMIT set -> retrieve all rows
for i := L+1 to R-1 do begin
AddSortedInteger(MatchIndex,MatchIndexCount,OrderedIndex[i]);
dec(Limit);
if Limit=0 then
Break; // reach LIMIT upperbound result count
end;
end;
result := true;
end;
function TSynTableFieldProperties.OrderedIndexUpdate(aOldIndex, aNewIndex: integer;
aOldRecordData, aNewRecordData: pointer): boolean;
var aOldIndexIndex: integer;
begin
result := false;
if (self=nil) or not Assigned(Owner.GetRecordData) then
exit; // avoid GPF
// update content
if aOldIndex<0 then
if aNewIndex<0 then begin
// both indexes equal -1 -> force sort
OrderedIndexSort(0,OrderedIndexCount-1);
OrderedIndexNotSorted := false;
end else begin
// added record
if tfoUnique in Options then begin
if fOrderedIndexFindAdd<0 then
raise ETableDataException.CreateUTF8(
'%.CheckConstraint call needed before %.OrderedIndexUpdate',[self,Name]);
OrderedIndexReverseSet(InsertInteger(OrderedIndex,OrderedIndexCount,
aNewIndex,fOrderedIndexFindAdd));
end else begin
AddInteger(OrderedIndex,OrderedIndexCount,aNewIndex);
OrderedIndexReverseSet(OrderedIndexCount-1);
OrderedIndexNotSorted := true; // -> OrderedIndexSort() call on purpose
end;
end else begin
// aOldIndex>=0: update a value
// retrieve position in OrderedIndex[] to be deleted/updated
if OrderedIndexReverse=nil then
OrderedIndexReverseSet(0) else // do OrderedIndexReverse[OrderedIndex[i]] := i
{assert(aOldIndex<length(OrderedIndexReverse))};
//assert(IntegerScanIndex(Pointer(OrderedIndex),OrderedIndexCount,aOldIndex)=OrderedIndexReverse[aOldIndex]);
aOldIndexIndex := OrderedIndexReverse[aOldIndex]; // use FAST reverse array
if aOldIndexIndex<0 then
exit; // invalid Old index
if aNewIndex<0 then begin
// deleted record
DeleteInteger(OrderedIndex,OrderedIndexCount,aOldIndexIndex);
Reverse(OrderedIndex,OrderedIndexCount,pointer(OrderedIndexReverse));
// no need to refresh OrderedIndex[], since data will remain sorted
end else begin
// updated record
OrderedIndex[aOldIndexIndex] := aNewIndex;
OrderedIndexReverseSet(aOldIndexIndex);
if (aOldRecordData<>nil) or (aOldIndex<>aNewIndex) then // not in-place update
with Owner do begin
if aOldRecordData=nil then
aOldRecordData := GetRecordData(aOldIndex,DataTemp1);
if aNewRecordData=nil then
aNewRecordData := GetRecordData(aNewIndex,DataTemp2);
if SortCompare(GetData(aOldRecordData,self),GetData(aNewRecordData,self))=0 then begin
// only sort if field content was modified -> MUCH faster in most case
result := true;
exit;
end;
end;
if tfoUnique in Options then begin
if fOrderedIndexFindAdd>=0 then begin
// we know which OrderedIndex[] has to be changed -> manual update
// - this is still a bottleneck in the current implementation, but
// I was not able to find out how to make it faster, and still
// being able to check unique field constraints without changing the
// OrderedIndex[] content from a simple list into e.g. a red-black
// tree: such a structure performs better, but uses much more memory
// and is to be implemented
// - it's still fast, faster than any DB AFAIK, around 500 updates
// per second with 1,000,000 records on a Core i7
// - it's still faster to refresh OrderedIndex[] than iterating
// through all items to validate the unique constraint
DeleteInteger(OrderedIndex,OrderedIndexCount,aOldIndexIndex);
if fOrderedIndexFindAdd>aOldIndexIndex then
dec(fOrderedIndexFindAdd);
InsertInteger(OrderedIndex,OrderedIndexCount,aNewIndex,fOrderedIndexFindAdd);
Reverse(OrderedIndex,OrderedIndexCount,pointer(OrderedIndexReverse));
end else
// slow full sort - with 1,000,000 items it's about 100 times slower
// (never called with common usage in SynBigTable unit)
OrderedIndexSort(0,OrderedIndexCount-1);
end else
OrderedIndexNotSorted := true; // will call OrderedIndexSort() on purpose
end;
end;
fOrderedIndexFindAdd := -1; // consume this value
result := true;
end;
procedure TSynTableFieldProperties.SaveTo(WR: TFileBufferWriter);
begin
WR.Write(Name);
WR.Write(@FieldType,SizeOf(FieldType));
WR.Write(@Options,SizeOf(Options));
WR.WriteVarUInt32Array(OrderedIndex,OrderedIndexCount,wkVarUInt32);
end;
function TSynTableFieldProperties.SBF(const Value: Int64): TSBFString;
var tmp: array[0..15] of AnsiChar;
begin
case FieldType of
tftInt32: begin // special version for handling negative values
PInteger(@tmp)^ := Value;
SetString(Result,tmp,SizeOf(Integer));
end;
tftUInt8, tftUInt16, tftUInt24, tftInt64:
SetString(Result,PAnsiChar(@Value),FieldSize);
tftVarUInt32:
SetString(Result,tmp,PAnsiChar(ToVarUInt32(Value,@tmp))-tmp);
tftVarInt32:
SetString(Result,tmp,PAnsiChar(ToVarInt32(Value,@tmp))-tmp);
tftVarUInt64:
SetString(Result,tmp,PAnsiChar(ToVarUInt64(Value,@tmp))-tmp);
tftVarInt64:
SetString(Result,tmp,PAnsiChar(ToVarInt64(Value,@tmp))-tmp);
else
result := '';
end;
end;
function TSynTableFieldProperties.SBF(const Value: Integer): TSBFString;
var tmp: array[0..15] of AnsiChar;
begin
case FieldType of
tftUInt8, tftUInt16, tftUInt24, tftInt32:
SetString(Result,PAnsiChar(@Value),FieldSize);
tftInt64: begin // special version for handling negative values
PInt64(@tmp)^ := Value;
SetString(Result,tmp,SizeOf(Int64));
end;
tftVarUInt32:
if Value<0 then // expect an unsigned integer
result := '' else
SetString(Result,tmp,PAnsiChar(ToVarUInt32(Value,@tmp))-tmp);
tftVarInt32:
SetString(Result,tmp,PAnsiChar(ToVarInt32(Value,@tmp))-tmp);
tftVarUInt64:
if cardinal(Value)>cardinal(maxInt) then
result := '' else // expect a 32 bit integer
SetString(Result,tmp,PAnsiChar(ToVarUInt64(Value,@tmp))-tmp);
tftVarInt64:
SetString(Result,tmp,PAnsiChar(ToVarInt64(Value,@tmp))-tmp);
else
result := '';
end;
end;
const
SBF_BOOL: array[boolean] of TSBFString =
(#0,#1);
{$ifndef NOVARIANTS}
function TSynTableFieldProperties.SBF(const Value: Variant): TSBFString;
var V64: Int64;
VC: Currency absolute V64;
VD: Double absolute V64;
begin // VarIsOrdinal/VarIsFloat/VarIsStr are buggy -> use field type
case FieldType of
tftBoolean:
result := SBF_BOOL[boolean(Value)];
tftUInt8, tftUInt16, tftUInt24, tftInt32, tftInt64,
tftVarUInt32, tftVarInt32, tftVarUInt64, tftVarInt64: begin
if not VariantToInt64(Value,V64) then
V64 := 0;
result := SBF(V64);
end;
tftCurrency: begin
VC := Value;
SetString(result,PAnsiChar(@VC),SizeOf(VC));
end;
tftDouble: begin
VD := Value;
SetString(result,PAnsiChar(@VD),SizeOf(VD));
end;
tftWinAnsi:
ToSBFStr(WinAnsiConvert.UTF8ToAnsi(VariantToUTF8(Value)),result);
tftUTF8:
ToSBFStr(VariantToUTF8(Value),result);
else
result := '';
end;
if result='' then
result := SBFDefault;
end;
{$endif}
function TSynTableFieldProperties.SBF(const Value: Boolean): TSBFString;
begin
if FieldType<>tftBoolean then
result := '' else
result := SBF_BOOL[Value];
end;
function TSynTableFieldProperties.SBFCurr(const Value: Currency): TSBFString;
begin
if FieldType<>tftCurrency then
result := '' else
SetString(Result,PAnsiChar(@Value),SizeOf(Value));
end;
procedure ToSBFStr(const Value: RawByteString; out Result: TSBFString);
var tmp: array[0..15] of AnsiChar;
Len, Head: integer;
begin
if PtrUInt(Value)=0 then
Result := #0 else begin
Len := {$ifdef FPC}length(Value){$else}PInteger(PtrUInt(Value)-SizeOf(integer))^{$endif};
Head := PAnsiChar(ToVarUInt32(Len,@tmp))-tmp;
SetLength(Result,Len+Head);
MoveFast(tmp,PByteArray(Result)[0],Head);
MoveFast(pointer(Value)^,PByteArray(Result)[Head],Len);
end;
end;
function TSynTableFieldProperties.SBF(const Value: RawUTF8): TSBFString;
begin
case FieldType of
tftUTF8:
ToSBFStr(Value,Result);
tftWinAnsi:
ToSBFStr(Utf8ToWinAnsi(Value),Result);
else
result := '';
end;
end;
function TSynTableFieldProperties.SBF(Value: pointer; ValueLen: integer): TSBFString;
var tmp: array[0..15] of AnsiChar;
Head: integer;
begin
if FieldType<>tftBlobInternal then
result := '' else
if (Value=nil) or (ValueLen=0) then
result := #0 else begin // inlined ToSBFStr() code
Head := PAnsiChar(ToVarUInt32(ValueLen,@tmp))-tmp;
SetString(Result,nil,ValueLen+Head);
MoveFast(tmp,PByteArray(Result)[0],Head);
MoveFast(Value^,PByteArray(Result)[Head],ValueLen);
end;
end;
function TSynTableFieldProperties.SBFFloat(const Value: Double): TSBFString;
begin
if FieldType<>tftDouble then
result := '' else
SetString(Result,PAnsiChar(@Value),SizeOf(Value));
end;
function TSynTableFieldProperties.SBFFromRawUTF8(const aValue: RawUTF8): TSBFString;
var Curr: Currency;
begin
case FieldType of
tftBoolean:
if (SynCommons.GetInteger(pointer(aValue))<>0) or IdemPropNameU(aValue,'true') then
result := #1 else
result := #0; // store false by default
tftUInt8, tftUInt16, tftUInt24, tftInt32, tftVarInt32:
result := SBF(SynCommons.GetInteger(pointer(aValue)));
tftVarUInt32, tftInt64, tftVarUInt64, tftVarInt64:
result := SBF(SynCommons.GetInt64(pointer(aValue)));
tftCurrency: begin
PInt64(@Curr)^ := StrToCurr64(pointer(aValue));
result := SBFCurr(Curr);
end;
tftDouble:
result := SBFFloat(GetExtended(pointer(aValue)));
// text storage - WinAnsi could use less space than UTF-8
tftUTF8, tftWinAnsi:
result := SBF(aValue);
else
result := ''; // tftBlob* fields e.g. must be handled directly
end;
end;
function TSynTableFieldProperties.GetInteger(RecordBuffer: pointer): Integer;
begin
if (self=nil) or (RecordBuffer=nil) or (Owner=nil) then
result := 0 else begin
RecordBuffer := Owner.GetData(RecordBuffer,self);
case FieldType of
tftBoolean, tftUInt8:
result := PByte(RecordBuffer)^;
tftUInt16:
result := PWord(RecordBuffer)^;
tftUInt24:
// PInteger()^ and $ffffff -> possible GPF on Memory Mapped file
result := PWord(RecordBuffer)^+integer(PByteArray(RecordBuffer)^[2])shl 16;
tftInt32:
result := PInteger(RecordBuffer)^;
tftInt64:
result := PInt64(RecordBuffer)^;
// some variable-size field value
tftVarUInt32:
result := FromVarUInt32(PByte(RecordBuffer));
tftVarInt32:
result := FromVarInt32(PByte(RecordBuffer));
tftVarUInt64:
result := FromVarUInt64(PByte(RecordBuffer));
tftVarInt64:
result := FromVarInt64(PByte(RecordBuffer));
else
result := 0;
end;
end;
end;
function TSynTableFieldProperties.GetInt64(RecordBuffer: pointer): Int64;
var PB: PByte;
begin
if (self=nil) or (RecordBuffer=nil) or (Owner=nil) then
result := 0 else begin
PB := Owner.GetData(RecordBuffer,self);
case FieldType of
tftInt64:
result := PInt64(PB)^;
tftVarUInt64:
result := FromVarUInt64(PB);
tftVarInt64:
result := FromVarInt64(PB);
else
result := GetInteger(RecordBuffer);
end;
end;
end;
function TSynTableFieldProperties.GetBoolean(RecordBuffer: pointer): Boolean;
begin
result := boolean(GetInteger(RecordBuffer));
end;
function TSynTableFieldProperties.GetCurrency(RecordBuffer: pointer): Currency;
begin
if (self=nil) or (RecordBuffer=nil) or (Owner=nil) then
result := 0 else
case FieldType of
tftCurrency:
result := PCurrency(Owner.GetData(RecordBuffer,self))^;
else
result := GetInt64(RecordBuffer);
end;
end;
function TSynTableFieldProperties.GetDouble(RecordBuffer: pointer): Double;
begin
if (self=nil) or (RecordBuffer=nil) or (Owner=nil) then
result := 0 else
case FieldType of
tftDouble:
result := unaligned(PDouble(Owner.GetData(RecordBuffer,self))^);
else
result := GetInt64(RecordBuffer);
end;
end;
function TSynTableFieldProperties.GetRawUTF8(RecordBuffer: pointer): RawUTF8;
begin
if (self=nil) or (RecordBuffer=nil) or (Owner=nil) then
result := '' else begin
RecordBuffer := Owner.GetData(RecordBuffer,self);
if RecordBuffer<>nil then
result := GetValue(RecordBuffer) else // will do conversion to text
result := '';
end;
end;
function TSynTableFieldProperties.AddFilterOrValidate(aFilter: TSynFilterOrValidate): TSynFilterOrValidate;
2021-04-23 10:11:05 +02:00
procedure Add(var List: TSynObjectList);
begin
if List=nil then
List := TSynObjectList.Create;
List.Add(result);
end;
begin
result := aFilter;
if (self=nil) or (result=nil) then
result := nil else
if aFilter.InheritsFrom(TSynFilter) then
Add(Filters) else
if aFilter.InheritsFrom(TSynValidate) then
Add(Validates) else
result := nil;
end;
function TSynTableFieldProperties.Validate(RecordBuffer: pointer;
RecordIndex: integer): string;
var i: integer;
Value: RawUTF8;
aValidate: TSynValidate;
aValidateTable: TSynValidateTable absolute aValidate;
begin
result := '';
if (self=nil) or (Validates=nil) then
exit;
Value := GetRawUTF8(RecordBuffer); // TSynTableValidate needs RawUTF8 text
for i := 0 to Validates.Count-1 do begin
aValidate := Validates.List[i];
if aValidate.InheritsFrom(TSynValidateTable) then begin
aValidateTable.ProcessField := self;
aValidateTable.ProcessRecordIndex := RecordIndex;
end;
if not aValidate.Process(FieldNumber,Value,result) then begin
if result='' then
// no custom message -> show a default message
result := format(sValidationFailed,[
GetCaptionFromClass(aValidate.ClassType)]);
break;
end;
end;
end;
{$ifdef SORTCOMPAREMETHOD}
function TSynTableFieldProperties.SortCompare(P1, P2: PUTF8Char): PtrInt;
var i, L: integer;
label minus,plus,zer;
begin
if P1<>P2 then
if P1<>nil then
if P2<>nil then
case FieldType of
tftBoolean, tftUInt8:
result := PByte(P1)^-PByte(P2)^;
tftUInt16:
result := PWord(P1)^-PWord(P2)^;
tftUInt24:
result := PtrInt(PWord(P1)^)+PtrInt(P1[2])shl 16
-PtrInt(PWord(P2)^)-PtrInt(P2[2]) shl 16;
tftInt32:
result := PInteger(P1)^-PInteger(P2)^;
tftDouble: begin
unaligned(PDouble(@SortCompareTmp)^) := unaligned(PDouble(P1)^)-unaligned(PDouble(P2)^);
if unaligned(PDouble(@SortCompareTmp)^)<0 then
goto minus else
if unaligned(PDouble(@SortCompareTmp)^)>0 then
goto plus else
goto zer;
end;
tftVarUInt32:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
result := FromVarUInt32(PB1)-FromVarUInt32(PB2);
end;
tftVarInt32:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
result := FromVarInt32(PB1)-FromVarInt32(PB2);
end;
{$ifdef CPUX86} // circumvent comparison slowness (and QWord bug)
tftInt64, tftCurrency:
result := SortInt64(PInt64(P1)^,PInt64(P2)^);
tftVarUInt64:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
result := SortQWord(FromVarUInt64(PB1),FromVarUInt64(PB2));
end;
tftVarInt64:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
result := SortInt64(FromVarInt64(PB1),FromVarInt64(PB2));
end;
{$else}
{$ifdef CPU64} // PtrInt = Int64
tftInt64, tftCurrency:
result := PInt64(P1)^-PInt64(P2)^;
tftVarUInt64:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
result := FromVarUInt64(PB1)-FromVarUInt64(PB2);
end;
tftVarInt64:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
result := FromVarInt64(PB1)-FromVarInt64(PB2);
end;
{$else}
tftInt64, tftCurrency: begin
PInt64(@SortCompareTmp)^ := PInt64(P1)^-PInt64(P2)^;
if PInt64(@SortCompareTmp)^<0 then
goto minus else
if PInt64(@SortCompareTmp)^>0 then
goto plus else
goto zer;
end;
tftVarUInt64:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
PInt64(@SortCompareTmp)^ := FromVarUInt64(PB1)-FromVarUInt64(PB2);
if PInt64(@SortCompareTmp)^<0 then
goto minus else
if PInt64(@SortCompareTmp)^>0 then
goto plus else
goto zer;
end;
tftVarInt64:
with SortCompareTmp do begin
PB1 := Pointer(P1);
PB2 := Pointer(P2);
PInt64(@SortCompareTmp)^ := FromVarInt64(PB1)-FromVarInt64(PB2);
if PInt64(@SortCompareTmp)^<0 then
goto minus else
if PInt64(@SortCompareTmp)^>0 then
goto plus else
goto zer;
end;
{$endif}
{$endif}
tftWinAnsi, tftUTF8, tftBlobInternal:
begin
with SortCompareTmp do begin
if PtrInt(P1^)<=$7F then begin
L1 := PtrInt(P1^);
inc(P1);
end else begin
PB1 := pointer(P1);
L1 := FromVarUInt32High(PB1);
P1 := pointer(PB1);
end;
if PtrInt(P2^)<=$7F then begin
L2 := PtrInt(P2^);
inc(P2);
end else begin
PB2 := pointer(P2);
L2 := FromVarUInt32High(PB2);
P2 := pointer(PB2);
end;
end;
with SortCompareTmp do begin
L := L1;
if L2>L then
L := L2;
end;
if tfoCaseInsensitive in Options then begin
i := 0;
while i<L do begin
result := PtrInt(NormToUpperAnsi7[P1[i]])-PtrInt(NormToUpperAnsi7[P2[i]]);
if result<>0 then
exit else
inc(i);
end;
end else begin
i := 0;
while i<L do begin
result := PtrInt(P1[i])-PtrInt(P2[i]);
if result<>0 then
exit else
inc(i);
end;
end;
with SortCompareTmp do
result := L1-L2;
end;
else
goto zer;
end else
plus: result := 1 else // P2=nil
minus:result := -1 else // P1=nil
zer:result := 0; // P1=P2
end;
{$endif}
function CompareOperator(FieldType: TSynTableFieldType; SBF, SBFEnd: PUTF8Char;
Value: Int64; Oper: TCompareOperator): boolean;
var V: Int64;
PB: PByte absolute SBF;
begin
result := true;
if PB<>nil then
repeat
case FieldType of
tftBoolean, tftUInt8:
V := PB^;
tftUInt16:
V := PWord(PB)^;
tftUInt24:
// PInteger()^ and $ffffff -> possible GPF on Memory Mapped file
V := PWord(PB)^+integer(PByteArray(PB)^[2])shl 16;
tftInt32:
V := PInteger(PB)^;
tftInt64:
V := PInt64(PB)^;
// some variable-size field value
tftVarUInt32:
V := FromVarUInt32(PB);
tftVarInt32:
V := FromVarInt32(PB);
tftVarUInt64:
V := FromVarUInt64(PB);
tftVarInt64:
V := FromVarInt64(PB);
else V := 0; // makes compiler happy
end;
case Oper of
soEqualTo: if V=Value then exit;
soNotEqualTo: if V<>Value then exit;
soLessThan: if V<Value then exit;
soLessThanOrEqualTo: if V<=Value then exit;
soGreaterThan: if V>Value then exit;
soGreaterThanOrEqualTo: if V>=Value then exit;
else break;
end;
// not found: go to next value
if SBFEnd=nil then
break; // only one value to be checked
if FIELD_FIXEDSIZE[FieldType]>0 then
inc(SBF,FIELD_FIXEDSIZE[FieldType]); // FromVar*() already updated PB/SBF
until SBF>=SBFEnd;
result := false; // not found
end;
function CompareOperator(SBF, SBFEnd: PUTF8Char;
Value: double; Oper: TCompareOperator): boolean;
begin
result := true;
if SBF<>nil then
repeat
case Oper of
soEqualTo: if unaligned(PDouble(SBF)^)=Value then exit;
soNotEqualTo: if unaligned(PDouble(SBF)^)<>Value then exit;
soLessThan: if unaligned(PDouble(SBF)^)<Value then exit;
soLessThanOrEqualTo: if unaligned(PDouble(SBF)^)<=Value then exit;
soGreaterThan: if unaligned(PDouble(SBF)^)>Value then exit;
soGreaterThanOrEqualTo: if unaligned(PDouble(SBF)^)>=Value then exit;
else break;
end;
// not found: go to next value
if SBFEnd=nil then
break; // only one value to be checked
inc(SBF,SizeOf(Value));
until SBF>=SBFEnd;
result := false; // not found
end;
function CompareOperator(FieldType: TSynTableFieldType; SBF, SBFEnd: PUTF8Char;
Value: PUTF8Char; ValueLen: integer; Oper: TCompareOperator;
CaseSensitive: boolean): boolean; overload;
var L, Cmp: PtrInt;
PB: PByte;
tmp: array[byte] of AnsiChar;
begin
result := true;
if SBF<>nil then
repeat
// get length of text in the SBF encoded buffer
if integer(SBF^)<=$7f then begin
L := integer(SBF^);
inc(SBF);
end else begin
PB := Pointer(SBF);
L := FromVarUInt32(PB);
SBF := pointer(PB);
end;
// perform comparison: returns nil on match
case Oper of
soEqualTo..soGreaterThanOrEqualTo: begin
Cmp := L-ValueLen;
if Cmp<0 then
L := ValueLen;
if CaseSensitive then
Cmp := StrCompL(SBF,Value,L,Cmp) else
Cmp := StrCompIL(SBF,Value,L,Cmp);
case Oper of
soEqualTo: if Cmp=0 then exit;
soNotEqualTo: if Cmp<>0 then exit;
soLessThan: if Cmp<0 then exit;
soLessThanOrEqualTo: if Cmp<=0 then exit;
soGreaterThan: if Cmp>0 then exit;
soGreaterThanOrEqualTo: if Cmp>=0 then exit;
end;
end;
soBeginWith:
if ValueLen>=L then
if CaseSensitive then begin
if StrCompL(SBF,Value,ValueLen,0)=0 then
exit;
end else
if StrCompIL(SBF,Value,ValueLen,0)=0 then
exit;
soContains: begin
dec(L,ValueLen);
while L>=0 do begin
2021-04-23 10:11:05 +02:00
while (L>=0) and not(tcWord in TEXT_CHARS[SBF^]) do begin
dec(L);
inc(SBF);
end; // begin of next word reached
if L<0 then
Break; // not enough chars to contain the Value
if CaseSensitive then begin
if StrCompL(SBF,Value,ValueLen,0)=0 then
exit;
end else
if StrCompIL(SBF,Value,ValueLen,0)=0 then
exit;
2021-04-23 10:11:05 +02:00
while (L>=0) and (tcWord in TEXT_CHARS[SBF^]) do begin
dec(L);
inc(SBF);
end; // end of word reached
end;
if SBFEnd=nil then
break; // only one value to be checked
inc(SBF,ValueLen); // custom inc(SBF,L);
if SBF<SBFEnd then
continue else break;
end;
soSoundsLikeEnglish,
soSoundsLikeFrench,
soSoundsLikeSpanish: begin
if L>high(tmp) then
Cmp := high(tmp) else
Cmp := L;
tmp[Cmp] := #0; // TSynSoundEx expect the buffer to be #0 terminated
MoveFast(SBF^,tmp,Cmp);
case FieldType of
tftWinAnsi:
if PSynSoundEx(Value)^.Ansi(tmp) then
exit;
tftUTF8:
if PSynSoundEx(Value)^.UTF8(tmp) then
exit;
else break;
end;
end;
else break;
end;
// no match -> go to the end of the SBF buffer
if SBFEnd=nil then
exit; // only one value to be checked
inc(SBF,L);
if SBF>=SBFEnd then
break;
until false;
end;
{ TSynValidateTableUniqueField }
function TSynValidateTableUniqueField.Process(aFieldIndex: integer;
const Value: RawUTF8; var ErrorMsg: string): boolean;
var S: TSBFString;
begin
result := false;
if (self=nil) or (Value='') or (ProcessField=nil) then
exit; // void field can't be unique
if not (tfoIndex in ProcessField.Options) then
exit; // index should be always created by TSynTable.AfterFieldModif
S := ProcessField.SBFFromRawUTF8(Value);
if S='' then
exit; // void field can't be unique
if ProcessField.OrderedIndexFindAdd(Pointer(S))>=0 then
// there is some place to insert the Value -> not existing yet -> OK
result := true else begin
// RecordIndex=-1 in case of adding, or the physical index of the updated record
if (ProcessRecordIndex>=0) and
(ProcessField.OrderedIndex[ProcessField.OrderedIndexFind(Pointer(S))]=
ProcessRecordIndex) then
// allow update of the record
result := true else
// found a dupplicated value
ErrorMsg := sValidationFieldDuplicate;
end;
end;
{ TSynTableStatement }
const
NULL_UPP = ord('N')+ord('U')shl 8+ord('L')shl 16+ord('L')shl 24;
constructor TSynTableStatement.Create(const SQL: RawUTF8;
GetFieldIndex: TSynTableFieldIndex; SimpleFieldsBits: TSQLFieldBits;
FieldProp: TSynTableFieldProperties);
var Prop, whereBefore: RawUTF8;
P, B: PUTF8Char;
ndx,err,len,selectCount,whereCount: integer;
whereWithOR,whereNotClause: boolean;
function GetPropIndex: integer;
begin
if not GetNextFieldProp(P,Prop) then
result := -1 else
if IsRowID(pointer(Prop)) then
result := 0 else begin // 0 = ID field
result := GetFieldIndex(Prop);
if result>=0 then // -1 = no valid field name
inc(result); // otherwise: PropertyIndex+1
end;
end;
function SetFields: boolean;
var select: TSynTableStatementSelect;
B: PUTF8Char;
begin
result := false;
FillcharFast(select,SizeOf(select),0);
select.Field := GetPropIndex; // 0 = ID, otherwise PropertyIndex+1
if select.Field<0 then begin
if P^<>'(' then // Field not found -> try function(field)
exit;
P := GotoNextNotSpace(P+1);
select.FunctionName := Prop;
inc(fSelectFunctionCount);
if IdemPropNameU(Prop,'COUNT') and (P^='*') then begin
select.Field := 0; // count( * ) -> count(ID)
select.FunctionKnown := funcCountStar;
P := GotoNextNotSpace(P+1);
end else begin
if IdemPropNameU(Prop,'DISTINCT') then
select.FunctionKnown := funcDistinct else
if IdemPropNameU(Prop,'MAX') then
select.FunctionKnown := funcMax;
select.Field := GetPropIndex;
if select.Field<0 then
exit;
end;
if P^<>')' then
exit;
P := GotoNextNotSpace(P+1);
end else
if P^='.' then begin // MongoDB-like field.subfield1.subfield2
B := P;
repeat
inc(P);
2021-04-23 10:11:05 +02:00
until not (jcJsonIdentifier in JSON_CHARS[P^]);
FastSetString(select.SubField,B,P-B);
fHasSelectSubFields := true;
end;
if P^ in ['+','-'] then begin
select.ToBeAdded := GetNextItemInteger(P,' ');
if select.ToBeAdded=0 then
exit;
P := GotoNextNotSpace(P);
end;
if IdemPChar(P,'AS ') then begin
inc(P,3);
if not GetNextFieldProp(P,select.Alias) then
exit;
end;
SetLength(fSelect,selectCount+1);
fSelect[selectCount] := select;
inc(selectCount);
result := true;
end;
function GetWhereValue(var Where: TSynTableStatementWhere): boolean;
var B: PUTF8Char;
begin
result := false;
P := GotoNextNotSpace(P);
Where.ValueSQL := P;
if PWord(P)^=ord(':')+ord('(') shl 8 then
inc(P,2); // ignore :(...): parameter (no prepared statements here)
if P^ in ['''','"'] then begin
// SQL String statement
P := UnQuoteSQLStringVar(P,Where.Value);
if P=nil then
exit; // end of string before end quote -> incorrect
{$ifndef NOVARIANTS}
RawUTF8ToVariant(Where.Value,Where.ValueVariant);
{$endif}
if FieldProp<>nil then
// create a SBF formatted version of the WHERE value
Where.ValueSBF := FieldProp.SBFFromRawUTF8(Where.Value);
end else
if (PInteger(P)^ and $DFDFDFDF=NULL_UPP) and (P[4] in [#0..' ',';']) then begin
// NULL statement
Where.Value := NULL_STR_VAR; // not void
{$ifndef NOVARIANTS}
SetVariantNull(Where.ValueVariant);
{$endif}
inc(P,4);
end else begin
// numeric statement or 'true' or 'false' (OK for NormalizeValue)
B := P;
repeat
inc(P);
until P^ in [#0..' ',';',')',','];
SetString(Where.Value,B,P-B);
{$ifndef NOVARIANTS}
Where.ValueVariant := VariantLoadJSON(Where.Value);
{$endif}
Where.ValueInteger := GetInteger(pointer(Where.Value),err);
if FieldProp<>nil then
if Where.Value<>'?' then
if (FieldProp.FieldType in FIELD_INTEGER) and (err<>0) then
// we expect a true INTEGER value here
Where.Value := '' else
// create a SBF formatted version of the WHERE value
Where.ValueSBF := FieldProp.SBFFromRawUTF8(Where.Value);
end;
if PWord(P)^=ord(')')+ord(':')shl 8 then
inc(P,2); // ignore :(...): parameter
Where.ValueSQLLen := P-Where.ValueSQL;
P := GotoNextNotSpace(P);
if (P^=')') and (Where.FunctionName='') then begin
B := P;
repeat
inc(P);
until not (P^ in [#1..' ',')']);
while P[-1]=' ' do dec(P); // trim right space
SetString(Where.ParenthesisAfter,B,P-B);
P := GotoNextNotSpace(P);
end;
result := true;
end;
{$ifndef NOVARIANTS}
function GetWhereValues(var Where: TSynTableStatementWhere): boolean;
var v: TSynTableStatementWhereDynArray;
n, w: integer;
tmp: RawUTF8;
begin
result := false;
if Where.ValueSQLLen<=2 then
exit;
SetString(tmp,PAnsiChar(Where.ValueSQL)+1,Where.ValueSQLLen-2);
P := pointer(tmp); // parse again the IN (...,...,... ) expression
n := 0;
try
repeat
if n=length(v) then
SetLength(v,NextGrow(n));
if not GetWhereValue(v[n]) then
exit;
inc(n);
if P^=#0 then
break;
if P^<>',' then
exit;
inc(P);
until false;
finally
P := Where.ValueSQL+Where.ValueSQLLen; // continue parsing as usual
end;
with TDocVariantData(Where.ValueVariant) do begin
InitFast(n,dvArray);
for w := 0 to n-1 do
AddItem(v[w].ValueVariant);
Where.Value := ToJSON;
end;
result := true;
end;
{$endif}
function GetWhereExpression(FieldIndex: integer; var Where: TSynTableStatementWhere): boolean;
var B: PUTF8Char;
begin
result := false;
Where.ParenthesisBefore := whereBefore;
Where.JoinedOR := whereWithOR;
Where.NotClause := whereNotClause;
Where.Field := FieldIndex; // 0 = ID, otherwise PropertyIndex+1
if P^='.' then begin // MongoDB-like field.subfield1.subfield2
B := P;
repeat
inc(P);
2021-04-23 10:11:05 +02:00
until not (jcJsonIdentifier in JSON_CHARS[P^]);
FastSetString(Where.SubField,B,P-B);
fWhereHasSubFields := true;
P := GotoNextNotSpace(P);
end;
case P^ of
'=': Where.Operator := opEqualTo;
'>': if P[1]='=' then begin
inc(P);
Where.Operator := opGreaterThanOrEqualTo;
end else
Where.Operator := opGreaterThan;
'<': case P[1] of
'=': begin
inc(P);
Where.Operator := opLessThanOrEqualTo;
end;
'>': begin
inc(P);
Where.Operator := opNotEqualTo;
end;
else
Where.Operator := opLessThan;
end;
'i','I':
case P[1] of
's','S': begin
P := GotoNextNotSpace(P+2);
if IdemPChar(P,'NULL') then begin
Where.Value := NULL_STR_VAR;
Where.Operator := opIsNull;
Where.ValueSQL := P;
Where.ValueSQLLen := 4;
{$ifndef NOVARIANTS}
TVarData(Where.ValueVariant).VType := varNull;
{$endif}
inc(P,4);
result := true;
end else
if IdemPChar(P,'NOT NULL') then begin
Where.Value := 'not null';
Where.Operator := opIsNotNull;
Where.ValueSQL := P;
Where.ValueSQLLen := 8;
{$ifndef NOVARIANTS}
TVarData(Where.ValueVariant).VType := varNull;
{$endif}
inc(P,8);
result := true; // leave ValueVariant=unassigned
end;
exit;
end;
{$ifndef NOVARIANTS}
'n','N': begin
Where.Operator := opIn;
P := GotoNextNotSpace(P+2);
if P^<>'(' then
exit; // incorrect SQL statement
B := P; // get the IN() clause as JSON
inc(P);
while (P^<>')') or (P[1]=':') do // handle :(...): within the clause
if P^=#0 then
exit else
inc(P);
inc(P);
SetString(Where.Value,PAnsiChar(B),P-B);
Where.ValueSQL := B;
Where.ValueSQLLen := P-B;
result := GetWhereValues(Where);
exit;
end;
{$endif}
end; // 'i','I':
'l','L':
if IdemPChar(P+1,'IKE') then begin
inc(P,3);
Where.Operator := opLike;
end else
exit;
else exit; // unknown operator
end;
// we got 'WHERE FieldName operator ' -> handle value
inc(P);
result := GetWhereValue(Where);
end;
label lim,lim2;
begin
P := pointer(SQL);
if (P=nil) or (self=nil) then
exit; // avoid GPF
P := GotoNextNotSpace(P); // trim left
if not IdemPChar(P,'SELECT ') then
exit else // handle only SELECT statement
inc(P,7);
// 1. get SELECT clause: set bits in Fields from CSV field IDs in SQL
selectCount := 0;
P := GotoNextNotSpace(P); // trim left
if P^=#0 then
exit; // no SQL statement
if P^='*' then begin // all simple (not TSQLRawBlob/TSQLRecordMany) fields
inc(P);
len := GetBitsCount(SimpleFieldsBits,MAX_SQLFIELDS)+1;
SetLength(fSelect,len);
selectCount := 1; // Select[0].Field := 0 -> ID
for ndx := 0 to MAX_SQLFIELDS-1 do
if ndx in SimpleFieldsBits then begin
fSelect[selectCount].Field := ndx+1;
inc(selectCount);
if selectCount=len then
break;
end;
GetNextFieldProp(P,Prop);
end else
if not SetFields then
exit else // we need at least one field name
if P^<>',' then
GetNextFieldProp(P,Prop) else
repeat
while P^ in [',',#1..' '] do inc(P); // trim left
until not SetFields; // add other CSV field names
// 2. get FROM clause
if not IdemPropNameU(Prop,'FROM') then exit; // incorrect SQL statement
GetNextFieldProp(P,Prop);
fTableName := Prop;
// 3. get WHERE clause
whereCount := 0;
whereWithOR := false;
whereNotClause := false;
whereBefore := '';
GetNextFieldProp(P,Prop);
if IdemPropNameU(Prop,'WHERE') then begin
repeat
B := P;
if P^='(' then begin
fWhereHasParenthesis := true;
repeat
inc(P);
until not (P^ in [#1..' ','(']);
while P[-1]=' ' do dec(P); // trim right space
SetString(whereBefore,B,P-B);
B := P;
end;
ndx := GetPropIndex;
if ndx<0 then begin
if IdemPropNameU(Prop,'NOT') then begin
whereNotClause := true;
continue;
end;
if P^='(' then begin
inc(P);
SetLength(fWhere,whereCount+1);
with fWhere[whereCount] do begin
ParenthesisBefore := whereBefore;
JoinedOR := whereWithOR;
NotClause := whereNotClause;
FunctionName := UpperCase(Prop);
// Byte/Word/Integer/Cardinal/Int64/CurrencyDynArrayContains(BlobField,I64)
len := length(Prop);
if (len>16) and
IdemPropName('DynArrayContains',PUTF8Char(@PByteArray(Prop)[len-16]),16) then
Operator := opContains else
Operator := opFunction;
B := P;
Field := GetPropIndex;
if Field<0 then
P := B else
if P^<>',' then
break else
P := GotoNextNotSpace(P+1);
if (P^=')') or
(GetWhereValue(fWhere[whereCount]) and (P^=')')) then begin
inc(P);
break;
end;
end;
end;
P := B;
break;
end;
SetLength(fWhere,whereCount+1);
if not GetWhereExpression(ndx,fWhere[whereCount]) then
exit; // invalid SQL statement
inc(whereCount);
GetNextFieldProp(P,Prop);
if IdemPropNameU(Prop,'OR') then
whereWithOR := true else
if IdemPropNameU(Prop,'AND') then
whereWithOR := false else
goto lim2;
whereNotClause := false;
whereBefore := '';
until false;
// 4. get optional LIMIT/OFFSET/ORDER clause
lim:P := GotoNextNotSpace(P);
while (P<>nil) and not(P^ in [#0,';']) do begin
GetNextFieldProp(P,Prop);
lim2: if IdemPropNameU(Prop,'LIMIT') then
fLimit := GetNextItemCardinal(P,' ') else
if IdemPropNameU(Prop,'OFFSET') then
fOffset := GetNextItemCardinal(P,' ') else
if IdemPropNameU(Prop,'ORDER') then begin
GetNextFieldProp(P,Prop);
if IdemPropNameU(Prop,'BY') then begin
repeat
ndx := GetPropIndex; // 0 = ID, otherwise PropertyIndex+1
if ndx<0 then
exit; // incorrect SQL statement
AddFieldIndex(fOrderByField,ndx);
if P^<>',' then begin // check ORDER BY ... ASC/DESC
B := P;
if GetNextFieldProp(P,Prop) then
if IdemPropNameU(Prop,'DESC') then
fOrderByDesc := true else
if not IdemPropNameU(Prop,'ASC') then
P := B;
break;
end;
P := GotoNextNotSpace(P+1);
until P^ in [#0,';'];
end else
exit; // incorrect SQL statement
end else
if IdemPropNameU(Prop,'GROUP') then begin
GetNextFieldProp(P,Prop);
if IdemPropNameU(Prop,'BY') then begin
repeat
ndx := GetPropIndex; // 0 = ID, otherwise PropertyIndex+1
if ndx<0 then
exit; // incorrect SQL statement
AddFieldIndex(fGroupByField,ndx);
if P^<>',' then
break;
P := GotoNextNotSpace(P+1);
until P^ in [#0,';'];
end else
exit; // incorrect SQL statement
end else
2021-04-23 10:11:05 +02:00
if (Prop<>'') or not(GotoNextNotSpace(P)^ in [#0, ';']) then
exit else // incorrect SQL statement
break; // reached the end of the statement
end;
end else
if Prop<>'' then
goto lim2; // handle LIMIT OFFSET ORDER
fSQLStatement := SQL; // make a private copy e.g. for Where[].ValueSQL
end;
procedure TSynTableStatement.SelectFieldBits(var Fields: TSQLFieldBits;
var withID: boolean; SubFields: PRawUTF8Array);
var i: integer;
f: ^TSynTableStatementSelect;
begin
FillcharFast(Fields,SizeOf(Fields),0);
withID := false;
f := pointer(Select);
for i := 1 to Length(Select) do begin
if f^.Field=0 then
withID := true else
include(Fields,f^.Field-1);
if (SubFields<>nil) and fHasSelectSubFields then
SubFields^[f^.Field] := f^.SubField;
inc(f);
end;
end;
{$ifndef DELPHI5OROLDER}
{ TSynTableData }
procedure TSynTableData.CheckVTableInitialized;
begin
if VTable=nil then
raise ETableDataException.Create('TSynTableData non initialized');
end;
{$ifndef NOVARIANTS}
2021-04-23 10:11:05 +02:00
function TSynTableData.GetField(const FieldName: RawUTF8): Variant;
begin
GetFieldVariant(FieldName,result);
end;
function TSynTableData.GetFieldVarData(FieldName: PUTF8Char; FieldNameLen: PtrInt;
var Value: TVarData): boolean;
var aField: TSynTableFieldProperties;
begin
if IsRowID(FieldName,FieldNameLen) then begin
PVariant(@Value)^ := VID;
result := true;
end else begin
CheckVTableInitialized;
aField := VTable.GetFieldFromNameLen(FieldName,FieldNameLen);
if aField<>nil then begin
aField.GetVariant(VTable.GetData(pointer(VValue),aField),PVariant(@Value)^);
result := true;
end else
result := false;
end;
end;
procedure TSynTableData.GetFieldVariant(const FieldName: RawUTF8; var result: Variant);
var aField: TSynTableFieldProperties;
begin
if IsRowID(Pointer(FieldName)) then
result := VID else begin
CheckVTableInitialized;
aField := VTable.FieldFromName[FieldName];
if aField=nil then
raise ETableDataException.CreateUTF8('Unknown % property',[FieldName]) else
aField.GetVariant(VTable.GetData(pointer(VValue),aField),result);
end;
end;
function TSynTableData.GetFieldValue(aField: TSynTableFieldProperties): Variant;
begin
CheckVTableInitialized;
aField.GetVariant(VTable.GetData(pointer(VValue),aField),result);
end;
{$endif NOVARIANTS}
procedure TSynTableData.FilterSBFValue;
begin
CheckVTableInitialized;
VTable.Filter(VValue);
end;
function TSynTableData.GetFieldSBFValue(aField: TSynTableFieldProperties): TSBFString;
var FieldBuffer: PAnsiChar;
begin
CheckVTableInitialized;
FieldBuffer := VTable.GetData(pointer(VValue),aField);
SetString(Result,FieldBuffer,aField.GetLength(FieldBuffer));
end;
procedure TSynTableData.Init(aTable: TSynTable; aID: Integer);
begin
2021-04-23 10:11:05 +02:00
VType := SynTableVariantVarType;
VID := aID;
2021-04-23 10:11:05 +02:00
VTable := aTable;
VValue := VTable.DefaultRecordData;
end;
procedure TSynTableData.Init(aTable: TSynTable; aID: Integer;
RecordBuffer: pointer; RecordBufferLen: integer);
begin
2021-04-23 10:11:05 +02:00
VType := SynTableVariantVarType;
VTable := aTable;
if (RecordBufferLen=0) or (RecordBuffer=nil) then begin
VID := 0;
VValue := VTable.DefaultRecordData;
end else begin
VID := aID;
SetString(VValue,PAnsiChar(RecordBuffer),RecordBufferLen);
end;
end;
{$ifndef NOVARIANTS}
2021-04-23 10:11:05 +02:00
procedure TSynTableData.SetField(const FieldName: RawUTF8;
const Value: Variant);
var F: TSynTableFieldProperties;
begin
CheckVTableInitialized;
if IsRowID(Pointer(FieldName)) then
VID := Value else begin
F := VTable.FieldFromName[FieldName];
if F=nil then
raise ETableDataException.CreateUTF8('Unknown % property',[FieldName]) else
SetFieldValue(F,Value);
end;
end;
procedure TSynTableData.SetFieldValue(aField: TSynTableFieldProperties; const Value: Variant);
begin
SetFieldSBFValue(aField,aField.SBF(Value));
end;
{$endif}
procedure TSynTableData.SetFieldSBFValue(aField: TSynTableFieldProperties;
const Value: TSBFString);
var NewValue: TSBFString;
begin
CheckVTableInitialized;
if (aField.FieldSize>0) and (VValue<>'') then begin
// fixed size content: fast in-place update
MoveFast(pointer(Value)^,VValue[aField.Offset+1],aField.FieldSize)
// VValue[F.Offset+1] above will call UniqueString(VValue), even under FPC
end else begin
// variable-length update
VTable.UpdateFieldData(pointer(VValue),length(VValue),
aField.FieldNumber,NewValue,Value);
VValue := NewValue;
end;
end;
function TSynTableData.ValidateSBFValue(RecordIndex: integer): string;
begin
CheckVTableInitialized;
Result := VTable.Validate(Pointer(VValue),RecordIndex);
end;
{$endif DELPHI5OROLDER}
{ ************ text search and functions ****************** }
// code below adapted from ZMatchPattern.pas - http://www.zeoslib.sourceforge.net
procedure TMatch.MatchMain;
var RangeStart, RangeEnd: PtrInt;
c: AnsiChar;
flags: set of(Invert, MemberMatch);
begin
while ((State = sNONE) and (P <= PMax)) do begin
c := Upper[Pattern[P]];
if T > TMax then begin
if (c = '*') and (P + 1 > PMax) then
State := sVALID else
State := sABORT;
exit;
end else
case c of
'?': ;
'*':
MatchAfterStar;
'[': begin
inc(P);
byte(flags) := 0;
if Pattern[P] in ['!','^'] then begin
include(flags, Invert);
inc(P);
end;
if (Pattern[P] = ']') then begin
State := sPATTERN;
exit;
end;
c := Upper[Text[T]];
while Pattern[P] <> ']' do begin
RangeStart := P;
RangeEnd := P;
inc(P);
if P > PMax then begin
State := sPATTERN;
exit;
end;
if Pattern[P] = '-' then begin
inc(P);
RangeEnd := P;
if (P > PMax) or (Pattern[RangeEnd] = ']') then begin
State := sPATTERN;
exit;
end;
inc(P);
end;
if P > PMax then begin
State := sPATTERN;
exit;
end;
if RangeStart < RangeEnd then begin
if (c >= Upper[Pattern[RangeStart]]) and (c <= Upper[Pattern[RangeEnd]]) then begin
include(flags, MemberMatch);
break;
end;
end
else
if (c >= Upper[Pattern[RangeEnd]]) and (c <= Upper[Pattern[RangeStart]]) then begin
include(flags, MemberMatch);
break;
end;
end;
if ((Invert in flags) and (MemberMatch in flags)) or
not ((Invert in flags) or (MemberMatch in flags)) then begin
State := sRANGE;
exit;
end;
if MemberMatch in flags then
while (P <= PMax) and (Pattern[P] <> ']') do
inc(P);
if P > PMax then begin
State := sPATTERN;
exit;
end;
end;
else
if c <> Upper[Text[T]] then
State := sLITERAL;
end;
inc(P);
inc(T);
end;
if State = sNONE then
if T <= TMax then
State := sEND else
State := sVALID;
end;
procedure TMatch.MatchAfterStar;
var retryT, retryP: PtrInt;
begin
if (TMax = 1) or (P = PMax) then begin
State := sVALID;
exit;
end else
if (PMax = 0) or (TMax = 0) then begin
State := sABORT;
exit;
end;
while ((T <= TMax) and (P < PMax)) and (Pattern[P] in ['?', '*']) do begin
if Pattern[P] = '?' then
inc(T);
inc(P);
end;
if T >= TMax then begin
State := sABORT;
exit;
end else
if P >= PMax then begin
State := sVALID;
exit;
end;
repeat
if (Upper[Pattern[P]] = Upper[Text[T]]) or (Pattern[P] = '[') then begin
retryT := T;
retryP := P;
MatchMain;
if State = sVALID then
break;
State := sNONE; // retry until end of Text, (check below) or State valid
T := retryT;
P := retryP;
end;
inc(T);
if (T > TMax) or (P > PMax) then begin
State := sABORT;
exit;
end;
until State <> sNONE;
end;
function SearchAny(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
aMatch.State := sNONE;
aMatch.P := 0;
aMatch.T := 0;
aMatch.Text := aText;
aMatch.TMax := aTextLen - 1;
aMatch.MatchMain;
result := aMatch.State = sVALID;
end;
// faster alternative (without recursion) for only * ? (but not [...])
function SearchNoRange(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
{$ifdef CPUX86}
var
c: AnsiChar;
pat, txt: PtrInt; // use local registers
begin
aMatch.T := 0; // aMatch.P/T are used for retry positions after *
aMatch.Text := aText;
aMatch.TMax := aTextLen - 1;
pat := 0;
txt := 0;
repeat
if pat <= aMatch.PMax then begin
c := aMatch.Pattern[pat];
case c of
'?':
if txt <= aMatch.TMax then begin
inc(pat);
inc(txt);
continue;
end;
'*': begin
aMatch.P := pat;
aMatch.T := txt + 1;
inc(pat);
continue;
end;
else if (txt <= aMatch.TMax) and (c = aMatch.Text[txt]) then begin
inc(pat);
inc(txt);
continue;
end;
end;
end
else if txt > aMatch.TMax then
break;
txt := aMatch.T;
if (txt > 0) and (txt <= aMatch.TMax + 1) then begin
inc(aMatch.T);
pat := aMatch.P+1;
continue;
end;
result := false;
exit;
until false;
result := true;
end;
{$else} // optimized for x86_64/ARM with more registers
var
c: AnsiChar;
pat, patend, txtend, txtretry, patretry: PUTF8Char;
label
fin;
begin
pat := pointer(aMatch.Pattern);
if pat = nil then
goto fin;
patend := pat + aMatch.PMax;
patretry := nil;
txtend := aText + aTextLen - 1;
txtretry := nil;
repeat
if pat <= patend then begin
c := pat^;
if c <> '*' then
if c <> '?' then begin
if (aText <= txtend) and (c = aText^) then begin
inc(pat);
inc(aText);
continue;
end;
end
else begin // '?'
if aText <= txtend then begin
inc(pat);
inc(aText);
continue;
end;
end
else begin // '*'
inc(pat);
txtretry := aText + 1;
patretry := pat;
continue;
end;
end
else if aText > txtend then
break;
if (PtrInt(PtrUInt(txtretry)) > 0) and (txtretry <= txtend + 1) then begin
aText := txtretry;
inc(txtretry);
pat := patretry;
continue;
end;
fin:result := false;
exit;
until false;
result := true;
end;
{$endif CPUX86}
function SearchNoRangeU(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
var
c: AnsiChar;
pat, txt: PtrInt;
begin
aMatch.T := 0;
aMatch.Text := aText;
aMatch.TMax := aTextLen - 1;
pat := 0;
txt := 0;
repeat
if pat <= aMatch.PMax then begin
c := aMatch.Pattern[pat];
case c of
'?':
if txt <= aMatch.TMax then begin
inc(pat);
inc(txt);
continue;
end;
'*': begin
aMatch.P := pat;
aMatch.T := txt + 1;
inc(pat);
continue;
end;
else if (txt <= aMatch.TMax) and
(aMatch.Upper[c] = aMatch.Upper[aMatch.Text[txt]]) then begin
inc(pat);
inc(txt);
continue;
end;
end;
end
else if txt > aMatch.TMax then
break;
txt := aMatch.T;
if (txt > 0) and (txt <= aMatch.TMax + 1) then begin
inc(aMatch.T);
pat := aMatch.P+1;
continue;
end;
result := false;
exit;
until false;
result := true;
end;
function SimpleContainsU(t, tend, p: PUTF8Char; pmax: PtrInt; up: PNormTable): boolean;
{$ifdef HASINLINE}inline;{$endif}
// brute force case-insensitive search p[0..pmax] in t..tend-1
var first: AnsiChar;
i: PtrInt;
label next;
begin
first := up[p^];
repeat
if up[t^] <> first then begin
next: inc(t);
if t < tend then
continue else
break;
end;
for i := 1 to pmax do
if up[t[i]] <> up[p[i]] then
goto next;
result := true;
exit;
until false;
result := false;
end;
{$ifdef CPU64} // naive but very efficient code generation on FPC x86-64
function SimpleContains8(t, tend, p: PUTF8Char; pmax: PtrInt): boolean; inline;
label next;
var i, first: PtrInt;
begin
first := PPtrInt(p)^;
repeat
if PPtrInt(t)^ <> first then begin
next: inc(t);
if t < tend then
continue else
break;
end;
for i := 8 to pmax do
if t[i] <> p[i] then
goto next;
result := true;
exit;
until false;
result := false;
end;
{$endif CPU64}
function SimpleContains4(t, tend, p: PUTF8Char; pmax: PtrInt): boolean;
{$ifdef HASINLINE}inline;{$endif}
label next;
var i: PtrInt;
{$ifdef CPUX86} // circumvent lack of registers for this CPU
begin
repeat
if PCardinal(t)^ <> PCardinal(p)^ then begin
{$else}
first: cardinal;
begin
first := PCardinal(p)^;
repeat
if PCardinal(t)^ <> first then begin
{$endif}
next: inc(t);
if t < tend then
continue else
break;
end;
for i := 4 to pmax do
if t[i] <> p[i] then
goto next;
result := true;
exit;
until false;
result := false;
end;
function SimpleContains1(t, tend, p: PUTF8Char; pmax: PtrInt): boolean;
{$ifdef HASINLINE}inline;{$endif}
label next;
var i: PtrInt;
{$ifdef CPUX86}
begin
repeat
if t^ <> p^ then begin
{$else}
first: AnsiChar;
begin
first := p^;
repeat
if t^ <> first then begin
{$endif}
next: inc(t);
if t < tend then
continue else
break;
end;
for i := 1 to pmax do
if t[i] <> p[i] then
goto next;
result := true;
exit;
until false;
result := false;
end;
function CompareMemU(P1, P2: PUTF8Char; len: PtrInt; U: PNormTable): Boolean;
{$ifdef FPC}inline;{$endif}
begin // here we know that len>0
result := false;
repeat
dec(len);
if U[P1[len]] <> U[P2[len]] then
exit;
until len = 0;
result := true;
end;
function SearchVoid(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
result := aTextLen = 0;
end;
function SearchNoPattern(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
result := (aMatch.PMax + 1 = aTextlen) and CompareMem(aText, aMatch.Pattern, aTextLen);
end;
function SearchNoPatternU(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
result := (aMatch.PMax + 1 = aTextlen) and CompareMemU(aText, aMatch.Pattern, aTextLen, aMatch.Upper);
end;
function SearchContainsValid(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
result := true;
end;
function SearchContainsU(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
dec(aTextLen, aMatch.PMax);
if aTextLen > 0 then
result := SimpleContainsU(aText, aText + aTextLen, aMatch.Pattern, aMatch.PMax, aMatch.Upper)
else
result := false;
end;
function SearchContains1(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
dec(aTextLen, aMatch.PMax);
if aTextLen > 0 then
result := SimpleContains1(aText, aText + aTextLen, aMatch.Pattern, aMatch.PMax)
else
result := false;
end;
function SearchContains4(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
dec(aTextLen, aMatch.PMax);
if aTextLen > 0 then
result := SimpleContains4(aText, aText + aTextLen, aMatch.Pattern, aMatch.PMax)
else
result := false;
end;
{$ifdef CPU64}
function SearchContains8(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin // optimized e.g. to search an IP address as '*12.34.56.78*' in logs
dec(aTextLen, aMatch.PMax);
if aTextLen > 0 then
result := SimpleContains8(aText, aText + aTextLen, aMatch.Pattern, aMatch.PMax)
else
result := false;
end;
{$endif CPU64}
function SearchStartWith(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
result := (aMatch.PMax < aTextlen) and CompareMem(aText, aMatch.Pattern, aMatch.PMax + 1);
end;
function SearchStartWithU(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
result := (aMatch.PMax < aTextlen) and CompareMemU(aText, aMatch.Pattern, aMatch.PMax + 1, aMatch.Upper);
end;
function SearchEndWith(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
dec(aTextLen, aMatch.PMax);
result := (aTextlen >= 0) and CompareMem(aText + aTextLen, aMatch.Pattern, aMatch.PMax);
end;
function SearchEndWithU(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
dec(aTextLen, aMatch.PMax);
result := (aTextlen >= 0) and CompareMemU(aText + aTextLen, aMatch.Pattern, aMatch.PMax, aMatch.Upper);
end;
procedure TMatch.Prepare(const aPattern: RawUTF8; aCaseInsensitive, aReuse: boolean);
begin
Prepare(pointer(aPattern), length(aPattern), aCaseInsensitive, aReuse);
end;
procedure TMatch.Prepare(aPattern: PUTF8Char; aPatternLen: integer;
aCaseInsensitive, aReuse: boolean);
const SPECIALS: PUTF8Char = '*?[';
begin
Pattern := aPattern;
PMax := aPatternLen - 1; // search in Pattern[0..PMax]
if Pattern = nil then begin
Search := SearchVoid;
exit;
end;
if aCaseInsensitive and not IsCaseSensitive(aPattern,aPatternLen) then
aCaseInsensitive := false; // don't slow down e.g. number or IP search
if aCaseInsensitive then
Upper := @NormToUpperAnsi7 else
Upper := @NormToNorm;
Search := nil;
if aReuse then
if strcspn(Pattern, SPECIALS) > PMax then
if aCaseInsensitive then
Search := SearchNoPatternU
else
Search := SearchNoPattern
else if PMax > 0 then begin
if Pattern[PMax] = '*' then begin
if strcspn(Pattern + 1, SPECIALS) = PMax - 1 then
case Pattern[0] of
'*': begin // *something*
inc(Pattern);
dec(PMax, 2); // trim trailing and ending *
if PMax < 0 then
Search := SearchContainsValid
else if aCaseInsensitive then
Search := SearchContainsU
{$ifdef CPU64}
else if PMax >= 7 then
Search := SearchContains8
{$endif}
else if PMax >= 3 then
Search := SearchContains4
else
Search := SearchContains1;
end;
'?': // ?something*
if aCaseInsensitive then
Search := SearchNoRangeU
else
Search := SearchNoRange;
'[':
Search := SearchAny;
else begin
dec(PMax); // trim trailing *
if aCaseInsensitive then
Search := SearchStartWithU
else
Search := SearchStartWith;
end;
end;
end
else if (Pattern[0] = '*') and (strcspn(Pattern + 1, SPECIALS) >= PMax) then begin
inc(Pattern); // jump leading *
if aCaseInsensitive then
Search := SearchEndWithU
else
Search := SearchEndWith;
end;
end else
if Pattern[0] in ['*','?'] then
Search := SearchContainsValid;
if not Assigned(Search) then begin
aPattern := PosChar(Pattern, '[');
if (aPattern = nil) or (aPattern - Pattern > PMax) then
if aCaseInsensitive then
Search := SearchNoRangeU
else
Search := SearchNoRange
else
Search := SearchAny;
end;
end;
type // Holub and Durian (2005) SBNDM2 algorithm
// see http://www.cri.haifa.ac.il/events/2005/string/presentations/Holub.pdf
TSBNDMQ2Mask = array[AnsiChar] of cardinal;
PSBNDMQ2Mask = ^TSBNDMQ2Mask;
function SearchSBNDMQ2ComputeMask(const Pattern: RawUTF8; u: PNormTable): RawByteString;
var
i: PtrInt;
p: PAnsiChar absolute Pattern;
m: PSBNDMQ2Mask absolute result;
c: PCardinal;
begin
SetString(result, nil, SizeOf(m^));
FillCharFast(m^, SizeOf(m^), 0);
for i := 0 to length(Pattern) - 1 do begin
c := @m^[u[p[i]]]; // for FPC code generation
c^ := c^ or (1 shl i);
end;
end;
function SearchSBNDMQ2(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
var
mask: PSBNDMQ2Mask;
max, i, j: PtrInt;
state: cardinal;
begin
mask := pointer(aMatch^.Pattern); // was filled by SearchSBNDMQ2ComputeMask()
max := aMatch^.PMax;
i := max - 1;
dec(aTextLen);
if i < aTextLen then begin
repeat
state := mask[aText[i+1]] shr 1; // in two steps for better FPC codegen
state := state and mask[aText[i]];
if state = 0 then begin
inc(i, max); // fast skip
if i >= aTextLen then
break;
continue;
end;
j := i - max;
repeat
dec(i);
if i < 0 then
break;
state := (state shr 1) and mask[aText[i]];
until state = 0;
if i = j then begin
result := true;
exit;
end;
inc(i, max);
if i >= aTextLen then
break;
until false;
end;
result := false;
end;
function SearchSBNDMQ2U(aMatch: PMatch; aText: PUTF8Char; aTextLen: PtrInt): boolean;
var
u: PNormTable;
mask: PSBNDMQ2Mask;
max, i, j: PtrInt;
state: cardinal;
begin
mask := pointer(aMatch^.Pattern);
max := aMatch^.PMax;
u := aMatch^.Upper;
i := max - 1;
dec(aTextLen);
if i < aTextLen then begin
repeat
state := mask[u[aText[i+1]]] shr 1;
state := state and mask[u[aText[i]]];
if state = 0 then begin
inc(i, max);
if i >= aTextLen then
break;
continue;
end;
j := i - max;
repeat
dec(i);
if i < 0 then
break;
state := (state shr 1) and mask[u[aText[i]]];
until state = 0;
if i = j then begin
result := true;
exit;
end;
inc(i, max);
if i >= aTextLen then
break;
until false;
end;
result := false;
end;
procedure TMatch.PrepareContains(var aPattern: RawUTF8;
aCaseInsensitive: boolean);
begin
PMax := length(aPattern) - 1;
if aCaseInsensitive and not IsCaseSensitive(pointer(aPattern), PMax + 1) then
aCaseInsensitive := false;
if aCaseInsensitive then
Upper := @NormToUpperAnsi7
else
Upper := @NormToNorm;
if PMax < 0 then
Search := SearchContainsValid
else if PMax > 30 then
if aCaseInsensitive then
Search := SearchContainsU
else
Search := {$ifdef CPU64}SearchContains8{$else}SearchContains4{$endif}
else if PMax >= 1 then begin // len in [2..31] = PMax in [1..30]
aPattern := SearchSBNDMQ2ComputeMask(aPattern, Upper); // lookup table
if aCaseInsensitive then
Search := SearchSBNDMQ2U
else
Search := SearchSBNDMQ2;
end
else if aCaseInsensitive then
Search := SearchContainsU
else
Search := SearchContains1; // todo: use IndexByte() on FPC?
Pattern := pointer(aPattern);
end;
procedure TMatch.PrepareRaw(aPattern: PUTF8Char; aPatternLen: integer;
aSearch: TMatchSearchFunction);
begin
Pattern := aPattern;
PMax := aPatternLen - 1; // search in Pattern[0..PMax]
Search := aSearch;
end;
function TMatch.Match(const aText: RawUTF8): boolean;
begin
if aText <> '' then
result := Search(@self, pointer(aText), length(aText))
else
result := PMax < 0;
end;
function TMatch.Match(aText: PUTF8Char; aTextLen: PtrInt): boolean;
begin
if (aText <> nil) and (aTextLen > 0) then
result := Search(@self, aText, aTextLen)
else
result := PMax < 0;
end;
function TMatch.MatchThreadSafe(const aText: RawUTF8): boolean;
var local: TMatch; // thread-safe with no lock!
begin
local := self;
if aText <> '' then
result := local.Search(@local, pointer(aText), length(aText))
else
result := local.PMax < 0;
end;
function TMatch.MatchString(const aText: string): boolean;
var
local: TMatch; // thread-safe with no lock!
temp: TSynTempBuffer;
len: integer;
begin
if aText = '' then begin
result := PMax < 0;
exit;
end;
len := length(aText);
temp.Init(len * 3);
{$ifdef UNICODE}
len := RawUnicodeToUtf8(temp.buf, temp.len + 1, pointer(aText), len, [ccfNoTrailingZero]);
{$else}
len := CurrentAnsiConvert.AnsiBufferToUTF8(temp.buf, pointer(aText), len) - temp.buf;
{$endif}
local := self;
result := local.Search(@local, temp.buf, len);
temp.Done;
end;
function TMatch.Equals(const aAnother{$ifndef DELPHI5OROLDER}: TMatch{$endif}): boolean;
begin
result := (PMax = TMatch(aAnother).PMax) and (Upper = TMatch(aAnother).Upper) and
CompareMem(Pattern, TMatch(aAnother).Pattern, PMax + 1);
end;
function TMatch.PatternLength: integer;
begin
result := PMax + 1;
end;
function TMatch.PatternText: PUTF8Char;
begin
result := Pattern;
end;
function IsMatch(const Pattern, Text: RawUTF8; CaseInsensitive: boolean): boolean;
var match: TMatch;
begin
match.Prepare(Pattern, CaseInsensitive, {reuse=}false);
result := match.Match(Text);
end;
function IsMatchString(const Pattern, Text: string; CaseInsensitive: boolean): boolean;
var match: TMatch;
pat, txt: RawUTF8;
begin
StringToUTF8(Pattern, pat); // local variable is mandatory for FPC
StringToUTF8(Text, txt);
match.Prepare(pat, CaseInsensitive, {reuse=}false);
result := match.Match(txt);
end;
function SetMatchs(const CSVPattern: RawUTF8; CaseInsensitive: boolean;
out Match: TMatchDynArray): integer;
var P, S: PUTF8Char;
begin
result := 0;
P := pointer(CSVPattern);
if P <> nil then
repeat
S := P;
while not (P^ in [#0, ',']) do
inc(P);
if P <> S then begin
SetLength(Match, result + 1);
Match[result].Prepare(S, P - S, CaseInsensitive, {reuse=}true);
inc(result);
end;
if P^ = #0 then
break;
inc(P);
until false;
end;
function SetMatchs(CSVPattern: PUTF8Char; CaseInsensitive: boolean;
Match: PMatch; MatchMax: integer): integer;
var S: PUTF8Char;
begin
result := 0;
if (CSVPattern <> nil) and (MatchMax >= 0) then
repeat
S := CSVPattern;
while not (CSVPattern^ in [#0, ',']) do
inc(CSVPattern);
if CSVPattern <> S then begin
Match^.Prepare(S, CSVPattern - S, CaseInsensitive, {reuse=}true);
inc(result);
if result > MatchMax then
break;
inc(Match);
end;
if CSVPattern^ = #0 then
break;
inc(CSVPattern);
until false;
end;
function MatchExists(const One: TMatch; const Several: TMatchDynArray): boolean;
var
i: integer;
begin
result := true;
for i := 0 to high(Several) do
if Several[i].Equals(One) then
exit;
result := false;
end;
function MatchAdd(const One: TMatch; var Several: TMatchDynArray): boolean;
var
n: integer;
begin
result := not MatchExists(One, Several);
if result then begin
n := length(Several);
SetLength(Several, n + 1);
Several[n] := One;
end;
end;
function MatchAny(const Match: TMatchDynArray; const Text: RawUTF8): boolean;
var
m: PMatch;
i: integer;
begin
result := true;
if Match = nil then
exit;
m := pointer(Match);
for i := 1 to length(Match) do
if m^.Match(Text) then
exit
else
inc(m);
result := false;
end;
procedure FilterMatchs(const CSVPattern: RawUTF8; CaseInsensitive: boolean;
var Values: TRawUTF8DynArray);
var
match: TMatchDynArray;
m, n, i: integer;
begin
if SetMatchs(CSVPattern, CaseInsensitive, match) = 0 then
exit;
n := 0;
for i := 0 to high(Values) do
for m := 0 to high(match) do
if match[m].Match(Values[i]) then begin
if i <> n then
Values[n] := Values[i];
inc(n);
break;
end;
if n <> length(Values) then
SetLength(Values, n);
end;
{ TMatchs }
constructor TMatchs.Create(const aPatterns: TRawUTF8DynArray; CaseInsensitive: Boolean);
begin
inherited Create;
Subscribe(aPatterns, CaseInsensitive);
end;
function TMatchs.Match(const aText: RawUTF8): integer;
begin
result := Match(pointer(aText), length(aText));
end;
function TMatchs.Match(aText: PUTF8Char; aLen: integer): integer;
var
one: ^TMatchStore;
local: TMatch; // thread-safe with no lock!
begin
if (self = nil) or (fMatch = nil) then
result := -1 // no filter by name -> allow e.g. to process everything
else begin
one := pointer(fMatch);
if aLen <> 0 then begin
for result := 0 to fMatchCount - 1 do begin
local := one^.Pattern;
if local.Search(@local, aText, aLen) then
exit;
inc(one);
end;
end
else
for result := 0 to fMatchCount - 1 do
if one^.Pattern.PMax < 0 then
exit
else
inc(one);
result := -2;
end;
end;
function TMatchs.MatchString(const aText: string): integer;
var
temp: TSynTempBuffer;
len: integer;
begin
len := length(aText);
temp.Init(len * 3);
{$ifdef UNICODE}
len := RawUnicodeToUtf8(temp.buf, temp.len + 1, pointer(aText), len, [ccfNoTrailingZero]);
{$else}
len := CurrentAnsiConvert.AnsiBufferToUTF8(temp.buf, pointer(aText), len, true) - temp.buf;
{$endif}
result := Match(temp.buf, len);
temp.Done;
end;
procedure TMatchs.Subscribe(const aPatternsCSV: RawUTF8; CaseInsensitive: Boolean);
var
patterns: TRawUTF8DynArray;
begin
CSVToRawUTF8DynArray(pointer(aPatternsCSV), patterns);
Subscribe(patterns, CaseInsensitive);
end;
procedure TMatchs.Subscribe(const aPatterns: TRawUTF8DynArray; CaseInsensitive: Boolean);
var
i, j, m, n: integer;
found: ^TMatchStore;
pat: PRawUTF8;
begin
m := length(aPatterns);
if m = 0 then
exit;
n := fMatchCount;
SetLength(fMatch, n + m);
pat := pointer(aPatterns);
for i := 1 to m do begin
found := pointer(fMatch);
for j := 1 to n do
if StrComp(pointer(found^.PatternInstance), pointer(pat^)) = 0 then begin
found := nil;
break;
end
else
inc(found);
if found <> nil then
with fMatch[n] do begin
PatternInstance := pat^; // avoid GPF if aPatterns[] is released
Pattern.Prepare(PatternInstance, CaseInsensitive, {reuse=}true);
inc(n);
end;
inc(pat);
end;
fMatchCount := n;
if n <> length(fMatch) then
SetLength(fMatch, n);
end;
procedure SoundExComputeAnsi(var p: PAnsiChar; var result: cardinal; Values: PSoundExValues);
var n,v,old: PtrUInt;
begin
n := 0;
old := 0;
if Values<>nil then
repeat
{$ifdef USENORMTOUPPER}
v := NormToUpperByte[ord(p^)]; // also handle 8 bit WinAnsi (1252 accents)
{$else}
v := NormToUpperAnsi7Byte[ord(p^)]; // 7 bit char uppercase
{$endif}
2021-04-23 10:11:05 +02:00
if not (tcWord in TEXT_BYTES[v]) then break;
inc(p);
dec(v,ord('B'));
if v>high(TSoundExValues) then continue;
v := Values[v]; // get soundex value
if (v=0) or (v=old) then continue; // invalid or dopple value
old := v;
result := result shl SOUNDEX_BITS;
inc(result,v);
inc(n);
if n=((32-8)div SOUNDEX_BITS) then // first char use up to 8 bits
break; // result up to a cardinal size
until false;
end;
function SoundExComputeFirstCharAnsi(var p: PAnsiChar): cardinal;
label Err;
begin
if p=nil then begin
Err:result := 0;
exit;
end;
repeat
{$ifdef USENORMTOUPPER}
result := NormToUpperByte[ord(p^)]; // also handle 8 bit WinAnsi (CP 1252)
{$else}
result := NormToUpperAnsi7Byte[ord(p^)]; // 7 bit char uppercase
{$endif}
if result=0 then
goto Err; // end of input text, without a word
inc(p);
// trim initial spaces or 'H'
until AnsiChar(result) in ['A'..'G','I'..'Z'];
end;
procedure SoundExComputeUTF8(var U: PUTF8Char; var result: cardinal; Values: PSoundExValues);
var n,v,old: cardinal;
begin
n := 0;
old := 0;
if Values<>nil then
repeat
v := GetNextUTF8Upper(U);
2021-04-23 10:11:05 +02:00
if not (tcWord in TEXT_BYTES[v]) then break;
dec(v,ord('B'));
if v>high(TSoundExValues) then continue;
v := Values[v]; // get soundex value
if (v=0) or (v=old) then continue; // invalid or dopple value
old := v;
result := result shl SOUNDEX_BITS;
inc(result,v);
inc(n);
if n=((32-8)div SOUNDEX_BITS) then // first char use up to 8 bits
break; // result up to a cardinal size
until false;
end;
function SoundExComputeFirstCharUTF8(var U: PUTF8Char): cardinal;
label Err;
begin
if U=nil then begin
Err:result := 0;
exit;
end;
repeat
result := GetNextUTF8Upper(U);
if result=0 then
goto Err; // end of input text, without a word
// trim initial spaces or 'H'
until AnsiChar(result) in ['A'..'G','I'..'Z'];
end;
{ TSynSoundEx }
const
/// english Soundex pronunciation scores
// - defines the default values used for the SoundEx() function below
// (used if Values parameter is nil)
ValueEnglish: TSoundExValues =
// B C D E F G H I J K L M N O P Q R S T U V W X Y Z
(1,2,3,0,1,2,0,0,2,2,4,5,5,0,1,2,6,2,3,0,1,0,2,0,2);
/// french Soundex pronunciation scores
// - can be used to override default values used for the SoundEx()
// function below
ValueFrench: TSoundExValues =
// B C D E F G H I J K L M N O P Q R S T U V W X Y Z
(1,2,3,0,9,7,0,0,7,2,4,5,5,0,1,2,6,8,3,0,9,0,8,0,8);
/// spanish Soundex pronunciation scores
// - can be used to override default values used for the SoundEx()
// function below
ValueSpanish: TSoundExValues =
// B C D E F G H I J K L M N O P Q R S T U V W X Y Z
(1,2,3,0,1,2,0,0,0,2,0,5,5,0,1,2,6,2,3,0,1,0,2,0,2);
SOUNDEXVALUES: array[TSynSoundExPronunciation] of PSoundExValues =
(@ValueEnglish,@ValueFrench,@ValueSpanish,@ValueEnglish);
function TSynSoundEx.Ansi(A: PAnsiChar): boolean;
var Value, c: cardinal;
begin
result := false;
if A=nil then exit;
repeat
// test beginning of word
c := SoundExComputeFirstCharAnsi(A);
if c=0 then exit else
if c=FirstChar then begin
// here we had the first char match -> check if word match UpperValue
Value := c-(ord('A')-1);
SoundExComputeAnsi(A,Value,fValues);
if Value=search then begin
result := true; // UpperValue found!
exit;
end;
end else
repeat
if A^=#0 then exit else
{$ifdef USENORMTOUPPER}
2021-04-23 10:11:05 +02:00
if not(tcWord in TEXT_CHARS[NormToUpper[A^]]) then break else inc(A);
{$else} if not(tcWord in TEXT_CHARS[A^]) then break else inc(A); {$endif}
until false;
// find beginning of next word
repeat
if A^=#0 then exit else
{$ifdef USENORMTOUPPER}
2021-04-23 10:11:05 +02:00
if tcWord in TEXT_CHARS[NormToUpper[A^]] then break else inc(A);
{$else} if tcWord in TEXT_CHARS[A^] then break else inc(A); {$endif}
until false;
until false;
end;
function TSynSoundEx.UTF8(U: PUTF8Char): boolean;
var Value, c: cardinal;
V: PUTF8Char;
begin
result := false;
if U=nil then exit;
repeat
// find beginning of word
c := SoundExComputeFirstCharUTF8(U);
if c=0 then exit else
if c=FirstChar then begin
// here we had the first char match -> check if word match UpperValue
Value := c-(ord('A')-1);
SoundExComputeUTF8(U,Value,fValues);
if Value=search then begin
result := true; // UpperValue found!
exit;
end;
end else
repeat
c := GetNextUTF8Upper(U);
if c=0 then
exit;
2021-04-23 10:11:05 +02:00
until not(tcWord in TEXT_BYTES[c]);
// find beginning of next word
repeat
if U=nil then exit;
V := U;
c := GetNextUTF8Upper(U);
if c=0 then
exit;
2021-04-23 10:11:05 +02:00
until tcWord in TEXT_BYTES[c];
U := V;
until U=nil;
end;
function TSynSoundEx.Prepare(UpperValue: PAnsiChar; Lang: PSoundExValues): boolean;
begin
fValues := Lang;
Search := SoundExAnsi(UpperValue,nil,Lang);
if Search=0 then
result := false else begin
FirstChar := SoundExComputeFirstCharAnsi(UpperValue);
result := true;
end;
end;
function TSynSoundEx.Prepare(UpperValue: PAnsiChar; Lang: TSynSoundExPronunciation): boolean;
begin
result := Prepare(UpperValue,SOUNDEXVALUES[Lang]);
end;
function SoundExAnsi(A: PAnsiChar; next: PPAnsiChar;
Lang: PSoundExValues): cardinal;
begin
result := SoundExComputeFirstCharAnsi(A);
if result<>0 then begin
dec(result,ord('A')-1); // first Soundex char is first char
SoundExComputeAnsi(A,result,Lang);
end;
if next<>nil then begin
{$ifdef USENORMTOUPPER}
2021-04-23 10:11:05 +02:00
while tcWord in TEXT_CHARS[NormToUpper[A^]] do inc(A); // go to end of word
{$else}
2021-04-23 10:11:05 +02:00
while tcWord in TEXT_CHARS[A^] do inc(A); // go to end of word
{$endif}
next^ := A;
end;
end;
function SoundExAnsi(A: PAnsiChar; next: PPAnsiChar;
Lang: TSynSoundExPronunciation): cardinal;
begin
result := SoundExAnsi(A,next,SOUNDEXVALUES[Lang]);
end;
function SoundExUTF8(U: PUTF8Char; next: PPUTF8Char;
Lang: TSynSoundExPronunciation): cardinal;
begin
result := SoundExComputeFirstCharUTF8(U);
if result<>0 then begin
dec(result,ord('A')-1); // first Soundex char is first char
SoundExComputeUTF8(U,result,SOUNDEXVALUES[Lang]);
end;
if next<>nil then
next^ := FindNextUTF8WordBegin(U);
end;
{ ************ filtering and validation classes and functions *************** }
function IPToCardinal(P: PUTF8Char; out aValue: cardinal): boolean;
var i,c: cardinal;
b: array[0..3] of byte;
begin
aValue := 0;
result := false;
if (P=nil) or (IdemPChar(P,'127.0.0.1') and (P[9]=#0)) then
exit;
for i := 0 to 3 do begin
c := GetNextItemCardinal(P,'.');
if (c>255) or ((P=nil) and (i<3)) then
exit;
b[i] := c;
end;
if PCardinal(@b)^<>$0100007f then begin
aValue := PCardinal(@b)^;
result := true;
end;
end;
function IPToCardinal(const aIP: RawUTF8; out aValue: cardinal): boolean;
begin
result := IPToCardinal(pointer(aIP),aValue);
end;
function IPToCardinal(const aIP: RawUTF8): cardinal;
begin
IPToCardinal(pointer(aIP),result);
end;
function IsValidIP4Address(P: PUTF8Char): boolean;
var ndot: PtrInt;
V: PtrUInt;
begin
result := false;
if (P=nil) or not (P^ in ['0'..'9']) then
exit;
V := 0;
ndot := 0;
repeat
case P^ of
#0: break;
'.': if (P[-1]='.') or (V>255) then
exit else begin
inc(ndot);
V := 0;
end;
'0'..'9': V := (V*10)+ord(P^)-48;
else exit;
end;
inc(P);
until false;
if (ndot=3) and (V<=255) and (P[-1]<>'.') then
result := true;
end;
function IsValidEmail(P: PUTF8Char): boolean;
// Initial Author: Ernesto D'Spirito - UTF-8 version by AB
// http://www.howtodothings.com/computers/a1169-validating-email-addresses-in-delphi.html
const
// Valid characters in an "atom"
atom_chars: TSynAnsicharSet = [#33..#255] -
['(', ')', '<', '>', '@', ',', ';', ':', '\', '/', '"', '.', '[', ']', #127];
// Valid characters in a "quoted-string"
quoted_string_chars: TSynAnsicharSet = [#0..#255] - ['"', #13, '\'];
// Valid characters in a subdomain
letters_digits: TSynAnsicharSet = ['0'..'9', 'A'..'Z', 'a'..'z'];
type
States = (STATE_BEGIN, STATE_ATOM, STATE_QTEXT, STATE_QCHAR,
STATE_QUOTE, STATE_LOCAL_PERIOD, STATE_EXPECTING_SUBDOMAIN,
STATE_SUBDOMAIN, STATE_HYPHEN);
var
State: States;
subdomains: integer;
c: AnsiChar;
ch: PtrInt;
begin
State := STATE_BEGIN;
subdomains := 1;
if P<>nil then
repeat
ch := ord(P^);
if ch and $80=0 then
inc(P) else
ch := GetHighUTF8UCS4(P);
if (ch<=255) and (WinAnsiConvert.AnsiToWide[ch]<=255) then
// convert into WinAnsi char
c := AnsiChar(ch) else
// invalid char
c := #127;
case State of
STATE_BEGIN:
if c in atom_chars then
State := STATE_ATOM else
if c='"' then
State := STATE_QTEXT else
break;
STATE_ATOM:
if c='@' then
State := STATE_EXPECTING_SUBDOMAIN else
if c='.' then
State := STATE_LOCAL_PERIOD else
if not (c in atom_chars) then
break;
STATE_QTEXT:
if c='\' then
State := STATE_QCHAR else
if c='"' then
State := STATE_QUOTE else
if not (c in quoted_string_chars) then
break;
STATE_QCHAR:
State := STATE_QTEXT;
STATE_QUOTE:
if c='@' then
State := STATE_EXPECTING_SUBDOMAIN else
if c='.' then
State := STATE_LOCAL_PERIOD else
break;
STATE_LOCAL_PERIOD:
if c in atom_chars then
State := STATE_ATOM else
if c='"' then
State := STATE_QTEXT else
break;
STATE_EXPECTING_SUBDOMAIN:
if c in letters_digits then
State := STATE_SUBDOMAIN else
break;
STATE_SUBDOMAIN:
if c='.' then begin
inc(subdomains);
State := STATE_EXPECTING_SUBDOMAIN
end else
if c='-' then
State := STATE_HYPHEN else
if not (c in letters_digits) then
break;
STATE_HYPHEN:
if c in letters_digits then
State := STATE_SUBDOMAIN else
if c<>'-' then
break;
end;
if P^=#0 then begin
P := nil;
break;
end;
until false;
result := (State = STATE_SUBDOMAIN) and (subdomains >= 2);
end;
{ TSynFilterOrValidate }
constructor TSynFilterOrValidate.Create(const aParameters: RawUTF8);
begin
inherited Create;
SetParameters(aParameters); // should parse the JSON-encoded parameters
end;
constructor TSynFilterOrValidate.CreateUTF8(const Format: RawUTF8;
const Args, Params: array of const);
begin
Create(FormatUTF8(Format,Args,Params,true));
end;
procedure TSynFilterOrValidate.SetParameters(const value: RawUTF8);
begin
fParameters := value;
end;
function TSynFilterOrValidate.AddOnce(var aObjArray: TSynFilterOrValidateObjArray;
aFreeIfAlreadyThere: boolean): TSynFilterOrValidate;
var i: integer;
begin
if self<>nil then begin
for i := 0 to length(aObjArray)-1 do
if (PPointer(aObjArray[i])^=PPointer(self)^) and
(aObjArray[i].fParameters=fParameters) then begin
if aFreeIfAlreadyThere then
Free;
result := aObjArray[i];
exit;
end;
ObjArrayAdd(aObjArray,self);
end;
result := self;
end;
{ TSynFilterUpperCase }
procedure TSynFilterUpperCase.Process(aFieldIndex: integer; var value: RawUTF8);
begin
value := SynCommons.UpperCase(value);
end;
{ TSynFilterUpperCaseU }
procedure TSynFilterUpperCaseU.Process(aFieldIndex: integer; var value: RawUTF8);
begin
value := UpperCaseU(value);
end;
{ TSynFilterLowerCase }
procedure TSynFilterLowerCase.Process(aFieldIndex: integer; var value: RawUTF8);
begin
value := LowerCase(value);
end;
{ TSynFilterLowerCaseU }
procedure TSynFilterLowerCaseU.Process(aFieldIndex: integer; var value: RawUTF8);
begin
value := LowerCaseU(value);
end;
{ TSynFilterTrim }
procedure TSynFilterTrim.Process(aFieldIndex: integer; var value: RawUTF8);
begin
value := Trim(value);
end;
{ TSynFilterTruncate}
procedure TSynFilterTruncate.SetParameters(const value: RawUTF8);
var V: array[0..1] of TValuePUTF8Char;
tmp: TSynTempBuffer;
begin
tmp.Init(value);
JSONDecode(tmp.buf,['MaxLength','UTF8Length'],@V);
fMaxLength := GetCardinalDef(V[0].Value,0);
fUTF8Length := V[1].Idem('1') or V[1].Idem('true');
tmp.Done;
end;
procedure TSynFilterTruncate.Process(aFieldIndex: integer; var value: RawUTF8);
begin
if fMaxLength-1<cardinal(maxInt) then
if fUTF8Length then
Utf8TruncateToLength(value,fMaxLength) else
Utf8TruncateToUnicodeLength(value,fMaxLength);
end;
{ TSynValidateIPAddress }
function TSynValidateIPAddress.Process(aFieldIndex: integer; const value: RawUTF8;
var ErrorMsg: string): boolean;
begin
result := IsValidIP4Address(pointer(value));
if not result then
ErrorMsg := Format(sInvalidIPAddress,[UTF8ToString(value)]);
end;
{ TSynValidateEmail }
function TSynValidateEmail.Process(aFieldIndex: integer; const value: RawUTF8;
var ErrorMsg: string): boolean;
var TLD,DOM: RawUTF8;
i: integer;
const TopLevelTLD: array[0..19] of PUTF8Char = (
// see http://en.wikipedia.org/wiki/List_of_Internet_top-level_domains
'aero','asia','biz','cat','com','coop','edu','gov','info','int','jobs',
'mil','mobi','museum','name','net','org','pro','tel','travel'); // no xxx !
begin
if IsValidEmail(pointer(value)) then
repeat
2021-04-23 10:11:05 +02:00
DOM := lowercase(copy(value,PosExChar('@',value)+1,100));
if length(DOM)>63 then
break; // exceeded 63-character limit of a DNS name
if (ForbiddenDomains<>'') and (FindCSVIndex(pointer(ForbiddenDomains),DOM)>=0) then
break;
i := length(value);
while (i>0) and (value[i]<>'.') do dec(i);
TLD := lowercase(copy(value,i+1,100));
if (AllowedTLD<>'') and (FindCSVIndex(pointer(AllowedTLD),TLD)<0) then
break;
if (ForbiddenTLD<>'') and (FindCSVIndex(pointer(ForbiddenTLD),TLD)>=0) then
break;
if not fAnyTLD then
if FastFindPUTF8CharSorted(@TopLevelTLD,high(TopLevelTLD),pointer(TLD))<0 then
if length(TLD)<>2 then
break; // assume a two chars string is a ISO 3166-1 alpha-2 code
result := true;
exit;
until true;
ErrorMsg := Format(sInvalidEmailAddress,[UTF8ToString(value)]);
result := false;
end;
procedure TSynValidateEmail.SetParameters(const value: RawUTF8);
var V: array[0..3] of TValuePUTF8Char;
tmp: TSynTempBuffer;
begin
inherited;
tmp.Init(value);
JSONDecode(tmp.buf,['AllowedTLD','ForbiddenTLD','ForbiddenDomains','AnyTLD'],@V);
LowerCaseCopy(V[0].Value,V[0].ValueLen,fAllowedTLD);
LowerCaseCopy(V[1].Value,V[1].ValueLen,fForbiddenTLD);
LowerCaseCopy(V[2].Value,V[2].ValueLen,fForbiddenDomains);
AnyTLD := V[3].Idem('1') or V[3].Idem('true');
tmp.Done;
end;
{ TSynValidatePattern }
procedure TSynValidatePattern.SetParameters(const Value: RawUTF8);
begin
inherited SetParameters(Value);
fMatch.Prepare(Value, ClassType=TSynValidatePatternI, {reuse=}true);
end;
function TSynValidatePattern.Process(aFieldIndex: integer; const value: RawUTF8;
var ErrorMsg: string): boolean;
procedure SetErrorMsg;
begin
ErrorMsg := Format(sInvalidPattern,[UTF8ToString(value)]);
end;
begin
result := fMatch.Match(value);
if not result then
SetErrorMsg;
end;
{ TSynValidateNonVoidText }
function Character01n(n: integer): string;
begin
if n<0 then
n := 0 else
if n>1 then
n := 2;
result := GetCSVItemString(pointer(string(sCharacter01n)),n);
end;
procedure InvalidTextLengthMin(min: integer; var result: string);
begin
result := Format(sInvalidTextLengthMin,[min,Character01n(min)]);
end;
function TSynValidateNonVoidText.Process(aFieldIndex: integer; const value: RawUTF8;
var ErrorMsg: string): boolean;
begin
if value='' then begin
InvalidTextLengthMin(1,ErrorMsg);
result := false;
end else
result := true;
end;
{ TSynValidateText }
procedure TSynValidateText.SetErrorMsg(fPropsIndex, InvalidTextIndex,
MainIndex: integer; var result: string);
var P: PChar;
begin
P := pointer(string(sInvalidTextChar));
result := GetCSVItemString(P,MainIndex);
if fPropsIndex>0 then
result := Format(result,
[fProps[fPropsIndex],GetCSVItemString(P,InvalidTextIndex),
Character01n(fProps[fPropsIndex])]);
end;
function TSynValidateText.Process(aFieldIndex: integer; const value: RawUTF8;
var ErrorMsg: string): boolean;
var i, L: cardinal;
Min: array[2..7] of cardinal;
begin
result := false;
if fUTF8Length then
L := length(value) else
L := Utf8ToUnicodeLength(pointer(value));
if L<MinLength then
InvalidTextLengthMin(MinLength,ErrorMsg) else
if L>MaxLength then
ErrorMsg := Format(sInvalidTextLengthMax,[MaxLength,Character01n(MaxLength)]) else begin
FillCharFast(Min,SizeOf(Min),0);
L := length(value);
for i := 1 to L do
case value[i] of
' ':
inc(Min[7]);
'a'..'z': begin
inc(Min[2]);
inc(Min[5]);
end;
'A'..'Z': begin
inc(Min[2]);
inc(Min[6]);
end;
'0'..'9':
inc(Min[3]);
'_','!',';','.',',','/',':','?','%','$','=','"','#','@','(',')','{','}',
'+','''','-','*':
inc(Min[4]);
end;
for i := 2 to 7 do
if Min[i]<fProps[i] then begin
SetErrorMsg(i,i,0,ErrorMsg);
exit;
end else
if Min[i]>fProps[i+8] then begin
SetErrorMsg(i+8,i,1,ErrorMsg);
exit;
end;
if value<>'' then begin
if MaxLeftTrimCount<cardinal(maxInt) then begin
// if MaxLeftTrimCount is set, check against Value
i := 0;
while (i<L) and (value[i+1]=' ') do inc(i);
if i>MaxLeftTrimCount then begin
SetErrorMsg(0,0,8,ErrorMsg);
exit;
end;
end;
if MaxRightTrimCount<cardinal(maxInt) then begin
// if MaxRightTrimCount is set, check against Value
i := 0;
while (i<L) and (value[L-i]=' ') do dec(i);
if i>MaxRightTrimCount then begin
SetErrorMsg(0,0,9,ErrorMsg);
exit;
end;
end;
end;
result := true;
end;
end;
procedure TSynValidateText.SetParameters(const value: RawUTF8);
var V: array[0..high(TSynValidateTextProps)+1] of TValuePUTF8Char;
i: integer;
tmp: TSynTempBuffer;
const DEFAULT: TSynValidateTextProps = (
1,maxInt,0,0,0,0,0,0,maxInt,maxInt,maxInt,maxInt,maxInt,maxInt,maxInt,maxInt);
begin
if (MinLength=0) and (MaxLength=0) then // if not previously set
fProps := DEFAULT;
inherited SetParameters(value);
if value='' then
exit;
tmp.Init(value);
try
JSONDecode(tmp.buf,['MinLength','MaxLength',
'MinAlphaCount','MinDigitCount','MinPunctCount',
'MinLowerCount','MinUpperCount','MinSpaceCount',
'MaxLeftTrimCount','MaxRightTrimCount',
'MaxAlphaCount','MaxDigitCount','MaxPunctCount',
'MaxLowerCount','MaxUpperCount','MaxSpaceCount',
'UTF8Length'],@V);
for i := 0 to high(fProps) do
fProps[i] := GetCardinalDef(V[i].Value,fProps[i]);
with V[high(V)] do
fUTF8Length := Idem('1') or Idem('true');
finally
tmp.Done;
end;
end;
{ TSynValidatePassWord }
procedure TSynValidatePassWord.SetParameters(const value: RawUTF8);
const DEFAULT: TSynValidateTextProps = (
5,20,1,1,1,1,1,0,maxInt,maxInt,maxInt,maxInt,maxInt,maxInt,maxInt,0);
begin
// set default values for validating a strong password
fProps := DEFAULT;
// read custom parameters
inherited;
end;
{ ************ low-level buffer processing functions************************* }
{ TSynBloomFilter }
const
BLOOM_VERSION = 0;
BLOOM_MAXHASH = 32; // only 7 is needed for 1% false positive ratio
constructor TSynBloomFilter.Create(aSize: integer; aFalsePositivePercent: double);
const LN2 = 0.69314718056;
begin
inherited Create;
if aSize < 0 then
fSize := 1000 else
fSize := aSize;
if aFalsePositivePercent<=0 then
fFalsePositivePercent := 1 else
if aFalsePositivePercent>100 then
fFalsePositivePercent := 100 else
fFalsePositivePercent := aFalsePositivePercent;
// see http://stackoverflow.com/a/22467497
fBits := Round(-ln(fFalsePositivePercent/100)*aSize/(LN2*LN2));
fHashFunctions := Round(fBits/fSize*LN2);
if fHashFunctions=0 then
fHashFunctions := 1 else
if fHashFunctions>BLOOM_MAXHASH then
fHashFunctions := BLOOM_MAXHASH;
Reset;
end;
constructor TSynBloomFilter.Create(const aSaved: RawByteString; aMagic: cardinal);
begin
inherited Create;
if not LoadFrom(aSaved,aMagic) then
raise ESynException.CreateUTF8('%.Create with invalid aSaved content',[self]);
end;
procedure TSynBloomFilter.Insert(const aValue: RawByteString);
begin
Insert(pointer(aValue),length(aValue));
end;
procedure TSynBloomFilter.Insert(aValue: pointer; aValueLen: integer);
var h: integer;
h1,h2: cardinal; // https://goo.gl/Pls5wi
begin
if (self=nil) or (aValueLen<=0) or (fBits=0) then
exit;
h1 := crc32c(0,aValue,aValueLen);
if fHashFunctions=1 then
h2 := 0 else
h2 := crc32c(h1,aValue,aValueLen);
Safe.Lock;
try
for h := 0 to fHashFunctions-1 do begin
SetBitPtr(pointer(fStore),h1 mod fBits);
inc(h1,h2);
end;
inc(fInserted);
finally
Safe.UnLock;
end;
end;
function TSynBloomFilter.GetInserted: cardinal;
begin
Safe.Lock;
try
result := fInserted; // Delphi 5 does not support LockedInt64[]
finally
Safe.UnLock;
end;
end;
function TSynBloomFilter.MayExist(const aValue: RawByteString): boolean;
begin
result := MayExist(pointer(aValue),length(aValue));
end;
function TSynBloomFilter.MayExist(aValue: pointer; aValueLen: integer): boolean;
var h: integer;
h1,h2: cardinal; // https://goo.gl/Pls5wi
begin
result := false;
if (self=nil) or (aValueLen<=0) or (fBits=0) then
exit;
h1 := crc32c(0,aValue,aValueLen);
if fHashFunctions=1 then
h2 := 0 else
h2 := crc32c(h1,aValue,aValueLen);
Safe.Lock;
try
for h := 0 to fHashFunctions-1 do
if GetBitPtr(pointer(fStore),h1 mod fBits) then
inc(h1,h2) else
exit;
finally
Safe.UnLock;
end;
result := true;
end;
procedure TSynBloomFilter.Reset;
begin
Safe.Lock;
try
if fStore='' then
SetLength(fStore,(fBits shr 3)+1);
FillcharFast(pointer(fStore)^,length(fStore),0);
fInserted := 0;
finally
Safe.UnLock;
end;
end;
function TSynBloomFilter.SaveTo(aMagic: cardinal): RawByteString;
var W: TFileBufferWriter;
BufLen: integer;
temp: array[word] of byte;
begin
BufLen := length(fStore)+100;
if BufLen<=SizeOf(temp) then
W := TFileBufferWriter.Create(TRawByteStringStream,@temp,SizeOf(temp)) else
W := TFileBufferWriter.Create(TRawByteStringStream,BufLen);
try
SaveTo(W,aMagic);
W.Flush;
result := TRawByteStringStream(W.Stream).DataString;
finally
W.Free;
end;
end;
procedure TSynBloomFilter.SaveTo(aDest: TFileBufferWriter; aMagic: cardinal=$B1003F11);
begin
aDest.Write4(aMagic);
aDest.Write1(BLOOM_VERSION);
Safe.Lock;
try
aDest.Write8(fFalsePositivePercent);
aDest.Write4(fSize);
aDest.Write4(fBits);
aDest.Write1(fHashFunctions);
aDest.Write4(fInserted);
ZeroCompress(pointer(fStore),Length(fStore),aDest);
finally
Safe.UnLock;
end;
end;
function TSynBloomFilter.LoadFrom(const aSaved: RawByteString; aMagic: cardinal): boolean;
begin
result := LoadFrom(pointer(aSaved),length(aSaved));
end;
function TSynBloomFilter.LoadFrom(P: PByte; PLen: integer; aMagic: cardinal): boolean;
var start: PByte;
version: integer;
begin
result := false;
start := P;
if (P=nil) or (PLen<32) or (PCardinal(P)^<>aMagic) then
exit;
inc(P,4);
version := P^; inc(P);
if version>BLOOM_VERSION then
exit;
Safe.Lock;
try
fFalsePositivePercent := unaligned(PDouble(P)^); inc(P,8);
if (fFalsePositivePercent<=0) or (fFalsePositivePercent>100) then
exit;
fSize := PCardinal(P)^; inc(P,4);
fBits := PCardinal(P)^; inc(P,4);
if fBits<fSize then
exit;
fHashFunctions := P^; inc(P);
if fHashFunctions-1>=BLOOM_MAXHASH then
exit;
Reset;
fInserted := PCardinal(P)^; inc(P,4);
ZeroDecompress(P,PLen-(PAnsiChar(P)-PAnsiChar(start)),fStore);
result := length(fStore)=integer(fBits shr 3)+1;
finally
Safe.UnLock;
end;
end;
{ TSynBloomFilterDiff }
type
TBloomDiffHeader = packed record
kind: (bdDiff,bdFull,bdUpToDate);
size: cardinal;
inserted: cardinal;
revision: Int64;
crc: cardinal;
end;
procedure TSynBloomFilterDiff.Insert(aValue: pointer; aValueLen: integer);
begin
Safe.Lock;
try
inherited Insert(aValue,aValueLen);
inc(fRevision);
inc(fSnapshotInsertCount);
finally
Safe.UnLock;
end;
end;
procedure TSynBloomFilterDiff.Reset;
begin
Safe.Lock;
try
inherited Reset;
fSnapshotAfterInsertCount := fSize shr 5;
fSnapShotAfterMinutes := 30;
fSnapshotTimestamp := 0;
fSnapshotInsertCount := 0;
fRevision := UnixTimeUTC shl 31;
fKnownRevision := 0;
fKnownStore := '';
finally
Safe.UnLock;
end;
end;
procedure TSynBloomFilterDiff.DiffSnapshot;
begin
Safe.Lock;
try
fKnownRevision := fRevision;
fSnapshotInsertCount := 0;
SetString(fKnownStore,PAnsiChar(pointer(fStore)),length(fStore));
if fSnapShotAfterMinutes=0 then
fSnapshotTimestamp := 0 else
fSnapshotTimestamp := GetTickCount64+fSnapShotAfterMinutes*60000;
finally
Safe.UnLock;
end;
end;
function TSynBloomFilterDiff.SaveToDiff(const aKnownRevision: Int64): RawByteString;
var head: TBloomDiffHeader;
W: TFileBufferWriter;
temp: array[word] of byte;
begin
Safe.Lock;
try
if aKnownRevision=fRevision then
head.kind := bdUpToDate else
if (fKnownRevision=0) or
(fSnapshotInsertCount>fSnapshotAfterInsertCount) or
((fSnapshotInsertCount>0) and (fSnapshotTimestamp<>0) and
(GetTickCount64>fSnapshotTimestamp)) then begin
DiffSnapshot;
head.kind := bdFull;
end else
if (aKnownRevision<fKnownRevision) or (aKnownRevision>fRevision) then
head.kind := bdFull else
head.kind := bdDiff;
head.size := length(fStore);
head.inserted := fInserted;
head.revision := fRevision;
head.crc := crc32c(0,@head,SizeOf(head)-SizeOf(head.crc));
if head.kind=bdUpToDate then begin
SetString(result,PAnsiChar(@head),SizeOf(head));
exit;
end;
if head.size+100<=SizeOf(temp) then
W := TFileBufferWriter.Create(TRawByteStringStream,@temp,SizeOf(temp)) else
W := TFileBufferWriter.Create(TRawByteStringStream,head.size+100);
try
W.Write(@head,SizeOf(head));
case head.kind of
bdFull:
SaveTo(W);
bdDiff:
ZeroCompressXor(pointer(fStore),pointer(fKnownStore),head.size,W);
end;
W.Flush;
result := TRawByteStringStream(W.Stream).DataString;
finally
W.Free;
end;
finally
Safe.UnLock;
end;
end;
function TSynBloomFilterDiff.DiffKnownRevision(const aDiff: RawByteString): Int64;
var head: ^TBloomDiffHeader absolute aDiff;
begin
if (length(aDiff)<SizeOf(head^)) or (head.kind>high(head.kind)) or
(head.size<>cardinal(length(fStore))) or
(head.crc<>crc32c(0,pointer(head),SizeOf(head^)-SizeOf(head.crc))) then
result := 0 else
result := head.Revision;
end;
function TSynBloomFilterDiff.LoadFromDiff(const aDiff: RawByteString): boolean;
var head: ^TBloomDiffHeader absolute aDiff;
P: PByte;
PLen: integer;
begin
result := false;
P := pointer(aDiff);
PLen := length(aDiff);
if (PLen<SizeOf(head^)) or (head.kind>high(head.kind)) or
(head.crc<>crc32c(0,pointer(head),SizeOf(head^)-SizeOf(head.crc))) then
exit;
if (fStore<>'') and (head.size<>cardinal(length(fStore))) then
exit;
inc(P,SizeOf(head^));
dec(PLen,SizeOf(head^));
Safe.Lock;
try
case head.kind of
bdFull:
result := LoadFrom(P,PLen);
bdDiff:
if fStore<>'' then
result := ZeroDecompressOr(pointer(P),Pointer(fStore),PLen,head.size);
bdUpToDate:
result := true;
end;
if result then begin
fRevision := head.revision;
fInserted := head.inserted;
end;
finally
Safe.UnLock;
end;
end;
procedure ZeroCompress(P: PAnsiChar; Len: integer; Dest: TFileBufferWriter);
var PEnd,beg,zero: PAnsiChar;
crc: cardinal;
begin
Dest.WriteVarUInt32(Len);
PEnd := P+Len;
beg := P;
crc := 0;
while P<PEnd do begin
while (P^<>#0) and (P<PEnd) do inc(P);
zero := P;
while (P^=#0) and (P<PEnd) do inc(P);
if P-zero>3 then begin
Len := zero-beg;
crc := crc32c(crc,beg,Len);
Dest.WriteVarUInt32(Len);
Dest.Write(beg,Len);
Len := P-zero;
crc := crc32c(crc,@Len,SizeOf(Len));
Dest.WriteVarUInt32(Len-3);
beg := P;
end;
end;
Len := P-beg;
if Len>0 then begin
crc := crc32c(crc,beg,Len);
Dest.WriteVarUInt32(Len);
Dest.Write(beg,Len);
end;
Dest.Write4(crc);
end;
procedure ZeroCompressXor(New,Old: PAnsiChar; Len: cardinal; Dest: TFileBufferWriter);
var beg,same,index,crc,L: cardinal;
begin
Dest.WriteVarUInt32(Len);
beg := 0;
index := 0;
crc := 0;
while index<Len do begin
while (New[index]<>Old[index]) and (index<Len) do inc(index);
same := index;
while (New[index]=Old[index]) and (index<Len) do inc(index);
L := index-same;
if L>3 then begin
Dest.WriteVarUInt32(same-beg);
Dest.WriteXor(New+beg,Old+beg,same-beg,@crc);
crc := crc32c(crc,@L,SizeOf(L));
Dest.WriteVarUInt32(L-3);
beg := index;
end;
end;
L := index-beg;
if L>0 then begin
Dest.WriteVarUInt32(L);
Dest.WriteXor(New+beg,Old+beg,L,@crc);
end;
Dest.Write4(crc);
end;
procedure ZeroDecompress(P: PByte; Len: integer; {$ifdef FPC}var{$else}out{$endif} Dest: RawByteString);
var PEnd,D,DEnd: PAnsiChar;
DestLen,crc: cardinal;
begin
PEnd := PAnsiChar(P)+Len-4;
DestLen := FromVarUInt32(P);
SetString(Dest,nil,DestLen); // FPC uses var
D := pointer(Dest);
DEnd := D+DestLen;
crc := 0;
while PAnsiChar(P)<PEnd do begin
Len := FromVarUInt32(P);
if D+Len>DEnd then
break;
MoveFast(P^,D^,Len);
crc := crc32c(crc,D,Len);
inc(P,Len);
inc(D,Len);
if PAnsiChar(P)>=PEnd then
break;
Len := FromVarUInt32(P)+3;
if D+Len>DEnd then
break;
FillCharFast(D^,Len,0);
crc := crc32c(crc,@Len,SizeOf(Len));
inc(D,Len);
end;
if crc<>PCardinal(P)^ then
Dest := '';
end;
function ZeroDecompressOr(P,Dest: PAnsiChar; Len,DestLen: integer): boolean;
var PEnd,DEnd: PAnsiChar;
crc: cardinal;
begin
PEnd := P+Len-4;
if cardinal(DestLen)<>FromVarUInt32(PByte(P)) then begin
result := false;
exit;
end;
DEnd := Dest+DestLen;
crc := 0;
while (P<PEnd) and (Dest<DEnd) do begin
Len := FromVarUInt32(PByte(P));
if Dest+Len>DEnd then
break;
crc := crc32c(crc,P,Len);
OrMemory(pointer(Dest),pointer(P),Len);
inc(P,Len);
inc(Dest,Len);
if P>=PEnd then
break;
Len := FromVarUInt32(PByte(P))+3;
crc := crc32c(crc,@Len,SizeOf(Len));
inc(Dest,Len);
end;
result := crc=PCardinal(P)^;
end;
function Max(a,b: PtrInt): PtrInt; {$ifdef HASINLINE}inline;{$endif}
begin
if a > b then
result := a else
result := b;
end;
function Min(a,b: PtrInt): PtrInt; {$ifdef HASINLINE}inline;{$endif}
begin
if a < b then
result := a else
result := b;
end;
function Comp(a,b: PAnsiChar; len: PtrInt): PtrInt;
{$ifdef HASINLINE} inline;
var lenptr: PtrInt;
begin
result := 0;
lenptr := len-SizeOf(PtrInt);
if lenptr>=0 then
repeat
if PPtrInt(a+result)^<>PPtrInt(b+result)^ then
break;
inc(result,SizeOf(PtrInt));
until result>lenptr;
if result<len then
repeat
if a[result]<>b[result] then
exit;
inc(result);
until result=len;
end;
{$else} // eax = a, edx = b, ecx = len
asm // the 'rep cmpsb' version is slower on Intel Core CPU (not AMD)
or ecx,ecx
push ebx
push ecx
jz @ok
@1: mov bx,[eax]
lea eax,[eax+2]
cmp bl,[edx]
jne @ok
dec ecx
jz @ok
cmp bh,[edx+1]
lea edx,[edx+2]
jne @ok
dec ecx
jnz @1
@ok: pop eax
sub eax,ecx
pop ebx
end;
{$endif}
function CompReverse(a,b: pointer; len: PtrInt): PtrInt;
begin
result := 0;
if len>0 then
repeat
if PByteArray(a)[-result]<>PByteArray(b)[-result] then
exit;
inc(result);
until result=len;
end;
procedure movechars(s,d: PAnsiChar; t: PtrUInt);
{$ifdef HASINLINE}inline;{$endif}
// this code is sometimes used rather than MoveFast() for overlapping copy
begin
dec(PtrUInt(s), PtrUInt(d));
inc(t, PtrUInt(d));
repeat
d^ := s[PtrUInt(d)];
inc(d);
until PtrUInt(d)=t;
end;
function WriteCurOfs(curofs,curlen,curofssize: integer; sp: PAnsiChar): PAnsiChar;
begin
if curlen=0 then begin
sp^ := #0;
inc(sp);
end else begin
sp := Pointer(ToVarUInt32(curlen,PByte(sp)));
PInteger(sp)^ := curofs;
inc(sp,curofssize);
end;
result := sp;
end;
{$ifdef CPUINTEL} // crc32c SSE4.2 hardware accellerated dword hash
function crc32csse42(buf: pointer): cardinal;
2021-04-23 10:11:05 +02:00
{$ifdef CPUX86} {$ifdef FPC} nostackframe; assembler; {$endif}
asm
mov edx, eax
xor eax, eax
{$ifdef ISDELPHI2010}
crc32 eax, dword ptr[edx]
{$else}
db $F2, $0F, $38, $F1, $02
{$endif}
end;
{$else} {$ifdef FPC}nostackframe; assembler; asm {$else}
asm // ecx=buf (Linux: edi=buf)
.noframe
{$endif FPC}
xor eax, eax
crc32 eax, dword ptr[buf]
end;
{$endif CPUX86}
{$endif CPUINTEL}
function hash32prime(buf: pointer): cardinal;
begin // xxhash32-inspired - and won't pollute L1 cache with lookup tables
result := PCardinal(buf)^;
result := result xor (result shr 15);
result := result * 2246822519;
result := result xor (result shr 13);
result := result * 3266489917;
result := result xor (result shr 16);
end;
const
HTabBits = 18; // fits well with DeltaCompress(..,BufSize=2MB)
HTabMask = (1 shl HTabBits)-1; // =$3ffff
HListMask = $ffffff; // HTab[]=($ff,$ff,$ff)
type
PHTab = ^THTab; // HTabBits=18 -> SizeOf=767KB
THTab = packed array[0..HTabMask] of array[0..2] of byte;
function DeltaCompute(NewBuf, OldBuf, OutBuf, WorkBuf: PAnsiChar;
NewBufSize, OldBufSize, MaxLevel: PtrInt; HList, HTab: PHTab): PAnsiChar;
var i, curofs, curlen, curlevel, match, curofssize, h, oldh: PtrInt;
sp, pInBuf, pOut: PAnsiChar;
ofs: cardinal;
spb: PByte absolute sp;
hash: function(buf: pointer): cardinal;
begin
// 1. fill HTab[] with hashes for all old data
{$ifdef CPUINTEL}
if cfSSE42 in CpuFeatures then
hash := @crc32csse42 else
{$endif}
hash := @hash32prime;
FillCharFast(HTab^,SizeOf(HTab^),$ff); // HTab[]=HListMask by default
pInBuf := OldBuf;
oldh := -1; // force calculate first hash
sp := pointer(HList);
for i := 0 to OldBufSize-3 do begin
h := hash(pInBuf) and HTabMask;
inc(pInBuf);
if h=oldh then
continue;
oldh := h;
h := PtrInt(@HTab^[h]); // fast 24-bit data process
PCardinal(sp)^ := PCardinal(h)^;
PCardinal(h)^ := cardinal(i) or (PCardinal(h)^ and $ff000000);
inc(sp,3);
end;
// 2. compression init
if OldBufSize<=$ffff then
curofssize := 2 else
curofssize := 3;
curlen := -1;
curofs := 0;
pOut := OutBuf+7;
sp := WorkBuf;
// 3. handle identical leading bytes
match := Comp(OldBuf,NewBuf,Min(OldBufSize,NewBufSize));
if match>2 then begin
sp := WriteCurOfs(0,match,curofssize,sp);
sp^ := #0; inc(sp);
inc(NewBuf,match);
dec(NewBufSize,match);
end;
pInBuf := NewBuf;
// 4. main loop: identify longest sequences using hash, and store reference
if NewBufSize>=8 then
repeat
// hash 4 next bytes from NewBuf, and find longest match in OldBuf
ofs := PCardinal(@HTab^[hash(NewBuf) and HTabMask])^ and HListMask;
if ofs<>HListMask then begin // brute force search loop of best hash match
curlevel := MaxLevel;
repeat
with PHash128Rec(OldBuf+ofs)^ do
{$ifdef CPU64} // test 8 bytes
if PHash128Rec(NewBuf)^.Lo=Lo then begin
{$else}
if (PHash128Rec(NewBuf)^.c0=c0) and (PHash128Rec(NewBuf)^.c1=c1) then begin
{$endif}
match := Comp(@PHash128Rec(NewBuf)^.c2,@c2,Min(PtrUInt(OldBufSize)-ofs,NewBufSize)-8);
if match>curlen then begin // found a longer sequence
curlen := match;
curofs := ofs;
end;
end;
dec(curlevel);
ofs := PCardinal(@HList^[ofs])^ and HListMask;
until (ofs=HListMask) or (curlevel=0);
end;
// curlen = longest sequence length
if curlen<0 then begin // no sequence found -> copy one byte
dec(NewBufSize);
pOut^ := NewBuf^;
inc(NewBuf);
inc(pOut);
if NewBufSize>8 then // >=8 may overflow
continue else
break;
end;
inc(curlen,8);
sp := WriteCurOfs(curofs,curlen,curofssize,sp);
spb := ToVarUInt32(NewBuf-pInBuf,spb);
inc(NewBuf,curlen); // continue to search after the sequence
dec(NewBufSize,curlen);
curlen := -1;
pInBuf := NewBuf;
if NewBufSize>8 then // >=8 may overflow
continue else
break;
until false;
// 5. write remaining bytes
if NewBufSize>0 then begin
MoveFast(NewBuf^,pOut^,NewBufSize);
inc(pOut,NewBufSize);
inc(newBuf,NewBufSize);
end;
sp^ := #0; inc(sp);
spb := ToVarUInt32(NewBuf-pInBuf,spb);
// 6. write header
PInteger(OutBuf)^ := pOut-OutBuf-7;
h := sp-WorkBuf;
PInteger(OutBuf+3)^ := h;
OutBuf[6] := AnsiChar(curofssize);
// 7. copy commands
MoveFast(WorkBuf^,pOut^,h);
result := pOut+h;
end;
function ExtractBuf(GoodCRC: cardinal; p: PAnsiChar; var aUpd, Delta: PAnsiChar;
Old: PAnsiChar): TDeltaError;
var pEnd, buf, upd, src: PAnsiChar;
bufsize, datasize, leading, srclen: PtrUInt;
curofssize: byte;
begin
// 1. decompression init
upd := aUpd;
bufsize := PCardinal(p)^ and $00ffffff; inc(p,3);
datasize := PCardinal(p)^ and $00ffffff; inc(p,3);
curofssize := ord(p^); inc(p);
buf := p; inc(p,bufsize);
pEnd := p+datasize;
src := nil;
// 2. main loop
while p<pEnd do begin
// src/srclen = sequence to be copied
srclen := FromVarUInt32(PByte(P));
if srclen>0 then
if curofssize=2 then begin
src := Old+PWord(p)^;
inc(p,2);
end else begin
src := Old+PCardinal(p)^ and $00ffffff;
inc(p,3);
end;
// copy leading bytes
leading := FromVarUInt32(PByte(P));
if leading<>0 then begin
MoveFast(buf^,upd^,leading);
inc(buf,leading);
inc(upd,leading);
end;
// copy sequence
if srclen<>0 then begin
if PtrUInt(upd-src)<srclen then
movechars(src,upd,srclen) else
MoveFast(src^,upd^,srclen);
inc(upd,srclen);
end;
end;
// 3. result check
Delta := p;
if (p=pEnd) and (crc32c(0,aUpd,upd-aUpd)=GoodCRC) then // whole CRC is faster
result := dsSuccess else
result := dsCrcExtract;
aUpd := upd;
end;
procedure WriteByte(var P: PAnsiChar; V: Byte); {$ifdef HASINLINE}inline;{$endif}
begin
PByte(P)^ := V;
inc(P);
end;
procedure WriteInt(var P: PAnsiChar; V: Cardinal); {$ifdef HASINLINE}inline;{$endif}
begin
PCardinal(P)^ := V;
inc(P,4);
end;
const
FLAG_COPIED = 0;
FLAG_COMPRESS = 1;
FLAG_BEGIN = 2;
FLAG_END = 3;
function DeltaCompress(New, Old: PAnsiChar; NewSize, OldSize: integer;
out Delta: PAnsiChar; Level, BufSize: integer): integer;
var HTab, HList: PHTab;
d, workbuf: PAnsiChar;
db: PByte absolute d;
BufRead, OldRead, Trailing, NewSizeSave: PtrInt;
bigfile: boolean;
procedure CreateCopied;
begin
Getmem(Delta,NewSizeSave+17); // 17 = 4*Integer + 1*Byte
d := Delta;
db := ToVarUInt32(0,ToVarUInt32(NewSizeSave,db));
WriteByte(d,FLAG_COPIED); // block copied flag
db := ToVarUInt32(NewSizeSave,db);
WriteInt(d,crc32c(0,New,NewSizeSave));
MoveFast(New^,d^,NewSizeSave);
inc(d,NewSizeSave);
result := d-Delta;
end;
begin
// 1. special cases
if (NewSize=OldSize) and CompareMem(Old,New,NewSize) then begin
Getmem(Delta,1);
Delta^ := '=';
result := 1;
exit;
end;
NewSizeSave := NewSize;
if OldSize=0 then begin // Delta from nothing -> direct copy of whole block
CreateCopied;
exit;
end;
// 2. compression init
bigfile := OldSize>BufSize;
if BufSize>NewSize then
BufSize := NewSize;
if BufSize>$ffffff then
BufSize := $ffffff; // we store offsets with 2..3 bytes -> max 16MB chunk
Trailing := 0;
Getmem(workbuf,BufSize); // compression temporary buffers
Getmem(HList,BufSize*SizeOf(HList[0]));
Getmem(HTab,SizeOf(HTab^));
Getmem(Delta,Max(NewSize,OldSize)+4096); // Delta size max evalulation
try
d := Delta;
db := ToVarUInt32(NewSize,db); // Destination Size
// 3. handle leading and trailing identical bytes (for biggest files)
if bigfile then begin
BufRead := Comp(New,Old,Min(NewSize,OldSize)); // test 1st same chars
if BufRead>9 then begin // it happens very often
db := ToVarUInt32(BufRead,db); // blockSize = Size BufIdem
WriteByte(d,FLAG_BEGIN);
WriteInt(d,crc32c(0,New,BufRead));
inc(New,BufRead);
dec(NewSize,BufRead);
inc(Old,BufRead);
dec(OldSize,BufRead);
end; // test last same chars
BufRead := CompReverse(New+NewSize-1,Old+OldSize-1,Min(NewSize,OldSize));
if BufRead>5 then begin
if NewSize=BufRead then
dec(BufRead); // avoid block overflow
dec(OldSize,BufRead);
dec(NewSize,BufRead);
Trailing := BufRead;
end;
end;
// 4. main loop
repeat
BufRead := Min(BufSize,NewSize);
dec(NewSize,BufRead);
if (BufRead=0) and (Trailing>0) then begin
db := ToVarUInt32(Trailing,db);
WriteByte(d,FLAG_END); // block idem end flag -> BufRead := 0 not necessary
WriteInt(d,crc32c(0,New,Trailing));
break;
end;
OldRead := Min(BufSize,OldSize);
dec(OldSize,OldRead);
db := ToVarUInt32(OldRead,db);
If (BufRead<4) or (OldRead<4) or (BufRead div 4>OldRead) then begin
WriteByte(d,FLAG_COPIED); // block copied flag
db := ToVarUInt32(BufRead,db);
if BufRead=0 then
break;
WriteInt(d,crc32c(0,New,BufRead));
MoveFast(New^,d^,BufRead);
inc(New,BufRead);
inc(d,BufRead);
end else begin
WriteByte(d,FLAG_COMPRESS); // block compressed flag
WriteInt(d,crc32c(0,New,BufRead));
WriteInt(d,crc32c(0,Old,OldRead));
d := DeltaCompute(New,Old,d,workbuf,BufRead,OldRead,Level,HList,HTab);
inc(New,BufRead);
inc(Old,OldRead);
end;
until false;
// 5. release temp memory
finally
result := d-Delta;
Freemem(HTab);
Freemem(HList);
Freemem(workbuf);
end;
if result>=NewSizeSave+17 then begin
// Delta didn't compress well -> store it (with 17 bytes overhead)
Freemem(Delta);
CreateCopied;
end;
end;
function DeltaCompress(const New, Old: RawByteString;
Level, BufSize: integer): RawByteString;
begin
result := DeltaCompress(pointer(New),pointer(Old),
length(New),length(Old),Level,BufSize);
end;
function DeltaCompress(New, Old: PAnsiChar; NewSize, OldSize: integer;
Level, BufSize: integer): RawByteString;
var Delta: PAnsiChar;
DeltaLen: integer;
begin
DeltaLen := DeltaCompress(New,Old,Newsize,OldSize,Delta,Level,BufSize);
SetString(result,Delta,DeltaLen);
Freemem(Delta);
end;
function DeltaExtract(Delta,Old,New: PAnsiChar): TDeltaError;
var BufCRC: Cardinal;
Code: Byte;
Len, BufRead, OldRead: PtrInt;
db: PByte absolute Delta;
Upd: PAnsiChar;
begin
Result := dsSuccess;
Len := FromVarUInt32(db);
Upd := New;
repeat
OldRead := FromVarUInt32(db);
Code := db^; inc(db);
Case Code of
FLAG_COPIED: begin // block copied flag - copy new from Delta
BufRead := FromVarUInt32(db);
If BufRead=0 then
break;
If crc32c(0,Delta+4,BufRead)<>PCardinal(Delta)^ then begin
result := dsCrcCopy;
exit;
end;
inc(Delta,4);
MoveFast(Delta^,New^,BufRead);
if BufRead>=Len then
exit; // if Old=nil -> only copy new
inc(Delta,BufRead);
inc(New,BufRead);
end;
FLAG_COMPRESS: begin // block compressed flag - extract Delta from Old
BufCRC := PCardinal(Delta)^; inc(Delta,4);
if crc32c(0,Old,OldRead)<>PCardinal(Delta)^ then begin
result := dsCrcComp;
exit;
end;
inc(Delta,4);
result := ExtractBuf(BufCRC,Delta,New,Delta,Old);
if result<>dsSuccess then
exit;
end;
FLAG_BEGIN: begin // block idem begin flag
if crc32c(0,Old,OldRead)<>PCardinal(Delta)^ then begin
result := dsCrcBegin;
exit;
end;
inc(Delta,4);
MoveFast(Old^,New^,OldRead);
inc(New,OldRead);
end;
FLAG_END: begin // block idem end flag
if crc32c(0,Old,OldRead)<>PCardinal(Delta)^ then
Result := dsCrcEnd;
MoveFast(Old^,New^,OldRead);
inc(New,OldRead);
break;
end;
else begin
result := dsFlag;
exit;
end;
end; // Case Code of
inc(Old,OldRead);
until false;
if New-Upd<>Len then
result := dsLen;
end;
function DeltaExtract(const Delta,Old: RawByteString; out New: RawByteString): TDeltaError;
begin
if (Delta='') or (Delta='=') then begin
New := Old;
result := dsSuccess;
end else begin
SetLength(New,DeltaExtractSize(pointer(Delta)));
result := DeltaExtract(pointer(Delta),pointer(Old),pointer(New));
end;
end;
function DeltaExtractSize(const Delta: RawByteString): integer;
begin
result := DeltaExtractSize(pointer(Delta));
end;
function DeltaExtractSize(Delta: PAnsiChar): integer;
begin
if Delta=nil then
result := 0 else
result := FromVarUInt32(PByte(Delta));
end;
function ToText(err: TDeltaError): PShortString;
begin
result := GetEnumName(TypeInfo(TDeltaError),ord(err));
end;
{ TFastReader }
procedure TFastReader.Init(Buffer: pointer; Len: integer);
begin
P := Buffer;
Last := P+Len;
OnErrorOverflow := nil;
OnErrorData := nil;
Tag := 0;
end;
procedure TFastReader.Init(const Buffer: RawByteString);
begin
Init(pointer(Buffer),length(Buffer));
end;
procedure TFastReader.ErrorOverflow;
begin
if Assigned(OnErrorOverflow) then
OnErrorOverflow else
raise EFastReader.Create('Reached End of Input');
end;
procedure TFastReader.ErrorData(const fmt: RawUTF8; const args: array of const);
begin
if Assigned(OnErrorData) then
OnErrorData(fmt,args) else
raise EFastReader.CreateUTF8('Incorrect Data: '+fmt,args);
end;
function TFastReader.EOF: boolean;
begin
result := P>=Last;
end;
function TFastReader.RemainingLength: PtrUInt;
begin
result := PtrUInt(Last)-PtrUInt(P);
end;
function TFastReader.NextByte: byte;
begin
if P>=Last then
ErrorOverflow;
result := ord(P^);
inc(P);
end;
function TFastReader.NextByteSafe(dest: pointer): boolean;
begin
if P>=Last then
result := false
else begin
PAnsiChar(dest)^ := P^;
inc(P);
result := true;
end;
end;
function TFastReader.Next4: cardinal;
begin
if P+3>=Last then
ErrorOverflow;
result := PCardinal(P)^;
inc(P,4);
end;
function TFastReader.Next8: QWord;
begin
if P+7>=Last then
ErrorOverflow;
result := PQWord(P)^;
inc(P,8);
end;
function TFastReader.NextByteEquals(Value: byte): boolean;
begin
if P>=Last then
ErrorOverflow;
if ord(P^) = Value then begin
inc(P);
result := true;
end else
result := false;
end;
function TFastReader.Next(DataLen: PtrInt): pointer;
begin
if P+DataLen>Last then
ErrorOverflow;
result := P;
inc(P,DataLen);
end;
function TFastReader.NextSafe(out Data: Pointer; DataLen: PtrInt): boolean;
begin
if P+DataLen>Last then
result := false else begin
Data := P;
inc(P,DataLen);
result := true;
end;
end;
procedure TFastReader.Copy(out Dest; DataLen: PtrInt);
begin
if P+DataLen>Last then
ErrorOverflow;
MoveFast(P^,Dest,DataLen);
inc(P,DataLen);
end;
function TFastReader.CopySafe(out Dest; DataLen: PtrInt): boolean;
begin
if P+DataLen>Last then
result := false else begin
MoveFast(P^,Dest,DataLen);
inc(P,DataLen);
result := true;
end;
end;
procedure TFastReader.VarBlob(out result: TValueResult);
begin
result.Len := VarUInt32;
if P+result.Len>Last then
ErrorOverflow;
result.Ptr := P;
inc(P,result.Len);
end;
function TFastReader.VarBlob: TValueResult;
begin
result.Len := VarUInt32;
if P+result.Len>Last then
ErrorOverflow;
result.Ptr := P;
inc(P,result.Len);
end;
{$ifndef NOVARIANTS}
procedure TFastReader.NextVariant(var Value: variant;
CustomVariantOptions: PDocVariantOptions);
begin
P := VariantLoad(Value,P,CustomVariantOptions,Last);
if P=nil then
ErrorData('VariantLoad=nil',[]) else
if P>Last then
ErrorOverFlow;
end;
procedure TFastReader.NextDocVariantData(out Value: variant;
CustomVariantOptions: PDocVariantOptions);
var json: TValueResult;
temp: TSynTempBuffer;
begin
VarBlob(json);
if json.Len<=0 then
exit;
temp.Init(json.Ptr,json.Len); // parsing will modify input buffer in-place
try
if CustomVariantOptions=nil then
CustomVariantOptions := @JSON_OPTIONS[true];
TDocVariantData(Value).InitJSONInPlace(temp.buf,CustomVariantOptions^);
finally
temp.Done;
end;
end;
{$endif NOVARIANTS}
function TFastReader.VarInt32: integer;
begin
result := VarUInt32;
if result and 1<>0 then
// 1->1, 3->2..
result := result shr 1+1 else
// 0->0, 2->-1, 4->-2..
result := -(result shr 1);
end;
function TFastReader.VarInt64: Int64;
begin
result := VarUInt64;
if result and 1<>0 then
// 1->1, 3->2..
result := result shr 1+1 else
// 0->0, 2->-1, 4->-2..
result := -(result shr 1);
end;
function TFastReader.VarUInt32: cardinal;
var c: cardinal;
{$ifdef CPUX86} // not enough CPU registers
label err;
begin
result := ord(P^);
if P>=Last then
goto err;
inc(P);
if result<=$7f then
exit;
if P>=Last then
goto err;
c := ord(P^) shl 7;
inc(P);
result := result and $7F or c;
if c<=$7f shl 7 then
exit; // Values between 128 and 16256
if P>=Last then
goto err;
c := ord(P^) shl 14;
inc(P);
result := result and $3FFF or c;
if c<=$7f shl 14 then
exit; // Values between 16257 and 2080768
if P>=Last then
goto err;
c := ord(P^) shl 21;
inc(P);
result := result and $1FFFFF or c;
if c<=$7f shl 21 then
exit; // Values between 2080769 and 266338304
if P>=Last then
err:ErrorOverflow;
c := ord(P^) shl 28;
inc(P);
result := result and $FFFFFFF or c;
{$else}
s,l: PByte;
label err,fin;
begin
s := pointer(P);
l := pointer(Last);
result := s^;
if PAnsiChar(s)>=PAnsiChar(l) then
goto err;
inc(s);
if result<=$7f then
goto fin;
if PAnsiChar(s)>=PAnsiChar(l) then
goto err;
c := s^ shl 7;
inc(s);
result := result and $7F or c;
if c<=$7f shl 7 then
goto fin; // Values between 128 and 16256
if PAnsiChar(s)>=PAnsiChar(l) then
goto err;
c := s^ shl 14;
inc(s);
result := result and $3FFF or c;
if c<=$7f shl 14 then
goto fin; // Values between 16257 and 2080768
if PAnsiChar(s)>=PAnsiChar(l) then
goto err;
c := s^ shl 21;
inc(s);
result := result and $1FFFFF or c;
if c<=$7f shl 21 then
goto fin; // Values between 2080769 and 266338304
if PAnsiChar(s)>=PAnsiChar(l) then
err:ErrorOverflow;
c := s^ shl 28;
inc(s);
result := result and $FFFFFFF or c;
fin:
P := pointer(s);
{$endif}
end;
procedure TFastReader.VarNextInt;
{$ifdef CPUX86} // not enough CPU registers
begin
repeat
if P>=Last then
break; // reached end of input
if P^<=#$7f then
break; // reached end of VarUInt32/VarUInt64
inc(P);
until false;
inc(P);
{$else}
var s: PAnsiChar;
begin
s := P;
repeat
if s>=Last then
break; // reached end of input
if s^<=#$7f then
break; // reached end of VarUInt32/VarUInt64
inc(s);
until false;
P := s+1;
{$endif CPUX86}
end;
procedure TFastReader.VarNextInt(count: integer);
{$ifdef CPUX86} // not enough CPU registers
begin
if count=0 then
exit;
repeat
if P>=Last then
break; // reached end of input
if P^>#$7f then begin
inc(P);
continue; // didn't reach end of VarUInt32/VarUInt64
end;
inc(P);
dec(count);
if count=0 then
break;
until false;
{$else}
var s, max: PAnsiChar;
begin
if count=0 then
exit;
s := P;
max := Last;
repeat
if s>=max then
break; // reached end of input
if s^>#$7f then begin
inc(s);
continue; // didn't reach end of VarUInt32/VarUInt64
end;
inc(s);
dec(count);
if count=0 then
break;
until false;
P := s;
{$endif CPUX86}
end;
function TFastReader.PeekVarInt32(out value: PtrInt): boolean;
begin
result := PeekVarUInt32(PtrUInt(value));
if result then
if value and 1<>0 then
// 1->1, 3->2..
value := value shr 1+1 else
// 0->0, 2->-1, 4->-2..
value := -(value shr 1);
end;
function TFastReader.PeekVarUInt32(out value: PtrUInt): boolean;
var s: PAnsiChar;
begin
result := false;
s := P;
repeat
if s>=Last then
exit; // reached end of input -> returns false
if s^<=#$7f then
break; // reached end of VarUInt32
inc(s);
until false;
s := P;
value := VarUInt32; // fast value decode
P := s; // rewind
result := true;
end;
function TFastReader.VarUInt32Safe(out Value: cardinal): boolean;
var c, n, v: cardinal;
begin
result := false;
if P>=Last then
exit;
v := ord(P^);
inc(P);
if v>$7f then begin
n := 0;
v := v and $7F;
repeat
if P>=Last then
exit;
c := ord(P^);
inc(P);
inc(n,7);
if c<=$7f then break;
v := v or ((c and $7f) shl n);
until false;
v := v or (c shl n);
end;
Value := v;
result := true; // success
end;
function TFastReader.VarUInt64: QWord;
label err;
var c, n: PtrUInt;
begin
if P>=Last then
err: ErrorOverflow;
c := ord(P^);
inc(P);
if c>$7f then begin
result := c and $7F;
n := 0;
repeat
if P>=Last then
goto err;
c := ord(P^);
inc(P);
inc(n,7);
if c<=$7f then break;
result := result or (QWord(c and $7f) shl n);
until false;
result := result or (QWord(c) shl n);
end else
result := c;
end;
function TFastReader.VarString: RawByteString;
begin
with VarBlob do
SetString(result,Ptr,Len);
end;
procedure TFastReader.VarUTF8(out result: RawUTF8);
begin
with VarBlob do
FastSetString(result,Ptr,Len);
end;
function TFastReader.VarUTF8: RawUTF8;
begin
with VarBlob do
FastSetString(result,Ptr,Len);
end;
function TFastReader.VarShortString: shortstring;
begin
with VarBlob do
SetString(result,Ptr,Len);
end;
function TFastReader.VarUTF8Safe(out Value: RawUTF8): boolean;
var len: cardinal;
begin
if VarUInt32Safe(len) then
if len=0 then
result := true else
if P+len<=Last then begin
FastSetString(Value,P,len);
inc(P,len);
result := true;
end else
result := false else
result := false;
end;
procedure TFastReader.Read(var DA: TDynArray; NoCheckHash: boolean);
begin
P := DA.LoadFrom(P,nil,NoCheckHash,Last);
if P=nil then
ErrorData('TDynArray.LoadFrom %',[DA.ArrayTypeShort^]);
end;
function TFastReader.ReadVarUInt32Array(var Values: TIntegerDynArray): PtrInt;
var i: integer;
k: TFileBufferWriterKind;
begin
result := VarUInt32;
SetLength(Values,result);
Copy(k,1);
if k=wkUInt32 then begin
Copy(Values[0],result*4);
exit;
end;
Next(4); // format: Isize+varUInt32s
case k of
wkVarInt32: for i := 0 to result-1 do Values[i] := VarInt32;
wkVarUInt32: for i := 0 to result-1 do Values[i] := VarUInt32;
else ErrorData('ReadVarUInt32Array: unhandled kind=%', [ord(k)]);
end;
end;
function TFastReader.ReadCompressed(Load: TAlgoCompressLoad; BufferOffset: integer): RawByteString;
var comp: PAnsiChar;
complen: PtrUInt;
begin
complen := VarUInt32;
comp := Next(complen);
TAlgoCompress.Algo(comp,complen).Decompress(comp,complen,result,Load,BufferOffset);
end;
{ TSynTempWriter }
procedure TSynTempWriter.Init(maxsize: integer);
begin
if maxsize<=0 then
pos := tmp.InitOnStack else
pos := tmp.Init(maxsize);
end;
procedure TSynTempWriter.Done;
begin
tmp.Done;
end;
function TSynTempWriter.AsBinary: RawByteString;
begin
FastSetStringCP(result,tmp.buf,pos-PAnsiChar(tmp.buf),CP_RAWBYTESTRING);
end;
procedure TSynTempWriter.AsUTF8(var result: RawUTF8);
begin
FastSetString(result,tmp.buf,pos-PAnsiChar(tmp.buf));
end;
function TSynTempWriter.Position: PtrInt;
begin
result := pos-PAnsiChar(tmp.buf);
end;
procedure TSynTempWriter.wr(const val; len: PtrInt);
begin
if len<=0 then
exit;
if pos-PAnsiChar(tmp.buf)+len>tmp.len then
raise ESynException.CreateUTF8('TSynTempWriter(%) overflow',[tmp.len]);
MoveSmall(@val,pos,len);
inc(pos,len);
end;
procedure TSynTempWriter.wrb(b: byte);
begin
wr(b,1);
end;
procedure TSynTempWriter.wrint(int: integer);
begin
wr(int,4);
end;
procedure TSynTempWriter.wrptrint(int: PtrInt);
begin
wr(int,SizeOf(int));
end;
procedure TSynTempWriter.wrptr(ptr: pointer);
begin
wr(ptr,SizeOf(ptr));
end;
procedure TSynTempWriter.wrss(const str: shortstring);
begin
wr(str,ord(str[0])+1);
end;
procedure TSynTempWriter.wrs(const str: RawByteString);
begin
if str<>'' then
wr(pointer(str),length(str));
end;
procedure TSynTempWriter.wrw(w: word);
begin
wr(w,2);
end;
function TSynTempWriter.wrfillchar(count: integer; value: byte): PAnsiChar;
begin
if pos-PAnsiChar(tmp.buf)+count>tmp.len then
raise ESynException.CreateUTF8('TSynTempWriter(%) overflow',[tmp.len]);
FillCharFast(pos^,count,value);
result := pos;
inc(pos,count);
end;
{ TFileBufferWriter }
constructor TFileBufferWriter.Create(aFile: THandle; BufLen: integer);
begin
Create(THandleStream.Create(aFile),BufLen);
fInternalStream := true;
end;
constructor TFileBufferWriter.Create(const aFileName: TFileName; BufLen: integer;
Append: boolean);
var s: TStream;
begin
if Append and FileExists(aFileName) then begin
s := TFileStream.Create(aFileName,fmOpenWrite);
2021-04-23 10:11:05 +02:00
s.Seek(0,soEnd);
end else
s := TFileStream.Create(aFileName,fmCreate);
Create(s,BufLen);
fInternalStream := true;
end;
constructor TFileBufferWriter.Create(aStream: TStream; BufLen: integer);
begin
if BufLen>1 shl 22 then
fBufLen := 1 shl 22 else // 4 MB sounds right enough
if BufLen<32 then
fBufLen := 32;
fBufLen := BufLen;
fStream := aStream;
SetLength(fBufInternal,fBufLen);
fBuffer := pointer(fBufInternal);
end;
constructor TFileBufferWriter.Create(aClass: TStreamClass; BufLen: integer);
begin
Create(aClass.Create,BufLen);
fInternalStream := true;
end;
constructor TFileBufferWriter.Create(aStream: TStream; aTempBuf: pointer; aTempLen: integer);
begin
fBufLen := aTempLen;
fBuffer := aTempBuf;
fStream := aStream;
end;
constructor TFileBufferWriter.Create(aClass: TStreamClass; aTempBuf: pointer; aTempLen: integer);
begin
Create(aClass.Create,aTempBuf,aTempLen);
fInternalStream := true;
end;
destructor TFileBufferWriter.Destroy;
begin
if fInternalStream then
fStream.Free;
inherited;
end;
procedure TFileBufferWriter.InternalFlush;
begin
fStream.WriteBuffer(fBuffer^,fPos);
fPos := 0;
end;
function TFileBufferWriter.Flush: Int64;
begin
if fPos>0 then
InternalFlush;
result := fTotalWritten;
fTotalWritten := 0;
end;
procedure TFileBufferWriter.CancelAll;
begin
fTotalWritten := 0;
fPos := 0;
if fStream.ClassType = TRawByteStringStream then
TRawByteStringStream(fStream).Size := 0 else
fStream.Seek(0,soBeginning);
end;
procedure TFileBufferWriter.Write(Data: pointer; DataLen: PtrInt);
begin
if (DataLen<=0) or (Data=nil) then
exit;
inc(fTotalWritten,DataLen);
if fPos+DataLen>fBufLen then begin
if fPos>0 then
InternalFlush;
if DataLen>fBufLen then begin
fStream.WriteBuffer(Data^,DataLen);
exit;
end;
end;
MoveFast(Data^,fBuffer^[fPos],DataLen);
inc(fPos,DataLen);
end;
procedure TFileBufferWriter.WriteN(Data: Byte; Count: integer);
var len: integer;
begin
inc(fTotalWritten,Count);
while Count>0 do begin
if Count>fBufLen then
len := fBufLen else
len := Count;
if fPos+len>fBufLen then
InternalFlush;
FillCharFast(fBuffer^[fPos],len,Data);
inc(fPos,len);
dec(Count,len);
end;
end;
procedure TFileBufferWriter.Write1(Data: Byte);
begin
if fPos+1>fBufLen then
InternalFlush;
fBuffer^[fPos] := Data;
inc(fPos);
inc(fTotalWritten);
end;
procedure TFileBufferWriter.Write2(Data: Word);
begin
if fPos+2>fBufLen then
InternalFlush;
PWord(@fBuffer^[fPos])^ := Data;
inc(fPos,SizeOf(Word));
inc(fTotalWritten,SizeOf(Word));
end;
procedure TFileBufferWriter.Write4(Data: integer);
begin
if fPos+4>fBufLen then
InternalFlush;
PInteger(@fBuffer^[fPos])^ := Data;
inc(fPos,SizeOf(integer));
inc(fTotalWritten,SizeOf(integer));
end;
procedure TFileBufferWriter.Write4BigEndian(Data: integer);
begin
Write4(bswap32(Data));
end;
procedure TFileBufferWriter.Write8(const Data8Bytes);
begin
if fPos+8>fBufLen then
InternalFlush;
PInt64(@fBuffer^[fPos])^ := PInt64(@Data8Bytes)^;
inc(fPos,SizeOf(Int64));
inc(fTotalWritten,SizeOf(Int64));
end;
procedure TFileBufferWriter.Write(const Text: RawByteString);
var L: integer;
begin
L := length(Text);
if L=0 then
Write1(0) else begin
WriteVarUInt32(L);
Write(pointer(Text),L);
end;
end;
procedure TFileBufferWriter.WriteShort(const Text: ShortString);
var L: integer;
begin
L := ord(Text[0]);
if L<$80 then
Write(@Text[0],L+1) else begin
WriteVarUInt32(L);
Write(@Text[1],L);
end;
end;
procedure TFileBufferWriter.WriteBinary(const Data: RawByteString);
begin
Write(pointer(Data),Length(Data));
end;
function TFileBufferWriter.DirectWritePrepare(len: PtrInt; out tmp: RawByteString): PAnsiChar;
begin
if (len<=fBufLen) and (fPos+len>fBufLen) then
InternalFlush;
if fPos+len>fBufLen then begin
SetLength(tmp,len);
result := pointer(tmp);
end else
result := @fBuffer^[fPos]; // write directly into the buffer
end;
procedure TFileBufferWriter.DirectWriteFlush(len: PtrInt; const tmp: RawByteString);
begin
if tmp='' then begin
inc(fPos,len);
inc(fTotalWritten,len);
end else
Write(pointer(tmp),len);
end;
procedure TFileBufferWriter.WriteRecord(const Rec; RecTypeInfo: pointer);
var len: integer;
tmp: RawByteString;
P: PAnsiChar;
begin
len := RecordSaveLength(Rec,RecTypeInfo);
P := DirectWritePrepare(len,tmp);
if RecordSave(Rec,P,RecTypeInfo)-P<>len then
raise ESynException.CreateUTF8('%.WriteRecord: RecordSave?',[self]);
DirectWriteFlush(len,tmp);
end;
procedure TFileBufferWriter.WriteDynArray(const DA: TDynArray);
var len: integer;
tmp: RawByteString;
P: PAnsiChar;
begin
len := DA.SaveToLength;
P := DirectWritePrepare(len,tmp);
if DA.SaveTo(P)-P<>len then
raise ESynException.CreateUTF8('%.WriteDynArray: DA.SaveTo?',[self]);
DirectWriteFlush(len,tmp);
end;
{$ifndef NOVARIANTS}
procedure TFileBufferWriter.Write(const Value: variant);
var buf: PAnsiChar;
vt,len: integer;
tmp: RawByteString;
begin
vt := TVarData(Value).VType;
if vt>varAny then begin // avoid VariantSaveLength() call
Write(@TVarData(Value).VType,SizeOf(TVarData(Value).VType));
tmp := VariantSaveJSON(Value);
Write(tmp);
exit;
end;
len := VariantSaveLength(Value);
if len=0 then
raise ESynException.CreateUTF8('%.Write(VType=%) VariantSaveLength=0',[self,vt]);
buf := DirectWritePrepare(len,tmp);
if VariantSave(Value,buf)=nil then
raise ESynException.CreateUTF8('%.Write(VType=%) VariantSave=nil',[self,vt]);
DirectWriteFlush(len,tmp);
end;
procedure TFileBufferWriter.WriteDocVariantData(const Value: variant);
begin
with _Safe(Value)^ do
if Count=0 then
Write1(0) else
Write(ToJSON);
end;
{$endif NOVARIANTS}
procedure TFileBufferWriter.WriteXor(New,Old: PAnsiChar; Len: PtrInt; crc: PCardinal);
var L: integer;
Dest: PAnsiChar;
begin
if (New=nil) or (Old=nil) then
exit;
inc(fTotalWritten,Len);
while Len>0 do begin
Dest := pointer(fBuffer);
if fPos+Len>fBufLen then
InternalFlush else
inc(Dest,fPos);
if Len>fBufLen then
L := fBufLen else
L := Len;
XorMemory(pointer(Dest),pointer(New),pointer(Old),L);
if crc<>nil then
crc^ := crc32c(crc^,Dest,L);
inc(Old,L);
inc(New,L);
dec(Len,L);
inc(fPos,L);
end;
end;
procedure TFileBufferWriter.WriteRawUTF8DynArray(const Values: TRawUTF8DynArray;
ValuesCount: integer);
begin
WriteRawUTF8Array(pointer(Values),ValuesCount);
end;
procedure TFileBufferWriter.WriteRawUTF8Array(Values: PPtrUIntArray; ValuesCount: integer);
var n, i: integer;
fixedsize, len: PtrUInt;
P, PEnd: PByte;
PBeg: PAnsiChar;
begin
WriteVarUInt32(ValuesCount);
if ValuesCount=0 then
exit;
fixedsize := Values^[0];
if fixedsize<>0 then begin
fixedsize := PStrLen(fixedsize-_STRLEN)^;
for i := 1 to ValuesCount-1 do
if (Values^[i]=0) or (PStrLen(Values^[i]-_STRLEN)^<>TStrLen(fixedsize)) then begin
fixedsize := 0;
break;
end;
end;
WriteVarUInt32(fixedsize);
repeat
P := @fBuffer^[fPos];
PEnd := @fBuffer^[fBufLen-8];
if PtrUInt(P)<PtrUInt(PEnd) then begin
n := ValuesCount;
PBeg := PAnsiChar(P); // leave space for chunk size
inc(P,4);
if fixedsize=0 then
for i := 0 to ValuesCount-1 do
if Values^[i]=0 then begin
P^ := 0; // store length=0
inc(P);
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end else begin
len := PStrLen(Values^[i]-_STRLEN)^;
if PtrUInt(PEnd)-PtrUInt(P)<=len then begin
n := i;
break; // avoid buffer overflow
end;
P := ToVarUInt32(len,P);
MoveFast(pointer(Values^[i])^,P^,len); // here len>0
inc(P,len);
end else // fixedsize<>0:
for i := 0 to ValuesCount-1 do begin
if PtrUInt(PEnd)-PtrUInt(P)<=fixedsize then begin
n := i;
break; // avoid buffer overflow
end;
MoveFast(pointer(Values^[i])^,P^,fixedsize);
inc(P,fixedsize);
end;
len := PAnsiChar(P)-PBeg; // format: Isize+varUInt32s*strings
PInteger(PBeg)^ := len-4;
inc(fTotalWritten,len);
inc(fPos,len);
inc(PByte(Values),n*SizeOf(PtrInt));
dec(ValuesCount,n);
if ValuesCount=0 then
break;
end;
InternalFlush;
until false;
end;
procedure TFileBufferWriter.WriteRawUTF8List(List: TRawUTF8List;
StoreObjectsAsVarUInt32: Boolean);
var i: integer;
o: PPointerArray;
begin
if List=nil then
WriteVarUInt32(0) else begin
List.Safe.Lock;
try
WriteRawUTF8Array(pointer(List.TextPtr),List.Count);
o := List.ObjectPtr;
if o=nil then
StoreObjectsAsVarUInt32 := false; // no Objects[] values
Write(@StoreObjectsAsVarUInt32,1);
if StoreObjectsAsVarUInt32 then
for i := 0 to List.Count-1 do
WriteVarUInt32(PtrUInt(o[i]));
finally
List.Safe.UnLock;
end;
end;
end;
procedure TFileBufferWriter.WriteStream(aStream: TCustomMemoryStream;
aStreamSize: Integer);
begin
if aStreamSize<0 then
if aStream=nil then
aStreamSize := 0 else
aStreamSize := aStream.Size;
WriteVarUInt32(aStreamSize);
if aStreamSize>0 then
Write(aStream.Memory,aStreamSize);
end;
procedure TFileBufferWriter.WriteVarInt32(Value: PtrInt);
begin
if Value<=0 then
// 0->0, -1->2, -2->4..
Value := (-Value) shl 1 else
// 1->1, 2->3..
Value := (Value shl 1)-1;
WriteVarUInt32(Value);
end;
procedure TFileBufferWriter.WriteVarUInt32(Value: PtrUInt);
var pos: integer;
begin
if fPos+16>fBufLen then
InternalFlush;
pos := fPos;
fPos := PtrUInt(ToVarUInt32(Value,@fBuffer^[fPos]))-PtrUInt(fBuffer);
inc(fTotalWritten,PtrUInt(fPos-Pos));
end;
procedure TFileBufferWriter.WriteVarInt64(Value: Int64);
var pos: integer;
begin
if fPos+48>fBufLen then
InternalFlush;
pos := fPos;
fPos := PtrUInt(ToVarInt64(Value,@fBuffer^[fPos]))-PtrUInt(fBuffer);
inc(fTotalWritten,PtrUInt(fPos-Pos));
end;
procedure TFileBufferWriter.WriteVarUInt64(Value: QWord);
var pos: integer;
begin
if fPos+48>fBufLen then
InternalFlush;
pos := fPos;
fPos := PtrUInt(ToVarUInt64(Value,@fBuffer^[fPos]))-PtrUInt(fBuffer);
inc(fTotalWritten,PtrUInt(fPos-Pos));
end;
function CleverStoreInteger(p: PInteger; V, VEnd: PAnsiChar; pCount: integer;
var StoredCount: integer): PAnsiChar;
// Clever = store Values[i+1]-Values[i] (with special diff=1 count)
// format: Integer: firstValue, then:
// B:0 W:difference with previous
// B:1..253 = difference with previous
// B:254 W:byOne
// B:255 B:byOne
var i, d, byOne: integer;
begin
StoredCount := pCount;
if pCount<=0 then begin
result := V;
exit;
end;
i := p^;
PInteger(V)^ := p^;
inc(V,4);
dec(pCount);
inc(p);
byOne := 0;
if pCount>0 then
repeat
d := p^-i;
i := p^;
inc(p);
if d=1 then begin
dec(pCount);
inc(byOne);
if pCount>0 then continue;
end else
if d<0 then begin
result:= nil;
exit;
end;
if byOne<>0 then begin
case byOne of
1: begin V^ := #1; inc(V); end; // B:1..253 = difference with previous
2: begin PWord(V)^ := $0101; inc(V,2); end; // B:1..253 = difference
else
if byOne>255 then begin
while byOne>65535 do begin
PInteger(V)^ := $fffffe; inc(V,3); // store as many len=$ffff as necessary
dec(byOne,$ffff);
end;
PInteger(V)^ := byOne shl 8+$fe; inc(V,3); // B:254 W:byOne
end else begin
PWord(V)^ := byOne shl 8+$ff; inc(V,2); // B:255 B:byOne
end;
end; // case byOne of
if pCount=0 then break;
byOne := 0;
end;
if (d=0) or (d>253) then begin
while cardinal(d)>65535 do begin
PInteger(V)^ := $ffff00; inc(V,3); // store as many len=$ffff as necessary
dec(cardinal(d),$ffff);
end;
dec(pCount);
PInteger(V)^ := d shl 8; inc(V,3); // B:0 W:difference with previous
if (V<VEnd) and (pCount>0) then continue else break;
end else begin
dec(pCount);
V^ := AnsiChar(d); inc(V); // B:1..253 = difference with previous
if (V<VEnd) and (pCount>0) then continue else break;
end;
if V>=VEnd then
break; // avoid GPF
until false;
dec(StoredCount,pCount);
result := V;
end;
procedure TFileBufferWriter.WriteVarUInt32Array(const Values: TIntegerDynArray;
ValuesCount: integer; DataLayout: TFileBufferWriterKind);
begin
WriteVarUInt32Values(pointer(Values),ValuesCount,DataLayout);
end;
procedure TFileBufferWriter.WriteVarUInt32Values(Values: PIntegerArray;
ValuesCount: integer; DataLayout: TFileBufferWriterKind);
var n, i, pos, diff: integer;
P: PByte;
PBeg, PEnd: PAnsiChar;
begin
WriteVarUInt32(ValuesCount);
if ValuesCount=0 then
exit;
fBuffer^[fPos] := ord(DataLayout);
inc(fPos);
inc(fTotalWritten);
if DataLayout in [wkOffsetU, wkOffsetI] then begin
pos := fPos;
fPos := PtrUInt(ToVarUInt32(Values^[0],@fBuffer^[fPos]))-PtrUInt(fBuffer);
diff := Values^[1]-Values^[0];
inc(PInteger(Values));
dec(ValuesCount);
if ValuesCount=0 then begin
inc(fTotalWritten,PtrUInt(fPos-pos));
exit;
end;
if diff>0 then begin
for i := 1 to ValuesCount-1 do
if Values^[i]-Values^[i-1]<>diff then begin
diff := 0; // not always the same offset
break;
end;
end else
diff := 0;
fPos := PtrUInt(ToVarUInt32(diff,@fBuffer^[fPos]))-PtrUInt(fBuffer);
inc(fTotalWritten,PtrUInt(fPos-pos));
if diff<>0 then
exit; // same offset for all items (fixed sized records) -> quit now
end;
repeat
P := @fBuffer^[fPos];
PEnd := @fBuffer^[fBufLen-32];
if PtrUInt(P)<PtrUInt(PEnd) then begin
pos := fPos;
case DataLayout of
wkUInt32: begin
n := (fBufLen-fPos)shr 2;
if ValuesCount<n then
n := ValuesCount;
MoveFast(Values^,P^,n*4);
inc(P,n*4);
end;
wkVarInt32, wkVarUInt32, wkOffsetU, wkOffsetI: begin
PBeg := PAnsiChar(P); // leave space for chunk size
inc(P,4);
n := ValuesCount;
case DataLayout of
wkVarInt32:
for i := 0 to ValuesCount-1 do begin
P := ToVarInt32(Values^[i],P);
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end;
wkVarUInt32:
for i := 0 to ValuesCount-1 do begin
P := ToVarUInt32(Values^[i],P);
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end;
wkOffsetU:
for i := 0 to ValuesCount-1 do begin
P := ToVarUInt32(Values^[i]-Values^[i-1],P);
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end;
wkOffsetI:
for i := 0 to ValuesCount-1 do begin
P := ToVarInt32(Values^[i]-Values^[i-1],P);
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end;
end;
PInteger(PBeg)^ := PAnsiChar(P)-PBeg-4; // format: Isize+varUInt32s
end;
wkSorted: begin
PBeg := PAnsiChar(P)+4; // leave space for chunk size
P := PByte(CleverStoreInteger(pointer(Values),PBeg,PEnd,ValuesCount,n));
if P=nil then
raise ESynException.CreateUTF8('%.WriteVarUInt32Array: data not sorted',[self]);
PInteger(PBeg-4)^ := PAnsiChar(P)-PBeg; // format: Isize+cleverStorage
end;
end;
inc(PByte(Values),n*4);
fPos := PtrUInt(P)-PtrUInt(fBuffer);
inc(fTotalWritten,PtrUInt(fPos-pos));
dec(ValuesCount,n);
if ValuesCount=0 then
break;
end;
InternalFlush;
until false;
end;
procedure TFileBufferWriter.WriteVarUInt64DynArray(
const Values: TInt64DynArray; ValuesCount: integer; Offset: Boolean);
var n, i, pos: integer;
diff: Int64;
P, PEnd: PByte;
PI: PInt64Array;
PBeg: PAnsiChar;
begin
WriteVarUInt32(ValuesCount);
if ValuesCount=0 then
exit;
PI := pointer(Values);
pos := fPos;
if Offset then begin
fBuffer^[fPos] := 1;
fPos := PtrUInt(ToVarUInt64(PI^[0],@fBuffer^[fPos+1]))-PtrUInt(fBuffer);
diff := PI^[1]-PI^[0];
inc(PByte(PI),8);
dec(ValuesCount);
if ValuesCount=0 then begin
inc(fTotalWritten,PtrUInt(fPos-pos));
exit;
end;
if (diff>0) and (diff<MaxInt) then begin
for i := 1 to ValuesCount-1 do
if PI^[i]-PI^[i-1]<>diff then begin
diff := 0; // not always the same offset
break;
end;
end else
diff := 0;
fPos := PtrUInt(ToVarUInt32(diff,@fBuffer^[fPos]))-PtrUInt(fBuffer);
if diff<>0 then begin
inc(fTotalWritten,PtrUInt(fPos-Pos));
exit; // same offset for all items (fixed sized records) -> quit now
end;
end else begin
fBuffer^[fPos] := 0;
inc(fPos);
end;
inc(fTotalWritten,PtrUInt(fPos-Pos));
repeat
P := @fBuffer^[fPos];
PEnd := @fBuffer^[fBufLen-32];
if PtrUInt(P)<PtrUInt(PEnd) then begin
pos := fPos;
PBeg := PAnsiChar(P); // leave space for chunk size
inc(P,4);
n := ValuesCount;
if Offset then
for i := 0 to ValuesCount-1 do begin
P := ToVarUInt64(PI^[i]-PI^[i-1],P); // store diffs
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end
else
for i := 0 to ValuesCount-1 do begin
P := ToVarUInt64(PI^[i],P);
if PtrUInt(P)>=PtrUInt(PEnd) then begin
n := i+1;
break; // avoid buffer overflow
end;
end;
PInteger(PBeg)^ := PAnsiChar(P)-PBeg-4; // format: Isize+varUInt32/64s
inc(PByte(PI),n*8);
fPos := PtrUInt(P)-PtrUInt(fBuffer);
inc(fTotalWritten,PtrUInt(fPos-Pos));
dec(ValuesCount,n);
if ValuesCount=0 then
break;
end;
InternalFlush;
until false;
end;
function TFileBufferWriter.FlushAndCompress(nocompression: boolean; algo: TAlgoCompress;
BufferOffset: integer): RawByteString;
var trig: integer;
begin
if algo=nil then
algo := AlgoSynLZ;
trig := SYNLZTRIG[nocompression];
if fStream.Position=0 then // direct compression from internal buffer
result := algo.Compress(PAnsiChar(fBuffer),fPos,trig,false,BufferOffset) else begin
Flush;
result := algo.Compress((fStream as TRawByteStringStream).DataString,trig,false,BufferOffset);
end;
end;
{ TFileBufferReader }
procedure TFileBufferReader.Close;
begin
fMap.UnMap;
end;
procedure TFileBufferReader.ErrorInvalidContent;
begin
raise ESynException.Create('TFileBufferReader: invalid content');
end;
procedure TFileBufferReader.OpenFrom(aBuffer: pointer; aBufferSize: PtrUInt);
begin
fCurrentPos := 0;
fMap.Map(aBuffer,aBufferSize);
end;
procedure TFileBufferReader.OpenFrom(const aBuffer: RawByteString);
begin
OpenFrom(pointer(aBuffer),length(aBuffer));
end;
function TFileBufferReader.OpenFrom(Stream: TStream): boolean;
begin
result := false;
if Stream=nil then
exit;
if Stream.InheritsFrom(TFileStream) then
Open(TFileStream(Stream).Handle) else
if Stream.InheritsFrom(TCustomMemoryStream) then
with TCustomMemoryStream(Stream) do
OpenFrom(Memory,Size) else
exit;
result := true
end;
procedure TFileBufferReader.Open(aFile: THandle; aFileNotMapped: boolean);
begin
fCurrentPos := 0;
if aFileNotMapped then
FillcharFast(fMap,SizeOf(fMap),0) else
fMap.Map(aFile)
// if Windows failed to find a contiguous VA space -> fall back on direct read
end;
function TFileBufferReader.Read(Data: pointer; DataLen: PtrInt): integer;
var len: PtrInt;
begin
if DataLen>0 then
if fMap.Buffer<>nil then begin
// file up to 2 GB: use fast memory map
len := fMap.Size-fCurrentPos;
if len>DataLen then
len := DataLen;
if Data<>nil then
MoveFast(fMap.Buffer[fCurrentPos],Data^,len);
inc(fCurrentPos,len);
result := len;
end else
// file bigger than 2 GB: slower but accurate reading from file
if Data=nil then begin
2021-04-23 10:11:05 +02:00
FileSeek64(fMap.FileHandle,DataLen,soFromCurrent);
result := DataLen;
end else
result := FileRead(fMap.FileHandle,Data^,DataLen) else
// DataLen=0
result := 0;
end;
function TFileBufferReader.Read(out Text: RawByteString): integer;
begin
result := ReadVarUInt32;
if result=0 then
exit;
SetLength(Text,result);
if Read(pointer(Text),result)<>result then
ErrorInvalidContent;
end;
function TFileBufferReader.Read(out Text: RawUTF8): integer;
begin
result := ReadVarUInt32;
if result=0 then
exit;
SetLength(Text,result);
if Read(pointer(Text),result)<>result then
ErrorInvalidContent;
end;
function TFileBufferReader.ReadRawUTF8: RawUTF8;
begin
Read(result);
end;
procedure TFileBufferReader.ReadChunk(out P, PEnd: PByte; var BufTemp: RawByteString);
var len: integer;
begin // read Isize + buffer in P,PEnd
if (Read(@len,4)<>4) or (len<0) then
ErrorInvalidContent;
P := ReadPointer(len,BufTemp);
if P=nil then
ErrorInvalidContent;
PEnd := pointer(PtrUInt(P)+PtrUInt(len));
end;
function TFileBufferReader.CurrentMemory(DataLen: PtrUInt; PEnd: PPAnsiChar): pointer;
begin
if (fMap.Buffer=nil) or (fCurrentPos+DataLen>=fMap.Size) then
result := nil else begin
result := @fMap.Buffer[fCurrentPos];
if PEnd<>nil then
PEnd^ := @fMap.Buffer[fMap.Size];
inc(fCurrentPos,DataLen);
end;
end;
function TFileBufferReader.CurrentPosition: integer;
begin
if (fMap.Buffer=nil) or (fCurrentPos>=fMap.Size) then
result := -1 else
result := fCurrentPos;
end;
function TFileBufferReader.FileSize: Int64;
begin
result := fMap.FileSize;
end;
function TFileBufferReader.MappedBuffer: PAnsiChar;
begin
result := fMap.Buffer;
end;
function TFileBufferReader.ReadPointer(DataLen: PtrUInt;
var aTempData: RawByteString): pointer;
begin
if fMap.Buffer=nil then begin
// read from file
if DataLen>PtrUInt(Length(aTempData)) then begin
aTempData := ''; // so no move() call in SetLength() below
SetLength(aTempData,DataLen);
end;
if PtrUInt(FileRead(fMap.FileHandle,pointer(aTempData)^,DataLen))<>DataLen then
result := nil else // invalid content
result := pointer(aTempData);
end else
if DataLen+fCurrentPos>fMap.Size then
// invalid request
result := nil else begin
// get pointer to data from current memory map (no data copy)
result := @fMap.Buffer[fCurrentPos];
inc(fCurrentPos,DataLen);
end;
end;
function TFileBufferReader.ReadStream(DataLen: PtrInt): TCustomMemoryStream;
var FileCurrentPos: Int64;
begin
if DataLen<0 then
DataLen := ReadVarUInt32;
if DataLen<>0 then
if fMap.Buffer=nil then begin
FileCurrentPos := FileSeek64(fMap.FileHandle,0,soFromCurrent);
if FileCurrentPos+DataLen>fMap.Size then
// invalid content
result := nil else begin
// create a temporary memory map buffer stream
result := TSynMemoryStreamMapped.Create(fMap.FileHandle,DataLen,FileCurrentPos);
FileSeek64(fMap.FileHandle,DataLen,soFromCurrent);
end;
end else
if PtrUInt(DataLen)+fCurrentPos>fMap.Size then
// invalid content
result := nil else begin
// get pointer to data from current memory map (no data copy)
result := TSynMemoryStream.Create(@fMap.Buffer[fCurrentPos],DataLen);
inc(fCurrentPos,DataLen);
end else
// DataLen=0 -> invalid content
result := nil;
end;
function TFileBufferReader.ReadByte: PtrUInt;
begin
if fMap.Buffer<>nil then
if fCurrentPos>=fMap.Size then
// invalid request
result := 0 else begin
// read 8-bit from current memory map
result := ord(fMap.Buffer[fCurrentPos]);
inc(fCurrentPos);
end else begin
// read from file if >= 2 GB (slow, but works)
result := 0;
if FileRead(fMap.FileHandle,result,1)<>1 then
result := 0;
end;
end;
function TFileBufferReader.ReadCardinal: cardinal;
begin
if fMap.Buffer<>nil then
if fCurrentPos+3>=fMap.Size then
// invalid request
result := 0 else begin
// read 32-bit from current memory map
result := PCardinal(fMap.Buffer+fCurrentPos)^;
inc(fCurrentPos,4);
end else begin
// read from file if >= 2 GB (slow, but works)
result := 0;
if FileRead(fMap.FileHandle,result,4)<>4 then
result := 0;
end;
end;
function TFileBufferReader.ReadVarUInt32: PtrUInt;
var c, n: PtrUInt;
begin
result := ReadByte;
if result>$7f then begin
n := 0;
result := result and $7F;
repeat
c := ReadByte;
inc(n,7);
if c<=$7f then break;
result := result or ((c and $7f) shl n);
until false;
result := result or (c shl n);
end;
end;
function TFileBufferReader.ReadVarInt32: PtrInt;
begin
result := ReadVarUInt32;
if result and 1<>0 then
// 1->1, 3->2..
result := result shr 1+1 else
// 0->0, 2->-1, 4->-2..
result := -(result shr 1);
end;
function TFileBufferReader.ReadVarUInt64: QWord;
var c, n: PtrUInt;
begin
result := ReadByte;
if result>$7f then begin
n := 0;
result := result and $7F;
repeat
c := ReadByte;
inc(n,7);
if c<=$7f then break;
result := result or (QWord(c and $7f) shl n);
until false;
result := result or (QWord(c) shl n);
end;
end;
function TFileBufferReader.ReadVarInt64: Int64;
begin
result := ReadVarUInt64;
if result<>0 then
if result and 1<>0 then
// 1->1, 3->2..
result := result shr 1+1 else
// 0->0, 2->-1, 4->-2..
result := -(result shr 1);
end;
function CleverReadInteger(p, pEnd: PAnsiChar; V: PInteger): PtrUInt;
// Clever = decode Values[i+1]-Values[i] storage (with special diff=1 count)
var i, n: PtrUInt;
begin
result := PtrUInt(V);
i := PInteger(p)^; inc(p,4); // Integer: firstValue
V^ := i; inc(V);
if PtrUInt(p)<PtrUInt(pEnd) then
repeat
case p^ of
#0: begin // B:0 W:difference with previous
inc(i,PWord(p+1)^); inc(p,3);
V^ := i; inc(V);
if PtrUInt(p)<PtrUInt(pEnd) then continue else break;
end;
#254: begin // B:254 W:byOne
for n := 1 to PWord(p+1)^ do begin
inc(i);
V^ := i; inc(V);
end;
inc(p,3);
if PtrUInt(p)<PtrUInt(pEnd) then continue else break;
end;
#255: begin // B:255 B:byOne
for n := 1 to pByte(p+1)^ do begin
inc(i);
V^ := i; inc(V);
end;
inc(p,2);
if PtrUInt(p)<PtrUInt(pEnd) then continue else break;
end else begin // B:1..253 = difference with previous
inc(i,ord(p^)); inc(p);
V^ := i; inc(V);
if PtrUInt(p)<PtrUInt(pEnd) then continue else break;
end;
end; // case p^ of
until false;
result := (PtrUInt(V)-result) shr 2; // returns count of stored integer
end;
function TFileBufferReader.ReadVarUInt32Array(var Values: TIntegerDynArray): PtrInt;
var count, n, i, diff: integer;
DataLayout: TFileBufferWriterKind;
P, PEnd: PByte;
PI: PInteger;
PIA: PIntegerArray absolute PI;
BufTemp: RawByteString;
begin
result := ReadVarUInt32;
if result=0 then
exit;
DataLayout := TFileBufferWriterKind(ReadByte);
if DataLayout=wkFakeMarker then begin
result := -result;
exit;
end;
count := result;
if count>length(Values) then // only set length is not big enough
SetLength(Values,count);
PI := pointer(Values);
if DataLayout in [wkOffsetU, wkOffsetI] then begin
PI^ := ReadVarUInt32;
dec(count);
if count=0 then
exit;
diff := ReadVarUInt32;
if diff<>0 then begin
for i := 0 to count-1 do
PIA^[i+1] := PIA^[i]+diff;
exit;
end;
end;
if DataLayout=wkUInt32 then
Read(@Values[0],count*4) else begin
repeat
ReadChunk(P,PEnd,BufTemp); // raise ErrorInvalidContent on error
case DataLayout of
wkVarInt32:
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
PI^ := FromVarInt32(P);
dec(count);
inc(PI);
end;
wkVarUInt32:
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
PI^ := FromVarUInt32(P);
dec(count);
inc(PI);
end;
wkSorted: begin
n := CleverReadInteger(pointer(P),pointer(PEnd),PI);
dec(count,n);
inc(PByte(PI),n*4);
end;
wkOffsetU: begin
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
PIA^[1] := PIA^[0]+integer(FromVarUInt32(P));
dec(count);
inc(PI);
end;
if count<=0 then
inc(PI); // make sure PI=@Values[result]
end;
wkOffsetI: begin
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
PIA^[1] := PIA^[0]+FromVarInt32(P);
dec(count);
inc(PI);
end;
if count<=0 then
inc(PI); // make sure PI=@Values[result]
end;
else
ErrorInvalidContent;
end;
until count<=0;
if PI<>@Values[result] then
ErrorInvalidContent;
end;
end;
function TFileBufferReader.ReadVarUInt64Array(var Values: TInt64DynArray): PtrInt;
var count, i: integer;
P, PEnd: PByte;
v: PQWordArray;
diff: QWord;
BufTemp: RawByteString;
begin
result := ReadVarUInt32;
if result=0 then
exit;
count := result;
if count>length(Values) then // only set length if not big enough
SetLength(Values,count);
v := pointer(Values);
if boolean(ReadByte) then begin // values were stored as offsets
v^[0] := ReadVarUInt64; // read first value
dec(count);
diff := ReadVarUInt32;
if diff=0 then begin
// read all offsets, and compute (not fixed sized records)
repeat
ReadChunk(P,PEnd,BufTemp); // raise ErrorInvalidContent on error
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
FromVarUInt64Safe(P,PEnd,diff);
v^[1] := v^[0]+diff;
v := @v^[1];
dec(count);
end;
until count<=0;
end else
// same offset for all items (fixed sized records)
for i := 0 to count-1 do
v^[i+1] := v^[i]+diff;
end else
repeat // raw values were stored, not their offsets
ReadChunk(P,PEnd,BufTemp); // raise ErrorInvalidContent on error
while PtrUInt(P)<PtrUInt(PEnd) do begin
FromVarUInt64Safe(P,PEnd,v^[0]);
v := @v^[1];
dec(count);
if count=0 then
exit;
end;
until false;
end;
function TFileBufferReader.ReadRawUTF8List(List: TRawUTF8List): boolean;
var i,n: integer;
v: TRawUTF8DynArray;
o: TObjectDynArray;
StoreObjectsAsVarUInt32: Boolean;
begin
if (fMap.Buffer<>nil) and (List<>nil) then begin
List.BeginUpdate;
try
List.Clear;
n := ReadVarRawUTF8DynArray(v);
result := true;
if n=0 then
exit;
Read(@StoreObjectsAsVarUInt32,1);
if StoreObjectsAsVarUInt32 then begin
List.Flags := List.Flags-[fObjectsOwned]; // Int32 here, not instances
SetLength(o,length(v));
for i := 0 to n-1 do
o[i] := TObject(ReadVarUInt32);
end;
List.SetFrom(v,o); // fast assignment using refcnt
finally
List.EndUpdate;
end;
end else
result := false;
end;
function TFileBufferReader.ReadVarRawUTF8DynArray(var Values: TRawUTF8DynArray): PtrInt;
var count, len, fixedsize: integer;
P, PEnd: PByte;
PI: PRawUTF8;
BufTemp: RawByteString;
begin
result := ReadVarUInt32;
if result=0 then
exit;
count := result;
2021-04-23 10:11:05 +02:00
if count>length(Values) then // change Values[] length only if not big enough
SetLength(Values,count);
PI := pointer(Values);
fixedsize := ReadVarUInt32;
repeat
ReadChunk(P,PEnd,BufTemp); // raise ErrorInvalidContent on error
if fixedsize=0 then
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
len := FromVarUInt32(P);
if len>0 then begin
FastSetString(PI^,P,len);
inc(P,len);
end else
if PI^<>'' then
PI^ := '';
dec(count);
inc(PI);
end else
// fixed size strings case
while (count>0) and (PtrUInt(P)<PtrUInt(PEnd)) do begin
FastSetString(PI^,P,fixedsize);
inc(P,fixedsize);
dec(count);
inc(PI);
end;
until count<=0;
if PI<>@Values[result] then
ErrorInvalidContent;
end;
{$ifndef CPU64}
function TFileBufferReader.Seek(Offset: Int64): boolean;
begin
if (Offset<0) or (Offset>fMap.Size) then
result := False else
if fMap.Buffer=nil then
result := FileSeek64(fMap.FileHandle,Offset,soFromBeginning)=Offset else begin
fCurrentPos := PCardinal(@Offset)^;
result := true;
end;
end;
{$endif CPU64}
function TFileBufferReader.Seek(Offset: PtrInt): boolean;
begin
// we don't need to handle fMap=0 here
if fMap.Buffer=nil then
Result := FileSeek(fMap.FileHandle,Offset,0)=Offset else
if (fMap.Buffer<>nil) and (PtrUInt(Offset)<PPtrUInt(@fMap.Size)^) then begin
fCurrentPos := Offset;
result := true;
end else
result := false;
end;
{ ************ high-level storage classes ************************* }
{ TSynCache }
constructor TSynCache.Create(aMaxCacheRamUsed: cardinal; aCaseSensitive: boolean;
aTimeoutSeconds: cardinal);
begin
inherited Create;
fNameValue.Init(aCaseSensitive);
fMaxRamUsed := aMaxCacheRamUsed;
fTimeoutSeconds := aTimeoutSeconds;
end;
procedure TSynCache.ResetIfNeeded;
var tix: cardinal;
begin
if fRamUsed>fMaxRamUsed then
Reset;
if fTimeoutSeconds>0 then begin
tix := {$ifdef FPCLINUX}SynFPCLinux.{$endif}GetTickCount64 shr 10;
if fTimeoutTix>tix then
Reset;
fTimeoutTix := tix+fTimeoutSeconds;
end;
end;
procedure TSynCache.Add(const aValue: RawUTF8; aTag: PtrInt);
begin
2021-04-23 10:11:05 +02:00
if (self=nil) or (fFindLastKey='') then
exit;
ResetIfNeeded;
inc(fRamUsed,length(aValue));
2021-04-23 10:11:05 +02:00
fNameValue.Add(fFindLastKey,aValue,aTag);
fFindLastKey := '';
end;
function TSynCache.Find(const aKey: RawUTF8; aResultTag: PPtrInt): RawUTF8;
2021-04-23 10:11:05 +02:00
var ndx: integer;
begin
result := '';
if self=nil then
exit;
2021-04-23 10:11:05 +02:00
fFindLastKey := aKey;
if aKey='' then
2021-04-23 10:11:05 +02:00
exit;
ndx := fNameValue.Find(aKey);
if ndx<0 then
exit;
with fNameValue.List[ndx] do begin
result := Value;
if aResultTag<>nil then
aResultTag^ := Tag;
end;
end;
function TSynCache.AddOrUpdate(const aKey, aValue: RawUTF8; aTag: PtrInt): boolean;
var ndx: integer;
begin
result := false;
if self=nil then
exit; // avoid GPF
fSafe.Lock;
try
ResetIfNeeded;
ndx := fNameValue.DynArray.FindHashedForAdding(aKey,result);
with fNameValue.List[ndx] do begin
Name := aKey;
dec(fRamUsed,length(Value));
Value := aValue;
inc(fRamUsed,length(Value));
Tag := aTag;
end;
finally
fSafe.Unlock;
end;
end;
function TSynCache.Reset: boolean;
begin
result := false;
if self=nil then
exit; // avoid GPF
fSafe.Lock;
try
if Count<>0 then begin
2021-04-23 10:11:05 +02:00
fNameValue.DynArray.Clear;
fNameValue.DynArray.ReHash;
result := true; // mark something was flushed
end;
fRamUsed := 0;
fTimeoutTix := 0;
finally
fSafe.Unlock;
end;
end;
function TSynCache.Count: integer;
begin
if self=nil then begin
result := 0;
exit;
end;
fSafe.Lock;
try
result := fNameValue.Count;
finally
fSafe.Unlock;
end;
end;
{ TSynQueue }
constructor TSynQueue.Create(aTypeInfo: pointer);
begin
inherited Create;
fFirst := -1;
fLast := -2;
fValues.Init(aTypeInfo,fValueVar,@fCount);
end;
destructor TSynQueue.Destroy;
begin
WaitPopFinalize;
fValues.Clear;
inherited Destroy;
end;
procedure TSynQueue.Clear;
begin
fSafe.Lock;
try
fValues.Clear;
fFirst := -1;
fLast := -2;
finally
fSafe.UnLock;
end;
end;
function TSynQueue.Count: Integer;
begin
if self=nil then
result := 0 else begin
fSafe.Lock;
try
if fFirst<0 then
result := 0 else
if fFirst<=fLast then
result := fLast-fFirst+1 else
result := fCount-fFirst+fLast+1;
finally
fSafe.UnLock;
end;
end;
end;
function TSynQueue.Capacity: integer;
begin
if self=nil then
result := 0 else begin
fSafe.Lock;
try
result := fValues.Capacity;
finally
fSafe.UnLock;
end;
end;
end;
function TSynQueue.Pending: boolean;
begin // allow some false positive: fSafe.Lock not used here
result := (self<>nil) and (fFirst>=0);
end;
procedure TSynQueue.Push(const aValue);
begin
fSafe.Lock;
try
if fFirst<0 then begin
fFirst := 0; // start from the bottom of the void queue
fLast := 0;
if fCount=0 then
fValues.Count := 64;
end else
if fFirst<=fLast then begin // stored in-order
inc(fLast);
if fLast=fCount then
InternalGrow;
end else begin
inc(fLast);
if fLast=fFirst then begin // collision -> arrange
fValues.AddArray(fValueVar,0,fLast); // move 0..fLast to the end
fLast := fCount;
InternalGrow;
end;
end;
fValues.ElemCopyFrom(aValue,fLast);
finally
fSafe.UnLock;
end;
end;
procedure TSynQueue.InternalGrow;
var cap: integer;
begin
cap := fValues.Capacity;
if fFirst>cap-fCount then // use leading space if worth it
fLast := 0 else // append at the end
if fCount=cap then // reallocation needed
fValues.Count := cap+cap shr 3+64 else
fCount := cap; // fill trailing memory as much as possible
end;
function TSynQueue.Peek(out aValue): boolean;
begin
fSafe.Lock;
try
result := fFirst>=0;
if result then
fValues.ElemCopyAt(fFirst,aValue);
finally
fSafe.UnLock;
end;
end;
function TSynQueue.Pop(out aValue): boolean;
begin
fSafe.Lock;
try
result := fFirst>=0;
if result then begin
fValues.ElemMoveTo(fFirst,aValue);
if fFirst=fLast then begin
fFirst := -1; // reset whole store (keeping current capacity)
fLast := -2;
end else begin
inc(fFirst);
if fFirst=fCount then
fFirst := 0; // will retrieve from leading items
end;
end;
finally
fSafe.UnLock;
end;
end;
function TSynQueue.PopEquals(aAnother: pointer; aCompare: TDynArraySortCompare;
out aValue): boolean;
begin
fSafe.Lock;
try
result := (fFirst>=0) and Assigned(aCompare) and Assigned(aAnother) and
(aCompare(fValues.ElemPtr(fFirst)^,aAnother^)=0) and Pop(aValue);
finally
fSafe.UnLock;
end;
end;
function TSynQueue.InternalDestroying(incPopCounter: integer): boolean;
begin
fSafe.Lock;
try
result := wpfDestroying in fWaitPopFlags;
inc(fWaitPopCounter,incPopCounter);
finally
fSafe.UnLock;
end;
end;
function TSynQueue.InternalWaitDone(endtix: Int64; const idle: TThreadMethod): boolean;
begin
SleepHiRes(1);
if Assigned(idle) then
idle; // e.g. Application.ProcessMessages
result := InternalDestroying(0) or (GetTickCount64>endtix);
end;
function TSynQueue.WaitPop(aTimeoutMS: integer; const aWhenIdle: TThreadMethod;
out aValue; aCompared: pointer; aCompare: TDynArraySortCompare): boolean;
var endtix: Int64;
begin
result := false;
if not InternalDestroying(+1) then
try
endtix := GetTickCount64+aTimeoutMS;
repeat
if Assigned(aCompared) and Assigned(aCompare) then
result := PopEquals(aCompared,aCompare,aValue) else
result := Pop(aValue);
until result or InternalWaitDone(endtix,aWhenIdle);
finally
InternalDestroying(-1);
end;
end;
function TSynQueue.WaitPeekLocked(aTimeoutMS: integer; const aWhenIdle: TThreadMethod): pointer;
var endtix: Int64;
begin
result := nil;
if not InternalDestroying(+1) then
try
endtix := GetTickCount64+aTimeoutMS;
repeat
fSafe.Lock;
try
if fFirst>=0 then
result := fValues.ElemPtr(fFirst);
finally
if result=nil then
fSafe.UnLock; // caller should always Unlock once done
end;
until (result<>nil) or InternalWaitDone(endtix,aWhenIdle);
finally
InternalDestroying(-1);
end;
end;
procedure TSynQueue.WaitPopFinalize(aTimeoutMS: integer);
var endtix: Int64; // never wait forever
begin
fSafe.Lock;
try
include(fWaitPopFlags,wpfDestroying);
if fWaitPopCounter=0 then
exit;
finally
fSafe.UnLock;
end;
endtix := GetTickCount64+aTimeoutMS;
repeat
SleepHiRes(1); // ensure WaitPos() is actually finished
until (fWaitPopCounter=0) or (GetTickCount64>endtix);
end;
procedure TSynQueue.Save(out aDynArrayValues; aDynArray: PDynArray);
var n: integer;
DA: TDynArray;
begin
DA.Init(fValues.ArrayType,aDynArrayValues,@n);
fSafe.Lock;
try
DA.Capacity := Count; // pre-allocate whole array, and set its length
if fFirst>=0 then
if fFirst<=fLast then
DA.AddArray(fValueVar,fFirst,fLast-fFirst+1) else begin
DA.AddArray(fValueVar,fFirst,fCount-fFirst);
DA.AddArray(fValueVar,0,fLast+1);
end;
finally
fSafe.UnLock;
end;
if aDynArray<>nil then
aDynArray^.Init(fValues.ArrayType,aDynArrayValues);
end;
{ TObjectListSorted }
destructor TObjectListSorted.Destroy;
var i: integer;
begin
for i := 0 to fCount-1 do
fObjArray[i].Free;
inherited;
end;
function TObjectListSorted.FastLocate(const Value; out Index: Integer): boolean;
var n, i, cmp: integer;
begin
result := False;
n := Count;
if n=0 then // a void array is always sorted
Index := 0 else begin
dec(n);
if Compare(fObjArray[n],Value)<0 then begin // already sorted
Index := n+1; // returns false + last position index to insert
exit;
end;
Index := 0;
while Index<=n do begin // O(log(n)) binary search of the sorted position
i := (Index+n) shr 1;
cmp := Compare(fObjArray[i],Value);
if cmp=0 then begin
Index := i; // index of existing Elem
result := True;
exit;
end else
if cmp<0 then
Index := i+1 else
n := i-1;
end;
// Elem not found: returns false + the index where to insert
end;
end;
procedure TObjectListSorted.InsertNew(Item: TSynPersistentLock;
Index: integer);
begin
if fCount=length(fObjArray) then
SetLength(fObjArray,NextGrow(fCount));
if cardinal(Index)<cardinal(fCount) then
MoveFast(fObjArray[Index],fObjArray[Index+1],(fCount-Index)*SizeOf(TObject)) else
Index := fCount;
fObjArray[Index] := Item;
inc(fCount);
end;
function TObjectListSorted.Delete(const Value): boolean;
var i: integer;
begin
result := false;
fSafe.Lock;
try
if FastLocate(Value,i) and (cardinal(i)<cardinal(fCount)) then begin
fObjArray[i].Free;
dec(fCount);
if fCount>i then
MoveFast(fObjArray[i+1],fObjArray[i],(fCount-i)*SizeOf(TObject));
result := true;
end;
finally
fSafe.UnLock;
end;
end;
function TObjectListSorted.FindLocked(const Value): pointer;
var i: integer;
begin
result := nil;
fSafe.Lock;
try
if FastLocate(Value,i) then begin
result := fObjArray[i];
TSynPersistentLock(result).Safe.Lock;
end;
finally
fSafe.UnLock;
end;
end;
function TObjectListSorted.FindOrAddLocked(const Value; out added: boolean): pointer;
var i: integer;
begin
added := false;
fSafe.Lock;
try
if not FastLocate(Value,i) then begin
InsertNew(NewItem(Value),i);
added := true;
end;
result := fObjArray[i];
TSynPersistentLock(result).Safe.Lock;
finally
fSafe.UnLock;
end;
end;
{ TSynPersistentStore }
constructor TSynPersistentStore.Create(const aName: RawUTF8);
begin
Create;
fName := aName;
end;
constructor TSynPersistentStore.CreateFrom(const aBuffer: RawByteString;
aLoad: TAlgoCompressLoad);
begin
CreateFromBuffer(pointer(aBuffer),length(aBuffer),aLoad);
end;
constructor TSynPersistentStore.CreateFromBuffer(
aBuffer: pointer; aBufferLen: integer; aLoad: TAlgoCompressLoad);
begin
Create('');
LoadFrom(aBuffer,aBufferLen,aLoad);
end;
constructor TSynPersistentStore.CreateFromFile(const aFileName: TFileName;
aLoad: TAlgoCompressLoad);
begin
Create('');
LoadFromFile(aFileName,aLoad);
end;
procedure TSynPersistentStore.LoadFromReader;
begin
fReader.VarUTF8(fName);
end;
procedure TSynPersistentStore.SaveToWriter(aWriter: TFileBufferWriter);
begin
aWriter.Write(fName);
end;
procedure TSynPersistentStore.LoadFrom(const aBuffer: RawByteString;
aLoad: TAlgoCompressLoad);
begin
if aBuffer <> '' then
LoadFrom(pointer(aBuffer),length(aBuffer),aLoad);
end;
procedure TSynPersistentStore.LoadFrom(aBuffer: pointer; aBufferLen: integer;
aLoad: TAlgoCompressLoad);
var localtemp: RawByteString;
p: pointer;
temp: PRawByteString;
begin
if (aBuffer=nil) or (aBufferLen<=0) then
exit; // nothing to load
fLoadFromLastAlgo := TAlgoCompress.Algo(aBuffer,aBufferLen);
if fLoadFromLastAlgo = nil then
fReader.ErrorData('%.LoadFrom unknown TAlgoCompress AlgoID=%',
[self,PByteArray(aBuffer)[4]]);
temp := fReaderTemp;
if temp=nil then
temp := @localtemp;
p := fLoadFromLastAlgo.Decompress(aBuffer,aBufferLen,fLoadFromLastUncompressed,temp^,aLoad);
if p=nil then
fReader.ErrorData('%.LoadFrom %.Decompress failed',[self,fLoadFromLastAlgo]);
fReader.Init(p,fLoadFromLastUncompressed);
LoadFromReader;
end;
function TSynPersistentStore.LoadFromFile(const aFileName: TFileName;
aLoad: TAlgoCompressLoad): boolean;
var temp: RawByteString;
begin
temp := StringFromFile(aFileName);
result := temp<>'';
if result then
LoadFrom(temp,aLoad);
end;
procedure TSynPersistentStore.SaveTo(out aBuffer: RawByteString; nocompression: boolean;
BufLen: integer; ForcedAlgo: TAlgoCompress; BufferOffset: integer);
var writer: TFileBufferWriter;
temp: array[word] of byte;
begin
if BufLen<=SizeOf(temp) then
writer := TFileBufferWriter.Create(TRawByteStringStream,@temp,SizeOf(temp)) else
writer := TFileBufferWriter.Create(TRawByteStringStream,BufLen);
try
SaveToWriter(writer);
fSaveToLastUncompressed := writer.TotalWritten;
aBuffer := writer.FlushAndCompress(nocompression,ForcedAlgo,BufferOffset);
finally
writer.Free;
end;
end;
function TSynPersistentStore.SaveTo(nocompression: boolean; BufLen: integer;
ForcedAlgo: TAlgoCompress; BufferOffset: integer): RawByteString;
begin
SaveTo(result,nocompression,BufLen,ForcedAlgo,BufferOffset);
end;
function TSynPersistentStore.SaveToFile(const aFileName: TFileName;
nocompression: boolean; BufLen: integer; ForcedAlgo: TAlgoCompress): PtrUInt;
var temp: RawByteString;
begin
SaveTo(temp,nocompression,BufLen,ForcedAlgo);
if FileFromString(temp,aFileName) then
result := length(temp) else
result := 0;
end;
{ TSynPersistentStoreJson }
procedure TSynPersistentStoreJson.AddJSON(W: TTextWriter);
begin
W.AddPropJSONString('name', fName);
end;
function TSynPersistentStoreJson.SaveToJSON(reformat: TTextWriterJSONFormat): RawUTF8;
var
W: TTextWriter;
begin
W := DefaultTextWriterSerializer.CreateOwnedStream(65536);
try
W.Add('{');
AddJSON(W);
W.CancelLastComma;
W.Add('}');
W.SetText(result, reformat);
finally
W.Free;
end;
end;
{ TRawByteStringGroup }
procedure TRawByteStringGroup.Add(const aItem: RawByteString);
begin
if Values=nil then
Clear; // ensure all fields are initialized, even if on stack
if Count=Length(Values) then
SetLength(Values,NextGrow(Count));
with Values[Count] do begin
Position := self.Position;
Value := aItem;
end;
LastFind := Count;
inc(Count);
inc(Position,Length(aItem));
end;
procedure TRawByteStringGroup.Add(aItem: pointer; aItemLen: integer);
var tmp: RawByteString;
begin
SetString(tmp,PAnsiChar(aItem),aItemLen);
Add(tmp);
end;
{$ifndef DELPHI5OROLDER} // circumvent Delphi 5 compiler bug
procedure TRawByteStringGroup.Add(const aAnother: TRawByteStringGroup);
var i: integer;
s,d: PRawByteStringGroupValue;
begin
if aAnother.Values=nil then
exit;
if Values=nil then
Clear; // ensure all fields are initialized, even if on stack
if Count+aAnother.Count>Length(Values) then
SetLength(Values,Count+aAnother.Count);
s := pointer(aAnother.Values);
d := @Values[Count];
for i := 1 to aAnother.Count do begin
d^.Position := Position;
d^.Value := s^.Value;
inc(Position,length(s^.Value));
inc(s);
inc(d);
end;
inc(Count,aAnother.Count);
LastFind := Count-1;
end;
procedure TRawByteStringGroup.RemoveLastAdd;
begin
if Count>0 then begin
dec(Count);
dec(Position,Length(Values[Count].Value));
Values[Count].Value := ''; // release memory
LastFind := Count-1;
end;
end;
function TRawByteStringGroup.Equals(const aAnother: TRawByteStringGroup): boolean;
begin
if ((Values=nil) and (aAnother.Values<>nil)) or ((Values<>nil) and (aAnother.Values=nil)) or
(Position<>aAnother.Position) then
result := false else
if (Count<>1) or (aAnother.Count<>1) or (Values[0].Value<>aAnother.Values[0].Value) then
result := AsText=aAnother.AsText else
result := true;
end;
{$endif DELPHI5OROLDER}
procedure TRawByteStringGroup.Clear;
begin
Values := nil;
Position := 0;
Count := 0;
LastFind := 0;
end;
procedure TRawByteStringGroup.AppendTextAndClear(var aDest: RawByteString);
var d,i: integer;
v: PRawByteStringGroupValue;
begin
d := length(aDest);
SetLength(aDest,d+Position);
v := pointer(Values);
for i := 1 to Count do begin
MoveFast(pointer(v^.Value)^,PByteArray(aDest)[d+v^.Position],length(v^.Value));
inc(v);
end;
Clear;
end;
function TRawByteStringGroup.AsText: RawByteString;
begin
if Values=nil then
result := '' else begin
if Count>1 then
Compact;
result := Values[0].Value;
end;
end;
procedure TRawByteStringGroup.Compact;
var i: integer;
v: PRawByteStringGroupValue;
tmp: RawByteString;
begin
if (Values<>nil) and (Count>1) then begin
SetString(tmp,nil,Position);
v := pointer(Values);
for i := 1 to Count do begin
MoveFast(pointer(v^.Value)^,PByteArray(tmp)[v^.Position],length(v^.Value));
{$ifdef FPC}Finalize(v^.Value){$else}v^.Value := ''{$endif}; // free chunks
inc(v);
end;
Values[0].Value := tmp; // use result for absolute compaction ;)
if Count>128 then
SetLength(Values,128);
Count := 1;
LastFind := 0;
end;
end;
function TRawByteStringGroup.AsBytes: TByteDynArray;
var i: integer;
begin
result := nil;
if Values=nil then
exit;
SetLength(result,Position);
for i := 0 to Count-1 do
with Values[i] do
MoveFast(pointer(Value)^,PByteArray(result)[Position],length(Value));
end;
procedure TRawByteStringGroup.Write(W: TTextWriter; Escape: TTextWriterKind);
var i: integer;
begin
if Values<>nil then
for i := 0 to Count-1 do
with Values[i] do
W.Add(PUTF8Char(pointer(Value)),length(Value),Escape);
end;
procedure TRawByteStringGroup.WriteBinary(W: TFileBufferWriter);
var i: integer;
begin
if Values<>nil then
for i := 0 to Count-1 do
W.WriteBinary(Values[i].Value);
end;
procedure TRawByteStringGroup.WriteString(W: TFileBufferWriter);
begin
if Values=nil then begin
W.Write1(0);
exit;
end;
W.WriteVarUInt32(Position);
WriteBinary(W);
end;
procedure TRawByteStringGroup.AddFromReader(var aReader: TFastReader);
var complexsize: integer;
begin
complexsize := aReader.VarUInt32;
if complexsize>0 then // directly create a RawByteString from aReader buffer
Add(aReader.Next(complexsize),complexsize);
end;
function TRawByteStringGroup.Find(aPosition: integer): PRawByteStringGroupValue;
var i: integer;
begin
if (pointer(Values)<>nil) and (cardinal(aPosition)<cardinal(Position)) then begin
result := @Values[LastFind]; // this cache is very efficient in practice
if (aPosition>=result^.Position) and (aPosition<result^.Position+length(result^.Value)) then
exit;
result := @Values[1]; // seldom O(n) brute force search (in CPU L1 cache)
for i := 0 to Count-2 do
if result^.Position>aPosition then begin
dec(result);
LastFind := i;
exit;
end else
inc(result);
dec(result);
LastFind := Count-1;
end
else
result := nil;
end;
function TRawByteStringGroup.Find(aPosition, aLength: integer): pointer;
var P: PRawByteStringGroupValue;
i: integer;
label found;
begin
if (pointer(Values)<>nil) and (cardinal(aPosition)<cardinal(Position)) then begin
P := @Values[LastFind]; // this cache is very efficient in practice
i := aPosition-P^.Position;
if (i>=0) and (i+aLength<length(P^.Value)) then begin
result := @PByteArray(P^.Value)[i];
exit;
end;
P := @Values[1]; // seldom O(n) brute force search (in CPU L1 cache)
for i := 0 to Count-2 do
if P^.Position>aPosition then begin
LastFind := i;
found: dec(P);
dec(aPosition,P^.Position);
if aLength-aPosition<=length(P^.Value) then
result := @PByteArray(P^.Value)[aPosition] else
result := nil;
exit;
end else
inc(P);
LastFind := Count-1;
goto found;
end
else
result := nil;
end;
procedure TRawByteStringGroup.FindAsText(aPosition, aLength: integer; out aText: RawByteString);
var P: PRawByteStringGroupValue;
begin
P := Find(aPosition);
if P=nil then
exit;
dec(aPosition,P^.Position);
if (aPosition=0) and (length(P^.Value)=aLength) then
aText := P^.Value else // direct return if not yet compacted
if aLength-aPosition<=length(P^.Value) then
SetString(aText,PAnsiChar(@PByteArray(P^.Value)[aPosition]),aLength);
end;
function TRawByteStringGroup.FindAsText(aPosition, aLength: integer): RawByteString;
begin
FindAsText(aPosition,aLength,result);
end;
{$ifndef NOVARIANTS}
procedure TRawByteStringGroup.FindAsVariant(aPosition, aLength: integer; out aDest: variant);
var tmp: RawByteString;
begin
tmp := FindAsText(aPosition,aLength);
if tmp <> '' then
RawUTF8ToVariant(tmp,aDest);
end;
{$endif NOVARIANTS}
procedure TRawByteStringGroup.FindWrite(aPosition, aLength: integer;
W: TTextWriter; Escape: TTextWriterKind; TrailingCharsToIgnore: integer);
var P: pointer;
begin
P := Find(aPosition,aLength);
if P<>nil then
W.Add(PUTF8Char(P)+TrailingCharsToIgnore,aLength-TrailingCharsToIgnore,Escape);
end;
procedure TRawByteStringGroup.FindWriteBase64(aPosition, aLength: integer;
W: TTextWriter; withMagic: boolean);
var P: pointer;
begin
P := Find(aPosition,aLength);
if P<>nil then
W.WrBase64(P,aLength,withMagic);
end;
procedure TRawByteStringGroup.FindMove(aPosition, aLength: integer; aDest: pointer);
var P: pointer;
begin
P := Find(aPosition,aLength);
if P<>nil then
MoveFast(P^,aDest^,aLength);
end;
{ TPropNameList }
procedure TPropNameList.Init;
begin
Count := 0;
end;
function TPropNameList.FindPropName(const Value: RawUTF8): Integer;
begin
result := SynCommons.FindPropName(Pointer(Values),Value,Count);
end;
function TPropNameList.AddPropName(const Value: RawUTF8): Boolean;
begin
if (Value<>'') and (SynCommons.FindPropName(pointer(Values),Value,Count)<0) then begin
if Count=length(Values) then
SetLength(Values,NextGrow(Count));
Values[Count] := Value;
inc(Count);
result := true;
end else
result := false;
end;
{ ************ Security and Identifiers classes ************************** }
{ TSynUniqueIdentifierBits }
function TSynUniqueIdentifierBits.Counter: word;
begin
result := PWord(@Value)^ and $7fff;
end;
function TSynUniqueIdentifierBits.ProcessID: TSynUniqueIdentifierProcess;
begin
result := (PCardinal(@Value)^ shr 15) and $ffff;
end;
function TSynUniqueIdentifierBits.CreateTimeUnix: TUnixTime;
begin
result := Value shr 31;
end;
{$ifndef NOVARIANTS}
function TSynUniqueIdentifierBits.AsVariant: variant;
begin
ToVariant(result);
end;
procedure TSynUniqueIdentifierBits.ToVariant(out result: variant);
begin
TDocVariantData(result).InitObject(['Created',DateTimeToIso8601Text(CreateDateTime),
'Identifier',ProcessID,'Counter',Counter,'Value',Value,
'Hex',Int64ToHex(Value)],JSON_OPTIONS_FAST);
end;
{$endif NOVARIANTS}
{$ifndef DELPHI5OROLDER}
function TSynUniqueIdentifierBits.Equal(const Another: TSynUniqueIdentifierBits): boolean;
begin
result := Value=Another.Value;
end;
{$endif}
procedure TSynUniqueIdentifierBits.From(const AID: TSynUniqueIdentifier);
begin
Value := AID;
end;
function TSynUniqueIdentifierBits.CreateTimeLog: TTimeLog;
begin
PTimeLogBits(@result)^.From(UnixTimeToDateTime(Value shr 31));
end;
function TSynUniqueIdentifierBits.CreateDateTime: TDateTime;
begin
result := UnixTimeToDateTime(Value shr 31);
end;
function TSynUniqueIdentifierBits.ToHexa: RawUTF8;
begin
Int64ToHex(Value,result);
end;
function TSynUniqueIdentifierBits.FromHexa(const hexa: RawUTF8): boolean;
begin
result := (Length(hexa)=16) and HexDisplayToBin(pointer(hexa),@Value,SizeOf(Value));
end;
procedure TSynUniqueIdentifierBits.FromDateTime(const aDateTime: TDateTime);
begin
Value := DateTimeToUnixTime(aDateTime) shl 31;
end;
procedure TSynUniqueIdentifierBits.FromUnixTime(const aUnixTime: TUnixTime);
begin
Value := aUnixTime shl 31;
end;
{ TSynUniqueIdentifierGenerator }
const // fSafe.Padding[] slots
SYNUNIQUEGEN_COMPUTECOUNT = 0;
procedure TSynUniqueIdentifierGenerator.ComputeNew(
out result: TSynUniqueIdentifierBits);
var currentTime: cardinal;
begin
currentTime := UnixTimeUTC; // under Windows faster than GetTickCount64
fSafe.Lock;
try
if currentTime>fUnixCreateTime then begin
fUnixCreateTime := currentTime;
fLastCounter := 0; // reset
end;
if fLastCounter=$7fff then begin // collision (unlikely) -> cheat on timestamp
inc(fUnixCreateTime);
fLastCounter := 0;
end else
inc(fLastCounter);
result.Value := Int64(fLastCounter or fIdentifierShifted) or
(Int64(fUnixCreateTime) shl 31);
inc(fSafe.Padding[SYNUNIQUEGEN_COMPUTECOUNT].VInt64);
finally
fSafe.UnLock;
end;
end;
function TSynUniqueIdentifierGenerator.ComputeNew: Int64;
begin
ComputeNew(PSynUniqueIdentifierBits(@result)^);
end;
function TSynUniqueIdentifierGenerator.GetComputedCount: Int64;
begin
{$ifdef NOVARIANTS}
fSafe.Lock;
result := fSafe.Padding[SYNUNIQUEGEN_COMPUTECOUNT].VInt64;
fSafe.Unlock;
{$else}
result := fSafe.LockedInt64[SYNUNIQUEGEN_COMPUTECOUNT];
{$endif}
end;
procedure TSynUniqueIdentifierGenerator.ComputeFromDateTime(const aDateTime: TDateTime;
out result: TSynUniqueIdentifierBits);
begin // assume fLastCounter=0
ComputeFromUnixTime(DateTimeToUnixTime(aDateTime),result);
end;
procedure TSynUniqueIdentifierGenerator.ComputeFromUnixTime(const aUnixTime: TUnixTime;
out result: TSynUniqueIdentifierBits);
begin // assume fLastCounter=0
result.Value := aUnixTime shl 31;
if self<>nil then
result.Value := result.Value or fIdentifierShifted;
end;
constructor TSynUniqueIdentifierGenerator.Create(aIdentifier: TSynUniqueIdentifierProcess;
const aSharedObfuscationKey: RawUTF8);
var i, len: integer;
crc: cardinal;
begin
fIdentifier := aIdentifier;
fIdentifierShifted := aIdentifier shl 15;
fSafe.Init;
fSafe.Padding[SYNUNIQUEGEN_COMPUTECOUNT].VType := varInt64; // reset to 0
// compute obfuscation key using hash diffusion of the supplied text
len := length(aSharedObfuscationKey);
crc := crc32ctab[0,len and 1023];
for i := 0 to high(fCrypto)+1 do begin
crc := crc32ctab[0,crc and 1023] xor crc32ctab[3,i] xor
kr32(crc,pointer(aSharedObfuscationKey),len) xor
crc32c(crc,pointer(aSharedObfuscationKey),len) xor
fnv32(crc,pointer(aSharedObfuscationKey),len);
// do not modify those hashes above or you will break obfuscation pattern!
if i<=high(fCrypto) then
fCrypto[i] := crc else
fCryptoCRC := crc;
end;
// due to the weakness of the hash algorithms used, this approach is a bit
// naive and would be broken easily with brute force - but point here is to
// hide/obfuscate public values at end-user level (e.g. when publishing URIs),
// not implement strong security, so it sounds good enough for our purpose
end;
destructor TSynUniqueIdentifierGenerator.Destroy;
begin
fSafe.Done;
FillCharFast(fCrypto,SizeOf(fCrypto),0);
fCryptoCRC := 0;
inherited Destroy;
end;
type // compute a 24 hexadecimal chars (96 bits) obfuscated pseudo file name
TSynUniqueIdentifierObfuscatedBits = packed record
crc: cardinal;
id: TSynUniqueIdentifierBits;
end;
function TSynUniqueIdentifierGenerator.ToObfuscated(
const aIdentifier: TSynUniqueIdentifier): TSynUniqueIdentifierObfuscated;
var bits: TSynUniqueIdentifierObfuscatedBits;
key: cardinal;
begin
result := '';
if aIdentifier=0 then
exit;
bits.id.Value := aIdentifier;
if self=nil then
key := 0 else
key := crc32ctab[0,bits.id.ProcessID and 1023] xor fCryptoCRC;
bits.crc := crc32c(bits.id.ProcessID,@bits.id,SizeOf(bits.id)) xor key;
if self<>nil then
bits.id.Value := bits.id.Value xor PInt64(@fCrypto[high(fCrypto)-1])^;
result := BinToHex(@bits,SizeOf(bits));
end;
function TSynUniqueIdentifierGenerator.FromObfuscated(
const aObfuscated: TSynUniqueIdentifierObfuscated;
out aIdentifier: TSynUniqueIdentifier): boolean;
var bits: TSynUniqueIdentifierObfuscatedBits;
len: integer;
key: cardinal;
begin
result := false;
len := PosExChar('.',aObfuscated);
if len=0 then
len := Length(aObfuscated) else
dec(len); // trim right '.jpg'
if (len<>SizeOf(bits)*2) or
not SynCommons.HexToBin(pointer(aObfuscated),@bits,SizeOf(bits)) then
exit;
if self=nil then
key := 0 else begin
bits.id.Value := bits.id.Value xor PInt64(@fCrypto[high(fCrypto)-1])^;
key := crc32ctab[0,bits.id.ProcessID and 1023] xor fCryptoCRC;
end;
if crc32c(bits.id.ProcessID,@bits.id,SizeOf(bits.id)) xor key=bits.crc then begin
aIdentifier := bits.id.Value;
result := true;
end;
end;
{ TSynPersistentWithPassword }
destructor TSynPersistentWithPassword.Destroy;
begin
UniqueRawUTF8(fPassword);
FillZero(fPassword);
inherited Destroy;
end;
class function TSynPersistentWithPassword.ComputePassword(const PlainPassword: RawUTF8;
CustomKey: cardinal): RawUTF8;
var instance: TSynPersistentWithPassword;
begin
instance := TSynPersistentWithPassword.Create;
try
instance.Key := CustomKey;
instance.SetPassWordPlain(PlainPassword);
result := instance.fPassWord;
finally
instance.Free;
end;
end;
class function TSynPersistentWithPassword.ComputePassword(PlainPassword: pointer;
PlainPasswordLen: integer; CustomKey: cardinal): RawUTF8;
begin
result := ComputePassword(BinToBase64uri(PlainPassword,PlainPasswordLen));
end;
class function TSynPersistentWithPassword.ComputePlainPassword(const CypheredPassword: RawUTF8;
CustomKey: cardinal; const AppSecret: RawUTF8): RawUTF8;
var instance: TSynPersistentWithPassword;
begin
instance := TSynPersistentWithPassword.Create;
try
instance.Key := CustomKey;
instance.fPassWord := CypheredPassword;
result := instance.GetPassWordPlainInternal(AppSecret);
finally
instance.Free;
end;
end;
function TSynPersistentWithPassword.GetPasswordFieldAddress: pointer;
begin
result := @fPassword;
end;
function TSynPersistentWithPassword.GetKey: cardinal;
begin
if self=nil then
result := 0 else
result := fKey xor $A5abba5A;
end;
function TSynPersistentWithPassword.GetPassWordPlain: RawUTF8;
begin
result := GetPassWordPlainInternal('');
end;
function TSynPersistentWithPassword.GetPassWordPlainInternal(AppSecret: RawUTF8): RawUTF8;
var value,pass: RawByteString;
usr: RawUTF8;
i,j: integer;
begin
result := '';
if (self=nil) or (fPassWord='') then
exit;
if Assigned(TSynPersistentWithPasswordUserCrypt) then begin
if AppSecret='' then
ToText(ClassType,AppSecret);
usr := ExeVersion.User+':';
i := PosEx(usr,fPassword);
if (i=1) or ((i>0) and (fPassword[i-1]=',')) then begin
inc(i,length(usr));
j := PosEx(',',fPassword,i);
if j=0 then
j := length(fPassword)+1;
Base64ToBin(@fPassword[i],j-i,pass);
if pass<>'' then
result := TSynPersistentWithPasswordUserCrypt(pass,AppSecret,false);
end else begin
i := PosExChar(':',fPassword);
if i>0 then
raise ESynException.CreateUTF8('%.GetPassWordPlain unable to retrieve the '+
'stored value: current user is [%], but password in % was encoded for [%]',
[self,ExeVersion.User,AppSecret,copy(fPassword,1,i-1)]);
end;
end;
if result='' then begin
value := Base64ToBin(fPassWord);
SymmetricEncrypt(GetKey,value);
result := value;
end;
end;
procedure TSynPersistentWithPassword.SetPassWordPlain(const value: RawUTF8);
var tmp: RawByteString;
begin
if self=nil then
exit;
if value='' then begin
fPassWord := '';
exit;
end;
SetString(tmp,PAnsiChar(pointer(value)),Length(value)); // private copy
SymmetricEncrypt(GetKey,tmp);
fPassWord := BinToBase64(tmp);
end;
{ TSynConnectionDefinition }
constructor TSynConnectionDefinition.CreateFromJSON(const JSON: RawUTF8;
Key: cardinal);
var privateCopy: RawUTF8;
values: array[0..4] of TValuePUTF8Char;
begin
fKey := Key;
privateCopy := JSON;
JSONDecode(privateCopy,['Kind','ServerName','DatabaseName','User','Password'],@values);
fKind := values[0].ToString;
values[1].ToUTF8(fServerName);
values[2].ToUTF8(fDatabaseName);
values[3].ToUTF8(fUser);
values[4].ToUTF8(fPassWord);
end;
function TSynConnectionDefinition.SaveToJSON: RawUTF8;
begin
result := JSONEncode(['Kind',fKind,'ServerName',fServerName,
'DatabaseName',fDatabaseName,'User',fUser,'Password',fPassword]);
end;
{ TSynAuthenticationAbstract }
constructor TSynAuthenticationAbstract.Create;
begin
fSafe.Init;
fTokenSeed := Random32gsl;
fSessionGenerator := abs(fTokenSeed*PPtrInt(self)^);
fTokenSeed := abs(fTokenSeed*Random32gsl);
end;
destructor TSynAuthenticationAbstract.Destroy;
begin
fSafe.Done;
inherited;
end;
class function TSynAuthenticationAbstract.ComputeHash(Token: Int64;
const UserName,PassWord: RawUTF8): cardinal;
begin // rough authentication - xxHash32 is less reversible than crc32c
result := xxHash32(xxHash32(xxHash32(Token,@Token,SizeOf(Token)),
pointer(UserName),length(UserName)),pointer(Password),length(PassWord));
end;
function TSynAuthenticationAbstract.ComputeCredential(previous: boolean;
const UserName,PassWord: RawUTF8): cardinal;
var tok: Int64;
begin
tok := GetTickCount64 div 10000;
if previous then
dec(tok);
result := ComputeHash(tok xor fTokenSeed,UserName,PassWord);
end;
function TSynAuthenticationAbstract.CurrentToken: Int64;
begin
result := (GetTickCount64 div 10000) xor fTokenSeed;
end;
procedure TSynAuthenticationAbstract.AuthenticateUser(const aName, aPassword: RawUTF8);
begin
2021-04-23 10:11:05 +02:00
raise ESynException.CreateUTF8('%.AuthenticateUser() is not implemented',[self]);
end;
procedure TSynAuthenticationAbstract.DisauthenticateUser(const aName: RawUTF8);
begin
2021-04-23 10:11:05 +02:00
raise ESynException.CreateUTF8('%.DisauthenticateUser() is not implemented',[self]);
end;
2021-04-23 10:11:05 +02:00
function TSynAuthenticationAbstract.CheckCredentials(const UserName: RaWUTF8;
Hash: cardinal): boolean;
var password: RawUTF8;
2021-04-23 10:11:05 +02:00
begin
result := GetPassword(UserName,password) and
((ComputeCredential(false,UserName,password)=Hash) or
(ComputeCredential(true,UserName,password)=Hash));
end;
function TSynAuthenticationAbstract.CreateSession(const User: RawUTF8;
Hash: cardinal): integer;
begin
result := 0;
fSafe.Lock;
try
2021-04-23 10:11:05 +02:00
if not CheckCredentials(User,Hash) then
exit;
repeat
result := fSessionGenerator;
inc(fSessionGenerator);
until result<>0;
AddSortedInteger(fSessions,fSessionsCount,result);
finally
fSafe.UnLock;
end;
end;
function TSynAuthenticationAbstract.SessionExists(aID: integer): boolean;
begin
fSafe.Lock;
try
result := FastFindIntegerSorted(pointer(fSessions),fSessionsCount-1,aID)>=0;
finally
fSafe.UnLock;
end;
end;
procedure TSynAuthenticationAbstract.RemoveSession(aID: integer);
var i: integer;
begin
fSafe.Lock;
try
i := FastFindIntegerSorted(pointer(fSessions),fSessionsCount-1,aID);
if i>=0 then
DeleteInteger(fSessions,fSessionsCount,i);
finally
fSafe.UnLock;
end;
end;
{ TSynAuthentication }
constructor TSynAuthentication.Create(const aUserName,aPassword: RawUTF8);
begin
inherited Create;
fCredentials.Init(true);
if aUserName<>'' then
AuthenticateUser(aUserName,aPassword);
end;
function TSynAuthentication.GetPassword(const UserName: RawUTF8;
out Password: RawUTF8): boolean;
var i: integer;
begin // caller did protect this method via fSafe.Lock
i := fCredentials.Find(UserName);
if i<0 then begin
result := false;
exit;
end;
password := fCredentials.List[i].Value;
result := true;
end;
function TSynAuthentication.GetUsersCount: integer;
begin
fSafe.Lock;
try
result := fCredentials.Count;
finally
fSafe.UnLock;
end;
end;
procedure TSynAuthentication.AuthenticateUser(const aName, aPassword: RawUTF8);
begin
fSafe.Lock;
try
fCredentials.Add(aName,aPassword);
finally
fSafe.UnLock;
end;
end;
procedure TSynAuthentication.DisauthenticateUser(const aName: RawUTF8);
begin
fSafe.Lock;
try
fCredentials.Delete(aName);
finally
fSafe.UnLock;
end;
end;
{ TIPBan }
procedure TIPBan.LoadFromReader;
begin
inherited;
fReader.ReadVarUInt32Array(fIP4);
fCount := length(fIP4);
end;
procedure TIPBan.SaveToWriter(aWriter: TFileBufferWriter);
begin // wkSorted not efficient: too big diffs between IPs
aWriter.WriteVarUInt32Array(fIP4, fCount, wkUInt32);
end;
function TIPBan.Add(const aIP: RawUTF8): boolean;
var ip4: cardinal;
begin
result := false;
if (self=nil) or not IPToCardinal(aIP,ip4) then
exit;
fSafe.Lock;
try
AddSortedInteger(fIP4,fCount,ip4);
result := true;
finally
fSafe.UnLock;
end;
end;
function TIPBan.Delete(const aIP: RawUTF8): boolean;
var ip4: cardinal;
i: integer;
begin
result := false;
if (self=nil) or not IPToCardinal(aIP,ip4) then
exit;
fSafe.Lock;
try
i := FastFindIntegerSorted(pointer(fIP4),fCount-1,ip4);
if i<0 then
exit;
DeleteInteger(fIP4,fCount,i);
result := true;
finally
fSafe.UnLock;
end;
end;
function TIPBan.Exists(const aIP: RawUTF8): boolean;
var ip4: cardinal;
begin
result := false;
if (self=nil) or (fCount=0) or not IPToCardinal(aIP,ip4) then
exit;
fSafe.Lock;
try
if FastFindIntegerSorted(pointer(fIP4),fCount-1,ip4)>=0 then
result := true;
finally
fSafe.UnLock;
end;
end;
function TIPBan.DynArrayLocked: TDynArray;
begin
fSafe.Lock;
result.InitSpecific(TypeInfo(TCardinalDynArray),fIP4,djCardinal,@fCount);
end;
{ ************ Database types and classes ************************** }
{$ifdef FPC}{$push}{$endif}
{$WARNINGS OFF} // yes, we know there will be dead code below: we rely on it ;)
function IsZero(const Fields: TSQLFieldBits): boolean;
var f: TPtrIntArray absolute Fields;
begin
{$ifdef CPU64}
if MAX_SQLFIELDS=64 then
result := (f[0]=0) else
if MAX_SQLFields=128 then
result := (f[0]=0) and (f[1]=0) else
if MAX_SQLFields=192 then
result := (f[0]=0) and (f[1]=0) and (f[2]=0) else
if MAX_SQLFields=256 then
result := (f[0]=0) and (f[1]=0) and (f[2]=0) and (f[3]=0) else
{$else}
if MAX_SQLFIELDS=64 then
result := (f[0]=0) and (f[1]=0) else
if MAX_SQLFields=128 then
result := (f[0]=0) and (f[1]=0) and (f[2]=0) and (f[3]=0) else
if MAX_SQLFields=192 then
result := (f[0]=0) and (f[1]=0) and (f[2]=0) and (f[3]=0)
and (f[4]=0) and (f[5]=0) else
if MAX_SQLFields=256 then
result := (f[0]=0) and (f[1]=0) and (f[2]=0) and (f[3]=0)
and (f[4]=0) and (f[5]=0) and (f[6]=0) and (f[7]=0) else
{$endif}
result := IsZero(@Fields,SizeOf(Fields))
end;
function IsEqual(const A,B: TSQLFieldBits): boolean;
var a_: TPtrIntArray absolute A;
b_: TPtrIntArray absolute B;
begin
{$ifdef CPU64}
if MAX_SQLFIELDS=64 then
result := (a_[0]=b_[0]) else
if MAX_SQLFields=128 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) else
if MAX_SQLFields=192 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) and (a_[2]=b_[2]) else
if MAX_SQLFields=256 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) and (a_[2]=b_[2]) and (a_[3]=b_[3]) else
{$else}
if MAX_SQLFIELDS=64 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) else
if MAX_SQLFields=128 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) and (a_[2]=b_[2]) and (a_[3]=b_[3]) else
if MAX_SQLFields=192 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) and (a_[2]=b_[2]) and (a_[3]=b_[3])
and (a_[4]=b_[4]) and (a_[5]=b_[5]) else
if MAX_SQLFields=256 then
result := (a_[0]=b_[0]) and (a_[1]=b_[1]) and (a_[2]=b_[2]) and (a_[3]=b_[3])
and (a_[4]=b_[4]) and (a_[5]=b_[5]) and (a_[6]=b_[6]) and (a_[7]=b_[7]) else
{$endif}
result := CompareMemFixed(@A,@B,SizeOf(TSQLFieldBits))
end;
procedure FillZero(var Fields: TSQLFieldBits);
begin
if MAX_SQLFIELDS=64 then
PInt64(@Fields)^ := 0 else
if MAX_SQLFields=128 then begin
PInt64Array(@Fields)^[0] := 0;
PInt64Array(@Fields)^[1] := 0;
end else
if MAX_SQLFields=192 then begin
PInt64Array(@Fields)^[0] := 0;
PInt64Array(@Fields)^[1] := 0;
PInt64Array(@Fields)^[2] := 0;
end else
if MAX_SQLFields=256 then begin
PInt64Array(@Fields)^[0] := 0;
PInt64Array(@Fields)^[1] := 0;
PInt64Array(@Fields)^[2] := 0;
PInt64Array(@Fields)^[3] := 0;
end else
FillCharFast(Fields,SizeOf(Fields),0);
end;
{$ifdef FPC}{$pop}{$else}{$WARNINGS ON}{$endif}
procedure FieldBitsToIndex(const Fields: TSQLFieldBits; var Index: TSQLFieldIndexDynArray;
MaxLength,IndexStart: integer);
var i,n: integer;
sets: array[0..MAX_SQLFIELDS-1] of TSQLFieldIndex; // to avoid memory reallocation
begin
n := 0;
for i := 0 to MaxLength-1 do
if i in Fields then begin
sets[n] := i;
inc(n);
end;
SetLength(Index,IndexStart+n);
for i := 0 to n-1 do
Index[IndexStart+i] := sets[i];
end;
function FieldBitsToIndex(const Fields: TSQLFieldBits;
MaxLength: integer): TSQLFieldIndexDynArray;
begin
FieldBitsToIndex(Fields,result,MaxLength);
end;
function AddFieldIndex(var Indexes: TSQLFieldIndexDynArray; Field: integer): integer;
begin
result := length(Indexes);
SetLength(Indexes,result+1);
Indexes[result] := Field;
end;
function SearchFieldIndex(var Indexes: TSQLFieldIndexDynArray; Field: integer): integer;
begin
for result := 0 to length(Indexes)-1 do
if Indexes[result]=Field then
exit;
result := -1;
end;
procedure FieldIndexToBits(const Index: TSQLFieldIndexDynArray; var Fields: TSQLFieldBits);
var i: integer;
begin
FillCharFast(Fields,SizeOf(Fields),0);
for i := 0 to Length(Index)-1 do
if Index[i]>=0 then
include(Fields,Index[i]);
end;
function FieldIndexToBits(const Index: TSQLFieldIndexDynArray): TSQLFieldBits;
begin
FieldIndexToBits(Index,result);
end;
function DateToSQL(Date: TDateTime): RawUTF8;
begin
result := '';
if Date<=0 then
exit;
FastSetString(result,nil,13);
PCardinal(pointer(result))^ := JSON_SQLDATE_MAGIC;
DateToIso8601PChar(Date,PUTF8Char(pointer(result))+3,True);
end;
function DateToSQL(Year,Month,Day: Cardinal): RawUTF8;
begin
result := '';
if (Year=0) or (Month-1>11) or (Day-1>30) then
exit;
FastSetString(result,nil,13);
PCardinal(pointer(result))^ := JSON_SQLDATE_MAGIC;
DateToIso8601PChar(PUTF8Char(pointer(result))+3,True,Year,Month,Day);
end;
var
JSON_SQLDATE_MAGIC_TEXT: RawUTF8;
function DateTimeToSQL(DT: TDateTime; WithMS: boolean): RawUTF8;
begin
if DT<=0 then
result := '' else begin
if frac(DT)=0 then
result := JSON_SQLDATE_MAGIC_TEXT+DateToIso8601(DT,true) else
if trunc(DT)=0 then
result := JSON_SQLDATE_MAGIC_TEXT+TimeToIso8601(DT,true,'T',WithMS) else
result := JSON_SQLDATE_MAGIC_TEXT+DateTimeToIso8601(DT,true,'T',WithMS);
end;
end;
function TimeLogToSQL(const Timestamp: TTimeLog): RawUTF8;
begin
if Timestamp=0 then
result := '' else
result := JSON_SQLDATE_MAGIC_TEXT+PTimeLogBits(@Timestamp)^.Text(true);
end;
function Iso8601ToSQL(const S: RawByteString): RawUTF8;
begin
if IsIso8601(pointer(S),length(S)) then
result := JSON_SQLDATE_MAGIC_TEXT+S else
result := '';
end;
function SQLToDateTime(const ParamValueWithMagic: RawUTF8): TDateTime;
begin
result := Iso8601ToDateTimePUTF8Char(PUTF8Char(pointer(ParamValueWithMagic))+3,
length(ParamValueWithMagic)-3);
end;
const
NULL_LOW = ord('n')+ord('u')shl 8+ord('l')shl 16+ord('l')shl 24;
function SQLParamContent(P: PUTF8Char; out ParamType: TSQLParamType; out ParamValue: RawUTF8;
out wasNull: boolean): PUTF8Char;
var PBeg: PAnsiChar;
L: integer;
c: cardinal;
begin
ParamType := sptUnknown;
wasNull := false;
result := nil;
if P=nil then
exit;
while (P^<=' ') and (P^<>#0) do inc(P);
case P^ of
'''','"': begin
P := UnQuoteSQLStringVar(P,ParamValue);
if P=nil then
exit; // not a valid quoted string (e.g. unexpected end in middle of it)
ParamType := sptText;
L := length(ParamValue)-3;
if L>0 then begin
c := PInteger(ParamValue)^ and $00ffffff;
if c=JSON_BASE64_MAGIC then begin
// ':("\uFFF0base64encodedbinary"):' format -> decode
Base64MagicDecode(ParamValue); // wrapper function to avoid temp. string
ParamType := sptBlob;
end else
if (c=JSON_SQLDATE_MAGIC) and // handle ':("\uFFF112012-05-04"):' format
IsIso8601(PUTF8Char(pointer(ParamValue))+3,L) then begin
Delete(ParamValue,1,3); // return only ISO-8601 text
ParamType := sptDateTime; // identified as Date/Time
end;
end;
end;
'-','+','0'..'9': begin // allow 0 or + in SQL
// check if P^ is a true numerical value
PBeg := pointer(P);
ParamType := sptInteger;
repeat inc(P) until not (P^ in ['0'..'9']); // check digits
if P^='.' then begin
inc(P);
if P^ in ['0'..'9'] then begin
ParamType := sptFloat;
repeat inc(P) until not (P^ in ['0'..'9']); // check fractional digits
end else begin
ParamType := sptUnknown; // invalid '23023.' value
exit;
end;
end;
if byte(P^) and $DF=ord('E') then begin
ParamType := sptFloat;
inc(P);
if P^='+' then inc(P) else
if P^='-' then inc(P);
while P^ in ['0'..'9'] do inc(P);
end;
FastSetString(ParamValue,PBeg,P-PBeg);
end;
'n':
if PInteger(P)^=NULL_LOW then begin
inc(P,4);
wasNull := true;
end else
exit; // invalid content (only :(null): expected)
else
exit; // invalid content
end;
while (P^<=' ') and (P^<>#0) do inc(P);
if PWord(P)^<>Ord(')')+Ord(':')shl 8 then
// we expect finishing with P^ pointing at '):'
ParamType := sptUnknown else
// result<>nil only if value content in P^
result := P+2;
end;
function ExtractInlineParameters(const SQL: RawUTF8;
var Types: TSQLParamTypeDynArray; var Values: TRawUTF8DynArray;
var maxParam: integer; var Nulls: TSQLFieldBits): RawUTF8;
var ppBeg: integer;
P, Gen: PUTF8Char;
wasNull: boolean;
begin
maxParam := 0;
FillCharFast(Nulls,SizeOf(Nulls),0);
ppBeg := PosEx(RawUTF8(':('),SQL,1);
if (ppBeg=0) or (PosEx(RawUTF8('):'),SQL,ppBeg+2)=0) then begin
// SQL code with no valid :(...): internal parameters -> leave maxParam=0
result := SQL;
exit;
end;
// compute GenericSQL from SQL, converting :(...): into ?
FastSetString(result,pointer(SQL),length(SQL)); // private copy for unescape
P := pointer(result); // in-place string unescape (keep SQL untouched)
Gen := P+ppBeg-1; // Gen^ just before :(
inc(P,ppBeg+1); // P^ just after :(
repeat
Gen^ := '?'; // replace :(...): by ?
inc(Gen);
if length(Values)<=maxParam then
SetLength(Values,maxParam+16);
if length(Types)<=maxParam then
SetLength(Types,maxParam+64);
P := SQLParamContent(P,Types[maxParam],Values[maxParam],wasNull);
if P=nil then begin
maxParam := 0;
result := SQL;
exit; // any invalid parameter -> try direct SQL
end;
if wasNull then
include(Nulls,maxParam);
while (P^<>#0) and (PWord(P)^<>Ord(':')+Ord('(')shl 8) do begin
Gen^ := P^;
inc(Gen);
inc(P);
end;
if P^=#0 then
Break;
inc(P,2);
inc(maxParam);
until false;
// return generic SQL statement, with ? place-holders and params in Values[]
SetLength(result,Gen-pointer(result));
inc(maxParam);
end;
function InlineParameter(ID: Int64): shortstring;
begin
FormatShort(':(%):',[ID],result);
end;
function InlineParameter(const value: RawUTF8): RawUTF8;
begin
QuotedStrJSON(value,result,':(','):');
end;
function SQLVarLength(const Value: TSQLVar): integer;
begin
case Value.VType of
ftBlob:
result := Value.VBlobLen;
ftUTF8:
result := StrLen(Value.VText); // fast enough for our purpose
else
result := 0; // simple/ordinal values, or ftNull
end;
end;
{$ifndef NOVARIANTS}
procedure VariantToSQLVar(const Input: variant; var temp: RawByteString;
var Output: TSQLVar);
var wasString: boolean;
begin
Output.Options := [];
with TVarData(Input) do
if VType=varVariant or varByRef then
VariantToSQLVar(PVariant(VPointer)^,temp,Output) else
case VType of
varEmpty, varNull:
Output.VType := ftNull;
varByte: begin
Output.VType := ftInt64;
Output.VInt64 := VByte;
end;
varInteger: begin
Output.VType := ftInt64;
Output.VInt64 := VInteger;
end;
{$ifndef DELPHI5OROLDER}
varLongWord: begin
Output.VType := ftInt64;
Output.VInt64 := VLongWord;
end;
{$endif}
varWord64, varInt64: begin
Output.VType := ftInt64;
Output.VInt64 := VInt64;
end;
varSingle: begin
Output.VType := ftDouble;
Output.VDouble := VSingle;
end;
varDouble: begin // varDate would be converted into ISO8601 by VariantToUTF8()
Output.VType := ftDouble;
Output.VDouble := VDouble;
end;
varCurrency: begin
Output.VType := ftCurrency;
Output.VInt64 := VInt64;
end;
varString: begin // assume RawUTF8
Output.VType := ftUTF8;
Output.VText := VPointer;
end;
else // handle less current cases
if VariantToInt64(Input,Output.VInt64) then
Output.VType := ftInt64 else begin
VariantToUTF8(Input,RawUTF8(temp),wasString);
if wasString then begin
Output.VType := ftUTF8;
Output.VText := pointer(temp);
end else
Output.VType := ftNull;
end;
end;
end;
2021-04-23 10:11:05 +02:00
function VariantVTypeToSQLDBFieldType(VType: cardinal): TSQLDBFieldType;
begin
case VType of
varNull:
result := ftNull;
{$ifndef DELPHI5OROLDER}varShortInt, varWord, varLongWord,{$endif}
varSmallInt, varByte, varBoolean, varInteger, varInt64, varWord64:
result := ftInt64;
varSingle,varDouble:
result := ftDouble;
varDate:
result := ftDate;
varCurrency:
result := ftCurrency;
varString:
result := ftUTF8;
else
result := ftUnknown; // includes varEmpty
end;
end;
function VariantTypeToSQLDBFieldType(const V: Variant): TSQLDBFieldType;
var VD: TVarData absolute V;
tmp: TVarData;
begin
result := VariantVTypeToSQLDBFieldType(VD.VType);
case result of
ftUnknown:
if VD.VType=varEmpty then
result := ftUnknown else
if SetVariantUnRefSimpleValue(V,tmp) then
result := VariantTypeToSQLDBFieldType(variant(tmp)) else
result := ftUTF8;
ftUTF8:
if (VD.VString<>nil) and (PCardinal(VD.VString)^ and $ffffff=JSON_BASE64_MAGIC) then
result := ftBlob;
end;
end;
function TextToSQLDBFieldType(json: PUTF8Char): TSQLDBFieldType;
begin
if json=nil then
result := ftNull else
result := VariantVTypeToSQLDBFieldType(TextToVariantNumberType(json));
end;
function NullableInteger(const Value: Int64): TNullableInteger;
begin
PVariant(@result)^ := Value;
end;
function NullableIntegerIsEmptyOrNull(const V: TNullableInteger): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableIntegerToValue(const V: TNullableInteger; out Value: Int64): Boolean;
begin
Value := 0;
result := not VarDataIsEmptyOrNull(@V) and VariantToInt64(PVariant(@V)^,Value);
end;
function NullableIntegerToValue(const V: TNullableInteger): Int64;
begin
VariantToInt64(PVariant(@V)^,result);
end;
function NullableBoolean(Value: boolean): TNullableBoolean;
begin
PVariant(@result)^ := Value;
end;
function NullableBooleanIsEmptyOrNull(const V: TNullableBoolean): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableBooleanToValue(const V: TNullableBoolean; out Value: Boolean): Boolean;
begin
Value := false;
result := not VarDataIsEmptyOrNull(@V) and VariantToBoolean(PVariant(@V)^,Value);
end;
function NullableBooleanToValue(const V: TNullableBoolean): Boolean;
begin
VariantToBoolean(PVariant(@V)^,result);
end;
function NullableFloat(const Value: double): TNullableFloat;
begin
PVariant(@result)^ := Value;
end;
function NullableFloatIsEmptyOrNull(const V: TNullableFloat): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableFloatToValue(const V: TNullableFloat; out Value: Double): Boolean;
begin
Value := 0;
result := not VarDataIsEmptyOrNull(@V) and VariantToDouble(PVariant(@V)^,Value);
end;
function NullableFloatToValue(const V: TNullableFloat): Double;
begin
VariantToDouble(PVariant(@V)^,result);
end;
function NullableCurrency(const Value: currency): TNullableCurrency;
begin
PVariant(@result)^ := Value;
end;
function NullableCurrencyIsEmptyOrNull(const V: TNullableCurrency): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableCurrencyToValue(const V: TNullableCurrency; out Value: currency): Boolean;
begin
Value := 0;
result := not VarDataIsEmptyOrNull(@V) and VariantToCurrency(PVariant(@V)^,Value);
end;
function NullableCurrencyToValue(const V: TNullableCurrency): currency;
begin
VariantToCurrency(PVariant(@V)^,result);
end;
function NullableDateTime(const Value: TDateTime): TNullableDateTime;
begin
PVariant(@result)^ := Value;
end;
function NullableDateTimeIsEmptyOrNull(const V: TNullableDateTime): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableDateTimeToValue(const V: TNullableDateTime; out Value: TDateTime): Boolean;
begin
Value := 0;
result := not VarDataIsEmptyOrNull(@V) and VariantToDouble(PVariant(@V)^,Double(Value));
end;
function NullableDateTimeToValue(const V: TNullableDateTime): TDateTime;
begin
VariantToDouble(PVariant(@V)^,Double(result));
end;
function NullableTimeLog(const Value: TTimeLog): TNullableTimeLog;
begin
PVariant(@result)^ := Value;
end;
function NullableTimeLogIsEmptyOrNull(const V: TNullableTimeLog): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableTimeLogToValue(const V: TNullableTimeLog; out Value: TTimeLog): Boolean;
begin
Value := 0;
result := not VarDataIsEmptyOrNull(@V) and VariantToInt64(PVariant(@V)^,PInt64(@Value)^);
end;
function NullableTimeLogToValue(const V: TNullableTimeLog): TTimeLog;
begin
VariantToInt64(PVariant(@V)^,PInt64(@result)^);
end;
function NullableUTF8Text(const Value: RawUTF8): TNullableUTF8Text;
begin
VarClear(PVariant(@result)^);
TVarData(result).VType := varString;
TVarData(result).VAny := nil; // avoid GPF below
RawUTF8(TVarData(result).VAny) := Value;
end;
function NullableUTF8TextIsEmptyOrNull(const V: TNullableUTF8Text): Boolean;
begin
result := VarDataIsEmptyOrNull(@V);
end;
function NullableUTF8TextToValue(const V: TNullableUTF8Text; out Value: RawUTF8): boolean;
begin
result := not VarDataIsEmptyOrNull(@V) and VariantToUTF8(PVariant(@V)^,Value);
end;
function NullableUTF8TextToValue(const V: TNullableUTF8Text): RawUTF8;
var dummy: boolean;
begin
if VarDataIsEmptyOrNull(@V) then // VariantToUTF8() will return 'null'
result := '' else
VariantToUTF8(PVariant(@V)^,result,dummy);
end;
{$endif NOVARIANTS}
{ TJSONWriter }
procedure TJSONWriter.CancelAllVoid;
const VOIDARRAY: PAnsiChar = '[]'#10;
VOIDFIELD: PAnsiChar = '{"FieldCount":0}';
begin
CancelAll; // rewind JSON
if fExpand then // same as sqlite3_get_table()
inc(fTotalFileSize,fStream.Write(VOIDARRAY^,3)) else
inc(fTotalFileSize,fStream.Write(VOIDFIELD^,16));
end;
constructor TJSONWriter.Create(aStream: TStream; Expand, withID: boolean;
const Fields: TSQLFieldBits; aBufSize: integer);
begin
Create(aStream,Expand,withID,FieldBitsToIndex(Fields),aBufSize);
end;
constructor TJSONWriter.Create(aStream: TStream; Expand, withID: boolean;
2021-04-23 10:11:05 +02:00
const Fields: TSQLFieldIndexDynArray; aBufSize: integer;
aStackBuffer: PTextWriterStackBuffer);
begin
if aStream=nil then
2021-04-23 10:11:05 +02:00
if aStackBuffer<>nil then
CreateOwnedStream(aStackBuffer^) else
CreateOwnedStream(aBufSize) else
if aStackBuffer<>nil then
inherited Create(aStream,aStackBuffer,SizeOf(aStackBuffer^)) else
inherited Create(aStream,aBufSize);
fExpand := Expand;
fWithID := withID;
fFields := Fields;
end;
procedure TJSONWriter.AddColumns(aKnownRowsCount: integer);
var i: integer;
begin
if fExpand then begin
if twoForceJSONExtended in CustomOptions then
for i := 0 to High(ColNames) do
ColNames[i] := ColNames[i]+':' else
for i := 0 to High(ColNames) do
ColNames[i] := '"'+ColNames[i]+'":';
end else begin
AddShort('{"fieldCount":');
Add(length(ColNames));
if aKnownRowsCount>0 then begin
AddShort(',"rowCount":');
Add(aKnownRowsCount);
end;
AddShort(',"values":["');
// first row is FieldNames
for i := 0 to High(ColNames) do begin
AddString(ColNames[i]);
AddNoJSONEscape(PAnsiChar('","'),3);
end;
CancelLastChar('"');
fStartDataPosition := fStream.Position+(B-fTempBuf);
// B := buf-1 at startup -> need ',val11' position in
// "values":["col1","col2",val11,' i.e. current pos without the ','
end;
end;
procedure TJSONWriter.ChangeExpandedFields(aWithID: boolean;
const aFields: TSQLFieldIndexDynArray);
begin
if not Expand then
raise ESynException.CreateUTF8(
'%.ChangeExpandedFields() called with Expanded=false',[self]);
fWithID := aWithID;
fFields := aFields;
end;
procedure TJSONWriter.EndJSONObject(aKnownRowsCount,aRowsCount: integer;
aFlushFinal: boolean);
begin
CancelLastComma; // cancel last ','
Add(']');
if not fExpand then begin
if aKnownRowsCount=0 then begin
AddShort(',"rowCount":');
Add(aRowsCount);
end;
Add('}');
end;
Add(#10);
if aFlushFinal then
FlushFinal;
end;
procedure TJSONWriter.TrimFirstRow;
var P, PBegin, PEnd: PUTF8Char;
begin
if (self=nil) or not fStream.InheritsFrom(TMemoryStream) or
fExpand or (fStartDataPosition=0) then
exit;
// go to begin of first row
FlushToStream; // we need the data to be in fStream memory
// PBegin^=val11 in { "fieldCount":1,"values":["col1","col2",val11,"val12",val21,..] }
PBegin := TMemoryStream(fStream).Memory;
PEnd := PBegin+fStream.Position;
PEnd^ := #0; // mark end of current values
inc(PBegin,fStartDataPosition+1); // +1 to include ',' of ',val11'
// jump to end of first row
P := GotoNextJSONItem(PBegin,length(ColNames));
if P=nil then exit; // unexpected end
// trim first row data
if P^<>#0 then
MoveFast(P^,PBegin^,PEnd-P); // erase content
fStream.Seek(PBegin-P,soCurrent); // adjust current stream position
end;
{ ************ Expression Search Engine ************************** }
function ToText(r: TExprParserResult): PShortString;
begin
result := GetEnumName(TypeInfo(TExprParserResult), ord(r));
end;
function ToUTF8(r: TExprParserResult): RawUTF8;
begin
result := UnCamelCase(TrimLeftLowerCaseShort(ToText(r)));
end;
{ TExprNode }
function TExprNode.Append(node: TExprNode): boolean;
begin
result := node <> nil;
if result then
Last.fNext := node;
end;
constructor TExprNode.Create(nodeType: TExprNodeType);
begin
inherited Create;
fNodeType := nodeType;
end;
destructor TExprNode.Destroy;
begin
fNext.Free;
inherited Destroy;
end;
function TExprNode.Last: TExprNode;
begin
result := self;
while result.Next <> nil do
result := result.Next;
end;
{ TParserAbstract }
constructor TParserAbstract.Create;
begin
inherited Create;
Initialize;
end;
destructor TParserAbstract.Destroy;
begin
Clear;
inherited Destroy;
end;
procedure TParserAbstract.Clear;
begin
fWordCount := 0;
fWords := nil;
fExpression := '';
FreeAndNil(fFirstNode);
end;
function TParserAbstract.ParseExpr: TExprNode;
begin
result := ParseFactor;
ParseNextCurrentWord;
if (fCurrentWord = '') or (fCurrentWord = ')') then
exit;
if IdemPropNameU(fCurrentWord, fAndWord) then begin // w1 & w2 = w1 AND w2
ParseNextCurrentWord;
if result.Append(ParseExpr) then
result.Append(TExprNode.Create(entAnd));
exit;
end
else if IdemPropNameU(fCurrentWord, fOrWord) then begin // w1 + w2 = w1 OR w2
ParseNextCurrentWord;
if result.Append(ParseExpr) then
result.Append(TExprNode.Create(entOr));
exit;
end
else if fNoWordIsAnd and result.Append(ParseExpr) then // 'w1 w2' = 'w1 & w2'
result.Append(TExprNode.Create(entAnd));
end;
function TParserAbstract.ParseFactor: TExprNode;
begin
if fCurrentError <> eprSuccess then
result := nil
else if IdemPropNameU(fCurrentWord, fNotWord) then begin
ParseNextCurrentWord;
result := ParseFactor;
if fCurrentError <> eprSuccess then
exit;
result.Append(TExprNode.Create(entNot));
end
else
result := ParseTerm;
end;
function TParserAbstract.ParseTerm: TExprNode;
begin
result := nil;
if fCurrentError <> eprSuccess then
exit;
if fCurrentWord = '(' then begin
ParseNextCurrentWord;
result := ParseExpr;
if fCurrentError <> eprSuccess then
exit;
if fCurrentWord <> ')' then begin
FreeAndNil(result);
fCurrentError := eprMissingParenthesis;
end;
end
else if fCurrentWord = '' then begin
result := nil;
fCurrentError := eprMissingFinalWord;
end
else
try // calls meta-class overriden constructor
result := fWordClass.Create(self, fCurrentWord);
fCurrentError := TExprNodeWordAbstract(result).ParseWord;
if fCurrentError <> eprSuccess then begin
FreeAndNil(result);
exit;
end;
SetLength(fWords, fWordCount + 1);
fWords[fWordCount] := TExprNodeWordAbstract(result);
inc(fWordCount);
except
FreeAndNil(result);
fCurrentError := eprInvalidExpression;
end;
end;
function TParserAbstract.Parse(const aExpression: RawUTF8): TExprParserResult;
var
depth: integer;
n: TExprNode;
begin
Clear;
fCurrentError := eprSuccess;
fCurrent := pointer(aExpression);
ParseNextCurrentWord;
if fCurrentWord = '' then begin
result := eprNoExpression;
exit;
end;
fFirstNode := ParseExpr;
result := fCurrentError;
if result = eprSuccess then begin
depth := 0;
n := fFirstNode;
while n <> nil do begin
case n.NodeType of
entWord: begin
inc(depth);
if depth > high(fFoundStack) then begin
result := eprTooManyParenthesis;
break;
end;
end;
entOr, entAnd:
dec(depth);
end;
n := n.Next;
end;
end;
if result = eprSuccess then
fExpression := aExpression
else
Clear;
fCurrent := nil;
end;
class function TParserAbstract.ParseError(const aExpression: RawUTF8): RawUTF8;
var
parser: TParserAbstract;
res: TExprParserResult;
begin
parser := Create;
try
res := parser.Parse(aExpression);
if res = eprSuccess then
result := ''
else
result := ToUTF8(res);
finally
parser.Free;
end;
end;
function TParserAbstract.Execute: boolean;
var
n: TExprNode;
st: PBoolean;
begin // code below compiles very efficiently on FPC/x86-64
st := @fFoundStack;
n := fFirstNode;
repeat
case n.NodeType of
entWord: begin
st^ := TExprNodeWordAbstract(n).fFound;
inc(st); // see eprTooManyParenthesis above to avoid buffer overflow
end;
entNot:
PAnsiChar(st)[-1] := AnsiChar(ord(PAnsiChar(st)[-1]) xor 1);
entOr: begin
dec(st);
PAnsiChar(st)[-1] := AnsiChar(st^ or boolean(PAnsiChar(st)[-1]));
end; { TODO : optimize TExprParser OR when left member is already TRUE }
entAnd: begin
dec(st);
PAnsiChar(st)[-1] := AnsiChar(st^ and boolean(PAnsiChar(st)[-1]));
end;
end;
n := n.Next;
until n = nil;
result := boolean(PAnsiChar(st)[-1]);
end;
{ TExprParserAbstract }
procedure TExprParserAbstract.Initialize;
begin
fAndWord := '&';
fOrWord := '+';
fNotWord := '-';
fNoWordIsAnd := true;
end;
procedure TExprParserAbstract.ParseNextCurrentWord;
var
P: PUTF8Char;
begin
fCurrentWord := '';
P := fCurrent;
if P = nil then
exit;
while P^ in [#1..' '] do
inc(P);
if P^ = #0 then
exit;
if P^ in PARSER_STOPCHAR then begin
FastSetString(fCurrentWord, P, 1);
fCurrent := P + 1;
end
else begin
fCurrent := P;
ParseNextWord;
end;
end;
procedure TExprParserAbstract.ParseNextWord;
const
STOPCHAR = PARSER_STOPCHAR + [#0, ' '];
var
P: PUTF8Char;
begin
P := fCurrent;
while not(P^ in STOPCHAR) do
inc(P);
FastSetString(fCurrentWord, fCurrent, P - fCurrent);
fCurrent := P;
end;
{ TExprNodeWordAbstract }
constructor TExprNodeWordAbstract.Create(aOwner: TParserAbstract; const aWord: RawUTF8);
begin
inherited Create(entWord);
fWord := aWord;
fOwner := aOwner;
end;
{ TExprParserMatchNode }
type
TExprParserMatchNode = class(TExprNodeWordAbstract)
protected
fMatch: TMatch;
function ParseWord: TExprParserResult; override;
end;
PExprParserMatchNode = ^TExprParserMatchNode;
function TExprParserMatchNode.ParseWord: TExprParserResult;
begin
fMatch.Prepare(fWord, (fOwner as TExprParserMatch).fCaseSensitive, {reuse=}true);
result := eprSuccess;
end;
{ TExprParserMatch }
constructor TExprParserMatch.Create(aCaseSensitive: boolean);
begin
inherited Create;
fCaseSensitive := aCaseSensitive;
end;
procedure TExprParserMatch.Initialize;
begin
inherited Initialize;
fWordClass := TExprParserMatchNode;
end;
function TExprParserMatch.Search(const aText: RawUTF8): boolean;
begin
result := Search(pointer(aText), length(aText));
end;
function TExprParserMatch.Search(aText: PUTF8Char; aTextLen: PtrInt): boolean;
const // rough estimation of UTF-8 characters
IS_UTF8_WORD = ['0' .. '9', 'A' .. 'Z', 'a' .. 'z', #$80 ..#$ff];
var
P, PEnd: PUTF8Char;
n: PtrInt;
begin
P := aText;
if (P = nil) or (fWords = nil) then begin
result := false;
exit;
end;
if fMatchedLastSet > 0 then begin
n := fWordCount;
repeat
dec(n);
fWords[n].fFound := false;
until n = 0;
fMatchedLastSet := 0;
end;
PEnd := P + aTextLen;
while (P < PEnd) and (fMatchedLastSet < fWordCount) do begin
while not(P^ in IS_UTF8_WORD) do begin
inc(P);
if P = PEnd then
break;
end;
if P = PEnd then
break;
aText := P;
repeat
inc(P);
until (P = PEnd) or not(P^ in IS_UTF8_WORD);
aTextLen := P - aText; // now aText/aTextLen point to a word
n := fWordCount;
repeat
dec(n);
with TExprParserMatchNode(fWords[n]) do
if not fFound and fMatch.Match(aText, aTextLen) then begin
fFound := true;
inc(fMatchedLastSet);
end;
until n = 0;
end;
result := Execute;
end;
{ ************ Multi-Threading classes ************************** }
{ TPendingTaskList }
constructor TPendingTaskList.Create;
begin
inherited Create;
fTasks.InitSpecific(TypeInfo(TPendingTaskListItemDynArray),fTask,djInt64,@fCount);
end;
function TPendingTaskList.GetTimestamp: Int64;
begin
result := {$ifdef FPCLINUX}SynFPCLinux.{$endif}GetTickCount64;
end;
procedure TPendingTaskList.AddTask(aMilliSecondsDelayFromNow: integer;
const aTask: RawByteString);
var item: TPendingTaskListItem;
ndx: integer;
begin
item.Timestamp := GetTimestamp+aMilliSecondsDelayFromNow;
item.Task := aTask;
fSafe.Lock;
try
if fTasks.FastLocateSorted(item,ndx) then
inc(ndx); // always insert just after any existing timestamp
fTasks.FastAddSorted(ndx,item);
finally
fSafe.UnLock;
end;
end;
procedure TPendingTaskList.AddTasks(
const aMilliSecondsDelays: array of integer;
const aTasks: array of RawByteString);
var item: TPendingTaskListItem;
i,ndx: integer;
begin
if length(aTasks)<>length(aMilliSecondsDelays) then
exit;
item.Timestamp := GetTimestamp;
fSafe.Lock;
try
for i := 0 to High(aTasks) do begin
inc(item.Timestamp,aMilliSecondsDelays[i]);
item.Task := aTasks[i];
if fTasks.FastLocateSorted(item,ndx) then
inc(ndx); // always insert just after any existing timestamp
fTasks.FastAddSorted(ndx,item);
end;
finally
fSafe.UnLock;
end;
end;
function TPendingTaskList.GetCount: integer;
begin
if self=nil then
result := 0 else begin
fSafe.Lock;
try
result := fCount;
finally
fSafe.UnLock;
end;
end;
end;
function TPendingTaskList.NextPendingTask: RawByteString;
begin
result := '';
if (self=nil) or (fCount=0) then
exit;
fSafe.Lock;
try
if fCount>0 then
if GetTimestamp>=fTask[0].Timestamp then begin
result := fTask[0].Task;
fTasks.FastDeleteSorted(0);
end;
finally
fSafe.UnLock;
end;
end;
procedure TPendingTaskList.Clear;
begin
if (self=nil) or (fCount=0) then
exit;
fSafe.Lock;
try
fTasks.Clear;
finally
fSafe.UnLock;
end;
end;
{$ifndef LVCL} // LVCL does not implement TEvent
{ TSynBackgroundThreadAbstract }
constructor TSynBackgroundThreadAbstract.Create(const aThreadName: RawUTF8;
OnBeforeExecute,OnAfterExecute: TNotifyThreadEvent; CreateSuspended: boolean);
begin
fProcessEvent := TEvent.Create(nil,false,false,'');
fThreadName := aThreadName;
fOnBeforeExecute := OnBeforeExecute;
fOnAfterExecute := OnAfterExecute;
inherited Create(CreateSuspended{$ifdef FPC},512*1024{$endif}); // DefaultStackSize=512KB
end;
{$ifndef HASTTHREADSTART}
procedure TSynBackgroundThreadAbstract.Start;
begin
Resume;
end;
{$endif}
{$ifndef HASTTHREADTERMINATESET}
procedure TSynBackgroundThreadAbstract.Terminate;
begin
inherited Terminate; // FTerminated := True
TerminatedSet;
end;
{$endif}
procedure TSynBackgroundThreadAbstract.TerminatedSet;
begin
fProcessEvent.SetEvent; // ExecuteLoop should handle Terminated flag
end;
procedure TSynBackgroundThreadAbstract.WaitForNotExecuting(maxMS: integer);
var endtix: Int64;
begin
if fExecute=exRun then begin
endtix := SynCommons.GetTickCount64+maxMS;
repeat
SleepHiRes(1); // wait for Execute to finish
until (fExecute<>exRun) or (SynCommons.GetTickCount64>=endtix);
end;
end;
destructor TSynBackgroundThreadAbstract.Destroy;
begin
if fExecute=exRun then begin
Terminate;
WaitForNotExecuting(100);
end;
inherited Destroy;
FreeAndNil(fProcessEvent);
end;
procedure TSynBackgroundThreadAbstract.SetExecuteLoopPause(dopause: boolean);
begin
if Terminated or (dopause=fExecuteLoopPause) or (fExecute=exFinished) then
exit;
fExecuteLoopPause := dopause;
fProcessEvent.SetEvent; // notify Execute main loop
end;
procedure TSynBackgroundThreadAbstract.Execute;
begin
try
if fThreadName='' then
SetCurrentThreadName('%(%)',[self,pointer(self)]) else
SetCurrentThreadName('%',[fThreadName]);
if Assigned(fOnBeforeExecute) then
fOnBeforeExecute(self);
try
fExecute := exRun;
while not Terminated do
if fExecuteLoopPause then
FixedWaitFor(fProcessEvent,100) else
ExecuteLoop;
finally
if Assigned(fOnAfterExecute) then
fOnAfterExecute(self);
end;
finally
fExecute := exFinished;
end;
end;
{ TSynBackgroundThreadMethodAbstract }
constructor TSynBackgroundThreadMethodAbstract.Create(aOnIdle: TOnIdleSynBackgroundThread;
const aThreadName: RawUTF8; OnBeforeExecute,OnAfterExecute: TNotifyThreadEvent);
begin
fOnIdle := aOnIdle; // cross-platform may run Execute as soon as Create is called
fCallerEvent := TEvent.Create(nil,false,false,'');
fPendingProcessLock.Init;
inherited Create(aThreadName,OnBeforeExecute,OnAfterExecute);
end;
destructor TSynBackgroundThreadMethodAbstract.Destroy;
begin
SetPendingProcess(flagDestroying);
fProcessEvent.SetEvent; // notify terminated
FixedWaitForever(fCallerEvent); // wait for actual termination
FreeAndNil(fCallerEvent);
inherited Destroy;
fPendingProcessLock.Done;
end;
function TSynBackgroundThreadMethodAbstract.GetPendingProcess: TSynBackgroundThreadProcessStep;
begin
fPendingProcessLock.Lock;
result := fPendingProcessFlag;
fPendingProcessLock.UnLock;
end;
procedure TSynBackgroundThreadMethodAbstract.SetPendingProcess(State: TSynBackgroundThreadProcessStep);
begin
fPendingProcessLock.Lock;
fPendingProcessFlag := State;
fPendingProcessLock.UnLock;
end;
procedure TSynBackgroundThreadMethodAbstract.ExecuteLoop;
{$ifndef DELPHI5OROLDER}
var E: TObject;
{$endif}
begin
case FixedWaitFor(fProcessEvent,INFINITE) of
wrSignaled:
case GetPendingProcess of
flagDestroying: begin
fCallerEvent.SetEvent; // abort caller thread process
Terminate; // forces Execute loop ending
exit;
end;
flagStarted:
if not Terminated then
if fExecuteLoopPause then // pause -> try again later
fProcessEvent.SetEvent else
try
fBackgroundException := nil;
try
if Assigned(fOnBeforeProcess) then
fOnBeforeProcess(self);
try
Process;
finally
if Assigned(fOnAfterProcess) then
fOnAfterProcess(self);
end;
except
{$ifdef DELPHI5OROLDER}
on E: Exception do
fBackgroundException := ESynException.CreateUTF8(
'Redirected % [%]',[E,E.Message]);
{$else}
E := AcquireExceptionObject;
if E.InheritsFrom(Exception) then
fBackgroundException := Exception(E);
{$endif}
end;
finally
SetPendingProcess(flagFinished);
fCallerEvent.SetEvent;
end;
end;
end;
end;
function TSynBackgroundThreadMethodAbstract.AcquireThread: TSynBackgroundThreadProcessStep;
begin
fPendingProcessLock.Lock;
try
result := fPendingProcessFlag;
if result=flagIdle then begin // we just acquired the thread! congrats!
fPendingProcessFlag := flagStarted; // atomic set "started" flag
fCallerThreadID := ThreadID;
end;
finally
fPendingProcessLock.UnLock;
end;
end;
function TSynBackgroundThreadMethodAbstract.OnIdleProcessNotify(start: Int64): integer;
begin
result := {$ifdef FPCLINUX}SynFPCLinux.{$else}SynCommons.{$endif}GetTickCount64-start;
if result<0 then
result := MaxInt; // should happen only under XP -> ignore
if Assigned(fOnIdle) then
fOnIdle(self,result) ;
end;
procedure TSynBackgroundThreadMethodAbstract.WaitForFinished(start: Int64;
const onmainthreadidle: TNotifyEvent);
var E: Exception;
begin
if (self=nil) or not(fPendingProcessFlag in [flagStarted, flagFinished]) then
exit; // nothing to wait for
try
if Assigned(onmainthreadidle) then begin
while FixedWaitFor(fCallerEvent,100)=wrTimeout do
onmainthreadidle(self);
end else
{$ifdef MSWINDOWS} // do process the OnIdle only if UI
if Assigned(fOnIdle) then begin
while FixedWaitFor(fCallerEvent,100)=wrTimeout do
OnIdleProcessNotify(start);
end else
{$endif}
FixedWaitForever(fCallerEvent);
if fPendingProcessFlag<>flagFinished then
ESynException.CreateUTF8('%.WaitForFinished: flagFinished?',[self]);
if fBackgroundException<>nil then begin
E := fBackgroundException;
fBackgroundException := nil;
raise E; // raise background exception in the calling scope
end;
finally
fParam := nil;
fCallerThreadID := 0;
FreeAndNil(fBackgroundException);
SetPendingProcess(flagIdle);
if Assigned(fOnIdle) then
fOnIdle(self,-1); // notify finished
end;
end;
function TSynBackgroundThreadMethodAbstract.RunAndWait(OpaqueParam: pointer): boolean;
var start: Int64;
ThreadID: TThreadID;
begin
result := false;
ThreadID := GetCurrentThreadId;
if (self=nil) or (ThreadID=fCallerThreadID) then
// avoid endless loop when waiting in same thread (e.g. UI + OnIdle)
exit;
// 1. wait for any previous request to be finished (should not happen often)
if Assigned(fOnIdle) then
fOnIdle(self,0); // notify started
start := {$ifdef FPCLINUX}SynFPCLinux.{$else}SynCommons.{$endif}GetTickCount64;
repeat
case AcquireThread of
flagDestroying:
exit;
flagIdle:
break; // we acquired the background thread
end;
case OnIdleProcessNotify(start) of // Windows.GetTickCount64 res is 10-16 ms
0..20: SleepHiRes(0); // 10 microsec delay on POSIX
21..100: SleepHiRes(1);
101..900: SleepHiRes(5);
else SleepHiRes(50);
end;
until false;
// 2. process execution in the background thread
fParam := OpaqueParam;
fProcessEvent.SetEvent; // notify background thread for Call pending process
WaitForFinished(start,nil); // wait for flagFinished, then set flagIdle
result := true;
end;
function TSynBackgroundThreadMethodAbstract.GetOnIdleBackgroundThreadActive: boolean;
begin
result := (self<>nil) and Assigned(fOnIdle) and (GetPendingProcess<>flagIdle);
end;
{ TSynBackgroundThreadEvent }
constructor TSynBackgroundThreadEvent.Create(aOnProcess: TOnProcessSynBackgroundThread;
aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUTF8);
begin
inherited Create(aOnIdle,aThreadName);
fOnProcess := aOnProcess;
end;
procedure TSynBackgroundThreadEvent.Process;
begin
if not Assigned(fOnProcess) then
raise ESynException.CreateUTF8('Invalid %.RunAndWait() call',[self]);
fOnProcess(self,fParam);
end;
{ TSynBackgroundThreadMethod }
procedure TSynBackgroundThreadMethod.Process;
var Method: ^TThreadMethod;
begin
if fParam=nil then
raise ESynException.CreateUTF8('Invalid %.RunAndWait() call',[self]);
Method := fParam;
Method^();
end;
procedure TSynBackgroundThreadMethod.RunAndWait(Method: TThreadMethod);
var Met: TMethod absolute Method;
begin
inherited RunAndWait(@Met);
end;
{ TSynBackgroundThreadProcedure }
constructor TSynBackgroundThreadProcedure.Create(aOnProcess: TOnProcessSynBackgroundThreadProc;
aOnIdle: TOnIdleSynBackgroundThread; const aThreadName: RawUTF8);
begin
inherited Create(aOnIdle,aThreadName);
fOnProcess := aOnProcess;
end;
procedure TSynBackgroundThreadProcedure.Process;
begin
if not Assigned(fOnProcess) then
raise ESynException.CreateUTF8('Invalid %.RunAndWait() call',[self]);
fOnProcess(fParam);
end;
{ TSynParallelProcessThread }
procedure TSynParallelProcessThread.Process;
begin
if not Assigned(fMethod) then
exit;
fMethod(fIndexStart,fIndexStop);
fMethod := nil;
end;
procedure TSynParallelProcessThread.Start(
Method: TSynParallelProcessMethod; IndexStart, IndexStop: integer);
begin
fMethod := Method;
fIndexStart := IndexStart;
fIndexStop := IndexStop;
fProcessEvent.SetEvent; // notify execution
end;
{ TSynBackgroundThreadProcess }
constructor TSynBackgroundThreadProcess.Create(const aThreadName: RawUTF8;
aOnProcess: TOnSynBackgroundThreadProcess; aOnProcessMS: cardinal;
aOnBeforeExecute, aOnAfterExecute: TNotifyThreadEvent;
aStats: TSynMonitorClass; CreateSuspended: boolean);
begin
if not Assigned(aOnProcess) then
raise ESynException.CreateUTF8('%.Create(aOnProcess=nil)',[self]);
if aStats<>nil then
fStats := aStats.Create(aThreadName);
fOnProcess := aOnProcess;
fOnProcessMS := aOnProcessMS;
if fOnProcessMS=0 then
fOnProcessMS := INFINITE; // wait until ProcessEvent.SetEvent or Terminated
inherited Create(aThreadName,aOnBeforeExecute,aOnAfterExecute,CreateSuspended);
end;
destructor TSynBackgroundThreadProcess.Destroy;
begin
if fExecute=exRun then begin
Terminate;
WaitForNotExecuting(10000); // expect the background task to be finished
end;
inherited Destroy;
fStats.Free;
end;
procedure TSynBackgroundThreadProcess.ExecuteLoop;
var wait: TWaitResult;
begin
wait := FixedWaitFor(fProcessEvent,fOnProcessMS);
if not Terminated and (wait in [wrSignaled,wrTimeout]) then
if fExecuteLoopPause then // pause -> try again later
fProcessEvent.SetEvent else
try
if fStats<>nil then
fStats.ProcessStartTask;
try
fOnProcess(self,wait);
finally
if fStats<>nil then
fStats.ProcessEnd;
end;
except
on E: Exception do begin
if fStats<>nil then
fStats.ProcessErrorRaised(E);
if Assigned(fOnException) then
fOnException(E);
end;
end;
end;
{ TSynBackgroundTimer }
var
ProcessSystemUse: TSystemUse;
constructor TSynBackgroundTimer.Create(const aThreadName: RawUTF8;
aOnBeforeExecute, aOnAfterExecute: TNotifyThreadEvent; aStats: TSynMonitorClass);
begin
fTasks.Init(TypeInfo(TSynBackgroundTimerTaskDynArray),fTask);
fTaskLock.Init;
{$ifndef NOVARIANTS}
fTaskLock.LockedBool[0] := false;
{$endif}
inherited Create(aThreadName,EverySecond,1000,aOnBeforeExecute,aOnAfterExecute,aStats);
end;
destructor TSynBackgroundTimer.Destroy;
begin
if (ProcessSystemUse<>nil) and (ProcessSystemUse.fTimer=self) then
ProcessSystemUse.fTimer := nil; // allows processing by another background timer
inherited Destroy;
fTaskLock.Done;
end;
const
TIXPRECISION = 32; // GetTickCount64 resolution (for aOnProcessSecs=1)
procedure TSynBackgroundTimer.EverySecond(
Sender: TSynBackgroundThreadProcess; Event: TWaitResult);
var tix: Int64;
i,f,n: integer;
t: ^TSynBackgroundTimerTask;
todo: TSynBackgroundTimerTaskDynArray; // avoid lock contention
begin
if (fTask=nil) or Terminated then
exit;
tix := {$ifdef FPCLINUX}SynFPCLinux.{$else}SynCommons.{$endif}GetTickCount64;
n := 0;
fTaskLock.Lock;
try
variant(fTaskLock.Padding[0]) := true; // = fTaskLock.LockedBool[0]
try
for i := 0 to length(fTask)-1 do begin
t := @fTask[i];
if tix>=t^.NextTix then begin
SetLength(todo,n+1);
todo[n] := t^;
inc(n);
t^.FIFO := nil; // now owned by todo[n].FIFO
t^.NextTix := tix+((t^.Secs*1000)-TIXPRECISION);
end;
end;
finally
fTaskLock.UnLock;
end;
for i := 0 to n-1 do
with todo[i] do
if FIFO<>nil then
for f := 0 to length(FIFO)-1 do
try
OnProcess(self,Event,FIFO[f]);
except
end
else
try
OnProcess(self,Event,'');
except
end;
finally
{$ifdef NOVARIANTS}
fTaskLock.Lock;
variant(fTaskLock.Padding[0]) := false;
fTaskLock.UnLock;
{$else}
fTaskLock.LockedBool[0] := false;
{$endif}
end;
end;
function TSynBackgroundTimer.Find(const aProcess: TMethod): integer;
begin // caller should have made fTaskLock.Lock;
for result := length(fTask)-1 downto 0 do
with TMethod(fTask[result].OnProcess) do
if (Code=aProcess.Code) and (Data=aProcess.Data) then
exit;
result := -1;
end;
procedure TSynBackgroundTimer.Enable(
aOnProcess: TOnSynBackgroundTimerProcess; aOnProcessSecs: cardinal);
var task: TSynBackgroundTimerTask;
found: integer;
begin
if (self=nil) or Terminated or not Assigned(aOnProcess) then
exit;
if aOnProcessSecs=0 then begin
Disable(aOnProcess);
exit;
end;
task.OnProcess := aOnProcess;
task.Secs := aOnProcessSecs;
task.NextTix := {$ifdef FPCLINUX}SynFPCLinux.{$else}SynCommons.{$endif}GetTickCount64+
(aOnProcessSecs*1000-TIXPRECISION);
fTaskLock.Lock;
try
found := Find(TMethod(aOnProcess));
if found>=0 then
fTask[found] := task else
fTasks.Add(task);
finally
fTaskLock.UnLock;
end;
end;
function TSynBackgroundTimer.Processing: boolean;
begin
{$ifdef NOVARIANTS}
with fTaskLock.Padding[0] do
result := (VType=varBoolean) and VBoolean;
{$else}
result := fTaskLock.LockedBool[0];
{$endif}
end;
procedure TSynBackgroundTimer.WaitUntilNotProcessing(timeoutsecs: integer);
var timeout: Int64;
begin
if not Processing then
exit;
timeout := {$ifdef FPCLINUX}SynFPCLinux.{$else}SynCommons.{$endif}GetTickCount64+timeoutsecs*1000;
repeat
SleepHiRes(1);
until not Processing or
({$ifdef FPCLINUX}SynFPCLinux.{$else}SynCommons.{$endif}GetTickcount64>timeout);
end;
function TSynBackgroundTimer.ExecuteNow(aOnProcess: TOnSynBackgroundTimerProcess): boolean;
begin
result := Add(aOnProcess,#0,true);
end;
function TSynBackgroundTimer.EnQueue(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsg: RawUTF8; aExecuteNow: boolean): boolean;
begin
result := Add(aOnProcess,aMsg,aExecuteNow);
end;
function TSynBackgroundTimer.EnQueue(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsgFmt: RawUTF8; const Args: array of const; aExecuteNow: boolean): boolean;
var msg: RawUTF8;
begin
FormatUTF8(aMsgFmt,Args,msg);
result := Add(aOnProcess,msg,aExecuteNow);
end;
function TSynBackgroundTimer.Add(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsg: RawUTF8; aExecuteNow: boolean): boolean;
var found: integer;
begin
result := false;
if (self=nil) or Terminated or not Assigned(aOnProcess) then
exit;
fTaskLock.Lock;
try
found := Find(TMethod(aOnProcess));
if found>=0 then begin
with fTask[found] do begin
if aExecuteNow then
NextTix := 0;
if aMsg<>#0 then
AddRawUTF8(FIFO,aMsg);
end;
if aExecuteNow then
ProcessEvent.SetEvent;
result := true;
end;
finally
fTaskLock.UnLock;
end;
end;
function TSynBackgroundTimer.DeQueue(aOnProcess: TOnSynBackgroundTimerProcess;
const aMsg: RawUTF8): boolean;
var found: integer;
begin
result := false;
if (self=nil) or Terminated or not Assigned(aOnProcess) then
exit;
fTaskLock.Lock;
try
found := Find(TMethod(aOnProcess));
if found>=0 then
with fTask[found] do
result := DeleteRawUTF8(FIFO,FindRawUTF8(FIFO,aMsg));
finally
fTaskLock.UnLock;
end;
end;
function TSynBackgroundTimer.Disable(aOnProcess: TOnSynBackgroundTimerProcess): boolean;
var found: integer;
begin
result := false;
if (self=nil) or Terminated or not Assigned(aOnProcess) then
exit;
fTaskLock.Lock;
try
found := Find(TMethod(aOnProcess));
if found>=0 then begin
fTasks.Delete(found);
result := true;
end;
finally
fTaskLock.UnLock;
end;
end;
{ TSynParallelProcess }
constructor TSynParallelProcess.Create(ThreadPoolCount: integer; const ThreadName: RawUTF8;
OnBeforeExecute, OnAfterExecute: TNotifyThreadEvent;
MaxThreadPoolCount: integer);
var i: integer;
begin
inherited Create;
if ThreadPoolCount<0 then
raise ESynParallelProcess.CreateUTF8('%.Create(%,%)',[Self,ThreadPoolCount,ThreadName]);
if ThreadPoolCount>MaxThreadPoolCount then
ThreadPoolCount := MaxThreadPoolCount;
fThreadPoolCount := ThreadPoolCount;
fThreadName := ThreadName;
SetLength(fPool,fThreadPoolCount);
for i := 0 to fThreadPoolCount-1 do
fPool[i] := TSynParallelProcessThread.Create(nil,FormatUTF8('%#%/%',
[fThreadName,i+1,fThreadPoolCount]),OnBeforeExecute,OnAfterExecute);
end;
destructor TSynParallelProcess.Destroy;
begin
ObjArrayClear(fPool);
inherited;
end;
procedure TSynParallelProcess.ParallelRunAndWait(const Method: TSynParallelProcessMethod;
MethodCount: integer; const OnMainThreadIdle: TNotifyEvent);
var use,t,n,perthread: integer;
error: RawUTF8;
begin
if (MethodCount<=0) or not Assigned(Method) then
exit;
if not Assigned(OnMainThreadIdle) then
if (self=nil) or (MethodCount=1) or (fThreadPoolCount=0) then begin
Method(0,MethodCount-1); // no need (or impossible) to use background thread
exit;
end;
use := MethodCount;
t := fThreadPoolCount;
if not Assigned(OnMainThreadIdle) then
inc(t); // include current thread
if use>t then
use := t;
try
// start secondary threads
perthread := MethodCount div use;
if perthread=0 then
use := 1;
n := 0;
for t := 0 to use-2 do begin
repeat
case fPool[t].AcquireThread of
flagDestroying: // should not happen
raise ESynParallelProcess.CreateUTF8(
'%.ParallelRunAndWait [%] destroying',[self,fPool[t].fThreadName]);
flagIdle:
break; // acquired (should always be the case)
end;
SleepHiRes(1);
if Assigned(OnMainThreadIdle) then
OnMainThreadIdle(self);
until false;
fPool[t].Start(Method,n,n+perthread-1);
inc(n,perthread);
inc(fParallelRunCount);
end;
// run remaining items in the current/last thread
if n<MethodCount then begin
if Assigned(OnMainThreadIdle) then begin
fPool[use-1].Start(Method,n,MethodCount-1);
inc(use); // also wait for the last thread
end else
Method(n,MethodCount-1);
inc(fParallelRunCount);
end;
finally
// wait for the process to finish
for t := 0 to use-2 do
try
fPool[t].WaitForFinished(0,OnMainThreadIdle);
except
on E: Exception do
error := FormatUTF8('% % on thread % [%]',[error,E,fPool[t].fThreadName,E.Message]);
end;
if error<>'' then
raise ESynParallelProcess.CreateUTF8('%.ParallelRunAndWait: %',[self,error]);
end;
end;
{ TBlockingProcess }
constructor TBlockingProcess.Create(aTimeOutMs: integer; aSafe: PSynLocker);
begin
inherited Create(nil,false,false,'');
if aTimeOutMs<=0 then
fTimeOutMs := 3000 else // never wait for ever
fTimeOutMs := aTimeOutMs;
fSafe := aSafe;
end;
constructor TBlockingProcess.Create(aTimeOutMs: integer);
begin
fOwnedSafe := true;
Create(aTimeOutMS,NewSynLocker);
end;
destructor TBlockingProcess.Destroy;
begin
inherited Destroy;
if fOwnedSafe then
fSafe^.DoneAndFreeMem;
end;
function TBlockingProcess.WaitFor: TBlockingEvent;
begin
fSafe^.Lock;
try
result := fEvent;
if fEvent in [evRaised,evTimeOut] then
exit;
fEvent := evWaiting;
finally
fSafe^.UnLock;
end;
FixedWaitFor(self,fTimeOutMs);
fSafe^.Lock;
try
if fEvent<>evRaised then
fEvent := evTimeOut;
result := fEvent;
finally
fSafe^.UnLock;
end;
end;
function TBlockingProcess.WaitFor(TimeOutMS: integer): TBlockingEvent;
begin
if TimeOutMS <= 0 then
fTimeOutMs := 3000 // never wait for ever
else
fTimeOutMs := TimeOutMS;
result := WaitFor;
end;
function TBlockingProcess.NotifyFinished(alreadyLocked: boolean): boolean;
begin
result := false;
if not alreadyLocked then
fSafe^.Lock;
try
if fEvent in [evRaised,evTimeOut] then
exit; // ignore if already notified
fEvent := evRaised;
SetEvent; // notify caller to unlock "WaitFor" method
result := true;
finally
fSafe^.UnLock;
end;
end;
procedure TBlockingProcess.ResetInternal;
begin
ResetEvent;
fEvent := evNone;
end;
function TBlockingProcess.Reset: boolean;
begin
fSafe^.Lock;
try
result := fEvent<>evWaiting;
if result then
ResetInternal;
finally
fSafe^.UnLock;
end;
end;
procedure TBlockingProcess.Lock;
begin
fSafe^.Lock;
end;
procedure TBlockingProcess.Unlock;
begin
fSafe^.Unlock;
end;
{ TBlockingProcessPoolItem }
procedure TBlockingProcessPoolItem.ResetInternal;
begin
inherited ResetInternal; // set fEvent := evNone
fCall := 0;
end;
{ TBlockingProcessPool }
constructor TBlockingProcessPool.Create(aClass: TBlockingProcessPoolItemClass);
begin
inherited Create;
if aClass=nil then
fClass := TBlockingProcessPoolItem else
fClass := aClass;
2021-04-23 10:11:05 +02:00
fPool := TSynObjectListLocked.Create;
end;
const
CALL_DESTROYING = -1;
destructor TBlockingProcessPool.Destroy;
var i: integer;
someWaiting: boolean;
begin
fCallCounter := CALL_DESTROYING;
someWaiting := false;
for i := 0 to fPool.Count-1 do
with TBlockingProcessPoolItem(fPool.List[i]) do
if Event=evWaiting then begin
SetEvent; // release WaitFor (with evTimeOut)
someWaiting := true;
end;
if someWaiting then
SleepHiRes(10); // propagate the pending evTimeOut to the WaitFor threads
fPool.Free;
inherited;
end;
function TBlockingProcessPool.NewProcess(aTimeOutMs: integer): TBlockingProcessPoolItem;
var i: integer;
p: ^TBlockingProcessPoolItem;
begin
result := nil;
if fCallCounter=CALL_DESTROYING then
exit;
if aTimeOutMs<=0 then
aTimeOutMs := 3000; // never wait for ever
fPool.Safe.Lock;
try
p := pointer(fPool.List);
for i := 1 to fPool.Count do
if p^.Call=0 then begin
result := p^; // found a non-used entry
result.fTimeOutMs := aTimeOutMS;
break;
end else
inc(p);
if result=nil then begin
result := fClass.Create(aTimeOutMS);
fPool.Add(result);
end;
inc(fCallCounter); // 1,2,3,...
result.fCall := fCallCounter;
finally
fPool.Safe.UnLock;
end;
end;
function TBlockingProcessPool.FromCall(call: TBlockingProcessPoolCall;
locked: boolean): TBlockingProcessPoolItem;
var i: integer;
p: ^TBlockingProcessPoolItem;
begin
result := nil;
if (fCallCounter=CALL_DESTROYING) or (call<=0) then
exit;
fPool.Safe.Lock;
try
p := pointer(fPool.List);
for i := 1 to fPool.Count do
if p^.Call=call then begin
result := p^;
if locked then
result.Lock;
exit;
end else
inc(p);
finally
fPool.Safe.UnLock;
end;
end;
{$ifdef KYLIX3}
type
// see http://stackoverflow.com/a/3085509 about this known Kylix bug
TEventHack = class(THandleObject) // should match EXACTLY SyncObjs.pas source!
private
FEvent: TSemaphore;
FManualReset: Boolean;
end;
function FixedWaitFor(Event: TEvent; Timeout: LongWord): TWaitResult;
var E: TEventHack absolute Event;
procedure SetResult(res: integer);
begin
if res=0 then
result := wrSignaled else
if errno in [EAGAIN,ETIMEDOUT] then
result := wrTimeOut else begin
write(TimeOut,':',errno,' ');
result := wrError;
end;
end;
{.$define USESEMTRYWAIT}
// sem_timedwait() is slower than sem_trywait(), but consuming much less CPU
{$ifdef USESEMTRYWAIT}
var time: timespec;
{$else}
var start,current: Int64;
elapsed: LongWord;
{$endif}
begin
if Timeout=INFINITE then begin
SetResult(sem_wait(E.FEvent));
exit;
end;
if TimeOut=0 then begin
SetResult(sem_trywait(E.FEvent));
exit;
end;
{$ifdef USESEMTRYWAIT}
clock_gettime(CLOCK_REALTIME,time);
inc(time.tv_sec,TimeOut div 1000);
inc(time.tv_nsec,(TimeOut mod 1000)*1000000);
while time.tv_nsec>1000000000 do begin
inc(time.tv_sec);
dec(time.tv_nsec,1000000000);
end;
SetResult(sem_timedwait(E.FEvent,time));
{$else}
start := GetTickCount64;
repeat
if sem_trywait(E.FEvent)=0 then begin
result := wrSignaled;
break;
end;
current := GetTickCount64;
elapsed := current-start;
if elapsed=0 then
usleep(1) else
if elapsed>TimeOut then begin
result := wrTimeOut;
break;
end else
if elapsed<5 then
usleep(50) else
usleep(1000);
until false;
{$endif}
if E.FManualReset then begin
repeat until sem_trywait(E.FEvent)<>0; // reset semaphore state
sem_post(E.FEvent);
end;
end;
{$else KYLIX3} // original FPC or Windows implementation is OK
function FixedWaitFor(Event: TEvent; Timeout: LongWord): TWaitResult;
begin
result := Event.WaitFor(TimeOut);
end;
{$endif KYLIX3}
procedure FixedWaitForever(Event: TEvent);
begin
FixedWaitFor(Event,INFINITE);
end;
{$endif LVCL} // LVCL does not implement TEvent
{ ************ System Analysis types and classes ************************** }
function SystemInfoJson: RawUTF8;
var cpu,mem: RawUTF8;
begin
cpu := TSystemUse.Current(false).HistoryText(0,15,@mem);
with SystemInfo do
result := JSONEncode([
'host',ExeVersion.Host,'user',ExeVersion.User,'os',OSVersionText,
'cpu',CpuInfoText,'bios',BiosInfoText,
{$ifdef MSWINDOWS}{$ifndef CPU64}'wow64',IsWow64,{$endif}{$endif MSWINDOWS}
{$ifdef CPUINTEL}'cpufeatures', LowerCase(ToText(CpuFeatures, ' ')),{$endif}
'processcpu',cpu,'processmem',mem,
'freemem',TSynMonitorMemory.FreeAsText,
'disk',GetDiskPartitionsText(false,true)]);
end;
{ TProcessInfo }
{$ifdef MSWINDOWS}
type
TProcessMemoryCounters = record
cb: DWORD;
PageFaultCount: DWORD;
PeakWorkingSetSize: PtrUInt;
WorkingSetSize: PtrUInt;
QuotaPeakPagedPoolUsage: PtrUInt;
QuotaPagedPoolUsage: PtrUInt;
QuotaPeakNonPagedPoolUsage: PtrUInt;
QuotaNonPagedPoolUsage: PtrUInt;
PagefileUsage: PtrUInt;
PeakPagefileUsage: PtrUInt;
end;
const
PROCESS_QUERY_LIMITED_INFORMATION = $1000;
var
// PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION
OpenProcessAccess: DWORD;
// late-binding of Windows version specific API entries
GetSystemTimes: function(var lpIdleTime, lpKernelTime, lpUserTime: TFileTime): BOOL; stdcall;
GetProcessTimes: function(hProcess: THandle;
var lpCreationTime, lpExitTime, lpKernelTime, lpUserTime: TFileTime): BOOL; stdcall;
GetProcessMemoryInfo: function(Process: THandle;
var ppsmemCounters: TProcessMemoryCounters; cb: DWORD): BOOL; stdcall;
EnumProcessModules: function (hProcess: THandle; var lphModule: HMODULE; cb: DWORD;
var lpcbNeeded: DWORD): BOOL; stdcall;
EnumProcesses: function(lpidProcess: PDWORD; cb: DWORD; var cbNeeded: DWORD): BOOL; stdcall;
GetModuleFileNameExW: function(hProcess: THandle; hModule: HMODULE;
lpBaseName: PWideChar; nSize: DWORD): DWORD; stdcall;
// Vista+/WS2008+ (use GetModuleFileNameEx on XP)
QueryFullProcessImageNameW: function(hProcess: THandle; dwFlags: DWORD;
lpExeName: PWideChar; lpdwSize: PDWORD): BOOL; stdcall;
procedure InitWindowsAPI;
var Kernel, Psapi: THandle;
begin
if OSVersion>=wVista then
OpenProcessAccess := PROCESS_QUERY_LIMITED_INFORMATION else
OpenProcessAccess := PROCESS_QUERY_INFORMATION or PROCESS_VM_READ;
Kernel := GetModuleHandle(kernel32);
@GetSystemTimes := GetProcAddress(Kernel,'GetSystemTimes');
@GetProcessTimes := GetProcAddress(Kernel,'GetProcessTimes');
@QueryFullProcessImageNameW := GetProcAddress(Kernel,'QueryFullProcessImageNameW');
Psapi := LoadLibrary('Psapi.dll');
if Psapi>=32 then begin
@EnumProcesses := GetProcAddress(Psapi,'EnumProcesses');
@GetModuleFileNameExW := GetProcAddress(Psapi,'GetModuleFileNameExW');
@EnumProcessModules := GetProcAddress(Psapi, 'EnumProcessModules');
@GetProcessMemoryInfo := GetProcAddress(Psapi,'GetProcessMemoryInfo');
end;
end;
function EnumAllProcesses(out Count: Cardinal): TCardinalDynArray;
var n: cardinal;
begin
result := nil;
n := 2048;
repeat
SetLength(result, n);
if EnumProcesses(pointer(result), n * 4, Count) then
Count := Count shr 2 else
Count := 0;
if Count < n then begin
if Count = 0 then
result := nil;
exit;
end;
inc(n, 1024); // (very unlikely) too small buffer
until n>8192;
end;
function EnumProcessName(PID: Cardinal): RawUTF8;
var h: THandle;
len: DWORD;
name: array[0..4095] of WideChar;
begin
result := '';
if PID = 0 then
exit;
h := OpenProcess(OpenProcessAccess, false, PID);
if h <> 0 then
try
if Assigned(QueryFullProcessImageNameW) then begin
len := high(name);
if QueryFullProcessImageNameW(h, 0, name, @len) then
RawUnicodeToUtf8(name, len, result);
end else
if GetModuleFileNameExW(h,0,name,high(name))<>0 then
RawUnicodeToUtf8(name, StrLenW(name), result);
finally
CloseHandle(h);
end;
end;
function TProcessInfo.Init: boolean;
begin
FillCharFast(self,SizeOf(self),0);
result := Assigned(GetSystemTimes) and Assigned(GetProcessTimes) and
Assigned(GetProcessMemoryInfo); // no monitoring API under oldest Windows
end;
function TProcessInfo.Start: boolean;
var ftidl,ftkrn,ftusr: TFileTime;
sidl,skrn,susr: Int64;
begin
result := Assigned(GetSystemTimes) and GetSystemTimes(ftidl,ftkrn,ftusr);
if not result then
exit;
FileTimeToInt64(ftidl,sidl);
FileTimeToInt64(ftkrn,skrn);
FileTimeToInt64(ftusr,susr);
fDiffIdle := sidl-fSysPrevIdle;
fDiffKernel := skrn-fSysPrevKernel;
fDiffUser := susr-fSysPrevUser;
fDiffTotal := fDiffKernel+fDiffUser; // kernel time also includes idle time
dec(fDiffKernel, fDiffIdle);
fSysPrevIdle := sidl;
fSysPrevKernel := skrn;
fSysPrevUser := susr;
end;
function TProcessInfo.PerProcess(PID: cardinal; Now: PDateTime;
out Data: TSystemUseData; var PrevKernel, PrevUser: Int64): boolean;
var
h: THandle;
ftkrn,ftusr,ftp,fte: TFileTime;
pkrn,pusr: Int64;
mem: TProcessMemoryCounters;
begin
result := false;
FillCharFast(Data,SizeOf(Data),0);
h := OpenProcess(OpenProcessAccess,false,PID);
if h<>0 then
try
if GetProcessTimes(h,ftp,fte,ftkrn,ftusr) then begin
if Now<>nil then
Data.Timestamp := Now^;
FileTimeToInt64(ftkrn,pkrn);
FileTimeToInt64(ftusr,pusr);
if (PrevKernel<>0) and (fDiffTotal>0) then begin
Data.Kernel := ((pkrn-PrevKernel)*100)/fDiffTotal;
Data.User := ((pusr-PrevUser)*100)/fDiffTotal;
end;
PrevKernel := pkrn;
PrevUser := pusr;
FillCharFast(mem,SizeOf(mem),0);
mem.cb := SizeOf(mem);
if GetProcessMemoryInfo(h,mem,SizeOf(mem)) then begin
Data.WorkKB := mem.WorkingSetSize shr 10;
Data.VirtualKB := mem.PagefileUsage shr 10;
end;
result := true;
end;
finally
CloseHandle(h);
end;
end;
function TProcessInfo.PerSystem(out Idle,Kernel,User: currency): boolean;
begin
if fDiffTotal<=0 then begin
Idle := 0;
Kernel := 0;
User := 0;
result := false;
end else begin
Kernel := SimpleRoundTo2Digits((fDiffKernel*100)/fDiffTotal);
User := SimpleRoundTo2Digits((fDiffUser*100)/fDiffTotal);
Idle := 100-Kernel-User; // ensure sum is always 100%
result := true;
end;
end;
{$else} // not implemented yet (use /proc ?)
function TProcessInfo.Init: boolean;
begin
FillCharFast(self,SizeOf(self),0);
result := false;
end;
function TProcessInfo.Start: boolean;
begin
result := false;
end;
function TProcessInfo.PerProcess(PID: cardinal; Now: PDateTime;
out Data: TSystemUseData; var PrevKernel, PrevUser: Int64): boolean;
begin
result := false;
end;
function TProcessInfo.PerSystem(out Idle,Kernel,User: currency): boolean;
var P: PUTF8Char;
U, K, I, S: cardinal;
begin // see http://www.linuxhowtos.org/System/procstat.htm
result := false;
P := pointer(StringFromFile('/proc/stat', {nosize=}true));
if P=nil then
exit;
U := GetNextItemCardinal(P,' '){=user}+GetNextItemCardinal(P,' '){=nice};
K := GetNextItemCardinal(P,' '){=system};
I := GetNextItemCardinal(P,' '){=idle};
S := U+K+I;
Kernel := SimpleRoundTo2Digits((K*100)/S);
User := SimpleRoundTo2Digits((U*100)/S);
Idle := 100-Kernel-User; // ensure sum is always 100%
result := S<>0;
end; { TODO : use a diff approach for TProcessInfo.PerSystem on Linux }
{$endif MSWINDOWS}
{ TSystemUse }
procedure TSystemUse.BackgroundExecute(Sender: TSynBackgroundTimer;
Event: TWaitResult; const Msg: RawUTF8);
var i: integer;
now: TDateTime;
begin
if (fProcess=nil) or (fHistoryDepth=0) or not fProcessInfo.Start then
exit;
fTimer := Sender;
now := NowUTC;
fSafe.Lock;
try
inc(fDataIndex);
if fDataIndex>=fHistoryDepth then
fDataIndex := 0;
for i := high(fProcess) downto 0 do // backwards for fProcesses.Delete(i)
with fProcess[i] do
if fProcessInfo.PerProcess(ID,@now,Data[fDataIndex],PrevKernel,PrevUser) then begin
if Assigned(fOnMeasured) then
fOnMeasured(ID,Data[fDataIndex]);
end else
if UnsubscribeProcessOnAccessError then
// if GetLastError=ERROR_INVALID_PARAMETER then
fProcesses.Delete(i);
finally
fSafe.UnLock;
end;
end;
procedure TSystemUse.OnTimerExecute(Sender: TObject);
begin
BackgroundExecute(nil,wrSignaled,'');
end;
constructor TSystemUse.Create(const aProcessID: array of integer;
aHistoryDepth: integer);
var i: integer;
begin
inherited Create;
fProcesses.Init(TypeInfo(TSystemUseProcessDynArray),fProcess);
{$ifdef MSWINDOWS}
if not Assigned(GetSystemTimes) or not Assigned(GetProcessTimes) or
not Assigned(GetProcessMemoryInfo) then
exit; // no system monitoring API under oldest Windows
{$else}
exit; // not implemented yet
{$endif}
if aHistoryDepth<=0 then
aHistoryDepth := 1;
fHistoryDepth := aHistoryDepth;
SetLength(fProcess,length(aProcessID));
for i := 0 to high(aProcessID) do begin
{$ifdef MSWINDOWS}
if aProcessID[i]=0 then
fProcess[i].ID := GetCurrentProcessID else
{$endif}
fProcess[i].ID := aProcessID[i];
SetLength(fProcess[i].Data,fHistoryDepth);
end;
end;
constructor TSystemUse.Create(aHistoryDepth: integer);
begin
Create([0],aHistoryDepth);
end;
procedure TSystemUse.Subscribe(aProcessID: integer);
var i,n: integer;
begin
if self=nil then
exit;
{$ifdef MSWINDOWS}
if aProcessID=0 then
aProcessID := GetCurrentProcessID;
{$endif}
fSafe.Lock;
try
n := length(fProcess);
for i := 0 to n-1 do
if fProcess[i].ID=aProcessID then
exit; // already subscribed
SetLength(fProcess,n+1);
fProcess[n].ID := aProcessID;
SetLength(fProcess[n].Data,fHistoryDepth);
finally
fSafe.UnLock;
end;
end;
function TSystemUse.Unsubscribe(aProcessID: integer): boolean;
var i: integer;
begin
result := false;
if self=nil then
exit;
fSafe.Lock;
try
i := ProcessIndex(aProcessID);
if i>=0 then begin
fProcesses.Delete(i);
result := true;
end;
finally
fSafe.UnLock;
end;
end;
function TSystemUse.ProcessIndex(aProcessID: integer): integer;
begin // caller should have made fSafe.Enter
{$ifdef MSWINDOWS}
if aProcessID=0 then
aProcessID := GetCurrentProcessID;
{$endif}
if self<>nil then
for result := 0 to high(fProcess) do
if fProcess[result].ID=aProcessID then
exit;
result := -1;
end;
function TSystemUse.Data(out aData: TSystemUseData; aProcessID: integer=0): boolean;
var i: integer;
begin
result := false;
if self<>nil then begin
fSafe.Lock;
try
i := ProcessIndex(aProcessID);
if i>=0 then begin
with fProcess[i] do
aData := Data[fDataIndex];
result := aData.Timestamp<>0;
if result then
exit;
end;
finally
fSafe.UnLock;
end;
end;
FillCharFast(aData,SizeOf(aData),0);
end;
function TSystemUse.Data(aProcessID: integer): TSystemUseData;
begin
Data(result,aProcessID);
end;
function TSystemUse.KB(aProcessID: integer=0): cardinal;
begin
with Data(aProcessID) do
result := WorkKB+VirtualKB;
end;
function TSystemUse.Percent(aProcessID: integer): single;
begin
with Data(aProcessID) do
result := Kernel+User;
end;
function TSystemUse.PercentKernel(aProcessID: integer): single;
begin
result := Data(aProcessID).Kernel;
end;
function TSystemUse.PercentUser(aProcessID: integer): single;
begin
result := Data(aProcessID).User;
end;
function TSystemUse.PercentSystem(out Idle,Kernel,User: currency): boolean;
begin
result := fProcessInfo.PerSystem(Idle,Kernel,User);
end;
function TSystemUse.HistoryData(aProcessID,aDepth: integer): TSystemUseDataDynArray;
var i,n,last: integer;
begin
result := nil;
if self=nil then
exit;
fSafe.Lock;
try
i := ProcessIndex(aProcessID);
if i>=0 then
with fProcess[i] do begin
n := length(Data);
last := n-1;
if (aDepth>0) and (n>aDepth) then
n := aDepth;
SetLength(result,n); // make ordered copy
for i := 0 to n-1 do begin
if i<=fDataIndex then
result[i] := Data[fDataIndex-i] else begin
result[i] := Data[last];
dec(last);
end;
if PInt64(@result[i].Timestamp)^=0 then begin
SetLength(result,i); // truncate to latest available sample
break;
end;
end;
end;
finally
fSafe.UnLock;
end;
end;
function TSystemUse.History(aProcessID,aDepth: integer): TSingleDynArray;
var i,n: integer;
data: TSystemUseDataDynArray;
begin
result := nil;
data := HistoryData(aProcessID,aDepth);
n := length(data);
SetLength(result,n);
for i := 0 to n-1 do
result[i] := data[i].Kernel+data[i].User;
end;
class function TSystemUse.Current(aCreateIfNone: boolean): TSystemUse;
begin
if (ProcessSystemUse=nil) and aCreateIfNone then
GarbageCollectorFreeAndNil(ProcessSystemUse,TSystemUse.Create(60));
result := ProcessSystemUse;
end;
function TSystemUse.HistoryText(aProcessID,aDepth: integer;
aDestMemoryMB: PRawUTF8): RawUTF8;
var data: TSystemUseDataDynArray;
mem: RawUTF8;
i: integer;
begin
result := '';
data := HistoryData(aProcessID,aDepth);
{$ifdef LINUXNOTBSD} // bsd: see VM_LOADAVG
// https://www.retro11.de/ouxr/211bsd/usr/src/lib/libc/gen/getloadavg.c.html
if data = nil then
result := StringFromFile('/proc/loadavg',{HasNoSize=}true) else
{$endif LINUXNOTBSD}
for i := 0 to high(data) do
with data[i] do begin
result := FormatUTF8('%% ',[result,TruncTo2Digits(Kernel+User)]);
if aDestMemoryMB<>nil then
mem := FormatUTF8('%% ',[mem,TruncTo2Digits(WorkKB/1024)]);
end;
result := trim(result);
if aDestMemoryMB<>nil then
aDestMemoryMB^ := trim(mem);
end;
{$ifndef NOVARIANTS}
function TSystemUse.HistoryVariant(aProcessID,aDepth: integer): variant;
var res: TDocVariantData absolute result;
data: TSystemUseDataDynArray;
i: integer;
begin
VarClear(result);
data := HistoryData(aProcessID,aDepth);
res.InitFast(length(data),dvArray);
for i := 0 to high(data) do
res.AddItem(TruncTo2Digits(data[i].Kernel+data[i].User));
end;
{$endif NOVARIANTS}
function SortDynArrayDiskPartitions(const A,B): integer;
begin
result := SortDynArrayString(TDiskPartition(A).mounted,TDiskPartition(B).mounted);
end;
function GetDiskPartitions: TDiskPartitions;
{$ifdef MSWINDOWS} // DeviceIoControl(IOCTL_DISK_GET_PARTITION_INFO) requires root
var drives, drive, m, n: integer;
fn, volume: TFileName;
{$else}
var mounts, fs, mnt, typ: RawUTF8;
p: PUTF8Char;
fn: TFileName;
n: integer;
{$endif}
av, fr, tot: QWord;
begin
result := nil;
n := 0;
{$ifdef MSWINDOWS}
fn := '#:\';
drives := GetLogicalDrives;
m := 1 shl 2;
for drive := 3 to 26 do begin // retrieve partitions mounted as C..Z drives
if drives and m <> 0 then begin
fn[1] := char(64+drive);
if GetDiskInfo(fn,av,fr,tot,@volume) then begin
SetLength(result,n+1);
StringToUTF8(volume,result[n].name);
volume := '';
result[n].mounted := fn;
result[n].size := tot;
inc(n);
end;
end;
m := m shl 1;
end;
{$else} // see https://github.com/gagern/gnulib/blob/master/lib/mountlist.c
mounts := StringFromFile({$ifdef BSD}'/etc/mtab'{$else}'/proc/self/mounts'{$endif},
{hasnosize=}true);
p := pointer(mounts);
repeat
fs := '';
mnt := '';
typ := '';
ScanUTF8(GetNextLine(p,p),'%S %S %S',[@fs,@mnt,@typ]);
if (fs<>'') and (fs<>'rootfs') and (IdemPCharArray(pointer(fs),['/DEV/LOOP'])<0) and
(mnt<>'') and (mnt<>'/mnt') and (typ<>'') and
(IdemPCharArray(pointer(mnt),['/PROC/','/SYS/','/RUN/'])<0) and
(FindPropName(['autofs','proc','subfs','debugfs','devpts','fusectl','mqueue',
'rpc-pipefs','sysfs','devfs','kernfs','ignore','none','tmpfs','securityfs',
'ramfs','rootfs','devtmpfs','hugetlbfs','iso9660'],typ)<0) then begin
fn := UTF8ToString(mnt);
if GetDiskInfo(fn,av,fr,tot) and (tot>1 shl 20) then begin
//writeln('fs=',fs,' mnt=',mnt,' typ=',typ, ' av=',KB(av),' fr=',KB(fr),' tot=',KB(tot));
SetLength(result,n+1);
result[n].name := fs;
result[n].mounted := fn;
result[n].size := tot;
inc(n);
end;
end;
until p=nil;
DynArray(TypeInfo(TDiskPartitions),result).Sort(SortDynArrayDiskPartitions);
{$endif}
end;
var
_DiskPartitions: TDiskPartitions;
function GetDiskPartitionsText(nocache, withfreespace, nospace: boolean): RawUTF8;
const F: array[boolean] of RawUTF8 = ({$ifdef MSWINDOWS}'%: % (% / %)', '%: % (%/%)'
{$else}'% % (% / %)', '% % (%/%)'{$endif});
var i: integer;
parts: TDiskPartitions;
function GetInfo(var p: TDiskPartition): shortstring;
var av, fr, tot: QWord;
begin
if not withfreespace or not GetDiskInfo(p.mounted,av,fr,tot) then
{$ifdef MSWINDOWS}
FormatShort('%: % (%)',[p.mounted[1],p.name,KB(p.size,nospace)],result) else
2021-04-23 10:11:05 +02:00
FormatShort(F[nospace],[p.mounted[1],p.name,KB(fr,nospace),KB(tot,nospace)],result);
{$else}
FormatShort('% % (%)',[p.mounted,p.name,KB(p.size,nospace)],result) else
FormatShort(F[nospace],[p.mounted,p.name,KB(fr,nospace),KB(tot,nospace)],result);
{$endif}
end;
begin
if (_DiskPartitions=nil) or nocache then
_DiskPartitions := GetDiskPartitions;
parts := _DiskPartitions;
if parts=nil then
result := '' else
ShortStringToAnsi7String(GetInfo(parts[0]),result);
for i := 1 to high(parts) do
result := FormatUTF8('%, %',[result,GetInfo(parts[i])]);
end;
{ TSynMonitorMemory }
constructor TSynMonitorMemory.Create(aTextNoSpace: boolean);
begin
FAllocatedUsed := TSynMonitorOneSize.Create(aTextNoSpace);
FAllocatedReserved := TSynMonitorOneSize.Create(aTextNoSpace);
FPhysicalMemoryFree := TSynMonitorOneSize.Create(aTextNoSpace);
FVirtualMemoryFree := TSynMonitorOneSize.Create(aTextNoSpace);
FPagingFileTotal := TSynMonitorOneSize.Create(aTextNoSpace);
FPhysicalMemoryTotal := TSynMonitorOneSize.Create(aTextNoSpace);
FVirtualMemoryTotal := TSynMonitorOneSize.Create(aTextNoSpace);
FPagingFileFree := TSynMonitorOneSize.Create(aTextNoSpace);
end;
destructor TSynMonitorMemory.Destroy;
begin
FAllocatedReserved.Free;
FAllocatedUsed.Free;
FPhysicalMemoryFree.Free;
FVirtualMemoryFree.Free;
FPagingFileTotal.Free;
FPhysicalMemoryTotal.Free;
FVirtualMemoryTotal.Free;
FPagingFileFree.Free;
inherited Destroy;
end;
class function TSynMonitorMemory.FreeAsText(nospace: boolean): ShortString;
const F: array[boolean] of RawUTF8 = ('% / %', '%/%');
begin
with TSynMonitorMemory.Create(nospace) do
try
RetrieveMemoryInfo;
FormatShort(F[nospace],[fPhysicalMemoryFree.Text,fPhysicalMemoryTotal.Text],result);
finally
Free;
end;
end;
var
PhysicalAsTextCache: TShort16; // this value doesn't change usually
class function TSynMonitorMemory.PhysicalAsText(nospace: boolean): TShort16;
begin
if PhysicalAsTextCache='' then
with TSynMonitorMemory.Create(nospace) do
try
PhysicalAsTextCache := PhysicalMemoryTotal.Text;
finally
Free;
end;
result := PhysicalAsTextCache;
end;
class function TSynMonitorMemory.ToJSON: RawUTF8;
begin
with TSynMonitorMemory.Create(false) do
try
RetrieveMemoryInfo;
FormatUTF8('{Allocated:{reserved:%,used:%},Physical:{total:%,free:%,percent:%},'+
{$ifdef MSWINDOWS}'Virtual:{total:%,free:%},'+{$endif}'Paged:{total:%,free:%}}',
[fAllocatedReserved.Bytes shr 10,fAllocatedUsed.Bytes shr 10,
fPhysicalMemoryTotal.Bytes shr 10,fPhysicalMemoryFree.Bytes shr 10, fMemoryLoadPercent,
{$ifdef MSWINDOWS}fVirtualMemoryTotal.Bytes shr 10,fVirtualMemoryFree.Bytes shr 10,{$endif}
fPagingFileTotal.Bytes shr 10,fPagingFileFree.Bytes shr 10],result);
finally
Free;
end;
end;
{$ifndef NOVARIANTS}
class function TSynMonitorMemory.ToVariant: variant;
begin
result := _JsonFast(ToJSON);
end;
{$endif}
function TSynMonitorMemory.GetAllocatedUsed: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FAllocatedUsed;
end;
function TSynMonitorMemory.GetAllocatedReserved: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FAllocatedReserved;
end;
function TSynMonitorMemory.GetMemoryLoadPercent: integer;
begin
RetrieveMemoryInfo;
result := FMemoryLoadPercent;
end;
function TSynMonitorMemory.GetPagingFileFree: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FPagingFileFree;
end;
function TSynMonitorMemory.GetPagingFileTotal: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FPagingFileTotal;
end;
function TSynMonitorMemory.GetPhysicalMemoryFree: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FPhysicalMemoryFree;
end;
function TSynMonitorMemory.GetPhysicalMemoryTotal: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FPhysicalMemoryTotal;
end;
function TSynMonitorMemory.GetVirtualMemoryFree: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FVirtualMemoryFree;
end;
function TSynMonitorMemory.GetVirtualMemoryTotal: TSynMonitorOneSize;
begin
RetrieveMemoryInfo;
result := FVirtualMemoryTotal;
end;
{$ifdef MSWINDOWS}
{$ifndef UNICODE} // missing API for oldest Delphi
type
DWORDLONG = Int64;
TMemoryStatusEx = record
dwLength: DWORD;
dwMemoryLoad: DWORD;
ullTotalPhys: DWORDLONG;
ullAvailPhys: DWORDLONG;
ullTotalPageFile: DWORDLONG;
ullAvailPageFile: DWORDLONG;
ullTotalVirtual: DWORDLONG;
ullAvailVirtual: DWORDLONG;
ullAvailExtendedVirtual: DWORDLONG;
end;
// information about the system's current usage of both physical and virtual memory
function GlobalMemoryStatusEx(var lpBuffer: TMemoryStatusEx): BOOL;
stdcall; external kernel32;
{$endif}
{$endif}
function GetMemoryInfo(out info: TMemoryInfo; withalloc: boolean): boolean;
{$ifdef WITH_FASTMM4STATS}
var Heap: TMemoryManagerState;
sb: integer;
{$endif}
{$ifdef MSWINDOWS}
var global: TMemoryStatusEx;
{$ifdef FPC}mem: TProcessMemoryCounters;{$endif}
begin
FillCharFast(global,SizeOf(global),0);
global.dwLength := SizeOf(global);
result := GlobalMemoryStatusEx(global);
info.percent := global.dwMemoryLoad;
info.memtotal := global.ullTotalPhys;
info.memfree := global.ullAvailPhys;
info.filetotal := global.ullTotalPageFile;
info.filefree := global.ullAvailPageFile;
info.vmtotal := global.ullTotalVirtual;
info.vmfree := global.ullAvailVirtual;
{$ifdef FPC} // GetHeapStatus is only about current thread -> use WinAPI
if withalloc and Assigned(GetProcessMemoryInfo) then begin
FillcharFast(mem,SizeOf(mem),0);
mem.cb := SizeOf(mem);
GetProcessMemoryInfo(GetCurrentProcess,mem,SizeOf(mem));
info.allocreserved := mem.PeakWorkingSetSize;
info.allocused := mem.WorkingSetSize;
end;
{$endif FPC}
{$else}
{$ifdef BSD}
begin
FillCharFast(info,SizeOf(info),0);
info.memtotal := fpsysctlhwint({$ifdef DARWIN}HW_MEMSIZE{$else}HW_PHYSMEM{$endif});
info.memfree := info.memtotal-fpsysctlhwint(HW_USERMEM);
if info.memtotal<>0 then // avoid div per 0 exception
info.percent := ((info.memtotal-info.memfree)*100)div info.memtotal;
{$else}
var si: TSysInfo; // Linuxism
P: PUTF8Char;
{$ifdef FPC}mu: cardinal{$else}const mu=1{$endif};
begin
FillCharFast(info,SizeOf(info),0);
{$ifdef FPC}
result := SysInfo(@si)=0;
mu := si.mem_unit;
{$else}
result := SysInfo(si)=0; // some missing fields in Kylix' Libc
{$endif}
if si.totalram<>0 then // avoid div per 0 exception
info.percent := ((si.totalram-si.freeram)*100)div si.totalram;
info.memtotal := si.totalram*mu;
info.memfree := si.freeram*mu;
info.filetotal := si.totalswap*mu;
info.filefree := si.freeswap*mu;
if withalloc then begin
// virtual memory information is not available under Linux
P := pointer(StringFromFile('/proc/self/statm',{hasnosize=}true));
info.allocreserved := GetNextItemCardinal(P,' ')*SystemInfo.dwPageSize; // VmSize
info.allocused := GetNextItemCardinal(P,' ')*SystemInfo.dwPageSize; // VmRSS
end;
// GetHeapStatus is only about current thread -> use /proc/[pid]/statm
{$endif BSD}
{$endif MSWINDOWS}
{$ifdef WITH_FASTMM4STATS} // override OS information by actual FastMM4
if withalloc then begin
GetMemoryManagerState(Heap); // direct raw FastMM4 access
info.allocused := Heap.TotalAllocatedMediumBlockSize+Heap.TotalAllocatedLargeBlockSize;
info.allocreserved := Heap.ReservedMediumBlockAddressSpace+Heap.ReservedLargeBlockAddressSpace;
for sb := 0 to high(Heap.SmallBlockTypeStates) do
with Heap.SmallBlockTypeStates[sb] do begin
inc(info.allocused,UseableBlockSize*AllocatedBlockCount);
inc(info.allocreserved,ReservedAddressSpace);
end;
end;
{$endif WITH_FASTMM4STATS}
end;
procedure TSynMonitorMemory.RetrieveMemoryInfo;
var tix: cardinal;
info: TMemoryInfo;
begin
tix := GetTickCount64 shr 7; // allow 128 ms resolution for updates
if fLastMemoryInfoRetrievedTix<>tix then begin
fLastMemoryInfoRetrievedTix := tix;
if not GetMemoryInfo(info,{withalloc=}true) then
exit;
FMemoryLoadPercent := info.percent;
FPhysicalMemoryTotal.Bytes := info.memtotal;
FPhysicalMemoryFree.Bytes := info.memfree;
FPagingFileTotal.Bytes := info.filetotal;
FPagingFileFree.Bytes := info.filefree;
FVirtualMemoryTotal.Bytes := info.vmtotal;
FVirtualMemoryFree.Bytes := info.vmfree;
FAllocatedReserved.Bytes := info.allocreserved;
FAllocatedUsed.Bytes := info.allocused;
end;
end;
{ TSynMonitorDisk }
constructor TSynMonitorDisk.Create;
begin
fAvailableSize := TSynMonitorOneSize.Create({nospace=}false);
fFreeSize := TSynMonitorOneSize.Create({nospace=}false);
fTotalSize := TSynMonitorOneSize.Create({nospace=}false);
end;
destructor TSynMonitorDisk.Destroy;
begin
fAvailableSize.Free;
fFreeSize.Free;
fTotalSize.Free;
inherited;
end;
function TSynMonitorDisk.GetName: TFileName;
begin
RetrieveDiskInfo;
result := fName;
end;
function TSynMonitorDisk.GetAvailable: TSynMonitorOneSize;
begin
RetrieveDiskInfo;
result := fAvailableSize;
end;
function TSynMonitorDisk.GetFree: TSynMonitorOneSize;
begin
RetrieveDiskInfo;
result := fFreeSize;
end;
function TSynMonitorDisk.GetTotal: TSynMonitorOneSize;
begin
RetrieveDiskInfo;
result := fTotalSize;
end;
class function TSynMonitorDisk.FreeAsText: RawUTF8;
var name: TFileName;
avail,free,total: QWord;
begin
GetDiskInfo(name,avail,free,total);
FormatUTF8('% % / %',[name, KB(free),KB(total)],result);
end;
{$ifdef MSWINDOWS}
function GetDiskFreeSpaceExA(lpDirectoryName: PAnsiChar;
var lpFreeBytesAvailableToCaller, lpTotalNumberOfBytes,
lpTotalNumberOfFreeBytes: QWord): LongBool; stdcall; external kernel32;
function GetDiskFreeSpaceExW(lpDirectoryName: PWideChar;
var lpFreeBytesAvailableToCaller, lpTotalNumberOfBytes,
lpTotalNumberOfFreeBytes: QWord): LongBool; stdcall; external kernel32;
function DeviceIoControl(hDevice: THandle; dwIoControlCode: DWORD; lpInBuffer: Pointer;
nInBufferSize: DWORD; lpOutBuffer: Pointer; nOutBufferSize: DWORD;
var lpBytesReturned: DWORD; lpOverlapped: POverlapped): BOOL; stdcall; external kernel32;
{$endif}
function GetDiskInfo(var aDriveFolderOrFile: TFileName;
out aAvailableBytes, aFreeBytes, aTotalBytes: QWord
{$ifdef MSWINDOWS}; aVolumeName: PFileName = nil{$endif}): boolean;
{$ifdef MSWINDOWS}
var tmp: array[0..MAX_PATH-1] of Char;
dummy,flags: DWORD;
dn: TFileName;
begin
if aDriveFolderOrFile='' then
aDriveFolderOrFile := SysUtils.UpperCase(ExtractFileDrive(ExeVersion.ProgramFilePath));
dn := aDriveFolderOrFile;
if (dn<>'') and (dn[2]=':') and (dn[3]=#0) then
dn := dn+'\';
if (aVolumeName<>nil) and (aVolumeName^='') then begin
tmp[0] := #0;
GetVolumeInformation(pointer(dn),tmp,MAX_PATH,nil,dummy,flags,nil,0);
aVolumeName^ := tmp;
end;
result := {$ifdef UNICODE}GetDiskFreeSpaceExW{$else}GetDiskFreeSpaceExA{$endif}(
pointer(dn),aAvailableBytes,aTotalBytes,aFreeBytes);
{$else}
{$ifdef KYLIX3}
var fs: TStatFs64;
h: THandle;
begin
if aDriveFolderOrFile='' then
aDriveFolderOrFile := '.';
h := FileOpen(aDriveFolderOrFile,fmShareDenyNone);
result := fstatfs64(h,fs)=0;
FileClose(h);
aAvailableBytes := fs.f_bavail*fs.f_bsize;
aFreeBytes := aAvailableBytes;
aTotalBytes := fs.f_blocks*fs.f_bsize;
{$endif}
{$ifdef FPC}
var fs: tstatfs;
begin
if aDriveFolderOrFile='' then
aDriveFolderOrFile := '.';
result := fpStatFS(aDriveFolderOrFile,@fs)=0;
aAvailableBytes := QWord(fs.bavail)*QWord(fs.bsize);
aFreeBytes := aAvailableBytes; // no user Quota involved here
aTotalBytes := QWord(fs.blocks)*QWord(fs.bsize);
{$endif FPC}
{$endif MSWINDOWS}
end;
procedure TSynMonitorDisk.RetrieveDiskInfo;
var tix: cardinal;
begin
tix := GetTickCount64 shr 7; // allow 128 ms resolution for updates
if fLastDiskInfoRetrievedTix<>tix then begin
fLastDiskInfoRetrievedTix := tix;
GetDiskInfo(fName,PQWord(@fAvailableSize.Bytes)^,PQWord(@fFreeSize.Bytes)^,
PQWord(@fTotalSize.Bytes)^{$ifdef MSWINDOWS},@fVolumeName{$endif});
end;
end;
{ TTimeZoneData }
function TTimeZoneData.GetTziFor(year: integer): PTimeZoneInfo;
var i,last: PtrInt;
begin
if dyn=nil then
result := @tzi else
if year<=dyn[0].year then
result := @dyn[0].tzi else begin
last := high(dyn);
if year>=dyn[last].year then
result := @dyn[last].tzi else begin
for i := 1 to last do
if year<dyn[i].year then begin
result := @dyn[i-1].tzi;
exit;
end;
result := @tzi; // should never happen, but makes compiler happy
end;
end;
end;
{ TTimeZoneInformation }
constructor TSynTimeZone.Create;
begin
fZones.InitSpecific(TypeInfo(TTimeZoneDataDynArray),fZone,djRawUTF8);
end;
constructor TSynTimeZone.CreateDefault;
begin
Create;
{$ifdef LVCL}
LoadFromFile;
{$else}
{$ifdef MSWINDOWS}
LoadFromRegistry;
{$else}
LoadFromFile;
if fZones.Count=0 then
LoadFromResource; // if no .tz file is available, try from registry
{$endif}
{$endif}
end;
destructor TSynTimeZone.Destroy;
begin
inherited Destroy;
fIds.Free;
fDisplays.Free;
end;
var
SharedSynTimeZone: TSynTimeZone;
class function TSynTimeZone.Default: TSynTimeZone;
begin
if SharedSynTimeZone=nil then
GarbageCollectorFreeAndNil(SharedSynTimeZone,TSynTimeZone.CreateDefault);
result := SharedSynTimeZone;
end;
function TSynTimeZone.SaveToBuffer: RawByteString;
begin
result := SynLZCompress(fZones.SaveTo);
end;
procedure TSynTimeZone.SaveToFile(const FileName: TFileName);
var FN: TFileName;
begin
if FileName='' then
FN := ChangeFileExt(ExeVersion.ProgramFileName,'.tz') else
FN := FileName;
FileFromString(SaveToBuffer,FN);
end;
procedure TSynTimeZone.LoadFromBuffer(const Buffer: RawByteString);
begin
fZones.LoadFromBinary(AlgoSynLZ.Decompress(Buffer),{nohash=}true);
2021-04-23 10:11:05 +02:00
fZones.ReHash;
FreeAndNil(fIds);
FreeAndNil(fDisplays);
end;
procedure TSynTimeZone.LoadFromFile(const FileName: TFileName);
var FN: TFileName;
begin
if FileName='' then
FN := ChangeFileExt(ExeVersion.ProgramFileName,'.tz') else
FN := FileName;
LoadFromBuffer(StringFromFile(FN));
end;
procedure TSynTimeZone.LoadFromResource(Instance: THandle);
var buf: RawByteString;
begin
ResourceToRawByteString(ClassName,PChar(10),buf,Instance);
if buf<>'' then
LoadFromBuffer(buf);
end;
{$ifdef MSWINDOWS}
procedure TSynTimeZone.LoadFromRegistry;
const REGKEY = 'Software\Microsoft\Windows NT\CurrentVersion\Time Zones\';
var reg: TWinRegistry;
keys: TRawUTF8DynArray;
i,first,last,year,n: integer;
itemsize: DWORD;
item: TTimeZoneData;
begin
fZones.Clear;
if reg.ReadOpen(HKEY_LOCAL_MACHINE,REGKEY) then
keys := reg.ReadEnumEntries else
keys := nil; // make Delphi 6 happy
n := length(keys);
fZones.Capacity := n;
for i := 0 to n-1 do begin
Finalize(item);
FillcharFast(item.tzi,SizeOf(item.tzi),0);
if reg.ReadOpen(HKEY_LOCAL_MACHINE,REGKEY+keys[i],true) then begin
item.id := keys[i];
item.Display := reg.ReadString('Display');
itemsize := SizeOf(item.tzi);
RegQueryValueExW(reg.key,'TZI',nil,nil,@item.tzi,@itemsize);
if reg.ReadOpen(HKEY_LOCAL_MACHINE,REGKEY+keys[i]+'\Dynamic DST',true) then begin
// warning: never defined on XP/2003, and not for all entries
first := reg.ReadDword('FirstEntry');
last := reg.ReadDword('LastEntry');
if (first>0) and (last>=first) then begin
n := 0;
SetLength(item.dyn,last-first+1);
for year := first to last do begin
itemsize := SizeOf(TTimeZoneInfo);
if RegQueryValueExA(reg.key,pointer(UInt32ToUTF8(year)),nil,nil,
@item.dyn[n].tzi,@itemsize)=0 then begin
item.dyn[n].year := year;
inc(n);
end;
end;
SetLength(item.dyn,n);
end;
end;
fZones.Add(item);
end;
end;
reg.Close;
fZones.ReHash;
FreeAndNil(fIds);
FreeAndNil(fDisplays);
end;
{$endif MSWINDOWS}
function TSynTimeZone.GetDisplay(const TzId: TTimeZoneID): RawUTF8;
var ndx: integer;
begin
if self=nil then
ndx := -1 else
ndx := fZones.FindHashed(TzID);
if ndx<0 then
if TzID='UTC' then // e.g. on XP
result := TzID else
result := '' else
result := fZone[ndx].display;
end;
function TSynTimeZone.GetBiasForDateTime(const Value: TDateTime;
const TzId: TTimeZoneID; out Bias: integer; out HaveDaylight: boolean): boolean;
var ndx: integer;
d: TSynSystemTime;
tzi: PTimeZoneInfo;
std,dlt: TDateTime;
begin
if (self=nil) or (TzId='') then
ndx := -1 else
if TzID=fLastZone then
ndx := fLastIndex else begin
ndx := fZones.FindHashed(TzID);
fLastZone := TzID;
flastIndex := ndx;
end;
if ndx<0 then begin
Bias := 0;
HaveDayLight := false;
result := TzID='UTC'; // e.g. on XP
exit;
end;
d.FromDate(Value); // faster than DecodeDate
tzi := fZone[ndx].GetTziFor(d.Year);
if tzi.change_time_std.IsZero then begin
HaveDaylight := false;
Bias := tzi.Bias+tzi.bias_std;
end else begin
HaveDaylight := true;
std := tzi.change_time_std.EncodeForTimeChange(d.Year);
dlt := tzi.change_time_dlt.EncodeForTimeChange(d.Year);
if std<dlt then
if (std<=Value) and (Value<dlt) then
Bias := tzi.Bias+tzi.bias_std else
Bias := tzi.Bias+tzi.bias_dlt else
if (dlt<=Value) and (Value<std) then
Bias := tzi.Bias+tzi.bias_dlt else
Bias := tzi.Bias+tzi.bias_std;
end;
result := true;
end;
function TSynTimeZone.UtcToLocal(const UtcDateTime: TDateTime;
const TzId: TTimeZoneID): TDateTime;
var Bias: integer;
HaveDaylight: boolean;
begin
if (self=nil) or (TzId='') then
result := UtcDateTime else begin
GetBiasForDateTime(UtcDateTime,TzId,Bias,HaveDaylight);
result := ((UtcDateTime*MinsPerDay)-Bias)/MinsPerDay;
end;
end;
function TSynTimeZone.NowToLocal(const TzId: TTimeZoneID): TDateTime;
begin
result := UtcToLocal(NowUtc,TzId);
end;
function TSynTimeZone.LocalToUtc(const LocalDateTime: TDateTime; const TzID: TTimeZoneID): TDateTime;
var Bias: integer;
HaveDaylight: boolean;
begin
if (self=nil) or (TzId='') then
result := LocalDateTime else begin
GetBiasForDateTime(LocalDateTime,TzId,Bias,HaveDaylight);
result := ((LocalDateTime*MinsPerDay)+Bias)/MinsPerDay;
end;
end;
function TSynTimeZone.Ids: TStrings;
var i: integer;
begin
if fIDs=nil then begin
fIDs := TStringList.Create;
for i := 0 to length(fZone)-1 do
fIDs.Add(UTF8ToString(fZone[i].id));
end;
result := fIDs;
end;
function TSynTimeZone.Displays: TStrings;
var i: integer;
begin
if fDisplays=nil then begin
fDisplays := TStringList.Create;
for i := 0 to length(fZone)-1 do
fDisplays.Add(UTF8ToString(fZone[i].Display));
end;
result := fDisplays;
end;
{ ************ Markup (e.g. Emoji) process ************************** }
type
TTextWriterEscapeStyle = (tweBold,tweItalic,tweCode);
TTextWriterEscapeLineStyle = (
twlNone,twlParagraph,twlOrderedList,twlUnorderedList,twlBlockquote,twlCode4,twlCode3);
TTextWriterEscape = object
P,B,P2,B2: PUTF8Char;
W: TTextWriter;
st: set of TTextWriterEscapeStyle;
fmt: TTextWriterHTMLFormat;
esc: TTextWriterHTMLEscape;
lst: TTextWriterEscapeLineStyle;
procedure Start(dest: TTextWriter; src: PUTF8Char; escape: TTextWriterHTMLEscape);
function ProcessText(const stopchars: TSynByteSet): AnsiChar;
procedure ProcessHRef;
function ProcessLink: boolean;
procedure ProcessEmoji; {$ifdef HASINLINE}inline;{$endif}
procedure Toggle(style: TTextWriterEscapeStyle);
procedure SetLine(style: TTextWriterEscapeLineStyle);
procedure EndOfParagraph;
procedure NewMarkdownLine;
procedure AddHtmlEscapeWiki(dest: TTextWriter; src: PUTF8Char; escape: TTextWriterHTMLEscape);
procedure AddHtmlEscapeMarkdown(dest: TTextWriter; src: PUTF8Char; escape: TTextWriterHTMLEscape);
end;
procedure TTextWriterEscape.Start(dest: TTextWriter; src: PUTF8Char; escape: TTextWriterHTMLEscape);
begin
P := src;
W := dest;
st := [];
if heHtmlEscape in escape then
fmt := hfOutsideAttributes else
fmt := hfNone;
esc := escape;
lst := twlNone;
end;
function IsHttpOrHttps(P: PUTF8Char): boolean; {$ifdef HASINLINE}inline;{$endif}
begin
result := (PCardinal(P)^=ord('h')+ord('t')shl 8+ord('t') shl 16+ord('p')shl 24) and
((PCardinal(P+4)^ and $ffffff=ord(':')+ord('/')shl 8+ord('/') shl 16) or
(PCardinal(P+4)^=ord('s')+ord(':')shl 8+ord('/') shl 16+ord('/')shl 24));
end;
function TTextWriterEscape.ProcessText(const stopchars: TSynByteSet): AnsiChar;
begin
if P=nil then begin
result := #0;
exit;
end;
B := P;
while not(ord(P^) in stopchars) and not IsHttpOrHttps(P) do
inc(P);
W.AddHtmlEscape(B,P-B,fmt);
result := P^;
end;
procedure TTextWriterEscape.ProcessHRef;
begin
B := P;
while P^>' ' do inc(P);
W.AddShort('<a href="');
W.AddHtmlEscape(B,P-B,hfWithinAttributes);
W.AddShort('" rel="nofollow">');
W.AddHtmlEscape(B,P-B);
W.AddShort('</a>');
end;
function TTextWriterEscape.ProcessLink: boolean;
begin
inc(P);
B2 := P;
while not (P^ in [#0,']']) do inc(P);
P2 := P;
if PWord(P)^=ord(']')+ord('(')shl 8 then begin
inc(P,2);
B := P;
while not (P^ in [#0,')']) do inc(P);
if P^=')' then begin // [GitHub](https://github.com)
result := true;
exit;
end;
end;
P := B2; // rollback
result := false;
end;
procedure TTextWriterEscape.ProcessEmoji;
begin
if heEmojiToUTF8 in esc then
EmojiParseDots(P,W) else begin
W.Add(':');
inc(P);
end;
end;
procedure TTextWriterEscape.Toggle(style: TTextWriterEscapeStyle);
const HTML: array[tweBold..tweCode] of string[7] = ('strong>','em>','code>');
begin
W.Add('<');
if style in st then begin
W.Add('/');
exclude(st,style);
end else
include(st,style);
W.AddShort(HTML[style]);
end;
procedure TTextWriterEscape.EndOfParagraph;
begin
if tweBold in st then
Toggle(tweBold);
if tweItalic in st then
Toggle(tweItalic);
if P<>nil then
if PWord(P)^=$0a0d then
inc(P,2) else
inc(P);
end;
procedure TTextWriterEscape.SetLine(style: TTextWriterEscapeLineStyle);
const HTML: array[twlParagraph..twlCode3] of string[5] = ('p>','li>','li>','p>','code>','code>');
HTML2: array[twlOrderedList..twlCode3] of string[11] = ('ol>','ul>','blockquote>','pre>','pre>');
begin
if lst>=low(HTML) then begin
if (lst<twlCode4) or (lst<>style) then begin
W.Add('<','/');
W.AddShort(HTML[lst]);
end;
if (lst>=low(HTML2)) and (lst<>style) then begin
W.Add('<','/');
W.AddShort(HTML2[lst]);
end;
end;
if style>=low(HTML) then begin
if (style>=low(HTML2)) and (lst<>style) then begin
W.Add('<');
W.AddShort(HTML2[style]);
end;
if (style<twlCode4) or (lst<>style) then begin
W.Add('<');
W.AddShort(HTML[style]);
end;
end;
lst := style;
end;
procedure TTextWriterEscape.NewMarkdownLine;
label none;
var c: cardinal;
begin
if P=nil then
exit;
c := PCardinal(P)^;
if c and $ffffff=ord('`')+ord('`')shl 8+ord('`')shl 16 then begin
inc(P,3);
if lst=twlCode3 then begin
lst := twlCode4; // to close </code></pre>
NewMarkdownLine;
exit;
end;
SetLine(twlCode3);
end;
if lst=twlCode3 then
exit; // no prefix process within ``` code blocks
if c=$20202020 then begin
SetLine(twlCode4);
inc(P,4);
exit;
end;
P := GotoNextNotSpaceSameLine(P); // don't implement nested levels yet
case P^ of
'*','+','-':
if P[1]=' ' then
SetLine(twlUnorderedList) else
goto none;
'1'..'9': begin // first should be 1. then any ##. number to continue
B := P;
repeat inc(P) until not (P^ in ['0'..'9']);
if (P^='.') and ((lst=twlOrderedList) or (PWord(B)^=ord('1')+ord('.')shl 8)) then
SetLine(twlOrderedList) else begin
P := B;
none: if lst=twlParagraph then begin
c := PWord(P)^; // detect blank line to separate paragraphs
if c=$0a0d then
inc(P,2) else
if c and $ff=$0a then
inc(P) else begin
W.AddOnce(' ');
exit;
end;
end;
SetLine(twlParagraph);
exit;
end;
end;
'>':
if P[1]=' ' then
SetLine(twlBlockquote) else
goto none;
else
goto none;
end;
P := GotoNextNotSpaceSameLine(P+1);
end;
procedure TTextWriterEscape.AddHtmlEscapeWiki(dest: TTextWriter; src: PUTF8Char;
escape: TTextWriterHTMLEscape);
begin
Start(dest,src,escape);
SetLine(twlParagraph);
repeat
case ProcessText([0,10,13,ord('*'),ord('+'),ord('`'),ord('\'),ord(':')]) of
#0: break;
#10,#13: begin
EndOfParagraph;
SetLine(twlParagraph);
continue;
end;
'\': if P[1] in ['\','`','*','+'] then begin
inc(P);
W.Add(P^);
end else
W.Add('\');
'*':
Toggle(tweItalic);
'+':
Toggle(tweBold);
'`':
Toggle(tweCode);
'h': begin
ProcessHRef;
continue;
end;
':': begin
ProcessEmoji;
continue;
end;
end;
inc(P);
until false;
EndOfParagraph;
SetLine(twlNone);
end;
procedure TTextWriterEscape.AddHtmlEscapeMarkdown(dest: TTextWriter;
src: PUTF8Char; escape: TTextWriterHTMLEscape);
begin
Start(dest,src,escape);
NewMarkDownLine;
repeat
if lst>=twlCode4 then // no Markdown tags within code blocks
if ProcessText([0,10,13])=#0 then
break else begin
if PWord(P)^=$0a0d then
inc(P,2) else
inc(P);
W.AddCR; // keep LF within <pre>
NewMarkdownLine;
continue;
end else
case ProcessText([0,10,13,ord('*'),ord('_'),ord('`'),ord('\'),ord('['),ord('!'),ord(':')]) of
#0: break;
#10,#13: begin
EndOfParagraph;
NewMarkdownLine;
continue;
end;
'\': if P[1] in ['\','`','*','_','[',']','{','}','(',')','#','+','-','.','!'] then begin
inc(P);
W.Add(P^); // backslash escape
end else
W.Add('\');
'*','_':
if P[1]=P[0] then begin
inc(P); // **This text will be bold** or __This text will be bold__
Toggle(tweBold);
end else // *This text will be italic* or _This text will be italic_
Toggle(tweItalic);
'`':
Toggle(tweCode); // `This text will be code`
'[':
if ProcessLink then begin // [GitHub](https://github.com)
W.AddShort('<a href="');
W.AddHtmlEscape(B,P-B,hfWithinAttributes);
if IsHttpOrHttps(B) then
W.AddShort('" rel="nofollow">') else
W.Add('"','>');
W.AddHtmlEscape(B2,P2-B2,fmt);
W.AddShort('</a>'); // no continune -> need inc(P) over ending )
end else
W.Add('['); // not a true link -> just append
'!': begin
if P[1]='[' then begin
inc(P);
if ProcessLink then begin
W.AddShort('<img alt="');
W.AddHtmlEscape(B2,P2-B2,hfWithinAttributes);
W.AddShort('" src="');
W.AddNoJSONEscape(B,P-B);
W.AddShort('">');
inc(P);
continue;
end;
dec(P);
end;
W.Add('!'); // not a true image
end;
'h': begin
ProcessHRef;
continue;
end;
':': begin
ProcessEmoji;
continue;
end;
end;
inc(P);
until false;
EndOfParagraph;
SetLine(twlNone);
end;
function HtmlEscapeWiki(const wiki: RawUTF8; esc: TTextWriterHTMLEscape): RawUTF8;
var temp: TTextWriterStackBuffer;
W: TTextWriter;
begin
W := TTextWriter.CreateOwnedStream(temp);
try
AddHtmlEscapeWiki(W,pointer(wiki),esc);
W.SetText(result);
finally
W.Free;
end;
end;
function HtmlEscapeMarkdown(const md: RawUTF8; esc: TTextWriterHTMLEscape): RawUTF8;
var temp: TTextWriterStackBuffer;
W: TTextWriter;
begin
W := TTextWriter.CreateOwnedStream(temp);
try
AddHtmlEscapeMarkdown(W,pointer(md),esc);
W.SetText(result);
finally
W.Free;
end;
end;
procedure AddHtmlEscapeWiki(W: TTextWriter; P: PUTF8Char; esc: TTextWriterHTMLEscape);
var doesc: TTextWriterEscape;
begin
doesc.AddHtmlEscapeWiki(W,P,esc);
end;
procedure AddHtmlEscapeMarkdown(W: TTextWriter; P: PUTF8Char; esc: TTextWriterHTMLEscape);
var doesc: TTextWriterEscape;
begin
doesc.AddHtmlEscapeMarkdown(W,P,esc);
end;
function EmojiFromText(P: PUTF8Char; len: PtrInt): TEmoji;
begin // RTTI has shortstrings in adjacent L1 cache lines -> faster than EMOJI_TEXT[]
result := TEmoji(FindShortStringListTrimLowerCase(EMOJI_RTTI,ord(high(TEmoji))-1,P,len)+1);
end;
function EmojiParseDots(var P: PUTF8Char; W: TTextWriter): TEmoji;
var c: PUTF8Char;
begin
result := eNone;
inc(P); // ignore trailing ':'
c := P;
if c[-2]<=' ' then begin
if (c[1]<=' ') and (c^ in ['('..'|']) then
result := EMOJI_AFTERDOTS[c^]; // e.g. :)
if result=eNone then begin
while c^ in ['a'..'z','A'..'Z','_'] do
inc(c);
if (c^=':') and (c[1]<=' ') then // try e.g. :joy_cat:
result := EmojiFromText(P,c-P);
end;
if result<>eNone then begin
P := c+1; // continue parsing after the Emoji text
if W<>nil then
W.AddNoJSONEscape(pointer(EMOJI_UTF8[result]),4);
exit;
end;
end;
if W<>nil then
W.Add(':');
end;
procedure EmojiToDots(P: PUTF8Char; W: TTextWriter);
var B: PUTF8Char;
c: cardinal;
begin
if (P<>nil) and (W<>nil) then
repeat
B := P;
while (P^<>#0) and (PWord(P)^<>$9ff0) do
inc(P);
W.AddNoJSONEscape(B,P-B);
if P^=#0 then
break;
B := P;
c := NextUTF8UCS4(P)-$1f5ff;
if c<=cardinal(high(TEmoji)) then
W.AddNoJSONEscapeUTF8(EMOJI_TAG[TEmoji(c)]) else
W.AddNoJSONEscape(B,P-B);
until P^=#0;
end;
function EmojiToDots(const text: RawUTF8): RawUTF8;
var W: TTextWriter;
tmp: TTextWriterStackBuffer;
begin
if PosExChar(#$f0,text)=0 then begin
2021-04-23 10:11:05 +02:00
result := text; // no UTF-8 smiley for sure
exit;
end;
W := TTextWriter.CreateOwnedStream(tmp);
try
EmojiToDots(pointer(text),W);
W.SetText(result);
finally
W.Free;
end;
end;
procedure EmojiFromDots(P: PUTF8Char; W: TTextWriter);
var B: PUTF8Char;
begin
if (P<>nil) and (W<>nil) then
repeat
B := P;
while not(P^ in [#0,':']) do
inc(P);
W.AddNoJSONEscape(B,P-B);
if P^=#0 then
break;
EmojiParseDots(P,W);
until P^=#0;
end;
function EmojiFromDots(const text: RawUTF8): RawUTF8;
var W: TTextWriter;
tmp: TTextWriterStackBuffer;
begin
W := TTextWriter.CreateOwnedStream(tmp);
try
EmojiFromDots(pointer(text),W);
W.SetText(result);
finally
W.Free;
end;
end;
{ ************ Command Line and Console process ************************** }
var
TextAttr: integer = ord(ccDarkGray);
{$I-}
{$ifdef MSWINDOWS}
procedure InitConsole;
begin
StdOut := GetStdHandle(STD_OUTPUT_HANDLE);
if StdOut=INVALID_HANDLE_VALUE then
StdOut := 0;
end;
procedure TextColor(Color: TConsoleColor);
var oldAttr: integer;
begin
if StdOut=0 then
InitConsole;
oldAttr := TextAttr;
TextAttr := (TextAttr and $F0) or ord(Color);
if TextAttr<>oldAttr then
SetConsoleTextAttribute(StdOut,TextAttr);
end;
procedure TextBackground(Color: TConsoleColor);
var oldAttr: integer;
begin
if StdOut=0 then
InitConsole;
oldAttr := TextAttr;
TextAttr := (TextAttr and $0F) or (ord(Color) shl 4);
if TextAttr<>oldAttr then
SetConsoleTextAttribute(StdOut,TextAttr);
end;
function ConsoleKeyPressed(ExpectedKey: Word): Boolean;
var lpNumberOfEvents: DWORD;
lpBuffer: TInputRecord;
lpNumberOfEventsRead : DWORD;
nStdHandle: THandle;
begin
result := false;
nStdHandle := GetStdHandle(STD_INPUT_HANDLE);
lpNumberOfEvents := 0;
GetNumberOfConsoleInputEvents(nStdHandle,lpNumberOfEvents);
if lpNumberOfEvents<>0 then begin
PeekConsoleInput(nStdHandle,lpBuffer,1,lpNumberOfEventsRead);
if lpNumberOfEventsRead<>0 then
if lpBuffer.EventType=KEY_EVENT then
if lpBuffer.Event.KeyEvent.bKeyDown and
((ExpectedKey=0) or (lpBuffer.Event.KeyEvent.wVirtualKeyCode=ExpectedKey)) then
result := true else
FlushConsoleInputBuffer(nStdHandle) else
FlushConsoleInputBuffer(nStdHandle);
end;
end;
procedure ConsoleWaitForEnterKey;
{$ifdef DELPHI5OROLDER}
begin
readln;
end;
{$else}
var msg: TMsg;
begin
while not ConsoleKeyPressed(VK_RETURN) do begin
{$ifndef LVCL}
if GetCurrentThreadID=MainThreadID then
CheckSynchronize{$ifdef WITHUXTHEME}(1000){$endif} else
{$endif}
WaitMessage;
while PeekMessage(msg,0,0,0,PM_REMOVE) do
if Msg.Message=WM_QUIT then
exit else begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end;
end;
{$endif DELPHI5OROLDER}
function Utf8ToConsole(const S: RawUTF8): RawByteString;
begin
result := TSynAnsiConvert.Engine(CP_OEMCP).UTF8ToAnsi(S);
end;
{$else MSWINDOWS}
// we bypass crt.pp since this unit cancels the SIGINT signal
procedure TextColor(Color: TConsoleColor);
const AnsiTbl : string[8]='04261537';
begin
{$ifdef FPC}{$ifdef Linux}
if not stdoutIsTTY then
exit;
{$endif}{$endif}
if ord(color)=TextAttr then
exit;
TextAttr := ord(color);
if ord(color)>=8 then
write(#27'[1;3',AnsiTbl[(ord(color) and 7)+1],'m') else
write(#27'[0;3',AnsiTbl[(ord(color) and 7)+1],'m');
ioresult;
end;
procedure TextBackground(Color: TConsoleColor);
begin // not implemented yet - but not needed either
end;
procedure ConsoleWaitForEnterKey;
var c: AnsiChar;
begin
{$ifdef FPC}
if IsMultiThread and (GetCurrentThreadID=MainThreadID) then
repeat
CheckSynchronize(100);
if UnixKeyPending then
repeat
c := #0;
if FpRead(StdInputHandle,c,1)<>1 then
break;
if c in [#10,#13] then
exit;
until false;
until false else
{$endif FPC}
ReadLn;
end;
function Utf8ToConsole(const S: RawUTF8): RawByteString;
begin
result := S; // expect a UTF-8 console under Linux/BSD
end;
{$endif MSWINDOWS}
function ConsoleReadBody: RawByteString;
var len, n: integer;
P: PByte;
{$ifndef FPC}StdInputHandle: THandle;{$endif}
begin
result := '';
{$ifdef MSWINDOWS}
{$ifndef FPC}StdInputHandle := GetStdHandle(STD_INPUT_HANDLE);{$endif}
if not PeekNamedPipe(StdInputHandle,nil,0,nil,@len,nil) then
{$else}
if fpioctl(StdInputHandle,FIONREAD,@len)<0 then
{$endif}
len := 0;
SetLength(result,len);
P := pointer(result);
while len>0 do begin
n := FileRead(StdInputHandle,P^,len);
if n<=0 then begin
result := ''; // read error
break;
end;
dec(len,n);
inc(P,n);
end;
end;
function StringToConsole(const S: string): RawByteString;
begin
result := Utf8ToConsole(StringToUTF8(S));
end;
procedure ConsoleWrite(const Text: RawUTF8; Color: TConsoleColor; NoLineFeed, NoColor: boolean);
begin
if not NoColor then
TextColor(Color);
write(Utf8ToConsole(Text));
if not NoLineFeed then
writeln;
ioresult;
end;
procedure ConsoleWrite(const Fmt: RawUTF8; const Args: array of const;
Color: TConsoleColor; NoLineFeed: boolean);
var tmp: RawUTF8;
begin
FormatUTF8(Fmt,Args,tmp);
ConsoleWrite(tmp,Color,NoLineFeed);
end;
procedure ConsoleShowFatalException(E: Exception; WaitForEnterKey: boolean);
begin
ConsoleWrite(#13#10'Fatal exception ',cclightRed,true);
ConsoleWrite('%',[E.ClassName],ccWhite,true);
ConsoleWrite(' raised with message ',ccLightRed,true);
ConsoleWrite('%',[E.Message],ccLightMagenta);
TextColor(ccLightGray);
if WaitForEnterKey then begin
writeln(#13#10'Program will now abort');
{$ifndef LINUX}
writeln('Press [Enter] to quit');
if ioresult=0 then
Readln;
{$endif}
end;
ioresult;
end;
{$I+}
{$ifndef NOVARIANTS}
{ TCommandLine }
constructor TCommandLine.Create;
var i: integer;
p, sw: RawUTF8;
begin
inherited Create;
fValues.InitFast(ParamCount shr 1,dvObject);
for i := 1 to ParamCount do begin
p := StringToUTF8(ParamStr(i));
if p<>'' then
if p[1] in ['-','/'] then begin
if sw<>'' then
fValues.AddValue(sw,true); // -flag -switch value -> flag=true
sw := LowerCase(copy(p,2,100));
if sw='noprompt' then begin
fNoPrompt := true;
sw := '';
end;
end else
if sw<>'' then begin
fValues.AddValueFromText(sw,p,true);
sw := '';
end;
end;
if sw<>'' then
fValues.AddValue(sw,true); // trailing -flag
end;
constructor TCommandLine.Create(const switches: variant; aNoConsole: boolean);
begin
inherited Create;
fValues.InitCopy(switches,JSON_OPTIONS_FAST);
fNoPrompt := true;
fNoConsole := aNoConsole;
end;
constructor TCommandLine.Create(const NameValuePairs: array of const; aNoConsole: boolean);
begin
inherited Create;
fValues.InitObject(NameValuePairs,JSON_OPTIONS_FAST);
fNoPrompt := true;
fNoConsole := aNoConsole;
end;
constructor TCommandLine.CreateAsArray(firstParam: integer);
var i: integer;
begin
inherited Create;
fValues.InitFast(ParamCount,dvArray);
for i := firstParam to ParamCount do
fValues.AddItem(ParamStr(i));
end;
function TCommandLine.NoPrompt: boolean;
begin
result := fNoPrompt;
end;
function TCommandLine.ConsoleText(const LineFeed: RawUTF8): RawUTF8;
begin
result := RawUTF8ArrayToCSV(fLines,LineFeed);
end;
procedure TCommandLine.SetNoConsole(value: boolean);
begin
if value=fNoConsole then
exit;
if value then
fNoPrompt := true;
fNoConsole := false;
end;
procedure TCommandLine.TextColor(Color: TConsoleColor);
begin
if not fNoPrompt then
SynTable.TextColor(Color);
end;
procedure TCommandLine.Text(const Fmt: RawUTF8; const Args: array of const;
Color: TConsoleColor);
var msg: RawUTF8;
begin
FormatUTF8(Fmt,Args,msg);
{$I-}
if msg<>'' then begin
TextColor(Color);
AddRawUTF8(fLines,msg);
if not fNoConsole then
write(Utf8ToConsole(msg));
end;
if not fNoConsole then begin
writeln;
ioresult;
end;
{$I+}
end;
function TCommandLine.AsUTF8(const Switch, Default: RawUTF8;
const Prompt: string): RawUTF8;
var i: integer;
begin
i := fValues.GetValueIndex(Switch);
if i>=0 then begin // found
VariantToUTF8(fValues.Values[i],result);
fValues.Delete(i);
exit;
end;
result := Default;
if fNoPrompt or (Prompt='') then
exit;
TextColor(ccLightGray);
{$I-}
writeln(Prompt);
if ioresult<>0 then
exit; // no console -> no prompt
TextColor(ccCyan);
write(Switch);
if Default<>'' then
write(' [',Default,'] ');
write(': ');
TextColor(ccWhite);
readln(result);
writeln;
ioresult;
{$I+}
TextColor(ccLightGray);
result := trim(result);
if result='' then
result := Default;
end;
function TCommandLine.AsInt(const Switch: RawUTF8; Default: Int64;
const Prompt: string): Int64;
var res: RawUTF8;
begin
res := AsUTF8(Switch, Int64ToUtf8(Default), Prompt);
result := GetInt64Def(pointer(res),Default);
end;
function TCommandLine.AsDate(const Switch: RawUTF8; Default: TDateTime;
const Prompt: string): TDateTime;
var res: RawUTF8;
begin
res := AsUTF8(Switch, DateTimeToIso8601Text(Default), Prompt);
if res='0' then begin
result := 0;
exit;
end;
result := Iso8601ToDateTime(res);
if result=0 then
result := Default;
end;
function TCommandLine.AsEnum(const Switch, Default: RawUTF8; TypeInfo: pointer;
const Prompt: string): integer;
var res: RawUTF8;
begin
res := AsUTF8(Switch, Default, Prompt);
if not ToInteger(res,result) then
result := GetEnumNameValue(TypeInfo,pointer(res),length(res),true);
end;
function TCommandLine.AsArray: TRawUTF8DynArray;
begin
fValues.ToRawUTF8DynArray(result);
end;
function TCommandLine.AsJSON(Format: TTextWriterJSONFormat): RawUTF8;
begin
result := fValues.ToJSON('','',Format);
end;
function TCommandLine.AsString(const Switch: RawUTF8; const Default, Prompt: string): string;
begin
result := UTF8ToString(AsUTF8(Switch,StringToUTF8(Default),Prompt));
end;
{$endif NOVARIANTS}
procedure InitInternalTables;
var e: TEmoji;
begin
{$ifdef MSWINDOWS}
InitWindowsAPI;
{$else}
stdoutIsTTY := IsATTY(StdOutputHandle)=1;
{$endif MSWINDOWS}
SetLength(JSON_SQLDATE_MAGIC_TEXT,3);
PCardinal(pointer(JSON_SQLDATE_MAGIC_TEXT))^ := JSON_SQLDATE_MAGIC;
Assert(ord(high(TEmoji))=$4f+1);
EMOJI_RTTI := GetEnumName(TypeInfo(TEmoji),1); // ignore eNone=0
GetEnumTrimmedNames(TypeInfo(TEmoji),@EMOJI_TEXT);
EMOJI_TEXT[eNone] := '';
for e := succ(low(e)) to high(e) do begin
LowerCaseSelf(EMOJI_TEXT[e]);
EMOJI_TAG[e] := ':'+EMOJI_TEXT[e]+':';
SetLength(EMOJI_UTF8[e],4);
UCS4ToUTF8(ord(e)+$1f5ff,pointer(EMOJI_UTF8[e]));
end;
EMOJI_AFTERDOTS[')'] := eSmiley;
EMOJI_AFTERDOTS['('] := eFrowning;
EMOJI_AFTERDOTS['|'] := eExpressionless;
EMOJI_AFTERDOTS['/'] := eConfused;
EMOJI_AFTERDOTS['D'] := eLaughing;
EMOJI_AFTERDOTS['o'] := eOpen_mouth;
EMOJI_AFTERDOTS['O'] := eOpen_mouth;
EMOJI_AFTERDOTS['p'] := eYum;
EMOJI_AFTERDOTS['P'] := eYum;
EMOJI_AFTERDOTS['s'] := eScream;
EMOJI_AFTERDOTS['S'] := eScream;
end;
initialization
Assert(SizeOf(TSynTableFieldType)=1); // as expected by TSynTableFieldProperties
Assert(SizeOf(TSynTableFieldOptions)=1);
{$ifndef NOVARIANTS}
Assert(SizeOf(TSynTableData)=SizeOf(TVarData));
{$endif NOVARIANTS}
Assert(SizeOf(THTab)=$40000*3); // 786,432 bytes
Assert(SizeOf(TSynUniqueIdentifierBits)=SizeOf(TSynUniqueIdentifier));
InitInternalTables;
TTextWriter.RegisterCustomJSONSerializerFromText([
TypeInfo(TDiskPartitions),
'name:RawUTF8 mounted:string size:QWord',
TypeInfo(TSystemUseDataDynArray),
'Timestamp:TDateTime Kernel,User:single WorkDB,VirtualKB:cardinal']);
end.