CrackedRuby logo

CrackedRuby

FileTest

Overview

FileTest provides class methods for testing various properties of files and directories in Ruby. The module contains methods that check file existence, type, permissions, and other filesystem attributes without requiring file handles or raising exceptions for missing files.

Ruby implements FileTest as a module mixed into the File class, making its methods available both as FileTest.method_name and File.method_name. All FileTest methods accept string paths as arguments and return boolean values, with a few exceptions that return numeric values or nil.

The module handles cross-platform filesystem differences transparently, abstracting platform-specific file attribute checking into consistent Ruby methods. FileTest methods use the underlying operating system's file stat system calls to gather file information efficiently.

# Basic file existence check
FileTest.exist?('/etc/passwd')
# => true

# Check if path is a regular file
FileTest.file?('/home/user/document.txt')
# => true

# Test directory status
FileTest.directory?('/usr/local')
# => true

FileTest methods never modify the filesystem - they perform read-only inspections of file metadata. This makes them safe for use in conditional logic and validation scenarios where file state verification is required without side effects.

Basic Usage

The most frequently used FileTest methods check file existence and type. The exist? method returns true if a path exists regardless of type, while file? and directory? verify specific path types.

path = '/home/user/config.yml'

if FileTest.exist?(path)
  puts "Path exists"
  
  if FileTest.file?(path)
    puts "It's a regular file"
  elsif FileTest.directory?(path)
    puts "It's a directory"
  end
end

Permission testing methods check read, write, and execute access for the current process. These methods respect the effective user and group IDs of the running process, making them accurate for access control decisions.

config_file = '/etc/application.conf'

# Check permissions before attempting operations
if FileTest.readable?(config_file)
  content = File.read(config_file)
end

if FileTest.writable?('/var/log/application.log')
  File.open('/var/log/application.log', 'a') do |log|
    log.puts "Application started"
  end
end

# Verify executable status for scripts
script_path = '/usr/local/bin/deploy.sh'
if FileTest.executable?(script_path)
  system(script_path)
else
  puts "Script lacks execute permissions"
end

Size-related methods provide file dimensions and emptiness checks. The size method returns the file size in bytes, while zero? tests for empty files. The size? method returns the size for non-empty files or nil for empty files.

log_file = '/var/log/application.log'

# Get exact file size
file_size = FileTest.size(log_file)
puts "Log file is #{file_size} bytes"

# Check for empty files
if FileTest.zero?(log_file)
  puts "Log file is empty"
end

# size? returns nil for empty files, size for non-empty
actual_size = FileTest.size?(log_file)
if actual_size
  puts "File contains #{actual_size} bytes of data"
else
  puts "File is empty"
end

Symbolic link detection requires the symlink? method, which returns true only for symbolic links themselves, not their targets. This distinction becomes critical when processing directory trees that may contain links.

# Create test symbolic link
File.symlink('/etc/hosts', '/tmp/hosts_link')

puts FileTest.symlink?('/tmp/hosts_link')    # => true
puts FileTest.symlink?('/etc/hosts')         # => false
puts FileTest.file?('/tmp/hosts_link')       # => true (follows link)

Error Handling & Debugging

FileTest methods handle missing files and permission issues gracefully by returning false instead of raising exceptions. This behavior differs from File class methods that typically raise Errno exceptions for filesystem errors.

# FileTest never raises for missing files
puts FileTest.exist?('/nonexistent/path')    # => false
puts FileTest.file?('/missing/file.txt')     # => false
puts FileTest.readable?('/forbidden/file')   # => false

# Compare with File methods that raise exceptions
begin
  File.size('/nonexistent/file')
rescue Errno::ENOENT => e
  puts "File.size raised: #{e.message}"
end

Race conditions occur when files change between FileTest checks and subsequent operations. The time gap between testing and using a file creates windows where filesystem state can change.

# Problematic pattern - race condition possible
if FileTest.exist?(path) && FileTest.readable?(path)
  content = File.read(path)  # File might be deleted here
end

# Better approach - handle exceptions during actual operations
begin
  content = File.read(path)
rescue Errno::ENOENT
  puts "File not found when attempting to read"
rescue Errno::EACCES
  puts "Permission denied when reading file"
end

Permission checks reflect the effective user ID of the current process, not the actual file permissions. This means FileTest results may differ from ls -l output when running as different users or with setuid programs.

# Running as non-root user
file = '/root/.bashrc'

# File exists but not readable by current user
puts FileTest.exist?(file)     # => true
puts FileTest.readable?(file)  # => false

# Verify with actual file stat information
stat = File.stat(file)
printf "Permissions: %o\n", stat.mode & 0777
puts "Owner: #{stat.uid}, Group: #{stat.gid}"
puts "Current process: #{Process.uid}"

Network filesystems and special file types can produce unexpected results with FileTest methods. Remote filesystems may have different permission models or caching behaviors that affect test results.

