11

I figured this has to be in the RTL somewhere, but I looked and I can't find it.

function IsValidFilename(filename: string): boolean;
//returns True if it would be possible to create or open a file with
//this name, without modifying the current directory structure

In other words, it has to point to an existing folder on a valid local or network drive, and not contain any invalid characters. Do we have anything like that? (Bonus points if it checks the current user's access rights to be sure you can get at the folder in question.)

Mason Wheeler
  • 82,511
  • 50
  • 270
  • 477
  • 9
    I am a bit pedantic, I know, but *please* use `const FileName: string`! – Andreas Rejbrand Sep 24 '10 at 07:21
  • 1
    By the way, your question is really too vague. I assume that `DirectoryExists(ExtractFilePath(FileName))` is a necessary requirement, as is the fact that `ExtractFileName(FileName)` contains only valid characters. But that doesn't mean that you can create or even read a file in this directory. It is likely that the directory is read-only (e.g. Program Files, or Windows), so that you can open files, but not create files. It is also possible that you cannot even read (e.g., another user's directory). Also, what if the file exists but you are allowed to rewrite it? Should we return true or false? – Andreas Rejbrand Sep 24 '10 at 07:57
  • Even better: `const FileName: TFileName`. – NGLN Oct 10 '14 at 10:56
  • 1
    @AndreasRejbrand-He is going to do some IO operations (read/write a file). On a harddrive this will take AT LEAST 12ms (average time to put the head over the correct sector). In reality it will take hundredths of ms. I don't think that the gain (ns) added by CONST will do a difference in this case :) PS: I am not against using CONST. What I am saying is that in this case it won't make a difference. – Gabriel Jun 19 '17 at 10:14
  • Good question. Too bad there are no good answers! – Gabriel Aug 28 '17 at 15:36

9 Answers9

14

So, this question is a bit old but in case you're looking: With XE and newer you can use the TPath class in System.IOUtils.

Result := TPath.HasValidFileNameChars(AFileName, UseWildcards); 
Uli Gerhardt
  • 13,748
  • 1
  • 45
  • 83
Jessie
  • 156
  • 1
  • 2
  • 1
    System.IOUtils is unreliable. I don't know about HasValidFileNameChars but HasValidPathChars does not work - or at least is not enough to check if the path is valid. If you pass 'http://www.etc' as parameter to HasValidPathChars it will return true. So, you have also to use something like TPath.HasPathValidColon (which unfortunately is not public but private). – Gabriel Jun 19 '17 at 10:07
  • TPath.HasValidFileNameChars('c:\IOUtils_sucks.txt', false) will fail !!!!!!!!!!!!!!!! – Gabriel Aug 28 '17 at 15:35
  • According to David Heffernan 's analysis (https://stackoverflow.com/a/45347107/133516), `TPath.HasValidFileNameChars` is not reliable – Edwin Yip Jun 04 '19 at 03:43
  • @Z80 it fails because that is not a valid FILE name. It's a name of a PATH. That function expects only the file name part. – Marus Gradinaru Apr 20 '21 at 18:56
  • @MarusNebunu - I see. I expected to be able to use that function on "real life" files names (true file name + its path). So, the use has to split the FULL filename in filename and path and use HasValidPathChars + HasValidFileNameChars – Gabriel Apr 21 '21 at 13:58
10

As far as I know, there is not an RTL or Windows API function to validate a filename, so you must write your own function following the Windows File Naming Conventions:

The following fundamental rules enable applications to create and process valid names for files and directories, regardless of the file system:

  • Use a period to separate the base file name from the extension in the name of a directory or file.
  • Use a backslash () to separate the components of a path. The backslash divides the file name from the path to it, and one directory name from another directory name in a path. You cannot use a backslash in the name for the actual file or directory because it is a reserved character that separates the names into components.
  • Use a backslash as required as part of volume names, for example, the "C:\" in "C:\path\file" or the "\server\share" in "\server\share\path\file" for Universal Naming Convention (UNC) names. For more information about UNC names, see the Maximum Path Length Limitation section.
  • Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, even though some file systems (such as a POSIX-compliant file system) may consider them as different. Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior. For more information, see CreateFile.
  • Volume designators (drive letters) are similarly case-insensitive. For example, "D:\" and "d:\" refer to the same volume.
  • Use any character in the current code page for a name, including Unicode characters and characters in the extended character set (128–255), except for the following:

    • The following reserved characters:
      • < (less than)
      • > (greater than)
      • : (colon)
      • " (double quote)
      • / (forward slash)
      • \ (backslash)
      • | (vertical bar or pipe)
      • ? (question mark)
      • * (asterisk)
    • Integer value zero, sometimes referred to as the ASCII NUL character.
    • Characters whose integer representations are in the range from 1 through 31, except for alternate streams where these characters are allowed. For more information about file streams, see File Streams.
    • Any other character that the target file system does not allow.
  • Use a period as a directory component in a path to represent the current directory, for example ".\temp.txt". For more information, see Paths.
  • Use two consecutive periods (..) as a directory component in a path to represent the parent of the current directory, for example "..\temp.txt". For more information, see Paths.
  • Do not use the following reserved device names for the name of a file:

    CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9. Also avoid these names followed immediately by an extension; for example, NUL.txt is not recommended. For more information, see Namespaces.

  • Do not end a file or directory name with a space or a period. Although the underlying file system may support such names, the Windows shell and user interface does not. However, it is acceptable to specify a period as the first character of a name. For example, ".temp".

you can check this C++ article Validating file names for an complete example function to validate the windows file names.

