1
2--- Utility module for loading files into tables and
3-- saving tables into files.
4local persist = {}
5
6local core = require("luarocks.core.persist")
7local util = require("luarocks.util")
8local dir = require("luarocks.dir")
9local fs = require("luarocks.fs")
10
11persist.run_file = core.run_file
12persist.load_into_table = core.load_into_table
13
14local write_table
15
16--- Write a value as Lua code.
17-- This function handles only numbers and strings, invoking write_table
18-- to write tables.
19-- @param out table or userdata: a writer object supporting :write() method.
20-- @param v: the value to be written.
21-- @param level number: the indentation level
22-- @param sub_order table: optional prioritization table
23-- @see write_table
24function persist.write_value(out, v, level, sub_order)
25   if type(v) == "table" then
26      level = level or 0
27      write_table(out, v, level + 1, sub_order)
28   elseif type(v) == "string" then
29      if v:match("[\r\n]") then
30         local open, close = "[[", "]]"
31         local equals = 0
32         local v_with_bracket = v.."]"
33         while v_with_bracket:find(close, 1, true) do
34            equals = equals + 1
35            local eqs = ("="):rep(equals)
36            open, close = "["..eqs.."[", "]"..eqs.."]"
37         end
38         out:write(open.."\n"..v..close)
39      else
40         out:write(("%q"):format(v))
41      end
42   else
43      out:write(tostring(v))
44   end
45end
46
47local is_valid_plain_key
48do
49   local keywords = {
50      ["and"] = true,
51      ["break"] = true,
52      ["do"] = true,
53      ["else"] = true,
54      ["elseif"] = true,
55      ["end"] = true,
56      ["false"] = true,
57      ["for"] = true,
58      ["function"] = true,
59      ["goto"] = true,
60      ["if"] = true,
61      ["in"] = true,
62      ["local"] = true,
63      ["nil"] = true,
64      ["not"] = true,
65      ["or"] = true,
66      ["repeat"] = true,
67      ["return"] = true,
68      ["then"] = true,
69      ["true"] = true,
70      ["until"] = true,
71      ["while"] = true,
72   }
73   function is_valid_plain_key(k)
74      return type(k) == "string"
75             and k:match("^[a-zA-Z_][a-zA-Z0-9_]*$")
76             and not keywords[k]
77   end
78end
79
80local function write_table_key_assignment(out, k, level)
81   if is_valid_plain_key(k) then
82      out:write(k)
83   else
84      out:write("[")
85      persist.write_value(out, k, level)
86      out:write("]")
87   end
88
89   out:write(" = ")
90end
91
92--- Write a table as Lua code in curly brackets notation to a writer object.
93-- Only numbers, strings and tables (containing numbers, strings
94-- or other recursively processed tables) are supported.
95-- @param out table or userdata: a writer object supporting :write() method.
96-- @param tbl table: the table to be written.
97-- @param level number: the indentation level
98-- @param field_order table: optional prioritization table
99write_table = function(out, tbl, level, field_order)
100   out:write("{")
101   local sep = "\n"
102   local indentation = "   "
103   local indent = true
104   local i = 1
105   for k, v, sub_order in util.sortedpairs(tbl, field_order) do
106      out:write(sep)
107      if indent then
108         for _ = 1, level do out:write(indentation) end
109      end
110
111      if k == i then
112         i = i + 1
113      else
114         write_table_key_assignment(out, k, level)
115      end
116
117      persist.write_value(out, v, level, sub_order)
118      if type(v) == "number" then
119         sep = ", "
120         indent = false
121      else
122         sep = ",\n"
123         indent = true
124      end
125   end
126   if sep ~= "\n" then
127      out:write("\n")
128      for _ = 1, level - 1 do out:write(indentation) end
129   end
130   out:write("}")
131end
132
133--- Write a table as series of assignments to a writer object.
134-- @param out table or userdata: a writer object supporting :write() method.
135-- @param tbl table: the table to be written.
136-- @param field_order table: optional prioritization table
137-- @return true if successful; nil and error message if failed.
138local function write_table_as_assignments(out, tbl, field_order)
139   for k, v, sub_order in util.sortedpairs(tbl, field_order) do
140      if not is_valid_plain_key(k) then
141         return nil, "cannot store '"..tostring(k).."' as a plain key."
142      end
143      out:write(k.." = ")
144      persist.write_value(out, v, 0, sub_order)
145      out:write("\n")
146   end
147   return true
148end
149
150--- Write a table using Lua table syntax to a writer object.
151-- @param out table or userdata: a writer object supporting :write() method.
152-- @param tbl table: the table to be written.
153local function write_table_as_table(out, tbl)
154   out:write("return {\n")
155   for k, v, sub_order in util.sortedpairs(tbl) do
156      out:write("   ")
157      write_table_key_assignment(out, k, 1)
158      persist.write_value(out, v, 1, sub_order)
159      out:write(",\n")
160   end
161   out:write("}\n")
162end
163
164--- Save the contents of a table to a string.
165-- Each element of the table is saved as a global assignment.
166-- Only numbers, strings and tables (containing numbers, strings
167-- or other recursively processed tables) are supported.
168-- @param tbl table: the table containing the data to be written
169-- @param field_order table: an optional array indicating the order of top-level fields.
170-- @return persisted data as string; or nil and an error message
171function persist.save_from_table_to_string(tbl, field_order)
172   local out = {buffer = {}}
173   function out:write(data) table.insert(self.buffer, data) end
174   local ok, err = write_table_as_assignments(out, tbl, field_order)
175   if not ok then
176      return nil, err
177   end
178   return table.concat(out.buffer)
179end
180
181--- Save the contents of a table in a file.
182-- Each element of the table is saved as a global assignment.
183-- Only numbers, strings and tables (containing numbers, strings
184-- or other recursively processed tables) are supported.
185-- @param filename string: the output filename
186-- @param tbl table: the table containing the data to be written
187-- @param field_order table: an optional array indicating the order of top-level fields.
188-- @return boolean or (nil, string): true if successful, or nil and a
189-- message in case of errors.
190function persist.save_from_table(filename, tbl, field_order)
191   local out = io.open(filename, "w")
192   if not out then
193      return nil, "Cannot create file at "..filename
194   end
195   local ok, err = write_table_as_assignments(out, tbl, field_order)
196   out:close()
197   if not ok then
198      return nil, err
199   end
200   return true
201end
202
203--- Save the contents of a table as a module.
204-- The module contains a 'return' statement that returns the table.
205-- Only numbers, strings and tables (containing numbers, strings
206-- or other recursively processed tables) are supported.
207-- @param filename string: the output filename
208-- @param tbl table: the table containing the data to be written
209-- @return boolean or (nil, string): true if successful, or nil and a
210-- message in case of errors.
211function persist.save_as_module(filename, tbl)
212   local out = io.open(filename, "w")
213   if not out then
214      return nil, "Cannot create file at "..filename
215   end
216   write_table_as_table(out, tbl)
217   out:close()
218   return true
219end
220
221function persist.load_config_file_if_basic(filename, cfg)
222   local env = {
223      home = cfg.home
224   }
225   local result, err, errcode = persist.load_into_table(filename, env)
226   if errcode == "load" or errcode == "run" then
227      -- bad config file or depends on env, so error out
228      return nil, "Could not read existing config file " .. filename
229   end
230
231   local tbl
232   if errcode == "open" then
233      -- could not open, maybe file does not exist
234      tbl = {}
235   else
236      tbl = result
237      tbl.home = nil
238   end
239
240   return tbl
241end
242
243function persist.save_default_lua_version(prefix, lua_version)
244   local ok, err = fs.make_dir(prefix)
245   if not ok then
246      return nil, err
247   end
248   local fd, err = io.open(dir.path(prefix, "default-lua-version.lua"), "w")
249   if not fd then
250      return nil, err
251   end
252   fd:write('return "' .. lua_version .. '"\n')
253   fd:close()
254   return true
255end
256
257return persist
258