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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
|
--- Functions related to fetching and loading local and remote files.
local fetch = {}
local fs = require("luarocks.fs")
local dir = require("luarocks.dir")
local rockspecs = require("luarocks.rockspecs")
local signing = require("luarocks.signing")
local persist = require("luarocks.persist")
local util = require("luarocks.util")
local cfg = require("luarocks.core.cfg")
--- Fetch a local or remote file, using a local cache directory.
-- Make a remote or local URL/pathname local, fetching the file if necessary.
-- Other "fetch" and "load" functions use this function to obtain files.
-- If a local pathname is given, it is returned as a result.
-- @param url string: a local pathname or a remote URL.
-- @param mirroring string: mirroring mode.
-- If set to "no_mirror", then rocks_servers mirror configuration is not used.
-- @return (string, nil, nil, boolean) or (nil, string, [string]):
-- in case of success:
-- * the absolute local pathname for the fetched file
-- * nil
-- * nil
-- * `true` if the file was fetched from cache
-- in case of failure:
-- * nil
-- * an error message
-- * an optional error code.
function fetch.fetch_caching(url, mirroring)
local repo_url, filename = url:match("^(.*)/([^/]+)$")
local name = repo_url:gsub("[/:]","_")
local cache_dir = dir.path(cfg.local_cache, name)
local ok = fs.make_dir(cache_dir)
local cachefile = dir.path(cache_dir, filename)
local checkfile = cachefile .. ".check"
if (fs.file_age(checkfile) < 10 or
cfg.aggressive_cache and (not name:match("^manifest"))) and fs.exists(cachefile)
then
return cachefile, nil, nil, true
end
local lock, errlock
if ok then
lock, errlock = fs.lock_access(cache_dir)
end
if not (ok and lock) then
cfg.local_cache = fs.make_temp_dir("local_cache")
if not cfg.local_cache then
return nil, "Failed creating temporary local_cache directory"
end
cache_dir = dir.path(cfg.local_cache, name)
ok = fs.make_dir(cache_dir)
if not ok then
return nil, "Failed creating temporary cache directory "..cache_dir
end
lock = fs.lock_access(cache_dir)
end
local file, err, errcode, from_cache = fetch.fetch_url(url, cachefile, true, mirroring)
if not file then
fs.unlock_access(lock)
return nil, err or "Failed downloading "..url, errcode
end
local fd, err = io.open(checkfile, "wb")
if err then
fs.unlock_access(lock)
return nil, err
end
fd:write("!")
fd:close()
fs.unlock_access(lock)
return file, nil, nil, from_cache
end
local function ensure_trailing_slash(url)
return (url:gsub("/*$", "/"))
end
local function is_url_relative_to_rocks_servers(url, servers)
for _, item in ipairs(servers) do
if type(item) == "table" then
for i, s in ipairs(item) do
local base = ensure_trailing_slash(s)
if string.find(url, base, 1, true) == 1 then
return i, url:sub(#base + 1), item
end
end
end
end
end
local function download_with_mirrors(url, filename, cache, servers)
local idx, rest, mirrors = is_url_relative_to_rocks_servers(url, servers)
if not idx then
-- URL is not from a rock server
return fs.download(url, filename, cache)
end
-- URL is from a rock server: try to download it falling back to mirrors.
local err = "\n"
for i = idx, #mirrors do
local try_url = ensure_trailing_slash(mirrors[i]) .. rest
if i > idx then
util.warning("Failed downloading. Attempting mirror at " .. try_url)
end
local ok, name, from_cache = fs.download(try_url, filename, cache)
if ok then
return ok, name, from_cache
else
err = err .. name .. "\n"
end
end
return nil, err, "network"
end
--- Fetch a local or remote file.
-- Make a remote or local URL/pathname local, fetching the file if necessary.
-- Other "fetch" and "load" functions use this function to obtain files.
-- If a local pathname is given, it is returned as a result.
-- @param url string: a local pathname or a remote URL.
-- @param filename string or nil: this function attempts to detect the
-- resulting local filename of the remote file as the basename of the URL;
-- if that is not correct (due to a redirection, for example), the local
-- filename can be given explicitly as this second argument.
-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to
-- re-downloading the file.
-- @param mirroring string: mirroring mode.
-- If set to "no_mirror", then rocks_servers mirror configuration is not used.
-- @return (string, nil, nil, boolean) or (nil, string, [string]):
-- in case of success:
-- * the absolute local pathname for the fetched file
-- * nil
-- * nil
-- * `true` if the file was fetched from cache
-- in case of failure:
-- * nil
-- * an error message
-- * an optional error code.
function fetch.fetch_url(url, filename, cache, mirroring)
assert(type(url) == "string")
assert(type(filename) == "string" or not filename)
local protocol, pathname = dir.split_url(url)
if protocol == "file" then
local fullname = fs.absolute_name(pathname)
if not fs.exists(fullname) then
local hint = (not pathname:match("^/"))
and (" - note that given path in rockspec is not absolute: " .. url)
or ""
return nil, "Local file not found: " .. fullname .. hint
end
filename = filename or dir.base_name(pathname)
local dstname = fs.absolute_name(dir.path(".", filename))
local ok, err
if fullname == dstname then
ok = true
else
ok, err = fs.copy(fullname, dstname)
end
if ok then
return dstname
else
return nil, "Failed copying local file " .. fullname .. " to " .. dstname .. ": " .. err
end
elseif dir.is_basic_protocol(protocol) then
local ok, name, from_cache
if mirroring ~= "no_mirror" then
ok, name, from_cache = download_with_mirrors(url, filename, cache, cfg.rocks_servers)
else
ok, name, from_cache = fs.download(url, filename, cache)
end
if not ok then
return nil, "Failed downloading "..url..(name and " - "..name or ""), from_cache
end
return name, nil, nil, from_cache
else
return nil, "Unsupported protocol "..protocol
end
end
--- For remote URLs, create a temporary directory and download URL inside it.
-- This temporary directory will be deleted on program termination.
-- For local URLs, just return the local pathname and its directory.
-- @param url string: URL to be downloaded
-- @param tmpname string: name pattern to use for avoiding conflicts
-- when creating temporary directory.
-- @param filename string or nil: local filename of URL to be downloaded,
-- in case it can't be inferred from the URL.
-- @return (string, string) or (nil, string, [string]): absolute local pathname of
-- the fetched file and temporary directory name; or nil and an error message
-- followed by an optional error code
function fetch.fetch_url_at_temp_dir(url, tmpname, filename, cache)
assert(type(url) == "string")
assert(type(tmpname) == "string")
assert(type(filename) == "string" or not filename)
filename = filename or dir.base_name(url)
local protocol, pathname = dir.split_url(url)
if protocol == "file" then
if fs.exists(pathname) then
return pathname, dir.dir_name(fs.absolute_name(pathname))
else
return nil, "File not found: "..pathname
end
else
local temp_dir, err = fs.make_temp_dir(tmpname)
if not temp_dir then
return nil, "Failed creating temporary directory "..tmpname..": "..err
end
util.schedule_function(fs.delete, temp_dir)
local ok, err = fs.change_dir(temp_dir)
if not ok then return nil, err end
local file, err, errcode
if cache then
local cachefile
cachefile, err, errcode = fetch.fetch_caching(url)
if cachefile then
file = dir.path(temp_dir, filename)
fs.copy(cachefile, file)
end
end
if not file then
file, err, errcode = fetch.fetch_url(url, filename, cache)
end
fs.pop_dir()
if not file then
return nil, "Error fetching file: "..err, errcode
end
return file, temp_dir
end
end
-- Determine base directory of a fetched URL by extracting its
-- archive and looking for a directory in the root.
-- @param file string: absolute local pathname of the fetched file
-- @param temp_dir string: temporary directory in which URL was fetched.
-- @param src_url string: URL to use when inferring base directory.
-- @param src_dir string or nil: expected base directory (inferred
-- from src_url if not given).
-- @return (string, string) or (string, nil) or (nil, string):
-- The inferred base directory and the one actually found (which may
-- be nil if not found), or nil followed by an error message.
-- The inferred dir is returned first to avoid confusion with errors,
-- because it is never nil.
function fetch.find_base_dir(file, temp_dir, src_url, src_dir)
local ok, err = fs.change_dir(temp_dir)
if not ok then return nil, err end
fs.unpack_archive(file)
if not src_dir then
local rockspec = {
source = {
file = file,
dir = src_dir,
url = src_url,
}
}
ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
if ok then
src_dir = rockspec.source.dir
end
end
local inferred_dir = src_dir or dir.deduce_base_dir(src_url)
local found_dir = nil
if fs.exists(inferred_dir) then
found_dir = inferred_dir
else
util.printerr("Directory "..inferred_dir.." not found")
local files = fs.list_dir()
if files then
table.sort(files)
for i,filename in ipairs(files) do
if fs.is_dir(filename) then
util.printerr("Found "..filename)
found_dir = filename
break
end
end
end
end
fs.pop_dir()
return inferred_dir, found_dir
end
local function fetch_and_verify_signature_for(url, filename, tmpdir)
local sig_url = signing.signature_url(url)
local sig_file, err, errcode = fetch.fetch_url_at_temp_dir(sig_url, tmpdir)
if not sig_file then
return nil, "Could not fetch signature file for verification: " .. err, errcode
end
local ok, err = signing.verify_signature(filename, sig_file)
if not ok then
return nil, "Failed signature verification: " .. err
end
return fs.absolute_name(sig_file)
end
--- Obtain a rock and unpack it.
-- If a directory is not given, a temporary directory will be created,
-- which will be deleted on program termination.
-- @param rock_file string: URL or filename of the rock.
-- @param dest string or nil: if given, directory will be used as
-- a permanent destination.
-- @param verify boolean: if true, download and verify signature for rockspec
-- @return string or (nil, string, [string]): the directory containing the contents
-- of the unpacked rock.
function fetch.fetch_and_unpack_rock(url, dest, verify)
assert(type(url) == "string")
assert(type(dest) == "string" or not dest)
local name = dir.base_name(url):match("(.*)%.[^.]*%.rock")
local tmpname = "luarocks-rock-" .. name
local rock_file, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true)
if not rock_file then
return nil, "Could not fetch rock file: " .. err, errcode
end
local sig_file
if verify then
sig_file, err = fetch_and_verify_signature_for(url, rock_file, tmpname)
if err then
return nil, err
end
end
rock_file = fs.absolute_name(rock_file)
local unpack_dir
if dest then
unpack_dir = dest
local ok, err = fs.make_dir(unpack_dir)
if not ok then
return nil, "Failed unpacking rock file: " .. err
end
else
unpack_dir, err = fs.make_temp_dir(name)
if not unpack_dir then
return nil, "Failed creating temporary dir: " .. err
end
end
if not dest then
util.schedule_function(fs.delete, unpack_dir)
end
local ok, err = fs.change_dir(unpack_dir)
if not ok then return nil, err end
ok, err = fs.unzip(rock_file)
if not ok then
return nil, "Failed unpacking rock file: " .. rock_file .. ": " .. err
end
if sig_file then
ok, err = fs.copy(sig_file, ".")
if not ok then
return nil, "Failed copying signature file"
end
end
fs.pop_dir()
return unpack_dir
end
--- Back-end function that actually loads the local rockspec.
-- Performs some validation and postprocessing of the rockspec contents.
-- @param rel_filename string: The local filename of the rockspec file.
-- @param quick boolean: if true, skips some steps when loading
-- rockspec.
-- @return table or (nil, string): A table representing the rockspec
-- or nil followed by an error message.
function fetch.load_local_rockspec(rel_filename, quick)
assert(type(rel_filename) == "string")
local abs_filename = fs.absolute_name(rel_filename)
local basename = dir.base_name(abs_filename)
if basename ~= "rockspec" then
if not basename:match("(.*)%-[^-]*%-[0-9]*") then
return nil, "Expected filename in format 'name-version-revision.rockspec'."
end
end
local tbl, err = persist.load_into_table(abs_filename)
if not tbl then
return nil, "Could not load rockspec file "..abs_filename.." ("..err..")"
end
local rockspec, err = rockspecs.from_persisted_table(abs_filename, tbl, err, quick)
if not rockspec then
return nil, abs_filename .. ": " .. err
end
local name_version = rockspec.package:lower() .. "-" .. rockspec.version
if basename ~= "rockspec" and basename ~= name_version .. ".rockspec" then
return nil, "Inconsistency between rockspec filename ("..basename..") and its contents ("..name_version..".rockspec)."
end
return rockspec
end
--- Load a local or remote rockspec into a table.
-- This is the entry point for the LuaRocks tools.
-- Only the LuaRocks runtime loader should use
-- load_local_rockspec directly.
-- @param filename string: Local or remote filename of a rockspec.
-- @param location string or nil: Where to download. If not given,
-- a temporary dir is created.
-- @param verify boolean: if true, download and verify signature for rockspec
-- @return table or (nil, string, [string]): A table representing the rockspec
-- or nil followed by an error message and optional error code.
function fetch.load_rockspec(url, location, verify)
assert(type(url) == "string")
local name
local basename = dir.base_name(url)
if basename == "rockspec" then
name = "rockspec"
else
name = basename:match("(.*)%.rockspec")
if not name then
return nil, "Filename '"..url.."' does not look like a rockspec."
end
end
local tmpname = "luarocks-rockspec-"..name
local filename, err, errcode
if location then
local ok, err = fs.change_dir(location)
if not ok then return nil, err end
filename, err = fetch.fetch_url(url)
fs.pop_dir()
else
filename, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true)
end
if not filename then
return nil, err, errcode
end
if verify then
local _, err = fetch_and_verify_signature_for(url, filename, tmpname)
if err then
return nil, err
end
end
return fetch.load_local_rockspec(filename)
end
--- Download sources for building a rock using the basic URL downloader.
-- @param rockspec table: The rockspec table
-- @param extract boolean: Whether to extract the sources from
-- the fetched source tarball or not.
-- @param dest_dir string or nil: If set, will extract to the given directory;
-- if not given, will extract to a temporary directory.
-- @return (string, string) or (nil, string, [string]): The absolute pathname of
-- the fetched source tarball and the temporary directory created to
-- store it; or nil and an error message and optional error code.
function fetch.get_sources(rockspec, extract, dest_dir)
assert(rockspec:type() == "rockspec")
assert(type(extract) == "boolean")
assert(type(dest_dir) == "string" or not dest_dir)
local url = rockspec.source.url
local name = rockspec.name.."-"..rockspec.version
local filename = rockspec.source.file
local source_file, store_dir
local ok, err, errcode
if dest_dir then
ok, err = fs.change_dir(dest_dir)
if not ok then return nil, err, "dest_dir" end
source_file, err, errcode = fetch.fetch_url(url, filename)
fs.pop_dir()
store_dir = dest_dir
else
source_file, store_dir, errcode = fetch.fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename)
end
if not source_file then
return nil, err or store_dir, errcode
end
if rockspec.source.md5 then
if not fs.check_md5(source_file, rockspec.source.md5) then
return nil, "MD5 check for "..filename.." has failed.", "md5"
end
end
if extract then
local ok, err = fs.change_dir(store_dir)
if not ok then return nil, err end
ok, err = fs.unpack_archive(rockspec.source.file)
if not ok then return nil, err end
ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
if not ok then return nil, err end
fs.pop_dir()
end
return source_file, store_dir
end
function fetch.find_rockspec_source_dir(rockspec, store_dir)
local ok, err = fs.change_dir(store_dir)
if not ok then return nil, err end
local file_count, dir_count, found_dir = 0, 0, 0
if rockspec.source.dir and fs.exists(rockspec.source.dir) then
ok, err = true, nil
elseif rockspec.source.file and rockspec.source.dir then
ok, err = nil, "Directory "..rockspec.source.dir.." not found inside archive "..rockspec.source.file
elseif not rockspec.source.dir_set then -- and rockspec:format_is_at_least("3.0") then
local name = dir.base_name(rockspec.source.file or rockspec.source.url or "")
if name:match("%.lua$") or name:match("%.c$") then
if fs.is_file(name) then
rockspec.source.dir = "."
ok, err = true, nil
end
end
if not rockspec.source.dir then
for file in fs.dir() do
file_count = file_count + 1
if fs.is_dir(file) then
dir_count = dir_count + 1
found_dir = file
end
end
if dir_count == 1 then
rockspec.source.dir = found_dir
ok, err = true, nil
else
ok, err = nil, "Could not determine source directory from rock contents (" .. tostring(file_count).." file(s), "..tostring(dir_count).." dir(s))"
end
end
else
ok, err = nil, "Could not determine source directory, please set source.dir in rockspec."
end
fs.pop_dir()
assert(rockspec.source.dir or not ok)
return ok, err
end
--- Download sources for building a rock, calling the appropriate protocol method.
-- @param rockspec table: The rockspec table
-- @param extract boolean: When downloading compressed formats, whether to extract
-- the sources from the fetched archive or not.
-- @param dest_dir string or nil: If set, will extract to the given directory.
-- if not given, will extract to a temporary directory.
-- @return (string, string) or (nil, string): The absolute pathname of
-- the fetched source tarball and the temporary directory created to
-- store it; or nil and an error message.
function fetch.fetch_sources(rockspec, extract, dest_dir)
assert(rockspec:type() == "rockspec")
assert(type(extract) == "boolean")
assert(type(dest_dir) == "string" or not dest_dir)
-- auto-convert git://github.com URLs to use git+https
-- see https://github.blog/2021-09-01-improving-git-protocol-security-github/
if rockspec.source.url:match("^git://github%.com/")
or rockspec.source.url:match("^git://www%.github%.com/") then
rockspec.source.url = rockspec.source.url:gsub("^git://", "git+https://")
rockspec.source.protocol = "git+https"
end
local protocol = rockspec.source.protocol
local ok, err, proto
if dir.is_basic_protocol(protocol) then
proto = fetch
else
ok, proto = pcall(require, "luarocks.fetch."..protocol:gsub("[+-]", "_"))
if not ok then
return nil, "Unknown protocol "..protocol
end
end
if cfg.only_sources_from
and rockspec.source.pathname
and #rockspec.source.pathname > 0 then
if #cfg.only_sources_from == 0 then
return nil, "Can't download "..rockspec.source.url.." -- download from remote servers disabled"
elseif rockspec.source.pathname:find(cfg.only_sources_from, 1, true) ~= 1 then
return nil, "Can't download "..rockspec.source.url.." -- only downloading from "..cfg.only_sources_from
end
end
local source_file, store_dir = proto.get_sources(rockspec, extract, dest_dir)
if not source_file then return nil, store_dir end
ok, err = fetch.find_rockspec_source_dir(rockspec, store_dir)
if not ok then return nil, err, "source.dir", source_file, store_dir end
return source_file, store_dir
end
return fetch
|