Bob Stein
  • 16,271
  • 10
  • 88
  • 101
RRUZ
  • 134,889
  • 20
  • 356
  • 483
5

Is this too crude, Mason?

function CanCreateFile(const FileName: string): Boolean;
var
  H: THandle;
begin
  H := CreateFile(PChar(FileName), GENERIC_READ or GENERIC_WRITE, 0, nil,
       CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY or FILE_FLAG_DELETE_ON_CLOSE, 0);

  Result := H <> INVALID_HANDLE_VALUE;

  DeleteFile(FileName);
end;
RobertFrank
  • 7,332
  • 11
  • 53
  • 99
  • I guess that might work, if I called that `or FileExists(filename)`. – Mason Wheeler Sep 23 '10 at 23:44
  • 2
    For the full discussion: http://stackoverflow.com/questions/3599256/how-can-i-use-delphi-to-test-if-a-directory-is-writeable – Jørn E. Angeltveit Sep 23 '10 at 23:44
  • 2
    @Mason Wheeler: If the file exists, then CanCreateFile(FileName) or FileExists(FileName) will delete the existing file(!) and return false. If you use FileExists(FileName) or CanCreateFile(FileName) then the existing file will still be around and the result will be true. I would recommend to NEVER depend on lazy boolean expressions (i.e. unchecked "Complete boolean eval" in the project options), use an extra if ... then instead. – Jørn E. Angeltveit Sep 24 '10 at 08:59
  • 4
    You have an error in code, BTW. You can't define FileName as a variable, as it is already the name of the parameter. Delete the variable, and use PChar(FileName) instead. – Jørn E. Angeltveit Sep 24 '10 at 09:05
4

I do not have access to a Delphi compiler right now, but you could try

function IsValidFilename(const FileName: string): boolean;
begin
  result := DirectoryExists(ExtractFilePath(FileName)) and TPath.HasValidFileNameChars(ExtractFileName(FileName), false);
end;
Andreas Rejbrand
  • 105,602
  • 8
  • 282
  • 384
  • maybe you want to create the file in a folder that was not YET created. so, directoryexists will fail. – Gabriel Aug 28 '17 at 15:33
3

To see if it exists, use DirectoryExists() and FileExists(). To see if you can create/edit the file, use FileOpen() with ReadWrite and see if it succeeds.

Marcus Adams
  • 53,009
  • 9
  • 91
  • 143
3

try this code (taken from delphi faq)

const
  { for short 8.3 file names }
  ShortForbiddenChars : set of Char = [';', '=', '+', '<', '>', '|',
                                       '"', '[', ']', '\', '/', ''''];
  { for long file names }
  LongForbiddenChars  : set of Char = ['<', '>', '|', '"', '\', '/', ':', '*', '?'];

function TestFilename(Filename: String; islong: Boolean) : Boolean;
var
  I: integer;
begin
  Result := Filename <> '';
  if islong then
  begin
    for I := 1 to Length(Filename) do
      Result := Result and not (Filename[I] in LongForbiddenChars);
  end
  else
  begin
    for I := 1 to Length(Filename) do
      Result := Result and not (Filename[I] in ShortForbiddenChars);
  end;
end;
Omair Iqbal
  • 1,820
  • 1
  • 28
  • 41
2

Checking if Directory Exists:

In SysUtils, you have: DirectoryExists

Checking the filename:

The invalid file name characters are: \ / : * ? " < > | so you could check the filename like this:

for c in AFileName do
begin
  OK := NOT (C in ['\', '/', ':', '*', '?', '"', '<', '>', '|']);
  if not OK then Break;
end;

Checking if the folder is writable:

Duplicate of: How can I use Delphi to test if a Directory is writeable?

Community
  • 1
  • 1
Jørn E. Angeltveit
  • 3,029
  • 3
  • 22
  • 53
1

You can also use the SysUtils.CharInSet function :

function isFileNameValid(fileNameToCheck : String) : Boolean;
var
  invalidChars : Boolean;
  ch : Char;
begin
  invalidChars := False;
  for ch in fileNameToCheck do begin
    invalidChars := SysUtils.CharInSet(ch, ['\', '/', ':', '*', '?', '"', '<', '>', '|']);
    if invalidChars then
      Break;
  end;
  Result := not invalidChars;
end;
Viktor Anastasov
  • 1,093
  • 3
  • 17
  • 33
1

This is my version:

function MValidFileName(AFileName: String): Boolean;
const InvalidChars: set of AnsiChar = [#0..#31, '\', '/', ':', '*', '?', '"', '<', '>', '|'];
      InvalidWords: array[0..21] of String = ('CON', 'PRN', 'AUX', 'NUL', 'COM1',
       'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
       'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9');
var I, L, Lw: Integer;
begin
 Result:= False;
 L:= Length(AFileName);
 if (L = 0) or (L > 255) or (AFileName[L] = '.') or (AFileName[L] = ' ') then Exit;
 for I:= 1 to L do 
  if (Ord(AFileName[I]) <= 255) and (AnsiChar(AFileName[I]) in InvalidFileNameChars) then Exit;
 AFileName:= UpperCase(AFileName);
 for I:= 0 to 21 do begin
  Lw:= Length(InvalidWords[I]);
  if (Pos(InvalidWords[I], AFileName) = 1) and
   ((Lw = Length(AFileName)) or (AFileName[Lw+1] = '.')) then Exit;
 end;
 Result:= True;
end;
Marus Gradinaru
  • 2,824
  • 1
  • 26
  • 55