1--- Windows implementation of filesystem and platform abstractions.
2-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities
3-- used by this module.
4local win32 = {}
5
6local fs = require("luarocks.fs")
7
8local cfg = require("luarocks.core.cfg")
9local dir = require("luarocks.dir")
10local path = require("luarocks.path")
11local util = require("luarocks.util")
12
13-- Monkey patch io.popen and os.execute to make sure quoting
14-- works as expected.
15-- See http://lua-users.org/lists/lua-l/2013-11/msg00367.html
16local _prefix = "type NUL && "
17local _popen, _execute = io.popen, os.execute
18
19-- luacheck: push globals io os
20io.popen = function(cmd, ...) return _popen(_prefix..cmd, ...) end
21os.execute = function(cmd, ...) return _execute(_prefix..cmd, ...) end
22-- luacheck: pop
23
24--- Annotate command string for quiet execution.
25-- @param cmd string: A command-line string.
26-- @return string: The command-line, with silencing annotation.
27function win32.quiet(cmd)
28   return cmd.." 2> NUL 1> NUL"
29end
30
31--- Annotate command string for execution with quiet stderr.
32-- @param cmd string: A command-line string.
33-- @return string: The command-line, with stderr silencing annotation.
34function win32.quiet_stderr(cmd)
35   return cmd.." 2> NUL"
36end
37
38-- Split path into drive, root and the rest.
39-- Example: "c:\\hello\\world" becomes "c:" "\\" "hello\\world"
40-- if any part is missing from input, it becomes an empty string.
41local function split_root(pathname)
42   local drive = ""
43   local root = ""
44   local rest
45
46   local unquoted = pathname:match("^['\"](.*)['\"]$")
47   if unquoted then
48      pathname = unquoted
49   end
50
51   if pathname:match("^.:") then
52      drive = pathname:sub(1, 2)
53      pathname = pathname:sub(3)
54   end
55
56   if pathname:match("^[\\/]") then
57      root = pathname:sub(1, 1)
58      rest = pathname:sub(2)
59   else
60      rest = pathname
61   end
62
63   return drive, root, rest
64end
65
66--- Quote argument for shell processing. Fixes paths on Windows.
67-- Adds double quotes and escapes.
68-- @param arg string: Unquoted argument.
69-- @return string: Quoted argument.
70function win32.Q(arg)
71   assert(type(arg) == "string")
72   -- Use Windows-specific directory separator for paths.
73   -- Paths should be converted to absolute by now.
74   local drive, root, rest = split_root(arg)
75   if root ~= "" then
76      arg = arg:gsub("/", "\\")
77   end
78   if arg == "\\" then
79      return '\\' -- CHDIR needs special handling for root dir
80   end
81   -- URLs and anything else
82   arg = arg:gsub('\\(\\*)"', '\\%1%1"')
83   arg = arg:gsub('\\+$', '%0%0')
84   arg = arg:gsub('"', '\\"')
85   arg = arg:gsub('(\\*)%%', '%1%1"%%"')
86   return '"' .. arg .. '"'
87end
88
89--- Quote argument for shell processing in batch files.
90-- Adds double quotes and escapes.
91-- @param arg string: Unquoted argument.
92-- @return string: Quoted argument.
93function win32.Qb(arg)
94   assert(type(arg) == "string")
95   -- Use Windows-specific directory separator for paths.
96   -- Paths should be converted to absolute by now.
97   local drive, root, rest = split_root(arg)
98   if root ~= "" then
99      arg = arg:gsub("/", "\\")
100   end
101   if arg == "\\" then
102      return '\\' -- CHDIR needs special handling for root dir
103   end
104   -- URLs and anything else
105   arg = arg:gsub('\\(\\*)"', '\\%1%1"')
106   arg = arg:gsub('\\+$', '%0%0')
107   arg = arg:gsub('"', '\\"')
108   arg = arg:gsub('%%', '%%%%')
109   return '"' .. arg .. '"'
110end
111
112--- Return an absolute pathname from a potentially relative one.
113-- @param pathname string: pathname to convert.
114-- @param relative_to string or nil: path to prepend when making
115-- pathname absolute, or the current dir in the dir stack if
116-- not given.
117-- @return string: The pathname converted to absolute.
118function win32.absolute_name(pathname, relative_to)
119   assert(type(pathname) == "string")
120   assert(type(relative_to) == "string" or not relative_to)
121
122   relative_to = (relative_to or fs.current_dir()):gsub("[\\/]*$", "")
123   local drive, root, rest = split_root(pathname)
124   if root:match("[\\/]$") then
125      -- It's an absolute path already. Ensure is not quoted.
126      return drive .. root .. rest
127   else
128      -- It's a relative path, join it with base path.
129      -- This drops drive letter from paths like "C:foo".
130      return relative_to .. "/" .. rest
131   end
132end
133
134--- Return the root directory for the given path.
135-- For example, for "c:\hello", returns "c:\"
136-- @param pathname string: pathname to use.
137-- @return string: The root of the given pathname.
138function win32.root_of(pathname)
139   local drive, root, rest = split_root(fs.absolute_name(pathname))
140   return drive .. root
141end
142
143--- Create a wrapper to make a script executable from the command-line.
144-- @param script string: Pathname of script to be made executable.
145-- @param target string: wrapper target pathname (without wrapper suffix).
146-- @param name string: rock name to be used in loader context.
147-- @param version string: rock version to be used in loader context.
148-- @return boolean or (nil, string): True if succeeded, or nil and
149-- an error message.
150function win32.wrap_script(script, target, deps_mode, name, version, ...)
151   assert(type(script) == "string" or not script)
152   assert(type(target) == "string")
153   assert(type(deps_mode) == "string")
154   assert(type(name) == "string" or not name)
155   assert(type(version) == "string" or not version)
156
157   local batname = target .. ".bat"
158   local wrapper = io.open(batname, "wb")
159   if not wrapper then
160      return nil, "Could not open "..batname.." for writing."
161   end
162
163   local lpath, lcpath = path.package_paths(deps_mode)
164
165   local luainit = {
166      "package.path="..util.LQ(lpath..";").."..package.path",
167      "package.cpath="..util.LQ(lcpath..";").."..package.cpath",
168   }
169
170   local remove_interpreter = false
171   if target == "luarocks" or target == "luarocks-admin" then
172      if cfg.is_binary then
173         remove_interpreter = true
174      end
175      luainit = {
176         "package.path="..util.LQ(package.path),
177         "package.cpath="..util.LQ(package.cpath),
178      }
179   end
180
181   if name and version then
182      local addctx = "local k,l,_=pcall(require,'luarocks.loader') _=k " ..
183                     "and l.add_context('"..name.."','"..version.."')"
184      table.insert(luainit, addctx)
185   end
186
187   local argv = {
188      fs.Qb(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)),
189      "-e",
190      fs.Qb(table.concat(luainit, ";")),
191      script and fs.Qb(script) or "%I%",
192      ...
193   }
194   if remove_interpreter then
195      table.remove(argv, 1)
196      table.remove(argv, 1)
197      table.remove(argv, 1)
198   end
199
200   wrapper:write("@echo off\r\n")
201   wrapper:write("setlocal\r\n")
202   if not script then
203      wrapper:write([[IF "%*"=="" (set I=-i) ELSE (set I=)]] .. "\r\n")
204   end
205   wrapper:write("set "..fs.Qb("LUAROCKS_SYSCONFDIR="..cfg.sysconfdir) .. "\r\n")
206   wrapper:write(table.concat(argv, " ") .. " %*\r\n")
207   wrapper:write("exit /b %ERRORLEVEL%\r\n")
208   wrapper:close()
209   return true
210end
211
212function win32.is_actual_binary(name)
213   name = name:lower()
214   if name:match("%.bat$") or name:match("%.exe$") then
215      return true
216   end
217   return false
218end
219
220function win32.copy_binary(filename, dest)
221   local ok, err = fs.copy(filename, dest)
222   if not ok then
223      return nil, err
224   end
225   local exe_pattern = "%.[Ee][Xx][Ee]$"
226   local base = dir.base_name(filename)
227   dest = dir.dir_name(dest)
228   if base:match(exe_pattern) then
229      base = base:gsub(exe_pattern, ".lua")
230      local helpname = dest.."/"..base
231      local helper = io.open(helpname, "w")
232      if not helper then
233         return nil, "Could not open "..helpname.." for writing."
234      end
235      helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n')
236      helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n')
237      helper:close()
238   end
239   return true
240end
241
242--- Move a file on top of the other.
243-- The new file ceases to exist under its original name,
244-- and takes over the name of the old file.
245-- On Windows this is done by removing the original file and
246-- renaming the new file to its original name.
247-- @param old_file The name of the original file,
248-- which will be the new name of new_file.
249-- @param new_file The name of the new file,
250-- which will replace old_file.
251-- @return boolean or (nil, string): True if succeeded, or nil and
252-- an error message.
253function win32.replace_file(old_file, new_file)
254   os.remove(old_file)
255   return os.rename(new_file, old_file)
256end
257
258function win32.is_dir(file)
259   file = fs.absolute_name(file)
260   file = dir.normalize(file)
261   local fd, _, code = io.open(file, "r")
262   if code == 13 then -- directories return "Permission denied"
263      fd, _, code = io.open(file .. "\\", "r")
264      if code == 2 then -- directories return 2, files return 22
265         return true
266      end
267   end
268   if fd then
269      fd:close()
270   end
271   return false
272end
273
274function win32.is_file(file)
275   file = fs.absolute_name(file)
276   file = dir.normalize(file)
277   local fd, _, code = io.open(file, "r")
278   if code == 13 then -- if "Permission denied"
279      fd, _, code = io.open(file .. "\\", "r")
280      if code == 2 then -- directories return 2, files return 22
281         return false
282      elseif code == 22 then
283         return true
284      end
285   end
286   if fd then
287      fd:close()
288      return true
289   end
290   return false
291end
292
293--- Test is file/dir is writable.
294-- Warning: testing if a file/dir is writable does not guarantee
295-- that it will remain writable and therefore it is no replacement
296-- for checking the result of subsequent operations.
297-- @param file string: filename to test
298-- @return boolean: true if file exists, false otherwise.
299function win32.is_writable(file)
300   assert(file)
301   file = dir.normalize(file)
302   local result
303   local tmpname = 'tmpluarockstestwritable.deleteme'
304   if fs.is_dir(file) then
305      local file2 = dir.path(file, tmpname)
306      local fh = io.open(file2, 'wb')
307      result = fh ~= nil
308      if fh then fh:close() end
309      if result then
310         -- the above test might give a false positive when writing to
311         -- c:\program files\ because of VirtualStore redirection on Vista and up
312         -- So check whether it's really there
313         result = fs.exists(file2)
314      end
315      os.remove(file2)
316   else
317      local fh = io.open(file, 'r+b')
318      result = fh ~= nil
319      if fh then fh:close() end
320   end
321   return result
322end
323
324--- Create a temporary directory.
325-- @param name_pattern string: name pattern to use for avoiding conflicts
326-- when creating temporary directory.
327-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure.
328function win32.make_temp_dir(name_pattern)
329   assert(type(name_pattern) == "string")
330   name_pattern = dir.normalize(name_pattern)
331
332   local temp_dir = os.getenv("TMP") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-" .. tostring(math.floor(math.random() * 10000))
333   local ok, err = fs.make_dir(temp_dir)
334   if ok then
335      return temp_dir
336   else
337      return nil, err
338   end
339end
340
341function win32.tmpname()
342   local name = os.tmpname()
343   local tmp = os.getenv("TMP")
344   if tmp and name:sub(1, #tmp) ~= tmp then
345      name = (tmp .. "\\" .. name):gsub("\\+", "\\")
346   end
347   return name
348end
349
350function win32.current_user()
351   return os.getenv("USERNAME")
352end
353
354function win32.is_superuser()
355   return false
356end
357
358function win32.export_cmd(var, val)
359   return ("SET %s=%s"):format(var, val)
360end
361
362function win32.system_cache_dir()
363   return dir.path(fs.system_temp_dir(), "cache")
364end
365
366return win32
367