aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorV1K1NGbg <victor@ilchev.com>2024-08-03 14:56:20 +0300
committerV1K1NGbg <victor@ilchev.com>2024-08-05 20:51:31 +0300
commit417107d9d9661d4ab0ba6a63239374b200814fce (patch)
tree3d75b5e81a02c15ef61bb735432d85ee314e8bad
parenta22cc0d426e52254cca0ed623f1514b8391b48b3 (diff)
downloadluarocks-417107d9d9661d4ab0ba6a63239374b200814fce.tar.gz
luarocks-417107d9d9661d4ab0ba6a63239374b200814fce.tar.bz2
luarocks-417107d9d9661d4ab0ba6a63239374b200814fce.zip
patch
-rw-r--r--src/luarocks/tools/patch-original.lua716
-rw-r--r--src/luarocks/tools/patch.lua1235
-rw-r--r--src/luarocks/tools/patch.tl19
3 files changed, 1363 insertions, 607 deletions
diff --git a/src/luarocks/tools/patch-original.lua b/src/luarocks/tools/patch-original.lua
new file mode 100644
index 00000000..6f36d713
--- /dev/null
+++ b/src/luarocks/tools/patch-original.lua
@@ -0,0 +1,716 @@
1--- Patch utility to apply unified diffs.
2--
3-- http://lua-users.org/wiki/LuaPatch
4--
5-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license).
6-- Code is heavily based on the Python-based patch.py version 8.06-1
7-- Copyright (c) 2008 rainforce.org, MIT License
8-- Project home: http://code.google.com/p/python-patch/ .
9-- Version 0.1
10
11local patch = {}
12
13local fs = require("luarocks.fs")
14local fun = require("luarocks.fun")
15
16local io = io
17local os = os
18local string = string
19local table = table
20local format = string.format
21
22-- logging
23local debugmode = false
24local function debug(_) end
25local function info(_) end
26local function warning(s) io.stderr:write(s .. '\n') end
27
28-- Returns boolean whether string s2 starts with string s.
29local function startswith(s, s2)
30 return s:sub(1, #s2) == s2
31end
32
33-- Returns boolean whether string s2 ends with string s.
34local function endswith(s, s2)
35 return #s >= #s2 and s:sub(#s-#s2+1) == s2
36end
37
38-- Returns string s after filtering out any new-line characters from end.
39local function endlstrip(s)
40 return s:gsub('[\r\n]+$', '')
41end
42
43-- Returns shallow copy of table t.
44local function table_copy(t)
45 local t2 = {}
46 for k,v in pairs(t) do t2[k] = v end
47 return t2
48end
49
50local function exists(filename)
51 local fh = io.open(filename)
52 local result = fh ~= nil
53 if fh then fh:close() end
54 return result
55end
56local function isfile() return true end --FIX?
57
58local function string_as_file(s)
59 return {
60 at = 0,
61 str = s,
62 len = #s,
63 eof = false,
64 read = function(self, n)
65 if self.eof then return nil end
66 local chunk = self.str:sub(self.at, self.at + n - 1)
67 self.at = self.at + n
68 if self.at > self.len then
69 self.eof = true
70 end
71 return chunk
72 end,
73 close = function(self)
74 self.eof = true
75 end,
76 }
77end
78
79--
80-- file_lines(f) is similar to f:lines() for file f.
81-- The main difference is that read_lines includes
82-- new-line character sequences ("\n", "\r\n", "\r"),
83-- if any, at the end of each line. Embedded "\0" are also handled.
84-- Caution: The newline behavior can depend on whether f is opened
85-- in binary or ASCII mode.
86-- (file_lines - version 20080913)
87--
88local function file_lines(f)
89 local CHUNK_SIZE = 1024
90 local buffer = ""
91 local pos_beg = 1
92 return function()
93 local pos, chars
94 while 1 do
95 pos, chars = buffer:match('()([\r\n].)', pos_beg)
96 if pos or not f then
97 break
98 elseif f then
99 local chunk = f:read(CHUNK_SIZE)
100 if chunk then
101 buffer = buffer:sub(pos_beg) .. chunk
102 pos_beg = 1
103 else
104 f = nil
105 end
106 end
107 end
108 if not pos then
109 pos = #buffer
110 elseif chars == '\r\n' then
111 pos = pos + 1
112 end
113 local line = buffer:sub(pos_beg, pos)
114 pos_beg = pos + 1
115 if #line > 0 then
116 return line
117 end
118 end
119end
120
121local function match_linerange(line)
122 local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)")
123 if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end
124 if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end
125 if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end
126 return m1, m2, m3, m4
127end
128
129local function match_epoch(str)
130 return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]")
131end
132
133function patch.read_patch(filename, data)
134 -- define possible file regions that will direct the parser flow
135 local state = 'header'
136 -- 'header' - comments before the patch body
137 -- 'filenames' - lines starting with --- and +++
138 -- 'hunkhead' - @@ -R +R @@ sequence
139 -- 'hunkbody'
140 -- 'hunkskip' - skipping invalid hunk mode
141
142 local all_ok = true
143 local lineends = {lf=0, crlf=0, cr=0}
144 local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}}
145 local nextfileno = 0
146 local nexthunkno = 0 --: even if index starts with 0 user messages
147 -- number hunks from 1
148
149 -- hunkinfo holds parsed values, hunkactual - calculated
150 local hunkinfo = {
151 startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil,
152 invalid=false, text={}
153 }
154 local hunkactual = {linessrc=nil, linestgt=nil}
155
156 info(format("reading patch %s", filename))
157
158 local fp
159 if data then
160 fp = string_as_file(data)
161 else
162 fp = filename == '-' and io.stdin or assert(io.open(filename, "rb"))
163 end
164 local lineno = 0
165
166 for line in file_lines(fp) do
167 lineno = lineno + 1
168 if state == 'header' then
169 if startswith(line, "--- ") then
170 state = 'filenames'
171 end
172 -- state is 'header' or 'filenames'
173 end
174 if state == 'hunkbody' then
175 -- skip hunkskip and hunkbody code until definition of hunkhead read
176
177 if line:match"^[\r\n]*$" then
178 -- prepend space to empty lines to interpret them as context properly
179 line = " " .. line
180 end
181
182 -- process line first
183 if line:match"^[- +\\]" then
184 -- gather stats about line endings
185 local he = files.hunkends[nextfileno]
186 if endswith(line, "\r\n") then
187 he.crlf = he.crlf + 1
188 elseif endswith(line, "\n") then
189 he.lf = he.lf + 1
190 elseif endswith(line, "\r") then
191 he.cr = he.cr + 1
192 end
193 if startswith(line, "-") then
194 hunkactual.linessrc = hunkactual.linessrc + 1
195 elseif startswith(line, "+") then
196 hunkactual.linestgt = hunkactual.linestgt + 1
197 elseif startswith(line, "\\") then
198 -- nothing
199 else
200 hunkactual.linessrc = hunkactual.linessrc + 1
201 hunkactual.linestgt = hunkactual.linestgt + 1
202 end
203 table.insert(hunkinfo.text, line)
204 -- todo: handle \ No newline cases
205 else
206 warning(format("invalid hunk no.%d at %d for target file %s",
207 nexthunkno, lineno, files.target[nextfileno]))
208 -- add hunk status node
209 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
210 files.hunks[nextfileno][nexthunkno].invalid = true
211 all_ok = false
212 state = 'hunkskip'
213 end
214
215 -- check exit conditions
216 if hunkactual.linessrc > hunkinfo.linessrc or
217 hunkactual.linestgt > hunkinfo.linestgt
218 then
219 warning(format("extra hunk no.%d lines at %d for target %s",
220 nexthunkno, lineno, files.target[nextfileno]))
221 -- add hunk status node
222 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
223 files.hunks[nextfileno][nexthunkno].invalid = true
224 state = 'hunkskip'
225 elseif hunkinfo.linessrc == hunkactual.linessrc and
226 hunkinfo.linestgt == hunkactual.linestgt
227 then
228 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
229 state = 'hunkskip'
230
231 -- detect mixed window/unix line ends
232 local ends = files.hunkends[nextfileno]
233 if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) +
234 (ends.lf~=0 and 1 or 0) > 1
235 then
236 warning(format("inconsistent line ends in patch hunks for %s",
237 files.source[nextfileno]))
238 end
239 end
240 -- state is 'hunkbody' or 'hunkskip'
241 end
242
243 if state == 'hunkskip' then
244 if match_linerange(line) then
245 state = 'hunkhead'
246 elseif startswith(line, "--- ") then
247 state = 'filenames'
248 if debugmode and #files.source > 0 then
249 debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
250 files.source[nextfileno]))
251 end
252 end
253 -- state is 'hunkskip', 'hunkhead', or 'filenames'
254 end
255 local advance
256 if state == 'filenames' then
257 if startswith(line, "--- ") then
258 if fun.contains(files.source, nextfileno) then
259 all_ok = false
260 warning(format("skipping invalid patch for %s",
261 files.source[nextfileno+1]))
262 table.remove(files.source, nextfileno+1)
263 -- double source filename line is encountered
264 -- attempt to restart from this second line
265 end
266 -- Accept a space as a terminator, like GNU patch does.
267 -- Breaks patches containing filenames with spaces...
268 -- FIXME Figure out what does GNU patch do in those cases.
269 local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)")
270 if not match then
271 all_ok = false
272 warning(format("skipping invalid filename at line %d", lineno+1))
273 state = 'header'
274 else
275 if match_epoch(rest) then
276 files.epoch[nextfileno + 1] = true
277 end
278 table.insert(files.source, match)
279 end
280 elseif not startswith(line, "+++ ") then
281 if fun.contains(files.source, nextfileno) then
282 all_ok = false
283 warning(format("skipping invalid patch with no target for %s",
284 files.source[nextfileno+1]))
285 table.remove(files.source, nextfileno+1)
286 else
287 -- this should be unreachable
288 warning("skipping invalid target patch")
289 end
290 state = 'header'
291 else
292 if fun.contains(files.target, nextfileno) then
293 all_ok = false
294 warning(format("skipping invalid patch - double target at line %d",
295 lineno+1))
296 table.remove(files.source, nextfileno+1)
297 table.remove(files.target, nextfileno+1)
298 nextfileno = nextfileno - 1
299 -- double target filename line is encountered
300 -- switch back to header state
301 state = 'header'
302 else
303 -- Accept a space as a terminator, like GNU patch does.
304 -- Breaks patches containing filenames with spaces...
305 -- FIXME Figure out what does GNU patch do in those cases.
306 local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$"
307 local match, rest = line:match(re_filename)
308 if not match then
309 all_ok = false
310 warning(format(
311 "skipping invalid patch - no target filename at line %d",
312 lineno+1))
313 state = 'header'
314 else
315 table.insert(files.target, match)
316 nextfileno = nextfileno + 1
317 if match_epoch(rest) then
318 files.epoch[nextfileno] = true
319 end
320 nexthunkno = 0
321 table.insert(files.hunks, {})
322 table.insert(files.hunkends, table_copy(lineends))
323 table.insert(files.fileends, table_copy(lineends))
324 state = 'hunkhead'
325 advance = true
326 end
327 end
328 end
329 -- state is 'filenames', 'header', or ('hunkhead' with advance)
330 end
331 if not advance and state == 'hunkhead' then
332 local m1, m2, m3, m4 = match_linerange(line)
333 if not m1 then
334 if not fun.contains(files.hunks, nextfileno-1) then
335 all_ok = false
336 warning(format("skipping invalid patch with no hunks for file %s",
337 files.target[nextfileno]))
338 end
339 state = 'header'
340 else
341 hunkinfo.startsrc = tonumber(m1)
342 hunkinfo.linessrc = tonumber(m2 or 1)
343 hunkinfo.starttgt = tonumber(m3)
344 hunkinfo.linestgt = tonumber(m4 or 1)
345 hunkinfo.invalid = false
346 hunkinfo.text = {}
347
348 hunkactual.linessrc = 0
349 hunkactual.linestgt = 0
350
351 state = 'hunkbody'
352 nexthunkno = nexthunkno + 1
353 end
354 -- state is 'header' or 'hunkbody'
355 end
356 end
357 if state ~= 'hunkskip' then
358 warning(format("patch file incomplete - %s", filename))
359 all_ok = false
360 -- os.exit(?)
361 else
362 -- duplicated message when an eof is reached
363 if debugmode and #files.source > 0 then
364 debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
365 files.source[nextfileno]))
366 end
367 end
368
369 local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end
370 info(format("total files: %d total hunks: %d", #files.source, sum))
371 fp:close()
372 return files, all_ok
373end
374
375local function find_hunk(file, h, hno)
376 for fuzz=0,2 do
377 local lineno = h.startsrc
378 for i=0,#file do
379 local found = true
380 local location = lineno
381 for l, hline in ipairs(h.text) do
382 if l > fuzz then
383 -- todo: \ No newline at the end of file
384 if startswith(hline, " ") or startswith(hline, "-") then
385 local line = file[lineno]
386 lineno = lineno + 1
387 if not line or #line == 0 then
388 found = false
389 break
390 end
391 if endlstrip(line) ~= endlstrip(hline:sub(2)) then
392 found = false
393 break
394 end
395 end
396 end
397 end
398 if found then
399 local offset = location - h.startsrc - fuzz
400 if offset ~= 0 then
401 warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz)))
402 end
403 h.startsrc = location
404 h.starttgt = h.starttgt + offset
405 for _=1,fuzz do
406 table.remove(h.text, 1)
407 table.remove(h.text, #h.text)
408 end
409 return true
410 end
411 lineno = i
412 end
413 end
414 return false
415end
416
417local function load_file(filename)
418 local fp = assert(io.open(filename))
419 local file = {}
420 local readline = file_lines(fp)
421 while true do
422 local line = readline()
423 if not line then break end
424 table.insert(file, line)
425 end
426 fp:close()
427 return file
428end
429
430local function find_hunks(file, hunks)
431 for hno, h in ipairs(hunks) do
432 find_hunk(file, h, hno)
433 end
434end
435
436local function check_patched(file, hunks)
437 local lineno = 1
438 local ok, err = pcall(function()
439 if #file == 0 then
440 error('nomatch', 0)
441 end
442 for hno, h in ipairs(hunks) do
443 -- skip to line just before hunk starts
444 if #file < h.starttgt then
445 error('nomatch', 0)
446 end
447 lineno = h.starttgt
448 for _, hline in ipairs(h.text) do
449 -- todo: \ No newline at the end of file
450 if not startswith(hline, "-") and not startswith(hline, "\\") then
451 local line = file[lineno]
452 lineno = lineno + 1
453 if #line == 0 then
454 error('nomatch', 0)
455 end
456 if endlstrip(line) ~= endlstrip(hline:sub(2)) then
457 warning(format("file is not patched - failed hunk: %d", hno))
458 error('nomatch', 0)
459 end
460 end
461 end
462 end
463 end)
464 -- todo: display failed hunk, i.e. expected/found
465 return err ~= 'nomatch'
466end
467
468local function patch_hunks(srcname, tgtname, hunks)
469 local src = assert(io.open(srcname, "rb"))
470 local tgt = assert(io.open(tgtname, "wb"))
471
472 local src_readline = file_lines(src)
473
474 -- todo: detect linefeeds early - in apply_files routine
475 -- to handle cases when patch starts right from the first
476 -- line and no lines are processed. At the moment substituted
477 -- lineends may not be the same at the start and at the end
478 -- of patching. Also issue a warning about mixed lineends
479
480 local srclineno = 1
481 local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0}
482 for hno, h in ipairs(hunks) do
483 debug(format("processing hunk %d for file %s", hno, tgtname))
484 -- skip to line just before hunk starts
485 while srclineno < h.startsrc do
486 local line = src_readline()
487 -- Python 'U' mode works only with text files
488 if endswith(line, "\r\n") then
489 lineends["\r\n"] = lineends["\r\n"] + 1
490 elseif endswith(line, "\n") then
491 lineends["\n"] = lineends["\n"] + 1
492 elseif endswith(line, "\r") then
493 lineends["\r"] = lineends["\r"] + 1
494 end
495 tgt:write(line)
496 srclineno = srclineno + 1
497 end
498
499 for _,hline in ipairs(h.text) do
500 -- todo: check \ No newline at the end of file
501 if startswith(hline, "-") or startswith(hline, "\\") then
502 src_readline()
503 srclineno = srclineno + 1
504 else
505 if not startswith(hline, "+") then
506 src_readline()
507 srclineno = srclineno + 1
508 end
509 local line2write = hline:sub(2)
510 -- detect if line ends are consistent in source file
511 local sum = 0
512 for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end
513 if sum == 1 then
514 local newline
515 for k,v in pairs(lineends) do if v ~= 0 then newline = k end end
516 tgt:write(endlstrip(line2write) .. newline)
517 else -- newlines are mixed or unknown
518 tgt:write(line2write)
519 end
520 end
521 end
522 end
523 for line in src_readline do
524 tgt:write(line)
525 end
526 tgt:close()
527 src:close()
528 return true
529end
530
531local function strip_dirs(filename, strip)
532 if strip == nil then return filename end
533 for _=1,strip do
534 filename=filename:gsub("^[^/]*/", "")
535 end
536 return filename
537end
538
539local function write_new_file(filename, hunk)
540 local fh = io.open(filename, "wb")
541 if not fh then return false end
542 for _, hline in ipairs(hunk.text) do
543 local c = hline:sub(1,1)
544 if c ~= "+" and c ~= "-" and c ~= " " then
545 return false, "malformed patch"
546 end
547 fh:write(hline:sub(2))
548 end
549 fh:close()
550 return true
551end
552
553local function patch_file(source, target, epoch, hunks, strip, create_delete)
554 local create_file = false
555 if create_delete then
556 local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0
557 if is_src_epoch or source == "/dev/null" then
558 info(format("will create %s", target))
559 create_file = true
560 end
561 end
562 if create_file then
563 return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1])
564 end
565 source = strip_dirs(source, strip)
566 local f2patch = source
567 if not exists(f2patch) then
568 f2patch = strip_dirs(target, strip)
569 f2patch = fs.absolute_name(f2patch)
570 if not exists(f2patch) then --FIX:if f2patch nil
571 warning(format("source/target file does not exist\n--- %s\n+++ %s",
572 source, f2patch))
573 return false
574 end
575 end
576 if not isfile(f2patch) then
577 warning(format("not a file - %s", f2patch))
578 return false
579 end
580
581 source = f2patch
582
583 -- validate before patching
584 local file = load_file(source)
585 local hunkno = 1
586 local hunk = hunks[hunkno]
587 local hunkfind = {}
588 local validhunks = 0
589 local canpatch = false
590 local hunklineno
591 if not file then
592 return nil, "failed reading file " .. source
593 end
594
595 if create_delete then
596 if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then
597 local ok = os.remove(source)
598 if not ok then
599 return false
600 end
601 info(format("successfully removed %s", source))
602 return true
603 end
604 end
605
606 find_hunks(file, hunks)
607
608 local function process_line(line, lineno)
609 if not hunk or lineno < hunk.startsrc then
610 return false
611 end
612 if lineno == hunk.startsrc then
613 hunkfind = {}
614 for _,x in ipairs(hunk.text) do
615 if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then
616 hunkfind[#hunkfind+1] = endlstrip(x:sub(2))
617 end
618 end
619 hunklineno = 1
620
621 -- todo \ No newline at end of file
622 end
623 -- check hunks in source file
624 if lineno < hunk.startsrc + #hunkfind - 1 then
625 if endlstrip(line) == hunkfind[hunklineno] then
626 hunklineno = hunklineno + 1
627 else
628 debug(format("hunk no.%d doesn't match source file %s",
629 hunkno, source))
630 -- file may be already patched, but check other hunks anyway
631 hunkno = hunkno + 1
632 if hunkno <= #hunks then
633 hunk = hunks[hunkno]
634 return false
635 else
636 return true
637 end
638 end
639 end
640 -- check if processed line is the last line
641 if lineno == hunk.startsrc + #hunkfind - 1 then
642 debug(format("file %s hunk no.%d -- is ready to be patched",
643 source, hunkno))
644 hunkno = hunkno + 1
645 validhunks = validhunks + 1
646 if hunkno <= #hunks then
647 hunk = hunks[hunkno]
648 else
649 if validhunks == #hunks then
650 -- patch file
651 canpatch = true
652 return true
653 end
654 end
655 end
656 return false
657 end
658
659 local done = false
660 for lineno, line in ipairs(file) do
661 done = process_line(line, lineno)
662 if done then
663 break
664 end
665 end
666 if not done then
667 if hunkno <= #hunks and not create_file then
668 warning(format("premature end of source file %s at hunk %d",
669 source, hunkno))
670 return false
671 end
672 end
673 if validhunks < #hunks then
674 if check_patched(file, hunks) then
675 warning(format("already patched %s", source))
676 elseif not create_file then
677 warning(format("source file is different - %s", source))
678 return false
679 end
680 end
681 if not canpatch then
682 return true
683 end
684 local backupname = source .. ".orig"
685 if exists(backupname) then
686 warning(format("can't backup original file to %s - aborting",
687 backupname))
688 return false
689 end
690 local ok = os.rename(source, backupname)
691 if not ok then
692 warning(format("failed backing up %s when patching", source))
693 return false
694 end
695 patch_hunks(backupname, source, hunks)
696 info(format("successfully patched %s", source))
697 os.remove(backupname)
698 return true
699end
700
701function patch.apply_patch(the_patch, strip, create_delete)
702 local all_ok = true
703 local total = #the_patch.source
704 for fileno, source in ipairs(the_patch.source) do
705 local target = the_patch.target[fileno]
706 local hunks = the_patch.hunks[fileno]
707 local epoch = the_patch.epoch[fileno]
708 info(format("processing %d/%d:\t %s", fileno, total, source))
709 local ok = patch_file(source, target, epoch, hunks, strip, create_delete)
710 all_ok = all_ok and ok
711 end
712 -- todo: check for premature eof
713 return all_ok
714end
715
716return patch
diff --git a/src/luarocks/tools/patch.lua b/src/luarocks/tools/patch.lua
index 6f36d713..0b109313 100644
--- a/src/luarocks/tools/patch.lua
+++ b/src/luarocks/tools/patch.lua
@@ -1,59 +1,88 @@
1--- Patch utility to apply unified diffs. 1local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local assert = _tl_compat and _tl_compat.assert or assert; local debug = _tl_compat and _tl_compat.debug or debug; local io = _tl_compat and _tl_compat.io or io; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local os = _tl_compat and _tl_compat.os or os; local pairs = _tl_compat and _tl_compat.pairs or pairs; local pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table
2-- 2
3-- http://lua-users.org/wiki/LuaPatch 3
4-- 4
5-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license). 5
6-- Code is heavily based on the Python-based patch.py version 8.06-1 6
7-- Copyright (c) 2008 rainforce.org, MIT License 7
8-- Project home: http://code.google.com/p/python-patch/ . 8
9-- Version 0.1 9
10 10
11local patch = {} 11local patch = {Lineends = {}, Hunk = {}, File = {}, Files = {}, }
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
12 42
13local fs = require("luarocks.fs") 43local fs = require("luarocks.fs")
14local fun = require("luarocks.fun") 44local fun = require("luarocks.fun")
15 45
16local io = io
17local os = os
18local string = string
19local table = table
20local format = string.format
21 46
22-- logging 47
48
49
50
51
23local debugmode = false 52local debugmode = false
24local function debug(_) end 53local function debug(_) end
25local function info(_) end 54local function info(_) end
26local function warning(s) io.stderr:write(s .. '\n') end 55local function warning(s) io.stderr:write(s .. '\n') end
27 56
28-- Returns boolean whether string s2 starts with string s. 57
29local function startswith(s, s2) 58local function startswith(s, s2)
30 return s:sub(1, #s2) == s2 59 return s:sub(1, #s2) == s2
31end 60end
32 61
33-- Returns boolean whether string s2 ends with string s. 62
34local function endswith(s, s2) 63local function endswith(s, s2)
35 return #s >= #s2 and s:sub(#s-#s2+1) == s2 64 return #s >= #s2 and s:sub(#s - #s2 + 1) == s2
36end 65end
37 66
38-- Returns string s after filtering out any new-line characters from end. 67
39local function endlstrip(s) 68local function endlstrip(s)
40 return s:gsub('[\r\n]+$', '') 69 return s:gsub('[\r\n]+$', '')
41end 70end
42 71
43-- Returns shallow copy of table t. 72
44local function table_copy(t) 73local function table_copy(t)
45 local t2 = {} 74 local t2 = {}
46 for k,v in pairs(t) do t2[k] = v end 75 for k, v in pairs(t) do t2[k] = v end
47 return t2 76 return t2
48end 77end
49 78
50local function exists(filename) 79local function exists(filename)
51 local fh = io.open(filename) 80 local fh = io.open(filename)
52 local result = fh ~= nil 81 local result = fh ~= nil
53 if fh then fh:close() end 82 if fh then fh:close() end
54 return result 83 return result
55end 84end
56local function isfile() return true end --FIX? 85local function isfile() return true end
57 86
58local function string_as_file(s) 87local function string_as_file(s)
59 return { 88 return {
@@ -76,641 +105,643 @@ local function string_as_file(s)
76 } 105 }
77end 106end
78 107
79-- 108
80-- file_lines(f) is similar to f:lines() for file f. 109
81-- The main difference is that read_lines includes 110
82-- new-line character sequences ("\n", "\r\n", "\r"), 111
83-- if any, at the end of each line. Embedded "\0" are also handled. 112
84-- Caution: The newline behavior can depend on whether f is opened 113
85-- in binary or ASCII mode. 114
86-- (file_lines - version 20080913) 115
87-- 116
88local function file_lines(f) 117local function file_lines(f)
89 local CHUNK_SIZE = 1024 118 local CHUNK_SIZE = 1024
90 local buffer = "" 119 local buffer = ""
91 local pos_beg = 1 120 local pos_beg = 1
92 return function() 121 return function()
93 local pos, chars 122 local pos, chars
94 while 1 do 123 while 1 do
95 pos, chars = buffer:match('()([\r\n].)', pos_beg) 124 pos, chars = buffer:match('()([\r\n].)', pos_beg)
96 if pos or not f then 125 if pos or not f then
97 break 126 break
98 elseif f then 127 elseif f then
99 local chunk = f:read(CHUNK_SIZE) 128 local chunk = f:read(CHUNK_SIZE)
100 if chunk then 129 if chunk then
101 buffer = buffer:sub(pos_beg) .. chunk 130 buffer = buffer:sub(pos_beg) .. chunk
102 pos_beg = 1 131 pos_beg = 1
103 else 132 else
104 f = nil 133 f = nil
105 end 134 end
135 end
136 end
137 local posi = math.tointeger(pos)
138 if not posi then
139 posi = #buffer
140 elseif chars == '\r\n' then
141 posi = posi + 1
142 end
143 local line = buffer:sub(pos_beg, posi)
144 pos_beg = posi + 1
145 if #line > 0 then
146 return line
106 end 147 end
107 end 148 end
108 if not pos then
109 pos = #buffer
110 elseif chars == '\r\n' then
111 pos = pos + 1
112 end
113 local line = buffer:sub(pos_beg, pos)
114 pos_beg = pos + 1
115 if #line > 0 then
116 return line
117 end
118 end
119end 149end
120 150
121local function match_linerange(line) 151local function match_linerange(line)
122 local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") 152 local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)")
123 if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end 153 if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end
124 if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end 154 if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end
125 if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end 155 if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end
126 return m1, m2, m3, m4 156 return m1, m2, m3, m4
127end 157end
128 158
129local function match_epoch(str) 159local function match_epoch(str)
130 return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]") 160 return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]")
131end 161end
132 162
133function patch.read_patch(filename, data) 163function patch.read_patch(filename, data)
134 -- define possible file regions that will direct the parser flow
135 local state = 'header'
136 -- 'header' - comments before the patch body
137 -- 'filenames' - lines starting with --- and +++
138 -- 'hunkhead' - @@ -R +R @@ sequence
139 -- 'hunkbody'
140 -- 'hunkskip' - skipping invalid hunk mode
141
142 local all_ok = true
143 local lineends = {lf=0, crlf=0, cr=0}
144 local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}}
145 local nextfileno = 0
146 local nexthunkno = 0 --: even if index starts with 0 user messages
147 -- number hunks from 1
148
149 -- hunkinfo holds parsed values, hunkactual - calculated
150 local hunkinfo = {
151 startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil,
152 invalid=false, text={}
153 }
154 local hunkactual = {linessrc=nil, linestgt=nil}
155
156 info(format("reading patch %s", filename))
157
158 local fp
159 if data then
160 fp = string_as_file(data)
161 else
162 fp = filename == '-' and io.stdin or assert(io.open(filename, "rb"))
163 end
164 local lineno = 0
165
166 for line in file_lines(fp) do
167 lineno = lineno + 1
168 if state == 'header' then
169 if startswith(line, "--- ") then
170 state = 'filenames'
171 end
172 -- state is 'header' or 'filenames'
173 end
174 if state == 'hunkbody' then
175 -- skip hunkskip and hunkbody code until definition of hunkhead read
176
177 if line:match"^[\r\n]*$" then
178 -- prepend space to empty lines to interpret them as context properly
179 line = " " .. line
180 end
181 164
182 -- process line first 165 local state = 'header'
183 if line:match"^[- +\\]" then 166
184 -- gather stats about line endings 167
185 local he = files.hunkends[nextfileno] 168
186 if endswith(line, "\r\n") then 169
187 he.crlf = he.crlf + 1 170
188 elseif endswith(line, "\n") then 171
189 he.lf = he.lf + 1 172 local all_ok = true
190 elseif endswith(line, "\r") then 173 local lineends = { lf = 0, crlf = 0, cr = 0 }
191 he.cr = he.cr + 1 174 local files = { source = {}, target = {}, epoch = {}, hunks = {}, fileends = {}, hunkends = {} }
192 end 175 local nextfileno = 0
193 if startswith(line, "-") then 176 local nexthunkno = 0
194 hunkactual.linessrc = hunkactual.linessrc + 1 177
195 elseif startswith(line, "+") then 178
196 hunkactual.linestgt = hunkactual.linestgt + 1 179
197 elseif startswith(line, "\\") then 180 local hunkinfo = {
198 -- nothing 181 startsrc = nil, linessrc = nil, starttgt = nil, linestgt = nil,
199 else 182 invalid = false, text = {},
200 hunkactual.linessrc = hunkactual.linessrc + 1 183 }
201 hunkactual.linestgt = hunkactual.linestgt + 1 184 local hunkactual = { linessrc = nil, linestgt = nil }
202 end 185
203 table.insert(hunkinfo.text, line) 186 info(string.format("reading patch %s", filename))
204 -- todo: handle \ No newline cases 187
205 else 188 local fp
206 warning(format("invalid hunk no.%d at %d for target file %s", 189 if data then
207 nexthunkno, lineno, files.target[nextfileno])) 190 fp = string_as_file(data)
208 -- add hunk status node 191 else
209 table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) 192 fp = filename == '-' and io.stdin or assert(io.open(filename, "rb"))
210 files.hunks[nextfileno][nexthunkno].invalid = true 193 end
211 all_ok = false 194 local lineno = 0
212 state = 'hunkskip' 195
196 for line in file_lines(fp) do
197 lineno = lineno + 1
198 if state == 'header' then
199 if startswith(line, "--- ") then
200 state = 'filenames'
201 end
202
213 end 203 end
204 if state == 'hunkbody' then
205
206
207 if line:match("^[\r\n]*$") then
208
209 line = " " .. line
210 end
211
212
213 if line:match("^[- +\\]") then
214
215 local he = files.hunkends[nextfileno]
216 if endswith(line, "\r\n") then
217 he.crlf = he.crlf + 1
218 elseif endswith(line, "\n") then
219 he.lf = he.lf + 1
220 elseif endswith(line, "\r") then
221 he.cr = he.cr + 1
222 end
223 if startswith(line, "-") then
224 hunkactual.linessrc = hunkactual.linessrc + 1
225 elseif startswith(line, "+") then
226 hunkactual.linestgt = hunkactual.linestgt + 1
227 elseif startswith(line, "\\") then
228
229 else
230 hunkactual.linessrc = hunkactual.linessrc + 1
231 hunkactual.linestgt = hunkactual.linestgt + 1
232 end
233 table.insert(hunkinfo.text, line)
234
235 else
236 warning(string.format("invalid hunk no.%d at %d for target file %s",
237 nexthunkno, lineno, files.target[nextfileno]))
238
239 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
240 files.hunks[nextfileno][nexthunkno].invalid = true
241 all_ok = false
242 state = 'hunkskip'
243 end
244
245
246 if hunkactual.linessrc > hunkinfo.linessrc or
247 hunkactual.linestgt > hunkinfo.linestgt then
248
249 warning(string.format("extra hunk no.%d lines at %d for target %s",
250 nexthunkno, lineno, files.target[nextfileno]))
251
252 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
253 files.hunks[nextfileno][nexthunkno].invalid = true
254 state = 'hunkskip'
255 elseif hunkinfo.linessrc == hunkactual.linessrc and
256 hunkinfo.linestgt == hunkactual.linestgt then
257
258 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
259 state = 'hunkskip'
260
261
262 local ends = files.hunkends[nextfileno]
263 if (ends.cr ~= 0 and 1 or 0) + (ends.crlf ~= 0 and 1 or 0) +
264 (ends.lf ~= 0 and 1 or 0) > 1 then
265
266 warning(string.format("inconsistent line ends in patch hunks for %s",
267 files.source[nextfileno]))
268 end
269 end
214 270
215 -- check exit conditions
216 if hunkactual.linessrc > hunkinfo.linessrc or
217 hunkactual.linestgt > hunkinfo.linestgt
218 then
219 warning(format("extra hunk no.%d lines at %d for target %s",
220 nexthunkno, lineno, files.target[nextfileno]))
221 -- add hunk status node
222 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
223 files.hunks[nextfileno][nexthunkno].invalid = true
224 state = 'hunkskip'
225 elseif hunkinfo.linessrc == hunkactual.linessrc and
226 hunkinfo.linestgt == hunkactual.linestgt
227 then
228 table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
229 state = 'hunkskip'
230
231 -- detect mixed window/unix line ends
232 local ends = files.hunkends[nextfileno]
233 if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) +
234 (ends.lf~=0 and 1 or 0) > 1
235 then
236 warning(format("inconsistent line ends in patch hunks for %s",
237 files.source[nextfileno]))
238 end
239 end 271 end
240 -- state is 'hunkbody' or 'hunkskip' 272
241 end 273 if state == 'hunkskip' then
242 274 if match_linerange(line) then
243 if state == 'hunkskip' then 275 state = 'hunkhead'
244 if match_linerange(line) then 276 elseif startswith(line, "--- ") then
245 state = 'hunkhead' 277 state = 'filenames'
246 elseif startswith(line, "--- ") then 278 if debugmode and #files.source > 0 then
247 state = 'filenames' 279 debug(string.format("- %2d hunks for %s", #files.hunks[nextfileno],
248 if debugmode and #files.source > 0 then 280 files.source[nextfileno]))
249 debug(format("- %2d hunks for %s", #files.hunks[nextfileno], 281 end
250 files.source[nextfileno])) 282 end
251 end 283
252 end 284 end
253 -- state is 'hunkskip', 'hunkhead', or 'filenames' 285 local advance
254 end 286 if state == 'filenames' then
255 local advance 287 if startswith(line, "--- ") then
256 if state == 'filenames' then 288 if fun.contains(files.source, nextfileno) then
257 if startswith(line, "--- ") then 289 all_ok = false
258 if fun.contains(files.source, nextfileno) then 290 warning(string.format("skipping invalid patch for %s",
259 all_ok = false 291 files.source[nextfileno + 1]))
260 warning(format("skipping invalid patch for %s", 292 table.remove(files.source, nextfileno + 1)
261 files.source[nextfileno+1])) 293
262 table.remove(files.source, nextfileno+1) 294
263 -- double source filename line is encountered 295 end
264 -- attempt to restart from this second line 296
265 end 297
266 -- Accept a space as a terminator, like GNU patch does. 298
267 -- Breaks patches containing filenames with spaces... 299 local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)")
268 -- FIXME Figure out what does GNU patch do in those cases. 300 if not match then
269 local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)") 301 all_ok = false
270 if not match then 302 warning(string.format("skipping invalid filename at line %d", lineno + 1))
271 all_ok = false 303 state = 'header'
272 warning(format("skipping invalid filename at line %d", lineno+1)) 304 else
273 state = 'header' 305 if match_epoch(rest) then
274 else 306 files.epoch[nextfileno + 1] = true
275 if match_epoch(rest) then 307 end
276 files.epoch[nextfileno + 1] = true 308 table.insert(files.source, match)
277 end 309 end
278 table.insert(files.source, match) 310 elseif not startswith(line, "+++ ") then
279 end 311 if fun.contains(files.source, nextfileno) then
280 elseif not startswith(line, "+++ ") then 312 all_ok = false
281 if fun.contains(files.source, nextfileno) then 313 warning(string.format("skipping invalid patch with no target for %s",
282 all_ok = false 314 files.source[nextfileno + 1]))
283 warning(format("skipping invalid patch with no target for %s", 315 table.remove(files.source, nextfileno + 1)
284 files.source[nextfileno+1])) 316 else
285 table.remove(files.source, nextfileno+1) 317
286 else 318 warning("skipping invalid target patch")
287 -- this should be unreachable 319 end
288 warning("skipping invalid target patch")
289 end
290 state = 'header'
291 else
292 if fun.contains(files.target, nextfileno) then
293 all_ok = false
294 warning(format("skipping invalid patch - double target at line %d",
295 lineno+1))
296 table.remove(files.source, nextfileno+1)
297 table.remove(files.target, nextfileno+1)
298 nextfileno = nextfileno - 1
299 -- double target filename line is encountered
300 -- switch back to header state
301 state = 'header'
302 else
303 -- Accept a space as a terminator, like GNU patch does.
304 -- Breaks patches containing filenames with spaces...
305 -- FIXME Figure out what does GNU patch do in those cases.
306 local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$"
307 local match, rest = line:match(re_filename)
308 if not match then
309 all_ok = false
310 warning(format(
311 "skipping invalid patch - no target filename at line %d",
312 lineno+1))
313 state = 'header' 320 state = 'header'
314 else 321 else
315 table.insert(files.target, match) 322 if fun.contains(files.target, nextfileno) then
316 nextfileno = nextfileno + 1 323 all_ok = false
317 if match_epoch(rest) then 324 warning(string.format("skipping invalid patch - double target at line %d",
318 files.epoch[nextfileno] = true 325 lineno + 1))
326 table.remove(files.source, nextfileno + 1)
327 table.remove(files.target, nextfileno + 1)
328 nextfileno = nextfileno - 1
329
330
331 state = 'header'
332 else
333
334
335
336 local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$"
337 local match, rest = line:match(re_filename)
338 if not match then
339 all_ok = false
340 warning(string.format(
341 "skipping invalid patch - no target filename at line %d",
342 lineno + 1))
343 state = 'header'
344 else
345 table.insert(files.target, match)
346 nextfileno = nextfileno + 1
347 if match_epoch(rest) then
348 files.epoch[nextfileno] = true
349 end
350 nexthunkno = 0
351 table.insert(files.hunks, {})
352 table.insert(files.hunkends, table_copy(lineends))
353 table.insert(files.fileends, table_copy(lineends))
354 state = 'hunkhead'
355 advance = true
356 end
319 end 357 end
320 nexthunkno = 0 358 end
321 table.insert(files.hunks, {}) 359
322 table.insert(files.hunkends, table_copy(lineends))
323 table.insert(files.fileends, table_copy(lineends))
324 state = 'hunkhead'
325 advance = true
326 end
327 end
328 end 360 end
329 -- state is 'filenames', 'header', or ('hunkhead' with advance) 361 if not advance and state == 'hunkhead' then
330 end 362 local m1, m2, m3, m4 = match_linerange(line)
331 if not advance and state == 'hunkhead' then 363 if not m1 then
332 local m1, m2, m3, m4 = match_linerange(line) 364 if not fun.contains(files.hunks, nextfileno - 1) then
333 if not m1 then 365 all_ok = false
334 if not fun.contains(files.hunks, nextfileno-1) then 366 warning(string.format("skipping invalid patch with no hunks for file %s",
335 all_ok = false 367 files.target[nextfileno]))
336 warning(format("skipping invalid patch with no hunks for file %s", 368 end
337 files.target[nextfileno])) 369 state = 'header'
338 end 370 else
339 state = 'header' 371 hunkinfo.startsrc = tonumber(m1)
340 else 372 hunkinfo.linessrc = tonumber(tonumber(m2) or 1)
341 hunkinfo.startsrc = tonumber(m1) 373 hunkinfo.starttgt = tonumber(m3)
342 hunkinfo.linessrc = tonumber(m2 or 1) 374 hunkinfo.linestgt = tonumber(tonumber(m4) or 1)
343 hunkinfo.starttgt = tonumber(m3) 375 hunkinfo.invalid = false
344 hunkinfo.linestgt = tonumber(m4 or 1) 376 hunkinfo.text = {}
345 hunkinfo.invalid = false 377
346 hunkinfo.text = {} 378 hunkactual.linessrc = 0
347 379 hunkactual.linestgt = 0
348 hunkactual.linessrc = 0 380
349 hunkactual.linestgt = 0 381 state = 'hunkbody'
350 382 nexthunkno = nexthunkno + 1
351 state = 'hunkbody' 383 end
352 nexthunkno = nexthunkno + 1 384
385 end
386 end
387 if state ~= 'hunkskip' then
388 warning(string.format("patch file incomplete - %s", filename))
389 all_ok = false
390
391 else
392
393 if debugmode and #files.source > 0 then
394 debug(string.format("- %2d hunks for %s", #files.hunks[nextfileno],
395 files.source[nextfileno]))
353 end 396 end
354 -- state is 'header' or 'hunkbody' 397 end
355 end 398
356 end 399 local sum = 0; for _, hset in ipairs(files.hunks) do sum = sum + #hset end
357 if state ~= 'hunkskip' then 400 info(string.format("total files: %d total hunks: %d", #files.source, sum))
358 warning(format("patch file incomplete - %s", filename)) 401 fp:close()
359 all_ok = false 402 return files, all_ok
360 -- os.exit(?)
361 else
362 -- duplicated message when an eof is reached
363 if debugmode and #files.source > 0 then
364 debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
365 files.source[nextfileno]))
366 end
367 end
368
369 local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end
370 info(format("total files: %d total hunks: %d", #files.source, sum))
371 fp:close()
372 return files, all_ok
373end 403end
374 404
375local function find_hunk(file, h, hno) 405local function find_hunk(file, h, hno)
376 for fuzz=0,2 do 406 for fuzz = 0, 2 do
377 local lineno = h.startsrc 407 local lineno = h.startsrc
378 for i=0,#file do 408 for i = 0, #file do
379 local found = true 409 local found = true
380 local location = lineno 410 local location = lineno
381 for l, hline in ipairs(h.text) do 411 for l, hline in ipairs(h.text) do
382 if l > fuzz then 412 if l > fuzz then
383 -- todo: \ No newline at the end of file 413
384 if startswith(hline, " ") or startswith(hline, "-") then 414 if startswith(hline, " ") or startswith(hline, "-") then
385 local line = file[lineno] 415 local line = file[lineno]
386 lineno = lineno + 1 416 lineno = lineno + 1
387 if not line or #line == 0 then 417 if not line or #line == 0 then
388 found = false 418 found = false
389 break 419 break
420 end
421 if endlstrip(line) ~= endlstrip(hline:sub(2)) then
422 found = false
423 break
424 end
425 end
426 end
427 end
428 if found then
429 local offset = location - h.startsrc - fuzz
430 if offset ~= 0 then
431 warning(string.format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or string.format(" (fuzz %d)", fuzz)))
390 end 432 end
391 if endlstrip(line) ~= endlstrip(hline:sub(2)) then 433 h.startsrc = location
392 found = false 434 h.starttgt = h.starttgt + offset
393 break 435 for _ = 1, fuzz do
436 table.remove(h.text, 1)
437 table.remove(h.text, #h.text)
394 end 438 end
395 end 439 return true
396 end 440 end
397 end 441 lineno = i
398 if found then
399 local offset = location - h.startsrc - fuzz
400 if offset ~= 0 then
401 warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz)))
402 end
403 h.startsrc = location
404 h.starttgt = h.starttgt + offset
405 for _=1,fuzz do
406 table.remove(h.text, 1)
407 table.remove(h.text, #h.text)
408 end
409 return true
410 end 442 end
411 lineno = i 443 end
412 end 444 return false
413 end
414 return false
415end 445end
416 446
417local function load_file(filename) 447local function load_file(filename)
418 local fp = assert(io.open(filename)) 448 local fp = assert(io.open(filename))
419 local file = {} 449 local file = {}
420 local readline = file_lines(fp) 450 local readline = file_lines(fp)
421 while true do 451 while true do
422 local line = readline() 452 local line = readline()
423 if not line then break end 453 if not line then break end
424 table.insert(file, line) 454 table.insert(file, line)
425 end 455 end
426 fp:close() 456 fp:close()
427 return file 457 return file
428end 458end
429 459
430local function find_hunks(file, hunks) 460local function find_hunks(file, hunks)
431 for hno, h in ipairs(hunks) do 461 for hno, h in ipairs(hunks) do
432 find_hunk(file, h, hno) 462 find_hunk(file, h, hno)
433 end 463 end
434end 464end
435 465
436local function check_patched(file, hunks) 466local function check_patched(file, hunks)
437 local lineno = 1 467 local lineno = 1
438 local ok, err = pcall(function() 468 local ok, err = pcall(function()
439 if #file == 0 then 469 if #file == 0 then
440 error('nomatch', 0) 470 error('nomatch', 0)
441 end
442 for hno, h in ipairs(hunks) do
443 -- skip to line just before hunk starts
444 if #file < h.starttgt then
445 error('nomatch', 0)
446 end 471 end
447 lineno = h.starttgt 472 for hno, h in ipairs(hunks) do
448 for _, hline in ipairs(h.text) do 473
449 -- todo: \ No newline at the end of file 474 if #file < h.starttgt then
450 if not startswith(hline, "-") and not startswith(hline, "\\") then
451 local line = file[lineno]
452 lineno = lineno + 1
453 if #line == 0 then
454 error('nomatch', 0)
455 end
456 if endlstrip(line) ~= endlstrip(hline:sub(2)) then
457 warning(format("file is not patched - failed hunk: %d", hno))
458 error('nomatch', 0) 475 error('nomatch', 0)
459 end 476 end
460 end 477 lineno = h.starttgt
478 for _, hline in ipairs(h.text) do
479
480 if not startswith(hline, "-") and not startswith(hline, "\\") then
481 local line = file[lineno]
482 lineno = lineno + 1
483 if #line == 0 then
484 error('nomatch', 0)
485 end
486 if endlstrip(line) ~= endlstrip(hline:sub(2)) then
487 warning(string.format("file is not patched - failed hunk: %d", hno))
488 error('nomatch', 0)
489 end
490 end
491 end
461 end 492 end
462 end 493 end)
463 end) 494
464 -- todo: display failed hunk, i.e. expected/found 495 return err ~= 'nomatch'
465 return err ~= 'nomatch'
466end 496end
467 497
468local function patch_hunks(srcname, tgtname, hunks) 498local function patch_hunks(srcname, tgtname, hunks)
469 local src = assert(io.open(srcname, "rb")) 499 local src = assert(io.open(srcname, "rb"))
470 local tgt = assert(io.open(tgtname, "wb")) 500 local tgt = assert(io.open(tgtname, "wb"))
471 501
472 local src_readline = file_lines(src) 502 local src_readline = file_lines(src)
473 503
474 -- todo: detect linefeeds early - in apply_files routine 504
475 -- to handle cases when patch starts right from the first 505
476 -- line and no lines are processed. At the moment substituted 506
477 -- lineends may not be the same at the start and at the end 507
478 -- of patching. Also issue a warning about mixed lineends 508
479 509
480 local srclineno = 1 510 local srclineno = 1
481 local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0} 511 local lineends = { ['\n'] = 0, ['\r\n'] = 0, ['\r'] = 0 }
482 for hno, h in ipairs(hunks) do 512 for hno, h in ipairs(hunks) do
483 debug(format("processing hunk %d for file %s", hno, tgtname)) 513 debug(string.format("processing hunk %d for file %s", hno, tgtname))
484 -- skip to line just before hunk starts 514
485 while srclineno < h.startsrc do 515 while srclineno < h.startsrc do
486 local line = src_readline() 516 local line = src_readline()
487 -- Python 'U' mode works only with text files 517
488 if endswith(line, "\r\n") then 518 if endswith(line, "\r\n") then
489 lineends["\r\n"] = lineends["\r\n"] + 1 519 lineends["\r\n"] = lineends["\r\n"] + 1
490 elseif endswith(line, "\n") then 520 elseif endswith(line, "\n") then
491 lineends["\n"] = lineends["\n"] + 1 521 lineends["\n"] = lineends["\n"] + 1
492 elseif endswith(line, "\r") then 522 elseif endswith(line, "\r") then
493 lineends["\r"] = lineends["\r"] + 1 523 lineends["\r"] = lineends["\r"] + 1
524 end
525 tgt:write(line)
526 srclineno = srclineno + 1
494 end 527 end
495 tgt:write(line) 528
496 srclineno = srclineno + 1 529 for _, hline in ipairs(h.text) do
497 end 530
498 531 if startswith(hline, "-") or startswith(hline, "\\") then
499 for _,hline in ipairs(h.text) do 532 src_readline()
500 -- todo: check \ No newline at the end of file 533 srclineno = srclineno + 1
501 if startswith(hline, "-") or startswith(hline, "\\") then 534 else
502 src_readline() 535 if not startswith(hline, "+") then
503 srclineno = srclineno + 1 536 src_readline()
504 else 537 srclineno = srclineno + 1
505 if not startswith(hline, "+") then 538 end
506 src_readline() 539 local line2write = hline:sub(2)
507 srclineno = srclineno + 1 540
508 end 541 local sum = 0
509 local line2write = hline:sub(2) 542 for _, v in pairs(lineends) do if v > 0 then sum = sum + 1 end end
510 -- detect if line ends are consistent in source file 543 if sum == 1 then
511 local sum = 0 544 local newline
512 for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end 545 for k, v in pairs(lineends) do if v ~= 0 then newline = k end end
513 if sum == 1 then 546 tgt:write(endlstrip(line2write) .. newline)
514 local newline 547 else
515 for k,v in pairs(lineends) do if v ~= 0 then newline = k end end 548 tgt:write(line2write)
516 tgt:write(endlstrip(line2write) .. newline) 549 end
517 else -- newlines are mixed or unknown 550 end
518 tgt:write(line2write)
519 end
520 end 551 end
521 end 552 end
522 end 553 for line in src_readline do
523 for line in src_readline do 554 tgt:write(line)
524 tgt:write(line) 555 end
525 end 556 tgt:close()
526 tgt:close() 557 src:close()
527 src:close() 558 return true
528 return true
529end 559end
530 560
531local function strip_dirs(filename, strip) 561local function strip_dirs(filename, strip)
532 if strip == nil then return filename end 562 if strip == nil then return filename end
533 for _=1,strip do 563 for _ = 1, strip do
534 filename=filename:gsub("^[^/]*/", "") 564 filename = filename:gsub("^[^/]*/", "")
535 end 565 end
536 return filename 566 return filename
537end 567end
538 568
539local function write_new_file(filename, hunk) 569local function write_new_file(filename, hunk)
540 local fh = io.open(filename, "wb") 570 local fh = io.open(filename, "wb")
541 if not fh then return false end 571 if not fh then return false end
542 for _, hline in ipairs(hunk.text) do 572 for _, hline in ipairs(hunk.text) do
543 local c = hline:sub(1,1) 573 local c = hline:sub(1, 1)
544 if c ~= "+" and c ~= "-" and c ~= " " then 574 if c ~= "+" and c ~= "-" and c ~= " " then
545 return false, "malformed patch" 575 return false, "malformed patch"
546 end 576 end
547 fh:write(hline:sub(2)) 577 fh:write(hline:sub(2))
548 end 578 end
549 fh:close() 579 fh:close()
550 return true 580 return true
551end 581end
552 582
553local function patch_file(source, target, epoch, hunks, strip, create_delete) 583local function patch_file(source, target, epoch, hunks, strip, create_delete)
554 local create_file = false 584 local create_file = false
555 if create_delete then 585 if create_delete then
556 local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0 586 local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0
557 if is_src_epoch or source == "/dev/null" then 587 if is_src_epoch or source == "/dev/null" then
558 info(format("will create %s", target)) 588 info(string.format("will create %s", target))
559 create_file = true 589 create_file = true
560 end 590 end
561 end 591 end
562 if create_file then 592 if create_file then
563 return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1]) 593 return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1])
564 end 594 end
565 source = strip_dirs(source, strip) 595 source = strip_dirs(source, strip)
566 local f2patch = source 596 local f2patch = source
567 if not exists(f2patch) then 597 if not exists(f2patch) then
568 f2patch = strip_dirs(target, strip) 598 f2patch = strip_dirs(target, strip)
569 f2patch = fs.absolute_name(f2patch) 599 f2patch = fs.absolute_name(f2patch)
570 if not exists(f2patch) then --FIX:if f2patch nil 600 if not exists(f2patch) then
571 warning(format("source/target file does not exist\n--- %s\n+++ %s", 601 warning(string.format("source/target file does not exist\n--- %s\n+++ %s",
572 source, f2patch)) 602 source, f2patch))
603 return false
604 end
605 end
606
607 if not isfile() then
608 warning(string.format("not a file - %s", f2patch))
573 return false 609 return false
574 end 610 end
575 end 611
576 if not isfile(f2patch) then 612 source = f2patch
577 warning(format("not a file - %s", f2patch)) 613
578 return false 614
579 end 615 local file = load_file(source)
580 616 local hunkno = 1
581 source = f2patch 617 local hunk = hunks[hunkno]
582 618 local hunkfind = {}
583 -- validate before patching 619 local validhunks = 0
584 local file = load_file(source) 620 local canpatch = false
585 local hunkno = 1 621 local hunklineno
586 local hunk = hunks[hunkno] 622 if not file then
587 local hunkfind = {} 623 return nil, "failed reading file " .. source
588 local validhunks = 0 624 end
589 local canpatch = false 625
590 local hunklineno 626 if create_delete then
591 if not file then 627 if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then
592 return nil, "failed reading file " .. source 628 local ok = os.remove(source)
593 end 629 if not ok then
594 630 return false
595 if create_delete then 631 end
596 if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then 632 info(string.format("successfully removed %s", source))
597 local ok = os.remove(source) 633 return true
598 if not ok then
599 return false
600 end 634 end
601 info(format("successfully removed %s", source)) 635 end
602 return true
603 end
604 end
605 636
606 find_hunks(file, hunks) 637 find_hunks(file, hunks)
607 638
608 local function process_line(line, lineno) 639 local function process_line(line, lineno)
609 if not hunk or lineno < hunk.startsrc then 640 if not hunk or lineno < hunk.startsrc then
641 return false
642 end
643 if lineno == hunk.startsrc then
644 hunkfind = {}
645 for _, x in ipairs(hunk.text) do
646 if x:sub(1, 1) == ' ' or x:sub(1, 1) == '-' then
647 hunkfind[#hunkfind + 1] = endlstrip(x:sub(2))
648 end
649 end
650 hunklineno = 1
651
652
653 end
654
655 if lineno < hunk.startsrc + #hunkfind - 1 then
656 if endlstrip(line) == hunkfind[hunklineno] then
657 hunklineno = hunklineno + 1
658 else
659 debug(string.format("hunk no.%d doesn't match source file %s",
660 hunkno, source))
661
662 hunkno = hunkno + 1
663 if hunkno <= #hunks then
664 hunk = hunks[hunkno]
665 return false
666 else
667 return true
668 end
669 end
670 end
671
672 if lineno == hunk.startsrc + #hunkfind - 1 then
673 debug(string.format("file %s hunk no.%d -- is ready to be patched",
674 source, hunkno))
675 hunkno = hunkno + 1
676 validhunks = validhunks + 1
677 if hunkno <= #hunks then
678 hunk = hunks[hunkno]
679 else
680 if validhunks == #hunks then
681
682 canpatch = true
683 return true
684 end
685 end
686 end
610 return false 687 return false
611 end 688 end
612 if lineno == hunk.startsrc then 689
613 hunkfind = {} 690 local done = false
614 for _,x in ipairs(hunk.text) do 691 for lineno, line in ipairs(file) do
615 if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then 692 done = process_line(line, lineno)
616 hunkfind[#hunkfind+1] = endlstrip(x:sub(2)) 693 if done then
617 end 694 break
618 end 695 end
619 hunklineno = 1 696 end
620 697 if not done then
621 -- todo \ No newline at end of file 698 if hunkno <= #hunks and not create_file then
622 end 699 warning(string.format("premature end of source file %s at hunk %d",
623 -- check hunks in source file 700 source, hunkno))
624 if lineno < hunk.startsrc + #hunkfind - 1 then 701 return false
625 if endlstrip(line) == hunkfind[hunklineno] then
626 hunklineno = hunklineno + 1
627 else
628 debug(format("hunk no.%d doesn't match source file %s",
629 hunkno, source))
630 -- file may be already patched, but check other hunks anyway
631 hunkno = hunkno + 1
632 if hunkno <= #hunks then
633 hunk = hunks[hunkno]
634 return false
635 else
636 return true
637 end
638 end 702 end
639 end 703 end
640 -- check if processed line is the last line 704 if validhunks < #hunks then
641 if lineno == hunk.startsrc + #hunkfind - 1 then 705 if check_patched(file, hunks) then
642 debug(format("file %s hunk no.%d -- is ready to be patched", 706 warning(string.format("already patched %s", source))
643 source, hunkno)) 707 elseif not create_file then
644 hunkno = hunkno + 1 708 warning(string.format("source file is different - %s", source))
645 validhunks = validhunks + 1 709 return false
646 if hunkno <= #hunks then
647 hunk = hunks[hunkno]
648 else
649 if validhunks == #hunks then
650 -- patch file
651 canpatch = true
652 return true
653 end
654 end 710 end
655 end 711 end
656 return false 712 if not canpatch then
657 end 713 return true
658 714 end
659 local done = false 715 local backupname = source .. ".orig"
660 for lineno, line in ipairs(file) do 716 if exists(backupname) then
661 done = process_line(line, lineno) 717 warning(string.format("can't backup original file to %s - aborting",
662 if done then 718 backupname))
663 break
664 end
665 end
666 if not done then
667 if hunkno <= #hunks and not create_file then
668 warning(format("premature end of source file %s at hunk %d",
669 source, hunkno))
670 return false 719 return false
671 end 720 end
672 end 721 local ok = os.rename(source, backupname)
673 if validhunks < #hunks then 722 if not ok then
674 if check_patched(file, hunks) then 723 warning(string.format("failed backing up %s when patching", source))
675 warning(format("already patched %s", source))
676 elseif not create_file then
677 warning(format("source file is different - %s", source))
678 return false 724 return false
679 end 725 end
680 end 726 patch_hunks(backupname, source, hunks)
681 if not canpatch then 727 info(string.format("successfully patched %s", source))
682 return true 728 os.remove(backupname)
683 end 729 return true
684 local backupname = source .. ".orig"
685 if exists(backupname) then
686 warning(format("can't backup original file to %s - aborting",
687 backupname))
688 return false
689 end
690 local ok = os.rename(source, backupname)
691 if not ok then
692 warning(format("failed backing up %s when patching", source))
693 return false
694 end
695 patch_hunks(backupname, source, hunks)
696 info(format("successfully patched %s", source))
697 os.remove(backupname)
698 return true
699end 730end
700 731
701function patch.apply_patch(the_patch, strip, create_delete) 732function patch.apply_patch(the_patch, strip, create_delete)
702 local all_ok = true 733 local all_ok = true
703 local total = #the_patch.source 734 local total = #the_patch.source
704 for fileno, source in ipairs(the_patch.source) do 735 for fileno, source in ipairs(the_patch.source) do
705 local target = the_patch.target[fileno] 736 local target = the_patch.target[fileno]
706 local hunks = the_patch.hunks[fileno] 737 local hunks = the_patch.hunks[fileno]
707 local epoch = the_patch.epoch[fileno] 738 local epoch = the_patch.epoch[fileno]
708 info(format("processing %d/%d:\t %s", fileno, total, source)) 739 info(string.format("processing %d/%d:\t %s", fileno, total, source))
709 local ok = patch_file(source, target, epoch, hunks, strip, create_delete) 740 local ok = patch_file(tostring(source), tostring(target), epoch, hunks, strip, create_delete)
710 all_ok = all_ok and ok 741 all_ok = all_ok and ok
711 end 742 end
712 -- todo: check for premature eof 743
713 return all_ok 744 return all_ok
714end 745end
715 746
716return patch 747return patch
diff --git a/src/luarocks/tools/patch.tl b/src/luarocks/tools/patch.tl
index bd6c3d63..84de28b7 100644
--- a/src/luarocks/tools/patch.tl
+++ b/src/luarocks/tools/patch.tl
@@ -22,6 +22,14 @@ local record patch
22 invalid: boolean 22 invalid: boolean
23 text: {string} 23 text: {string}
24 end 24 end
25 record File --!
26 at: integer
27 str: string
28 len: integer
29 eof: boolean
30 read: function(File, integer): string
31 close: function(File): boolean
32 end
25 record Files 33 record Files
26 source: {string | number} --! 34 source: {string | number} --!
27 target: {string | number} 35 target: {string | number}
@@ -38,6 +46,7 @@ local fun = require("luarocks.fun")
38local type Lineends = patch.Lineends 46local type Lineends = patch.Lineends
39local type Hunk = patch.Hunk 47local type Hunk = patch.Hunk
40local type Files = patch.Files 48local type Files = patch.Files
49local type File = patch.File
41 50
42-- logging 51-- logging
43local debugmode = false 52local debugmode = false
@@ -75,13 +84,13 @@ local function exists(filename: string): boolean
75end 84end
76local function isfile(): boolean return true end --FIX? --! 85local function isfile(): boolean return true end --FIX? --!
77 86
78local function string_as_file(s: string): FILE --! 87local function string_as_file(s: string): File --!
79 return { 88 return {
80 at = 0, 89 at = 0,
81 str = s, 90 str = s,
82 len = #s, 91 len = #s,
83 eof = false, 92 eof = false,
84 read = function(self: FILE, n: number): string 93 read = function(self: File, n: integer): string
85 if self.eof then return nil end 94 if self.eof then return nil end
86 local chunk = self.str:sub(self.at, self.at + n - 1) 95 local chunk = self.str:sub(self.at, self.at + n - 1)
87 self.at = self.at + n 96 self.at = self.at + n
@@ -90,10 +99,10 @@ local function string_as_file(s: string): FILE --!
90 end 99 end
91 return chunk 100 return chunk
92 end, 101 end,
93 close = function(self: FILE): boolean 102 close = function(self: File): boolean
94 self.eof = true 103 self.eof = true
95 end, 104 end,
96 } as FILE 105 } as File
97end 106end
98 107
99-- 108--
@@ -178,7 +187,7 @@ function patch.read_patch(filename: string, data: string): Files, boolean
178 187
179 local fp: FILE 188 local fp: FILE
180 if data then 189 if data then
181 fp = string_as_file(data) 190 fp = string_as_file(data) as FILE --! cast
182 else 191 else
183 fp = filename == '-' and io.stdin or assert(io.open(filename, "rb") as (FILE, string)) --! use of cast 192 fp = filename == '-' and io.stdin or assert(io.open(filename, "rb") as (FILE, string)) --! use of cast
184 end 193 end