getPathsForPermissionCheck function

List<String> getPathsForPermissionCheck(
  1. String inputPath
)

Gets all paths that should be checked for permissions. This includes the original path, all intermediate symlink targets in the chain, and the final resolved path.

For example, if test.txt -> /etc/passwd -> /private/etc/passwd:

  • test.txt (original path)
  • /etc/passwd (intermediate symlink target)
  • /private/etc/passwd (final resolved path)

This is important for security: a deny rule for /etc/passwd should block access even if the file is actually at /private/etc/passwd (as on macOS).

Implementation

List<String> getPathsForPermissionCheck(String inputPath) {
  // Expand tilde notation defensively
  String path = inputPath;
  final homeDir = Platform.environment['HOME'] ?? '';
  if (path == '~') {
    path = homeDir;
  } else if (path.startsWith('~/')) {
    path = p.join(homeDir, path.substring(2));
  }

  final pathSet = <String>{};
  final fsImpl = getFsImplementation();

  // Always check the original path
  pathSet.add(path);

  // Block UNC paths before any filesystem access to prevent network
  // requests (DNS/SMB) during validation on Windows
  if (path.startsWith('//') || path.startsWith('\\\\')) {
    return pathSet.toList();
  }

  // Follow the symlink chain, collecting ALL intermediate targets
  try {
    String currentPath = path;
    final visited = <String>{};
    const maxDepth = 40; // Prevent runaway loops, matches typical SYMLOOP_MAX

    for (int depth = 0; depth < maxDepth; depth++) {
      // Prevent infinite loops from circular symlinks
      if (visited.contains(currentPath)) {
        break;
      }
      visited.add(currentPath);

      if (!fsImpl.existsSync(currentPath)) {
        // Path doesn't exist (new file case). existsSync follows symlinks,
        // so this is also reached for DANGLING symlinks.
        if (currentPath == path) {
          final resolved = resolveDeepestExistingAncestorSync(fsImpl, path);
          if (resolved != null) {
            pathSet.add(resolved);
          }
        }
        break;
      }

      final stats = fsImpl.lstatSync(currentPath);

      // Skip special file types that can cause issues
      if (stats.isFIFO() ||
          stats.isSocket() ||
          stats.isCharacterDevice() ||
          stats.isBlockDevice()) {
        break;
      }

      if (!stats.isSymbolicLink) {
        break;
      }

      // Get the immediate symlink target
      final target = fsImpl.readlinkSync(currentPath);

      // If target is relative, resolve it relative to the symlink's directory
      final absoluteTarget = p.isAbsolute(target)
          ? target
          : p.join(p.dirname(currentPath), target);

      // Add this intermediate target to the set
      pathSet.add(absoluteTarget);
      currentPath = absoluteTarget;
    }
  } catch (_) {
    // If anything fails during chain traversal, continue with what we have
  }

  // Also add the final resolved path using realpathSync for completeness
  final safeResult = safeResolvePath(fsImpl, path);
  if (safeResult.isSymlink && safeResult.resolvedPath != path) {
    pathSet.add(safeResult.resolvedPath);
  }

  return pathSet.toList();
}