# Special files may not behave as expected
device_files = ['/dev/null', '/dev/random', '/proc/cpuinfo']

device_files.each do |device|
  puts "#{device}:"
  puts "  exists: #{FileTest.exist?(device)}"
  puts "  file: #{FileTest.file?(device)}"
  puts "  readable: #{FileTest.readable?(device)}"
  puts "  size: #{FileTest.size(device) rescue 'unknown'}"
end

Debugging FileTest issues requires understanding the underlying filesystem behavior. The File.stat method provides detailed information that can explain FileTest results when behavior seems unexpected.

def debug_file_status(path)
  begin
    stat = File.stat(path)
    puts "Path: #{path}"
    puts "  Type: #{stat.ftype}"
    puts "  Mode: #{sprintf('%o', stat.mode)}"
    puts "  Size: #{stat.size}"
    puts "  Owner: #{stat.uid}:#{stat.gid}"
    puts "  Times: #{stat.mtime} (modified)"
    puts "FileTest results:"
    puts "  exist?: #{FileTest.exist?(path)}"
    puts "  file?: #{FileTest.file?(path)}"
    puts "  directory?: #{FileTest.directory?(path)}"
    puts "  readable?: #{FileTest.readable?(path)}"
    puts "  writable?: #{FileTest.writable?(path)}"
  rescue SystemCallError => e
    puts "Error accessing #{path}: #{e.message}"
  end
end

debug_file_status('/etc/passwd')
debug_file_status('/tmp/nonexistent')

Common Pitfalls

Symbolic link handling creates confusion because FileTest methods follow links by default. The symlink? method only returns true for the link itself, while other methods test the link target.

# Create symbolic link to demonstrate behavior
File.symlink('/etc/passwd', '/tmp/passwd_link')

# These test the link target (/etc/passwd)
puts FileTest.file?('/tmp/passwd_link')      # => true
puts FileTest.readable?('/tmp/passwd_link')  # => true
puts FileTest.size('/tmp/passwd_link')       # => size of /etc/passwd

# Only this tests the link itself
puts FileTest.symlink?('/tmp/passwd_link')   # => true

# Broken symbolic links cause unexpected behavior
File.symlink('/nonexistent/target', '/tmp/broken_link')
puts FileTest.exist?('/tmp/broken_link')     # => false (target doesn't exist)
puts FileTest.symlink?('/tmp/broken_link')   # => true (link exists)

The size method behaves differently from size? for empty files and non-existent files. This inconsistency catches developers who assume similar method names have similar error handling.

# Create empty file for testing
File.write('/tmp/empty_file', '')

# size returns 0 for empty files, raises for missing files
puts FileTest.size('/tmp/empty_file')        # => 0
begin
  FileTest.size('/tmp/missing_file')
rescue Errno::ENOENT => e
  puts "size raised: #{e.message}"
end

# size? returns 0 for empty files, nil for missing files
puts FileTest.size?('/tmp/empty_file')       # => nil
puts FileTest.size?('/tmp/missing_file')     # => nil

Permission testing becomes unreliable in environments with complex access control systems like SELinux or ACLs. FileTest methods only check traditional Unix permissions, ignoring extended attributes.

# Traditional permission check might pass
puts FileTest.writable?('/var/log/secure')   # => false (correctly)

# But extended attributes might block access even if permissions allow
# SELinux contexts, ACLs, and file attributes affect actual access
system('lsattr /var/log/secure')  # Check extended attributes
system('getfacl /var/log/secure') # Check ACLs if available

Time-of-check-to-time-of-use (TOCTOU) vulnerabilities emerge when FileTest results become stale between checking and acting. Concurrent processes or signal handlers can modify files after FileTest returns.

# Vulnerable pattern - file might change between check and use
if FileTest.writable?(config_file)
  # Another process could modify permissions here
  File.open(config_file, 'w') do |f|  # Might fail with EACCES
    f.write(new_config)
  end
end

# Safer approach - attempt operation and handle failures
begin
  File.open(config_file, 'w') do |f|
    f.write(new_config)
  end
rescue Errno::EACCES
  puts "Cannot write to #{config_file}: permission denied"
rescue Errno::ENOENT
  puts "Configuration file #{config_file} not found"
end

Directory traversal vulnerabilities can occur when using FileTest with user-provided paths. Attackers might use "../" sequences or symbolic links to access files outside intended directories.

# Dangerous - user input used directly
user_filename = params[:filename]  # Could be "../../../etc/passwd"
full_path = File.join('/safe/directory', user_filename)

if FileTest.readable?(full_path)
  content = File.read(full_path)  # Might read sensitive files
end

