Katkat

Path Resolution with Symbolic Links

AnthropicSoftware Engineer · Onsite

Last updated

Implement a filesystem path resolution function that normalizes paths containing '.', '..', and symbolic links while detecting cycles.

Problem Statement

Implement a function resolve_path(symlinks: dict[str, str], path: str) -> str | None that simulates filesystem path canonicalization (similar to POSIX realpath).

The filesystem model has the following rules:

  1. All paths are resolved starting from the root directory /.
  2. Path components are separated by /. Multiple consecutive slashes (e.g., //) should be treated as a single /.
  3. A component . refers to the current directory and can be ignored.
  4. A component .. refers to the parent directory. Going above / remains at /.
  5. symlinks is a dictionary where each key is a fully resolved, canonical absolute path pointing to a target path string. The target path may be absolute (e.g., /var/data) or relative (e.g., ../shared).
  6. When navigating path components from left to right, if the path resolved so far matches a key in symlinks, that prefix must be replaced by the symlink's target before processing continues:
    • If the target is an absolute path, resolution restarts from / with the new target.
    • If the target is a relative path, it resolves relative to the parent directory of the symlink.
  7. If a symbolic link loop (cycle) or excessive indirection (more than 40 symlink expansions) is encountered, return None.

The final returned path must be canonical: starting with /, containing no . or .., no trailing slashes (except for the root / itself), and having all symbolic links fully resolved.

Interface

def resolve_path(symlinks: dict[str, str], path: str) -> str | None:
    pass

Constraints

  • 1 <= len(path) <= 1000
  • 0 <= len(symlinks) <= 200
  • Keys and values in symlinks have lengths between 1 and 1000.
  • Path components consist of alphanumeric characters, underscores, hyphens, and dots.
  • The maximum number of symlink dereferences allowed during resolution is 40.

Examples

Example 1

symlinks = {
    "/usr/local/bin": "/opt/bin"
}
path = "/usr/local/bin/python"

# Output: "/opt/bin/python"
# Explanation:
# Resolving "/usr/local/bin" matches a symlink pointing to "/opt/bin".
# The remainder "python" is appended, yielding "/opt/bin/python".

Example 2

symlinks = {
    "/a/b": "../c",
    "/c": "/d"
}
path = "/a/b/test"

# Output: "/d/test"
# Explanation:
# 1. Navigating to "/a/b" triggers the symlink "../c".
# 2. "../c" relative to "/a/b"'s parent ("/a") resolves to "/c".
# 3. "/c" is also a symlink pointing to "/d".
# 4. Appending the remaining component "test" yields "/d/test".

Example 3

symlinks = {
    "/loop": "/loop/nested"
}
path = "/loop"

# Output: None
# Explanation: The symlink forms an infinite expansion loop.

Edge Cases

  • Root directory edge cases: resolve_path({}, "/../../..") should return "/".
  • Symlink targets that themselves contain . or .. or point to other symlinks.
  • Trailing slashes in the input path or symlink targets.

Hints

Hint 1

Process path components sequentially from left to right using a stack to keep track of the resolved canonical directory chain.

Hint 2

After resolving each component, check if the current canonical path prefix exists in the symlink dictionary.

Hint 3

When a symlink is encountered, replace the current path prefix with the target and re-prepend any remaining unprocessed path components; track total dereference count to catch cycles.

Follow-up questions

  • How would you optimize path lookups if symlinks formed a large, static tree queried millions of times?
  • How does POSIX path resolution handle dangling symlinks or permissions at intermediate path components?
  • How would you adapt your algorithm to run concurrently in a multi-threaded filesystem where symlinks can be modified while being resolved?