{ ***************************************************************************** This file is part of LazUtils. See the file COPYING.modifiedLGPL.txt, included in this distribution, for details about the license. ***************************************************************************** Implement TMask and related classes. It is fast and optimized, and fully supports Unicode. Also supports DOS/Windows compatible mask which behaves differently from standard mask. Author: José Mejuto Changes and improvements by Juha Manninen and Bart Broersma } unit Masks; {$T-} // TODO: Fix compilation with -Sy. {$mode objfpc}{$H+} {.$define debug_maskcompiled} {.$define debug_anycharornone} interface uses Classes, SysUtils, Contnrs, // LazUtils LazUtilsStrConsts, LazUTF8; type { EMaskError } EMaskError=class(EConvertError) public type TMaskExceptionCode=(mecInternalError, mecInvalidCharMask, mecMissingClose, mecIncompleteMask, mecInvalidEscapeChar, mecInvalidUTF8Sequence ); protected FCode: TMaskExceptionCode; public constructor Create(const msg: string; aCode: TMaskExceptionCode); constructor CreateFmt(const msg: string; args: array of const; aCode: TMaskExceptionCode); property Code: TMaskExceptionCode read FCode; end; TMaskOpCode=(mocAnyChar, //treat ? as a wildcard to match exactly one char mocAnyCharOrNone, //treat [?] to match any char or the absence of a char mocAnyText, //treat * as a wildcard to mach zero or any mumber of chars mocRange, //treat [a-c] to match either 'a', 'b' or 'c'. '-' is always treated as a range indicator. //to have a literal '-' in a range, you must escape it with EscapeChar (defaults to '\'): [+-\-] matches '+', ',', or '-'. mocSet, //treat [a-c] to match either 'a', '-' or 'c' mocNegateGroup, //treat [!a-c] to not match 'a', 'b', or 'c', but match any other char. Requires mocRange and/or mocSet mocEscapeChar //treat EscapeChar (defaults to '\') to take the next char as a literal, so '\*' is treated as a literal '*'. ); TMaskOpCodes=set of TMaskOpCode; const AllMaskOpCodes=[mocAnyChar, mocAnyCharOrNone, mocAnyText, mocRange, mocSet, mocNegateGroup, mocEscapeChar]; // Match [x] literally, not as a range. // Leave out mocAnyCharOrNone, mocRange and mocSet. MaskOpCodesDisableRange=[mocAnyChar, mocAnyText, mocNegateGroup, mocEscapeChar]; // Interpret [?] as literal question mark instead of 0..1 chars wildcard. // Disable backslash escaping characters like "\?". // Leave out mocAnyCharOrNone and mocEscapeChar MaskOpCodesNoEscape=[mocAnyChar, mocAnyText, mocRange, mocSet, mocNegateGroup]; DefaultMaskOpCodes=MaskOpCodesNoEscape; (* Windows mask works in a different mode than regular mask, it has too many quirks and corner cases inherited from CP/M, then adapted to DOS (8.3) file names and adapted again for long file names. Anyth?ng.abc = "?" matches exactly 1 char Anyth*ng.abc = "*" matches 0 or more of chars ------- Quirks ------- *) type TWindowsQuirk=(wqAnyExtension, // Anything*.* : ".*" is removed. Also makes "foo.*" match "foo" wqFilenameEnd, // Anything??.abc : "?" matches 1 or 0 chars (except '.') // (Not the same as "Anything*.abc", but the same // as regex "Anything.{0,2}\.abc") // Internally converted to "Anything[??].abc" wqExtension3More, // Anything.abc : Matches "Anything.abc" but also "Anything.abc*" (3 char extension) // Anything.ab : Matches "Anything.ab" and never "anything.abcd" // *.pas : Matches "Unit1.pas.bak". Not good. wqEmptyIsAny, // "" : Empty string matches anything "*" wqAllByExtension, // .abc : Runs as "*.abc" (Not in use anymore) wqNoExtension); // Anything*. : Matches "Anything*" without extension TWindowsQuirks=set of TWindowsQuirk; const AllWindowsQuirks=[wqAnyExtension, wqFilenameEnd, wqExtension3More, wqEmptyIsAny, wqAllByExtension, wqNoExtension]; // Leave out wqExtension3More and wqAllByExtension DefaultWindowsQuirks=[wqAnyExtension, wqEmptyIsAny, wqNoExtension]; type // Backwards compatible options. TMaskOption = (moCaseSensitive, moDisableSets); TMaskOptions = set of TMaskOption; { TMaskBase } TMaskBase = class private procedure SetAutoReverseRange(AValue: Boolean); procedure SetCaseSensitive(AValue: Boolean); procedure SetMaskEscapeChar(AValue: Char); procedure SetMaskOpCodesAllowed(AValue: TMaskOpCodes); protected // Literal = It must match // Range = Match any char in the range // Negate = Negate match in a group // AnyChar = It matches any char, but one must match // AnyCharOrNone = Matches one or none char (only in a group) // AnyCharToNext = Matches any chars amount, if fail, restart in the // next position up to finish the mask or the matched string // OptionalChar = Optional char // CharsGroupBegin = Begin optional chars or ranges "[" // CharsGroupEnd = End optional chars or ranges "]" type TMaskParsedCode = ( Literal=0, Range=1, Negate=2, AnyChar=3, AnyCharOrNone=4, AnyCharToNext=5, OptionalChar=6, CharsGroupBegin=10, CharsGroupEnd=11 ); TMaskFailCause = ( mfcSuccess, mfcMatchStringExhausted, mfcMaskExhausted, mfcMaskNotMatch, mfcUnexpectedEnd ); const GROW_BY=100; DefaultSpecialChars=['*', '?', '[']; procedure Add(aLength: integer; aData: PBYTE); procedure Add(aValue: integer); procedure Add(aValue: TMaskParsedCode); procedure IncrementLastCounterBy(aOpcode: TMaskParsedCode; aValue: integer); protected fCaseSensitive: Boolean; fAutoReverseRange: Boolean; // If enabled, range [z-a] is interpreted as [a-z] fMaskIsCompiled: Boolean; fMaskCompiled: TBytes; fMaskCompiledIndex: integer; fMaskCompiledAllocated: integer; fMaskCompiledLimit: integer; fMaskLimit: integer; fMatchStringLimit: integer; fMatchMinimumLiteralBytes: SizeInt; fMatchMaximumLiteralBytes: SizeInt; fMaskOpcodesAllowed: TMaskOpCodes; // EscapeChar forces next char to be a literal one, not a wildcard. fMaskEscapeChar: Char; procedure PrepareCompile; class procedure Exception_InvalidCharMask(const aMaskChar: string; aOffset: integer=-1); static; class procedure Exception_MissingCloseChar(const aMaskChar: string; aOffset: integer=-1); static; class procedure Exception_IncompleteMask(); static; class procedure Exception_InvalidEscapeChar(); static; procedure Exception_InternalError(); //function intfMatches(aMatchOffset: integer; aMaskIndex: integer): TMaskFailCause; virtual; abstract; public constructor Create(aCaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes); constructor Create(aOptions: TMaskOptions); public property CaseSensitive: Boolean read fCaseSensitive write SetCaseSensitive; property AutoReverseRange: Boolean read fAutoReverseRange write SetAutoReverseRange; property EscapeChar: Char read fMaskEscapeChar write SetMaskEscapeChar; //Must be lower ASCII (#0-#127) property MaskOpCodes: TMaskOpCodes read fMaskOpcodesAllowed write SetMaskOpCodesAllowed; end; { TMaskUTF8 } TMaskUTF8 = class (TMaskBase) private fMatchString: String; // Used by Compile. fMaskInd: Integer; fCPLength: integer; // Size of codepoint. fLastOC: TMaskParsedCode; // Last OpCode. fMask: String; procedure AddAnyChar; procedure AddLiteral; procedure AddRange(lFirstRange, lSecondRange: Integer); procedure AddRangeReverse(lFirstRange, lSecondRange: Integer); procedure CompileRange; procedure CompileEscapeCharPlusLiteral; procedure CompileSpecialChars; procedure CompileAnyCharOrNone(QChar: Char; BracketsRequired: Boolean); function GetMask: String; virtual; procedure SetMask(AValue: String); virtual; protected fOriginalMask: String; function IsSpecialChar({%H-}AChar: Char): Boolean; virtual; procedure CompileOtherSpecialChars; virtual; class function CompareUTF8Sequences(const P1,P2: PChar): integer; static; function intfMatches(aMatchOffset: integer; aMaskIndex: integer): TMaskFailCause; //override; public {$IFDEF debug_maskcompiled} procedure DumpMaskCompiled; {$ENDIF} constructor Create(const aMask: String); overload; constructor Create(const aMask: String; aCaseSensitive: Boolean); overload; constructor Create(const aMask: String; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); virtual; overload; constructor Create(const aMask: String; aOptions: TMaskOptions); overload; deprecated 'Use Create with TMaskOpCodes params.'; // in Lazarus 2.3, remove in 2.5. procedure Compile; virtual; function Matches(const aStringToMatch: String): Boolean; virtual; function MatchesWindowsMask(const AFileName: String): Boolean; deprecated 'Create with TMaskWindows.Create, then call Matches.'; // in Lazarus 2.3, remove in 2.5. public property Mask: String read GetMask write SetMask; end; TMask = class(TMaskUTF8); { TWindowsMaskUTF8 } TWindowsMaskUTF8=class(TMask) private procedure SetMask(AValue: String); override; function GetMask: String; override; procedure SetWindowsQuirkAllowed(AValue: TWindowsQuirks); protected fWindowsQuirkAllowed: TWindowsQuirks; fWindowsQuirkInUse: TWindowsQuirks; fWindowsMask: String; procedure CompileOtherSpecialChars; override; function IsSpecialChar(AChar: Char): Boolean; override; class procedure SplitFileNameExtension(const aSourceFileName: String; out aFileName: String; out aExtension: String; aIsMask: Boolean=False); static; public constructor Create(const aMask: String; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); override; constructor Create(const aMask: String; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks); procedure Compile; override; function Matches(const aFileName: String): Boolean; override; public property Quirks: TWindowsQuirks read fWindowsQuirkAllowed write SetWindowsQuirkAllowed; end; TWindowsMask = class(TWindowsMaskUTF8); TMaskClass = class of TMaskUtf8; { TParseStringList } TParseStringList = class(TStringList) public constructor Create(const AText, ASeparators: String); end; { TMaskList } TMaskList = class private fAutoReverseRange: Boolean; fMasks: TObjectList; FMaskClass: TMaskClass; fMask: String; fSeparator: Char; fCaseSensitive: Boolean; fMaskOpcodes: TMaskOpcodes; function GetCount: Integer; function GetItem(Index: Integer): TMask; procedure SetAutoReverseRange(AValue: Boolean); procedure SetCaseSensitive(AValue: Boolean); procedure SetMask(AValue: String); virtual; procedure SetMaskOpCodes(AValue: TMaskOpCodes); protected function GetMaskClass: TMaskClass; virtual; procedure AddMasksToList(const aValue: String; aSeparator: Char; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); virtual; public constructor Create(const aValue: String; aSeparator: Char=';'; CaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes); virtual; //Remove in 2.5 constructor Create(const aValue: String; aSeparator: Char; aOptions: TMaskOptions); virtual; deprecated 'Use Create with TMaskOpcodes paramater'; destructor Destroy; override; function Matches(const AFileName: String): Boolean; // Deprecated in Lazarus 2.3, October 2021. Remove in 2.5. function MatchesWindowsMask(const AFileName: String): Boolean; deprecated 'Use a TWindowsMaskList instead.'; property Count: Integer read GetCount; property Items[Index: Integer]: TMask read GetItem; property Mask: String read fMask write SetMask; property MaskOpCodes: TMaskOpCodes read fMaskOpCodes write SetMaskOpCodes; property AutoReverseRange: Boolean read fAutoReverseRange write SetAutoReverseRange; property CaseSensitive: Boolean read fCaseSensitive write SetCaseSensitive; end; { TWindowsMaskList } TWindowsMaskList = class(TMaskList) private fWindowsQuirks: TWindowsQuirks; procedure SetQuirks(AValue: TWindowsQuirks); protected function GetMaskClass: TMaskClass; override; procedure AddMasksToList(const aValue: String; aSeparator: Char; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); override; public constructor Create(const aValue: String; aSeparator: Char=';'; aCaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes); override; constructor Create(const aValue: String; aSeparator: Char{=';'}; aCaseSensitive: Boolean{=False}; aOpcodesAllowed: TMaskOpCodes{=DefaultMaskOpCodes}; aWindowsQuirksAllowed: TWindowsQuirks{=DefaultWindowsQuirks}); overload; //reintroduce; //Remove in 2.5 constructor Create(const aValue: String; aSeparator: Char; aOptions: TMaskOptions); override; deprecated 'Use Create with TMaskOpcodes paramater'; property Quirks: TWindowsQuirks read fWindowsQuirks write SetQuirks; end; TMaskListClass = class of TMaskList; function MatchesMask(const FileName, Mask: String; CaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes): Boolean; function MatchesMask(const FileName, Mask: String; Options: TMaskOptions): Boolean; deprecated 'Use MatchesMask with TMaskOpCodes params.'; // in Lazarus 2.3, remove in 2.5. function MatchesWindowsMask(const FileName, Mask: String; CaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks=DefaultWindowsQuirks): Boolean; function MatchesWindowsMask(const FileName, Mask: String; Options: TMaskOptions): Boolean; function MatchesMaskList(const FileName, Mask: String; Separator: Char=';'; CaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes): Boolean; function MatchesMaskList(const FileName, Mask: String; Separator: Char; Options: TMaskOptions): Boolean; deprecated 'Use MatchesMaskList with TMaskOpCodes params.'; // in Lazarus 2.3, remove in 2.5. function MatchesWindowsMaskList(const FileName, Mask: String; Separator: Char=';'; CaseSensitive: Boolean=False; aOpcodesAllowed: TMaskOpCodes=DefaultMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks=DefaultWindowsQuirks): Boolean; function MatchesWindowsMaskList(const FileName, Mask: String; Separator: Char; Options: TMaskOptions): Boolean; deprecated 'Use MatchesWindowsMaskList with TMaskOpCodes params.'; // in Lazarus 2.3, remove in 2.5. function DbgS(O: TMaskOpCodes): String ; overload; function DbgS(Q: TWindowsQuirks): String ; overload; implementation function DbgS(O: TMaskOpCodes): String ; var S: String; Op: TMaskOpcode; begin Result := '['; for Op in O do begin WriteStr(S, Op); Result := Result + S + ','; end; if (Result[Length(Result)] = ',') then System.Delete(Result, Length(Result), 1); Result := Result + ']'; end; function DbgS(Q: TWindowsQuirks): String ; var S: String; Quirk: TWindowsQuirk; begin Result := '['; for Quirk in Q do begin WriteStr(S, Quirk); Result := Result + S + ','; end; if (Result[Length(Result)] = ',') then System.Delete(Result, Length(Result), 1); Result := Result + ']'; end; function EncodeDisableRange(Options: TMaskOptions): TMaskOpCodes; // Encode the Disable Range option from legacy TMaskOptions. begin if moDisableSets in Options then Result:=MaskOpCodesDisableRange else // Disable '\' escaping for legacy code. Result:=MaskOpCodesNoEscape; //DefaultMaskOpCodes; end; function MatchesMask(const FileName, Mask: String; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes): Boolean; var AMask: TMask; begin AMask := TMask.Create(Mask, CaseSensitive, aOpcodesAllowed); try Result := AMask.Matches(FileName); finally AMask.Free; end; end; function MatchesMask(const FileName, Mask: String; Options: TMaskOptions): Boolean; begin Result := MatchesMask(FileName, Mask, moCaseSensitive in Options, EncodeDisableRange(Options)); end; function MatchesWindowsMask(const FileName, Mask: String; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks): Boolean; var AMask: TWindowsMask; begin AMask := TWindowsMask.Create(Mask, CaseSensitive, aOpcodesAllowed, aWindowsQuirksAllowed); try Result := AMask.Matches(FileName); finally AMask.Free; end; end; function MatchesWindowsMask(const FileName, Mask: String; Options: TMaskOptions): Boolean; begin Result := MatchesWindowsMask(FileName, Mask, moCaseSensitive in Options, EncodeDisableRange(Options)); end; function MatchesMaskList(const FileName, Mask: String; Separator: Char; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes): Boolean; var AMaskList: TMaskList; begin AMaskList := TMaskList.Create(Mask, Separator, CaseSensitive, aOpcodesAllowed); try Result := AMaskList.Matches(FileName); finally AMaskList.Free; end; end; function MatchesMaskList(const FileName, Mask: String; Separator: Char; Options: TMaskOptions): Boolean; begin Result := MatchesMaskList(FileName, Mask, Separator, moCaseSensitive in Options, EncodeDisableRange(Options)); end; function MatchesWindowsMaskList(const FileName, Mask: String; Separator: Char; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks): Boolean; var AMaskList: TWindowsMaskList; begin AMaskList := TWindowsMaskList.Create(Mask, Separator, CaseSensitive, aOpcodesAllowed, aWindowsQuirksAllowed); try Result := AMaskList.Matches(FileName); finally AMaskList.Free; end; end; function MatchesWindowsMaskList(const FileName, Mask: String; Separator: Char; Options: TMaskOptions): Boolean; begin Result := MatchesWindowsMaskList(FileName, Mask, Separator, moCaseSensitive in Options, EncodeDisableRange(Options)); end; { TWindowsMaskList } procedure TWindowsMaskList.SetQuirks(AValue: TWindowsQuirks); var i: Integer; begin if fWindowsQuirks = AValue then Exit; fWindowsQuirks := AValue; //if (wqFilenameEnd in fWindowsQuirks) then // Include(fMaskOpcodes, mocAnyCharOrNone); for i := 0 to fMasks.Count - 1 do begin TWindowsMask(fMasks.Items[i]).Quirks := FWindowsQuirks; TWindowsMask(fMasks.Items[i]).MaskOpCodes := fMaskOpcodes; //TWindowsMask(fMasks.Items[i]).Compile; //to apply Quirks end; //fMasks.Clear; //AddMasksToList(fMask, fSeparator, fCaseSensitive, fMaskOpCodes); end; function TWindowsMaskList.GetMaskClass: TMaskClass; begin Result := TWindowsMask; end; procedure TWindowsMaskList.AddMasksToList(const aValue: String; aSeparator: Char; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); var i: Integer; begin inherited AddMasksToList(aValue, aSeparator, aCaseSensitive, aOpcodesAllowed); if (FWindowsQuirks <> DefaultWindowsQuirks) then //inherited did not pass Quirks to the constructor begin for i := 0 to fMasks.Count - 1 do begin TWindowsMask(fMasks.Items[i]).fWindowsQuirkAllowed := FWindowsQuirks; //TWindowsMask(fMasks.Items[i]).Compile; //to apply Quirks end; end; end; constructor TWindowsMaskList.Create(const aValue: String; aSeparator: Char; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); begin Create(aValue, aSeparator, aCaseSensitive, aOpcodesAllowed, DefaultWindowsQuirks); end; constructor TWindowsMaskList.Create(const aValue: String; aSeparator: Char; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks); begin fWindowsQuirks := aWindowsQuirksAllowed; //if (wqFilenameEnd in aWindowsQuirksAllowed) then // Include(aOpcodesAllowed, mocAnyCharOrNone); inherited Create(aValue, aSeparator, aCaseSensitive, aOpcodesAllowed); end; constructor TWindowsMaskList.Create(const aValue: String; aSeparator: Char; aOptions: TMaskOptions); begin Create(aValue, aSeparator, (moCaseSensitive in aOptions), DefaultMaskOpCodes, DefaultWindowsQuirks); end; { EMaskError } constructor EMaskError.Create(const msg: string; aCode: TMaskExceptionCode); begin CreateFmt(msg,[],aCode); end; constructor EMaskError.CreateFmt(const msg: string; args: array of const; aCode: TMaskExceptionCode); begin FCode:=aCode; Inherited CreateFmt(msg,args); end; { TMaskBase } procedure TMaskBase.SetAutoReverseRange(AValue: Boolean); begin if fAutoReverseRange = AValue then Exit; fAutoReverseRange := AValue; fMaskIsCompiled := False; end; procedure TMaskBase.SetCaseSensitive(AValue: Boolean); begin if fCaseSensitive = AValue then Exit; fCaseSensitive := AValue; fMaskIsCompiled := False; end; procedure TMaskBase.SetMaskEscapeChar(AValue: Char); begin if fMaskEscapeChar=AValue then Exit; if fMaskEscapeChar>#127 then Exception_InvalidEscapeChar(); fMaskEscapeChar:=AValue; fMaskIsCompiled:=False; end; procedure TMaskBase.SetMaskOpCodesAllowed(AValue: TMaskOpCodes); begin if fMaskOpcodesAllowed = AValue then Exit; fMaskOpcodesAllowed := AValue; fMaskIsCompiled := False; end; procedure TMaskBase.Add(aLength: integer; aData: PBYTE); var lCounter: integer; begin if fMaskCompiledIndex+aLength>=fMaskCompiledAllocated then begin fMaskCompiledAllocated:=fMaskCompiledAllocated+aLength+GROW_BY; SetLength(fMaskCompiled,fMaskCompiledAllocated); end; for lCounter := 0 to Pred(aLength) do begin fMaskCompiled[fMaskCompiledIndex]:=(aData+lCounter)^; inc(fMaskCompiledIndex); end; end; procedure TMaskBase.Add(aValue: integer); begin Add(sizeof(aValue),@aValue); end; procedure TMaskBase.Add(aValue: TMaskParsedCode); var v: BYTE; begin //writeln('Add: ',aValue); v:=BYTE(aValue); Add(1,@v); end; procedure TMaskBase.IncrementLastCounterBy(aOpcode: TMaskParsedCode; aValue: integer); var p: PInteger; begin //writeln('TMaskBase.IncrementLastCounterBy: aOPcode=',aOpcode,', aValue=',aValue); fMaskCompiledIndex:=fMaskCompiledIndex-sizeof(aValue); if TMaskParsedCode(fMaskCompiled[fMaskCompiledIndex-1])<>aOpcode then Exception_InternalError(); P:=@fMaskCompiled[fMaskCompiledIndex]; Add(P^+aValue); end; procedure TMaskBase.PrepareCompile; begin fMaskCompiled := nil; fMaskCompiledAllocated := 0; fMaskCompiledIndex := 0; fMaskIsCompiled:=False; end; class procedure TMaskBase.Exception_InvalidCharMask(const aMaskChar: string; aOffset: integer); begin if aOffset>=0 then raise EMaskError.CreateFmt(rsInvalidCharMaskAt, [aMaskChar, aOffset], mecInvalidCharMask) else raise EMaskError.CreateFmt(rsInvalidCharMask, [aMaskChar], mecInvalidCharMask); end; class procedure TMaskBase.Exception_MissingCloseChar(const aMaskChar: string; aOffset: integer); begin if aOffset>=0 then raise EMaskError.CreateFmt(rsMissingCloseCharMaskAt, [aMaskChar, aOffset], mecMissingClose) else raise EMaskError.CreateFmt(rsMissingCloseCharMask, [aMaskChar], mecMissingClose); end; class procedure TMaskBase.Exception_IncompleteMask(); begin raise EMaskError.CreateFmt(rsIncompleteMask, [], mecIncompleteMask); end; class procedure TMaskBase.Exception_InvalidEscapeChar(); begin raise EMaskError.Create(rsInvalidEscapeChar, mecInvalidEscapeChar); end; procedure TMaskBase.Exception_InternalError(); begin raise EMaskError.CreateFmt(rsInternalError, [self.ClassName], mecInternalError); end; constructor TMaskBase.Create(aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); begin fCaseSensitive:=aCaseSensitive; fMaskOpcodesAllowed:=aOpcodesAllowed; fAutoReverseRange:=True; fMaskEscapeChar:='\'; end; constructor TMaskBase.Create(aOptions: TMaskOptions); begin Create(moCaseSensitive in aOptions, EncodeDisableRange(aOptions)); end; { TMask } procedure TMaskUTF8.AddAnyChar; begin Add(TMaskParsedCode.AnyChar); inc(fMatchMinimumLiteralBytes,1); if fMatchMaximumLiteralBytesfMaskLimit then exit; // ==> inc(aPosition,llCPL); aFirstSequence:=aPosition; end else begin aFirstSequence:=aPosition; end; llCPL:=UTF8CodepointSizeFast(@fMask[aPosition]); inc(aPosition,llCPL); if aPosition+llCPL>fMaskLimit then exit; // ==> // It is not a range, return false if fMask[aPosition]<>'-' then exit; llCPL:=UTF8CodepointSizeFast(@fMask[aPosition]); inc(aPosition,llCPL); if (mocEscapeChar in FMaskOpcodesAllowed) and (fMask[aPosition]=FMaskEscapeChar) then begin llCPL:=UTF8CodepointSizeFast(@fMask[aPosition]); if aPosition+llCPL>fMaskLimit then exit; // ==> inc(aPosition,llCPL); aSecondSequence:=aPosition; end else begin aSecondSequence:=aPosition; end; Result:=true; end; var lCharsGroupInsertSize, lFirstRange, lSecondRange: integer; begin //writeln('CompileRange: fMask[fMaskInd]=',fMask[fMaskInd]); if ([mocSet,mocRange,mocAnyCharOrNone]*fMaskOpCodesAllowed = [mocAnyCharOrNone]) or ((mocAnyCharOrNone in fMaskOpcodesAllowed) and (fMaskIndfMaskLimit then Exception_MissingCloseChar(']',fMaskLimit); end;//not AnyCharOrNone //writeln('CompileRange end: fMask[fMaskInd]=',fMask[fMaskInd]); end; function TMaskUTF8.GetMask: String; begin Result := fOriginalMask; end; procedure TMaskUtf8.CompileEscapeCharPlusLiteral; begin inc(fMaskInd,fCPLength); if fMaskInd<=fMaskLimit then begin fCPLength:=UTF8CodepointSizeFast(@fMask[fMaskInd]); AddLiteral; inc(fMaskInd,fCPLength); end else Exception_IncompleteMask(); end; procedure TMaskUtf8.CompileSpecialChars; begin case fMask[fMaskInd] of '*': begin if mocAnyText in fMaskOpcodesAllowed then begin if fLastOC<>TMaskParsedCode.AnyCharToNext then begin Add(TMaskParsedCode.AnyCharToNext); fLastOC:=TMaskParsedCode.AnyCharToNext; // * = No limit fMatchMaximumLiteralBytes:=High(fMatchMaximumLiteralBytes); end; end else AddLiteral; end; '?': begin if mocAnyChar in fMaskOpcodesAllowed then AddAnyChar else AddLiteral; end; '[': begin if ([mocSet,mocRange, mocAnyCharOrNone] * fMaskOpcodesAllowed <> []) //in this case the '[' always mus be the start of a Range, a Set or AnyCharOrNone then begin //it's just a bit easier later on if we handle this before we call CompileRange //if ([mocSet,mocRange]*fMaskOpCodesAllowed = []) {only mocAnyCharOrNone enabled} // and (fMaskInd'?') {next char is not '?'} then // Exception_InvalidCharMask(fMask[fMaskInd+1], fMaskInd+1); CompileRange; end else begin //mocSet,MocRange and mocAnyCharOrNone are all disabled { if (fMask[fMaskInd]=FMaskEscapeChar) and (mocEscapeChar in FMaskOpcodesAllowed) then begin //DEAD CODE? //This codepath could only be chosen if, at this point '[' is the escapechar //but, if that is the case, it should already have been handled in Compile //and CompileRange should not have been called. //Maybe I'm wrong, so I just comment this section out instead of //completely removing it // next is Literal inc(fMaskInd,fCPLength); if fMaskInd<=fMaskLimit then begin fCPLength:=UTF8CodepointSizeFast(@fMask[fMaskInd]); end else begin Exception_IncompleteMask(); end; end; //DEAD CODE?? } AddLiteral; end; end; otherwise begin CompileOtherSpecialChars; end; end; end; procedure TMaskUTF8.CompileAnyCharOrNone(QChar: Char; BracketsRequired: Boolean); var QCount, lCharsGroupInsertSize: Integer; begin //if any of the 2 conditions is true, this procedure should not have been called. {$IFDEF debug_anycharornone} writeln('CompileAnyCharOrNone: QChar=#',Ord(QChar),', BracketsRequired=',BracketsRequired,', fMask[fMaskInd]=',fMask[fMaskInd]); if (BracketsRequired and (fMask[fMaskInd]<>'[')) or ((not BracketsRequired) and (fMask[fMaskInd]<>QChar)) then Exception_InternalError(); {$ENDIF} if BracketsRequired then begin Inc(fMaskInd); //consume the '[' //the following happens when mocSet amd mocRange are disabled and the first char after the bracket is not a '?' if fMask[fMaskInd]<>QChar then Exception_InvalidCharMask(fMask[fMaskInd], fMaskInd); end; QCount:=1; while (fMaskInd+QCount<=fMaskLimit) and (fMask[fMaskInd+QCount]=QChar) do Inc(QCount); {$IFDEF debug_anycharornone} writeln('CompileAnyCharOrNone: Nr of AnyCharOrNone-tokens: ',QCount); if (fMaskInd+QCount>fMaskLimit) then writeln('(fMaskInd+QCount>fMaskLimit): ',fMaskInd+QCount,'>',fMaskLimit); {$ENDIF} //is Last found QChar also last character of the mask, while we expect a closing ']'? if BracketsRequired and (fMaskInd+QCount>fMaskLimit) then Exception_MissingCloseChar(']',fMaskInd+QCount-1); {$IFDEF debug_anycharornone} if not (fMask[fMaskInd+QCount]=']') then writeln('fMask[fMaskInd+QCount]: expected ], found: ',fMask[fMaskInd+QCount]); {$ENDIF} if BracketsRequired and not (fMask[fMaskInd+QCount]=']') then Exception_InvalidCharMask(fMask[fMaskInd+QCount],fMaskInd+QCount); Add(TMaskParsedCode.CharsGroupBegin); lCharsGroupInsertSize:=fMaskCompiledIndex; Add(0); Add(TMaskParsedCode.AnyCharOrNone); Add(1); if QCount>1 then IncrementLastCounterBy(TMaskParsedCode.AnyCharOrNone,QCount-1); PInteger(@fMaskCompiled[lCharsGroupInsertSize])^:=fMaskCompiledIndex; Add(TMaskParsedCode.CharsGroupEnd); fLastOC:=TMaskParsedCode.CharsGroupEnd; Inc(fMatchMaximumLiteralBytes,QCount*4); if BracketsRequired then Inc(fMaskInd,QCount) //go to ending ']' else Inc(fMaskInd,QCount-1); //go to last found QChar {$IFDEF debug_anycharornone} write('fMaskInd=',fMaskInd,', fMaskLimit=',fMaskLimit,' fMask[fMaskInd]=');if fMaskInd<=fMaskLimit then writeln('#',Ord(fMask[fMaskInd]),': ',fMask[fMaskInd])else writeln('>>'); writeln('CompileAnyCharOrNone end.'); {$ENDIF} end; procedure TMaskUTF8.Compile; begin PrepareCompile; if fCaseSensitive then fMask:=fOriginalMask else fMask:=UTF8LowerCase(fOriginalMask); fMaskLimit:=Length(fMask); fLastOC:=TMaskParsedCode.Literal; SetLength(fMaskCompiled,0); fMaskInd:=1; while fMaskInd<=fMaskLimit do begin //while fCPLength:=UTF8CodepointSizeFast(@fMask[fMaskInd]); if (mocEscapeChar in fMaskOpcodesAllowed) and (fMask[fMaskInd]=fMaskEscapeChar) then CompileEscapeCharPlusLiteral else begin // not an escaped literal //writeln('Compile while: fMask[',fMaskInd,']=',fMask[fMaskInd]); if (fMask[fMaskInd] in DefaultSpecialChars) or IsSpecialChar(fMask[fMaskInd]) then begin // handle special chars CompileSpecialChars; //writeln('After CompileSpecialChar'); //write('fMaskInd=',fMaskInd,', fMaskLimit=',fMaskLimit,' fMask[fMaskInd]=');if fMaskInd<=fMaskLimit then writeln('#',Ord(fMask[fMaskInd]),': ',fMask[fMaskInd])else writeln('>>'); end //handle special chars else begin //writeln('Compile: AddLiteral: fMask[',fMaskInd,']=',fMask[fMaskInd]); AddLiteral; end; inc(fMaskInd,fCPLength); end; //not an escaped literal end; //while SetLength(fMaskCompiled,fMaskCompiledIndex); fMaskCompiledLimit:=fMaskCompiledIndex-1; { fMaskIsCompiled Indicates that Compile was succesfull If you override this method make sure that it is only set to True if the overridden method also succeeds! } fMaskIsCompiled:=True; //writeln('Compile end.'); {$IFDEF debug_maskcompiled} writeln('fMaskInd=',fMaskInd,', fMaskLimit=',fMaskLimit); writeln('fMaskCompiled:'); writeln('fMaskCompiledLimit=',fMaskCompiledLimit); writeln('fMaskCompiledIndex=',fMaskCompiledIndex); DumpMaskCompiled; {$ENDIF} end; class function TMaskUTF8.CompareUTF8Sequences(const P1, P2: PChar): integer; var l1,l2: integer; l: integer; begin l1:=UTF8CodepointSizeFast(p1); l2:=UTF8CodepointSizeFast(p2); Result:=0; l:=0; while (l0 then exit; inc(l); end; Result:=l1-l2; end; function TMaskUTF8.intfMatches(aMatchOffset: integer; aMaskIndex: integer): TMaskFailCause; var c1,c2: PChar; lFailCause: TMaskFailCause; lNegateCharGroup: Boolean; lSkipOnSuccessGroup: integer; t1: Boolean; j: integer; lTryCounter: integer; begin lSkipOnSuccessGroup:=0; Result:=mfcUnexpectedEnd; lNegateCharGroup:=false; while aMaskIndex<=fMaskCompiledLimit do begin case TMaskParsedCode(fMaskCompiled[aMaskIndex]) of TMaskParsedCode.Literal: begin if aMatchOffset>fMatchStringLimit then exit(TMaskFailCause.mfcMatchStringExhausted); // Error, no char to match. inc(aMaskIndex); if CompareUTF8Sequences(@fMaskCompiled[aMaskIndex],@fMatchString[aMatchOffset])<>0 then exit(TMaskFailCause.mfcMaskNotMatch); inc(aMaskIndex,UTF8CodepointSizeFast(@fMaskCompiled[aMaskIndex])); inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end; TMaskParsedCode.AnyChar: begin inc(aMaskIndex); if aMatchOffset>fMatchStringLimit then exit(TMaskFailCause.mfcMatchStringExhausted); // Error, no char to match. inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end; TMaskParsedCode.Negate: begin lNegateCharGroup:=true; inc(aMaskIndex); end; TMaskParsedCode.CharsGroupBegin: begin lNegateCharGroup:=false; inc(aMaskIndex); lSkipOnSuccessGroup:=PInteger(@fMaskCompiled[aMaskIndex])^; inc(aMaskIndex,sizeof(integer)); end; TMaskParsedCode.CharsGroupEnd: begin if lNegateCharGroup then begin aMaskIndex:=lSkipOnSuccessGroup+1; inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end else exit(TMaskFailCause.mfcMaskNotMatch); end; TMaskParsedCode.OptionalChar: begin inc(aMaskIndex); if aMatchOffset>fMatchStringLimit then exit(TMaskFailCause.mfcMatchStringExhausted); // Error, no char to match. if CompareUTF8Sequences(@fMaskCompiled[aMaskIndex],@fMatchString[aMatchOffset])=0 then begin if lNegateCharGroup then exit(TMaskFailCause.mfcMaskNotMatch); aMaskIndex:=lSkipOnSuccessGroup+1; inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end else inc(aMaskIndex,UTF8CodepointSizeFast(@fMaskCompiled[aMaskIndex])); end; TMaskParsedCode.Range: begin if aMatchOffset>fMatchStringLimit then exit(TMaskFailCause.mfcMatchStringExhausted); // Error, no char to match. inc(aMaskIndex); c1:=@fMaskCompiled[aMaskIndex]; inc(aMaskIndex,UTF8CodepointSizeFast(C1)); c2:=@fMaskCompiled[aMaskIndex]; inc(aMaskIndex,UTF8CodepointSizeFast(C2)); t1:=(CompareUTF8Sequences(@fMatchString[aMatchOffset],c1)>=0) and (CompareUTF8Sequences(@fMatchString[aMatchOffset],c2)<=0); if t1 then begin if not lNegateCharGroup then begin //Jump to CharsGroupEnd+1 because if CharsGroupEnd is reached //it means that all optional chars and ranges have not matched the string. aMaskIndex:=lSkipOnSuccessGroup+1; inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end else exit(TMaskFailCause.mfcMaskNotMatch); end end; TMaskParsedCode.AnyCharToNext: begin // if last is "*", everything in remain data matches if aMaskIndex=fMaskCompiledLimit then exit(TMaskFailCause.mfcSuccess); if aMatchOffset>fMatchStringLimit then begin if aMaskIndex=fMaskCompiledLimit then exit(TMaskFailCause.mfcSuccess); exit(TMaskFailCause.mfcMatchStringExhausted); end; inc(aMaskIndex); while aMatchOffset<=fMatchStringLimit do begin lFailCause:=intfMatches(aMatchOffset,aMaskIndex); if lFailCause=TMaskFailCause.mfcSuccess then exit(TMaskFailCause.mfcSuccess) else if lFailCause=TMaskFailCause.mfcMatchStringExhausted then exit(TMaskFailCause.mfcMatchStringExhausted); inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end; exit(TMaskFailCause.mfcMatchStringExhausted); end; TMaskParsedCode.AnyCharOrNone: begin inc(aMaskIndex); lTryCounter:=PInteger(@fMaskCompiled[aMaskIndex])^; inc(aMaskIndex,sizeof(integer)); if TMaskParsedCode(fMaskCompiled[aMaskIndex])<>TMaskParsedCode.CharsGroupEnd then begin //writeln('TMaskUtf8.infMatches: error parsing AnyCharOrNone, missing CharsGroupEnd, fMaskCompiled[',aMaskIndex,']=',fMaskCompiled[aMaskIndex]); Exception_InternalError() end else aMaskIndex:=lSkipOnSuccessGroup+1; // Try to match remain mask eating, 0,1,2,...,lTryCounter chars. for j := 0 to lTryCounter do begin if aMatchOffset>fMatchStringLimit then begin if aMaskIndex=fMaskCompiledLimit+1 then exit(TMaskFailCause.mfcSuccess); exit(TMaskFailCause.mfcMatchStringExhausted); end; lFailCause:=intfMatches(aMatchOffset,aMaskIndex); if lFailCause=TMaskFailCause.mfcSuccess then exit(TMaskFailCause.mfcSuccess) else if lFailCause=TMaskFailCause.mfcMatchStringExhausted then exit(TMaskFailCause.mfcMatchStringExhausted); inc(aMatchOffset,UTF8CodepointSizeFast(@fMatchString[aMatchOffset])); end; exit(TMaskFailCause.mfcMatchStringExhausted); end; else // case begin writeln('TMaskUtf8.infMatches: XXXX InternalError'); Exception_InternalError(); end; end; end; if (aMaskIndex>fMaskCompiledLimit) and (aMatchOffset>fMatchStringLimit) then Result:=TMaskFailCause.mfcSuccess else if aMaskIndex>fMaskCompiledLimit then Result:=TMaskFailCause.mfcMaskExhausted else Result:=TMaskFailCause.mfcMatchStringExhausted; end; {$IFDEF debug_maskcompiled} procedure TMaskUTF8.DumpMaskCompiled; var i: Integer; b: Byte; begin for i := low(fMaskCompiled) to fMaskCompiledIndex-1 do begin b := fMaskCompiled[i]; writeln(i:2,': ',b:3); end; end; {$ENDIF} constructor TMaskUTF8.Create(const aMask: String); begin Create(aMask, False, DefaultMaskOpCodes); end; constructor TMaskUTF8.Create(const aMask: String; aCaseSensitive: Boolean); begin Create(aMask, aCaseSensitive, DefaultMaskOpCodes); end; constructor TMaskUTF8.Create(const aMask: String; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); begin inherited Create(aCaseSensitive,aOpcodesAllowed); fOriginalMask:=aMask; end; constructor TMaskUTF8.Create(const aMask: String; aOptions: TMaskOptions); begin inherited Create(aOptions); fOriginalMask:=aMask; end; function TMaskUTF8.Matches(const aStringToMatch: String): Boolean; begin if not fMaskIsCompiled then Compile; if fCaseSensitive then fMatchString:=aStringToMatch else fMatchString:=UTF8LowerCase(aStringToMatch); fMatchStringLimit:=length(fMatchString); if (fMatchStringLimit>=fMatchMinimumLiteralBytes) and (fMatchStringLimit<=fMatchMaximumLiteralBytes) then Result:=intfMatches(1,0)=TMaskFailCause.mfcSuccess else Result:=false; // There are too many or not enough bytes to match the string end; function TMaskUTF8.MatchesWindowsMask(const AFileName: String): Boolean; var WinMask: TWindowsMaskUTF8; begin WinMask:=TWindowsMaskUTF8.Create(fOriginalMask, CaseSensitive, fMaskOpCodesAllowed); try Result:=WinMask.Matches(AFileName); finally WinMask.Free; end; end; { TWindowsMask } procedure TWindowsMaskUTF8.SetMask(AValue: String); begin if (AValue = fWindowsMask) then Exit; inherited SetMask(AValue); fWindowsMask := AValue; end; function TWindowsMaskUTF8.GetMask: String; begin Result := fWindowsMask; end; procedure TWindowsMaskUTF8.SetWindowsQuirkAllowed(AValue: TWindowsQuirks); begin if fWindowsQuirkAllowed = AValue then Exit; fWindowsQuirkAllowed := AValue; //if (wqFilenameEnd in fWindowsQuirkAllowed) then // Include(fMaskOpcodesAllowed, mocAnyCharOrNone); fMaskIsCompiled := False; end; procedure TWindowsMaskUTF8.CompileOtherSpecialChars; begin inherited CompileOtherSpecialChars; if (fMask[fMaskInd]=#0) and not (wqFileNameEnd in self.fWindowsQuirkInUse) then Exception_InternalError; CompileAnyCharOrNone(#0, False); end; function TWindowsMaskUTF8.IsSpecialChar(AChar: Char): Boolean; begin Result := (AChar = #0); end; class procedure TWindowsMaskUTF8.SplitFileNameExtension( const aSourceFileName: String; out aFileName: String; out aExtension: String; aIsMask: Boolean); var j: Integer; lLowLimit: integer; begin // Default values aFileName:=aSourceFileName; aExtension:=''; // This is because .foo is considered a file name ".foo" as one. if aIsMask then lLowLimit:=0 else lLowLimit:=1; j:=Length(aSourceFileName); while j>lLowLimit do begin if aSourceFileName[j]='.' then begin aFileName:=copy(aSourceFileName,1,j-1); aExtension:=copy(aSourceFileName,j); break; end; dec(j); end; end; constructor TWindowsMaskUTF8.Create(const aMask: String; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); begin Create(aMask, aCaseSensitive, aOpcodesAllowed, DefaultWindowsQuirks); end; constructor TWindowsMaskUTF8.Create(const aMask: String; aCaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes; aWindowsQuirksAllowed: TWindowsQuirks); begin fWindowsQuirkAllowed:=aWindowsQuirksAllowed; fWindowsMask:=aMask; //if (wqFilenameEnd in fWindowsQuirkAllowed) then // Include(aOpcodesAllowed, mocAnyCharOrNone); inherited Create(aMask,aCaseSensitive,aOpcodesAllowed); end; procedure TWindowsMaskUTF8.Compile; function OptionalQMarksAtEnd(aMask: String): String; var //lCounter: integer; i: integer; begin //Change ending ? in #0 Result := aMask; for i := Length(aMask) downto 1 do begin if Result[i]='?' then Result[i]:=#0 else Exit; end; { lCounter:=0; for i := Length(aMask) downto 1 do begin if aMask[i]='?' then inc(lCounter) else break; end; if lCounter>0 then aMask:=copy(aMask,1,Length(aMask)-lCounter)+'['+StringOfChar('?',lCounter)+']'; Result:=aMask; } end; var lFileNameMask: String; lExtensionMask: String; lModifiedMask: String; ZeroPos: SizeInt; begin lModifiedMask:=fWindowsMask; //We cannot have #0 in the mask,since we use it internally ZeroPos:=Pos(#0, fWindowsMask); if ZeroPos>0 then Exception_InvalidCharMask('NullCharacter', ZeroPos); // Quirk "blah.*" = "blah*" if wqAnyExtension in fWindowsQuirkAllowed then begin if (RightStr(lModifiedMask,2)='.*') and (Length(lModifiedMask)>2) then begin //if RightStr(lModifiedMask,3)='*.*' then begin lModifiedMask:=copy(lModifiedMask,1,Length(lModifiedMask)-2); fWindowsQuirkInUse:=fWindowsQuirkInUse+[wqAnyExtension]; end; end; SplitFileNameExtension(lModifiedMask,lFileNameMask,lExtensionMask,true); // Quirk "blah.abc" = "blah.abc*" if wqExtension3More in fWindowsQuirkAllowed then begin if (Length(lExtensionMask)=4) and (Length(lFileNameMask)>0) then begin lExtensionMask:=lExtensionMask+'*'; fWindowsQuirkInUse:=fWindowsQuirkInUse+[wqExtension3More]; end; end; // Quirk "" = "*" if (Length(lFileNameMask)=0) and (Length(lExtensionMask)=0) then begin if wqEmptyIsAny in fWindowsQuirkAllowed then begin lFileNameMask:='*'; fWindowsQuirkInUse:=fWindowsQuirkInUse+[wqEmptyIsAny]; end; end else begin // Quirk ".abc" if wqAllByExtension in fWindowsQuirkAllowed then begin if (Length(lFileNameMask)=0) and (length(lExtensionMask)>0) then begin if lExtensionMask[1]='.' then begin lFileNameMask:='*'; fWindowsQuirkInUse:=fWindowsQuirkInUse+[wqAllByExtension]; end; end; end; end; // Quirk "file???.ab?" matches "file1.ab1" and "file123.ab" if wqFilenameEnd in fWindowsQuirkAllowed then begin lFileNameMask:=OptionalQMarksAtEnd(lFileNameMask); lExtensionMask:=OptionalQMarksAtEnd(lExtensionMask); if (Pos(#0, lFileNameMask)>0) or (Pos(#0, lExtensionMask)>0) then fWindowsQuirkInUse:=fWindowsQuirkInUse+[wqFilenameEnd]; end; if wqNoExtension in fWindowsQuirkAllowed then begin if Length(lExtensionMask)=1 then begin fWindowsQuirkInUse:=fWindowsQuirkInUse+[wqNoExtension]; lExtensionMask:=''; end; end; inherited Mask:=lFileNameMask+lExtensionMask; inherited Compile; end; function TWindowsMaskUTF8.Matches(const aFileName: String): Boolean; var lFileName, lExtension: String; begin if not fMaskIsCompiled then Compile; if wqNoExtension in fWindowsQuirkInUse then begin SplitFileNameExtension(aFileName,lFileName,lExtension,false); // wqNoExtension = Empty extension //if lExtension<>'' then exit(false); // Its not clear if a file "file." should match an "*." mask because // there is no way in Windows that a file ends with a dot. if (lExtension<>'') and (lExtension<>'.') then exit(false) end else if wqAnyExtension in fWindowsQuirkInUse then begin SplitFileNameExtension(aFileName,lFileName,lExtension,false); Result:=inherited Matches(lFileName); exit; end; Result:=Inherited Matches(aFileName); end; { TParseStringList } constructor TParseStringList.Create(const AText, ASeparators: String); var I, S: Integer; begin inherited Create; S := 1; for I := 1 to Length(AText) do begin if Pos(AText[I], ASeparators) > 0 then begin if I > S then Add(Copy(AText, S, I - S)); S := I + 1; end; end; if Length(AText) >= S then Add(Copy(AText, S, Length(AText) - S + 1)); end; { TMaskList } constructor TMaskList.Create(const aValue: String; aSeparator: Char; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); begin fMask := aValue; fSeparator := aSeparator; fCaseSensitive := CaseSensitive; fMaskOpcodes := aOpcodesAllowed; fAutoReverseRange := True; fMasks := TObjectList.Create(True); FMaskClass := GetMaskClass; AddMasksToList(aValue, aSeparator, CaseSensitive, aOpcodesAllowed); end; constructor TMaskList.Create(const aValue: String; aSeparator: Char; aOptions: TMaskOptions); var CaseSens: Boolean; Opcodes: TMaskOpCodes; begin CaseSens:=moCaseSensitive in aOptions; if moDisableSets in aOptions then Opcodes:=MaskOpCodesDisableRange else Opcodes:=DefaultMaskOpCodes; Create(aValue, aSeparator, CaseSens, Opcodes); end; destructor TMaskList.Destroy; begin fMasks.Free; inherited Destroy; end; function TMaskList.GetItem(Index: Integer): TMask; begin Result := TMask(fMasks.Items[Index]); end; procedure TMaskList.SetAutoReverseRange(AValue: Boolean); var i: Integer; begin if fAutoReverseRange = AValue then Exit; fAutoReverseRange := AValue; for i := 0 to fMasks.Count - 1 do TMask(fMasks.Items[i]).AutoReverseRange := fAutoReverseRange; end; procedure TMaskList.SetCaseSensitive(AValue: Boolean); var i: Integer; begin if fCaseSensitive = AValue then Exit; fCaseSensitive := AValue; for i := 0 to fMasks.Count - 1 do TMask(fMasks.Items[i]).CaseSensitive := fCaseSensitive; end; procedure TMaskList.SetMask(AValue: String); begin if fMask = AValue then Exit; fMask := AValue; fMasks.Clear; AddMasksToList(fMask, fSeparator, fCaseSensitive, fMaskOpCodes); end; procedure TMaskList.SetMaskOpCodes(AValue: TMaskOpCodes); var i: Integer; begin if FMaskOpCodes = AValue then Exit; fMaskOpCodes := AValue; for i := 0 to fMasks.Count - 1 do TMask(fMasks.Items[i]).MaskOpCodes := fMaskOpcodes; end; function TMaskList.GetMaskClass: TMaskClass; begin Result := TMask; end; procedure TMaskList.AddMasksToList(const aValue: String; aSeparator: Char; CaseSensitive: Boolean; aOpcodesAllowed: TMaskOpCodes); var S: TParseStringList; i: Integer; begin S := TParseStringList.Create(aValue, aSeparator); try for i := 0 to S.Count-1 do begin fMasks.Add(FMaskClass.Create(S[i], CaseSensitive, aOpcodesAllowed)); end; finally S.Free; end; end; function TMaskList.GetCount: Integer; begin Result := fMasks.Count; end; function TMaskList.Matches(const AFileName: String): Boolean; var I: Integer; begin Result := False; for I := 0 to fMasks.Count-1 do if ((fMasks.Items[I]) as FMaskClass).Matches(AFileName) then Exit(True); end; function TMaskList.MatchesWindowsMask(const AFileName: String): Boolean; //use the same hack as in TMask.MatchesWindowsMask var WML: TWindowsMaskList; begin WML := TWindowsMaskList.Create(fMask, fSeparator, fCaseSensitive, fMaskOpcodes, DefaultWindowsQuirks); try Result := WML.Matches(AFileName); finally WML.Free; end; end; end.