summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/luaotfload/luaotfload-realpath.lua
blob: 34f60f4d82e36b7d96983fd01284c845c899bcf8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
-- Helper to resolve symlinks in paths

local sub = string.sub
local symlinkattributes = lfs.symlinkattributes
local attributes = lfs.attributes
local currentdir = lfs.currentdir
local concat = table.concat
local move = table.move
local newtable = lua.newtable
local setmetatable = setmetatable

-- Windows should just adapt reasonable path conventions like every other operating system.
-- But some people like to do their own thing, so we need lots of special casing here.
local windows_style_paths = os.type ~= 'unix'

-- Marker key for elements of result_tree to indicate the path components entry and the file mode
local path_components, file_mode = {}, {}
local tree_root

local split_path do
  local l = lpeg
  local separator = windows_style_paths and l.S'/\\' or l.S'/'
  -- We do not allow empty segments here because they get ignored.
  local segment = l.C((1 - separator)^1)
  -- Duplicate and trailing separators are dropped.
  local unc = windows_style_paths and separator * separator * l.Cg(l.P(1)^0 * -1, 'unc') or l.P(false)
  local drive_letter = windows_style_paths and l.Cg(l.R('az', 'AZ') * ':', 'drive') or l.P(false)
  local path_pat = l.Ct(unc + drive_letter^-1 * (l.Cc'' * separator^1)^-1 * (segment * separator^1)^0 * segment^-1 * -1)
  function split_path(path)
    local splitted = path_pat:match(path)
    if not splitted then
      error'Invalid path rejected'
    elseif splitted.unc then
      error'Unsupported UNC path encountered'
    elseif splitted.drive and splitted[1] ~= '' then
      error'Unsupported relative path with drive letter encountered'
    end
    return splitted
  end
end

local function recombine_path(components)
  local joined = concat(components, '/')
  if components.drive then
    joined = components.drive .. joined
  end
  return joined
end

local function lookup_split_path_in_tree(components, tree)
  if components[1] == '' then
    if windows_style_paths then
      tree = tree_root[components.drive or tree[path_components].drive]
    else
      tree = tree_root
    end
  end
  for i=1, #components do
    local next_tree = tree[components[i]]
    if not next_tree then
      return nil, string.format("Unable to find %q in %q", components[i], recombine_path(tree[path_components]))
    end
    tree = next_tree
  end
  return tree
end

local tree_meta
tree_meta = {
  __index = function(parent, component)
    local parent_components = parent[path_components]
    local depth = #parent_components
    local components = move(parent_components, 1, depth, 1, newtable(depth + 1, 1))
    components.drive, components[depth + 1] = parent_components.drive, component
    local path = recombine_path(components)

    local mode = symlinkattributes(path, 'mode')
    if not mode then
      parent[component] = false
      return false
    end
    if mode == 'link' then
      local target = symlinkattributes(path, 'target')
      local splitted_target = split_path(target)
      local target_tree = lookup_split_path_in_tree(splitted_target, parent) or false
      parent[component] = target_tree
      return target_tree
    end

    local child = {
      [path_components] = components,
      [file_mode] = mode,
    }
    if mode == 'directory' then
      setmetatable(child, tree_meta)
      child['.'] = child
      child['..'] = parent
    end
    parent[component] = child
    return child
  end,
}

-- We assume that the directory structure does not change during our run.
function build_root_dir(drive)
  local root_dir = setmetatable({
    [path_components] = {'', drive = drive},
    [file_mode] = 'directory', -- "If [your root is not a directory] you are having a bad problem and you will not go to space today".
  }, tree_meta)
  root_dir['.'] = root_dir
  root_dir['..'] = root_dir
  return {[''] = root_dir}
end
tree_root = windows_style_paths and setmetatable({}, {__index = function(t, drive)
  local root_dir = build_root_dir(drive)
  t[drive] = root_dir
  return root_dir
end}) or build_root_dir()

local function resolve_path_to_tree(path)
  local splitted = split_path(path)
  if splitted[1] == '' and (not windows_style_paths or splitted.drive) then -- Optimization to avoid currentdir lookup.
    return lookup_split_path_in_tree(splitted, tree_root)
  else
    local splitted_currentdir = split_path(currentdir())
    local current_tree = assert(lookup_split_path_in_tree(splitted_currentdir, tree_root))
    return lookup_split_path_in_tree(splitted, current_tree)
  end
end

local function resolve_path(path)
  local tree, err = resolve_path_to_tree(path)
  if not tree then return tree, err end
  return recombine_path(tree[path_components]), tree[file_mode]
end

return {
  realpath = resolve_path,
}