# Safer approach - validate and constrain paths
def safe_file_access(base_dir, filename)
  # Reject paths with directory traversal attempts
  return nil if filename.include?('..')
  
  full_path = File.join(base_dir, filename)
  real_path = File.realpath(full_path) rescue nil
  return nil unless real_path
  
  # Ensure resolved path stays within base directory
  base_real = File.realpath(base_dir)
  return nil unless real_path.start_with?(base_real)
  
  FileTest.readable?(real_path) ? real_path : nil
end

Case sensitivity handling varies between filesystems, causing portable code to behave differently on different platforms. macOS uses case-insensitive filesystems by default, while Linux uses case-sensitive filesystems.

# Create file for testing
File.write('/tmp/TestFile.txt', 'content')

# Results vary by filesystem case sensitivity
puts FileTest.exist?('/tmp/TestFile.txt')    # => true (always)
puts FileTest.exist?('/tmp/testfile.txt')    # => depends on filesystem
puts FileTest.exist?('/tmp/TESTFILE.TXT')    # => depends on filesystem

# Portable filename comparison requires normalization
def portable_file_exists?(path)
  # On case-insensitive systems, check exact case match
  dir = File.dirname(path)
  basename = File.basename(path)
  
  return false unless FileTest.exist?(path)
  
  # Verify exact case on case-insensitive filesystems
  actual_files = Dir.entries(dir) rescue []
  actual_files.include?(basename)
end

Reference

File Existence and Type Methods

Method Parameters Returns Description
exist?(path) path (String) Boolean True if path exists (file, directory, or special file)
exists?(path) path (String) Boolean Deprecated alias for exist?
file?(path) path (String) Boolean True if path is a regular file
directory?(path) path (String) Boolean True if path is a directory
symlink?(path) path (String) Boolean True if path is a symbolic link
pipe?(path) path (String) Boolean True if path is a named pipe (FIFO)
socket?(path) path (String) Boolean True if path is a socket
blockdev?(path) path (String) Boolean True if path is a block device
chardev?(path) path (String) Boolean True if path is a character device

Permission Test Methods

Method Parameters Returns Description
readable?(path) path (String) Boolean True if file is readable by effective user
readable_real?(path) path (String) Boolean True if file is readable by real user
writable?(path) path (String) Boolean True if file is writable by effective user
writable_real?(path) path (String) Boolean True if file is writable by real user
executable?(path) path (String) Boolean True if file is executable by effective user
executable_real?(path) path (String) Boolean True if file is executable by real user

Size and Content Methods

Method Parameters Returns Description
size(path) path (String) Integer File size in bytes, raises if file missing
size?(path) path (String) Integer or nil File size in bytes, nil if missing or empty
zero?(path) path (String) Boolean True if file exists and is empty

Special Attribute Methods

Method Parameters Returns Description
setuid?(path) path (String) Boolean True if file has setuid bit set
setgid?(path) path (String) Boolean True if file has setgid bit set
sticky?(path) path (String) Boolean True if file has sticky bit set
owned?(path) path (String) Boolean True if file is owned by effective user
grpowned?(path) path (String) Boolean True if file is owned by effective group

File Type Constants

Constant Value Description
S_IFMT 0170000 File type mask
S_IFREG 0100000 Regular file
S_IFDIR 0040000 Directory
S_IFLNK 0120000 Symbolic link
S_IFBLK 0060000 Block device
S_IFCHR 0020000 Character device
S_IFIFO 0010000 Named pipe
S_IFSOCK 0140000 Socket

Permission Constants

Constant Octal Description
S_ISUID 04000 Set user ID on execution
S_ISGID 02000 Set group ID on execution
S_ISVTX 01000 Sticky bit
S_IRUSR 00400 Owner read permission
S_IWUSR 00200 Owner write permission
S_IXUSR 00100 Owner execute permission
S_IRGRP 00040 Group read permission
S_IWGRP 00020 Group write permission
S_IXGRP 00010 Group execute permission
S_IROTH 00004 Other read permission
S_IWOTH 00002 Other write permission
S_IXOTH 00001 Other execute permission

Method Availability and Aliases

FileTest methods are available in three ways:

  • FileTest.method_name(path) - Direct module method call
  • File.method_name(path) - Through File class inclusion
  • test(?char, path) - Single character test operators (deprecated)

Error Handling Behavior

All FileTest methods return false for missing files instead of raising exceptions. This differs from File class methods:

# FileTest methods return false for missing files
FileTest.exist?('/missing')     # => false
FileTest.readable?('/missing')  # => false
FileTest.size?('/missing')      # => nil

# File methods raise exceptions
File.size('/missing')           # raises Errno::ENOENT
File.read('/missing')           # raises Errno::ENOENT

Platform Differences

Feature Unix/Linux Windows macOS
Case sensitivity Sensitive Insensitive Configurable
Symbolic links Full support Limited support Full support
Execute permissions Proper bits File extensions Proper bits
Special files All types Limited types Most types
Setuid/setgid Supported Not applicable Supported