Skip to content
Navigation Menu
{{ message }}
forked from rjpcomputing/luaforwindows
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatch.lua
More file actions
712 lines (663 loc) · 21.8 KB
/
Copy pathpatch.lua
File metadata and controls
712 lines (663 loc) · 21.8 KB
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
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
-- patch.lua - Patch utility to apply unified diffs
--
-- http://lua-users.org/wiki/LuaPatch
--
-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license).
-- Code is heavilly based on the Python-based patch.py version 8.06-1
-- Copyright (c) 2008 rainforce.org, MIT License
-- Project home: http://code.google.com/p/python-patch/ .
module("luarocks.tools.patch", package.seeall)
local fs = require("luarocks.fs")
local version = '0.1'
local io = io
local os = os
local string = string
local table = table
local format = string.format
-- logging
local debugmode = false
local function debug(s) end
local function info(s) end
local function warning(s) io.stderr:write(s .. '\n') end
-- Returns boolean whether string s2 starts with string s.
local function startswith(s, s2)
return s:sub(1, #s2) == s2
end
-- Returns boolean whether string s2 ends with string s.
local function endswith(s, s2)
return #s >= #s2 and s:sub(#s-#s2+1) == s2
end
-- Returns string s after filtering out any new-line characters from end.
local function endlstrip(s)
return s:gsub('[\r\n]+$', '')
end
-- Returns shallow copy of table t.
local function table_copy(t)
local t2 = {}
for k,v in pairs(t) do t2[k] = v end
return t2
end
-- Returns boolean whether array t contains value v.
local function array_contains(t, v)
for _,v2 in ipairs(t) do if v == v2 then return true end end
return false
end
local function exists(filename)
local fh = io.open(filename)
local result = fh ~= nil
if fh then fh:close() end
return result
end
local function isfile() return true end --FIX?
local function read_file(filename)
local fh, err, oserr = io.open(filename, 'rb')
if not fh then return fh, err, oserr end
local data, err, oserr = fh:read'*a'
fh:close()
if not data then return nil, err, oserr end
return data
end
local function write_file(filename, data)
local fh, err, oserr = io.open(filename 'wb')
if not fh then return fh, err, oserr end
local status, err, oserr = fh:write(data)
fh:close()
if not status then return nil, err, oserr end
return true
end
local function file_copy(src, dest)
local data, err, oserr = read_file(src)
if not data then return data, err, oserr end
local status, err, oserr = write_file(dest)
if not status then return status, err, oserr end
return true
end
local function string_as_file(s)
return {
at = 0,
str = s,
len = #s,
eof = false,
read = function(self, n)
if self.eof then return nil end
local chunk = self.str:sub(self.at, self.at+n)
self.at = self.at + n
if self.at > self.len then
self.eof = true
end
return chunk
end,
close = function(self)
self.eof = true
end,
}
end
--
-- file_lines(f) is similar to f:lines() for file f.
-- The main difference is that read_lines includes
-- new-line character sequences ("\n", "\r\n", "\r"),
-- if any, at the end of each line. Embedded "\0" are also handled.
-- Caution: The newline behavior can depend on whether f is opened
-- in binary or ASCII mode.
-- (file_lines - version 20080913)
--
local function file_lines(f)
local CHUNK_SIZE = 1024
local buffer = ""
local pos_beg = 1
return function()
local pos, chars
while 1 do
pos, chars = buffer:match('()([\r\n].)', pos_beg)
if pos or not f then
break
elseif f then
local chunk = f:read(CHUNK_SIZE)
if chunk then
buffer = buffer:sub(pos_beg) .. chunk
pos_beg = 1
else
f = nil
end
end
end
if not pos then
pos = #buffer
elseif chars == '\r\n' then
pos = pos + 1
end
local line = buffer:sub(pos_beg, pos)
pos_beg = pos + 1
if #line > 0 then
return line
end
end
end
local function match_linerange(line)
local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)")
if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end
if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end
if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end
return m1, m2, m3, m4
end
function read_patch(filename, data)
-- define possible file regions that will direct the parser flow
local state = 'header'
-- 'header' - comments before the patch body
-- 'filenames' - lines starting with --- and +++
-- 'hunkhead' - @@ -R +R @@ sequence
-- 'hunkbody'
-- 'hunkskip' - skipping invalid hunk mode
local all_ok = true
local lineends = {lf=0, crlf=0, cr=0}
local files = {source={}, target={}, hunks={}, fileends={}, hunkends={}}
local nextfileno = 0
local nexthunkno = 0 --: even if index starts with 0 user messages
-- number hunks from 1
-- hunkinfo holds parsed values, hunkactual - calculated
local hunkinfo = {
startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil,
invalid=false, text={}
}
local hunkactual = {linessrc=nil, linestgt=nil}
info(format("reading patch %s", filename))
local fp
if data then
fp = string_as_file(data)
else
fp = filename == '-' and io.stdin or assert(io.open(filename, "rb"))
end
local lineno = 0
for line in file_lines(fp) do
lineno = lineno + 1
if state == 'header' then
if startswith(line, "--- ") then
state = 'filenames'
end
-- state is 'header' or 'filenames'
end
if state == 'hunkbody' then
-- skip hunkskip and hunkbody code until definition of hunkhead read
-- process line first
if line:match"^[- +\\]" or line:match"^[\r\n]*$" then
-- gather stats about line endings
local he = files.hunkends[nextfileno]
if endswith(line, "\r\n") then
he.crlf = he.crlf + 1
elseif endswith(line, "\n") then
he.lf = he.lf + 1
elseif endswith(line, "\r") then
he.cr = he.cr + 1
end
if startswith(line, "-") then
hunkactual.linessrc = hunkactual.linessrc + 1
elseif startswith(line, "+") then
hunkactual.linestgt = hunkactual.linestgt + 1
elseif startswith(line, "\\") then
-- nothing
else
hunkactual.linessrc = hunkactual.linessrc + 1
hunkactual.linestgt = hunkactual.linestgt + 1
end
table.insert(hunkinfo.text, line)
-- todo: handle \ No newline cases
else
warning(format("invalid hunk no.%d at %d for target file %s",
nexthunkno, lineno, files.target[nextfileno]))
-- add hunk status node
table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
files.hunks[nextfileno][nexthunkno].invalid = true
all_ok = false
state = 'hunkskip'
end
-- check exit conditions
if hunkactual.linessrc > hunkinfo.linessrc or
hunkactual.linestgt > hunkinfo.linestgt
then
warning(format("extra hunk no.%d lines at %d for target %s",
nexthunkno, lineno, files.target[nextfileno]))
-- add hunk status node
table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
files.hunks[nextfileno][nexthunkno].invalid = true
state = 'hunkskip'
elseif hunkinfo.linessrc == hunkactual.linessrc and
hunkinfo.linestgt == hunkactual.linestgt
then
table.insert(files.hunks[nextfileno], table_copy(hunkinfo))
state = 'hunkskip'
-- detect mixed window/unix line ends
local ends = files.hunkends[nextfileno]
if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) +
(ends.lf~=0 and 1 or 0) > 1
then
warning(format("inconsistent line ends in patch hunks for %s",
files.source[nextfileno]))
end
if debugmode then
local debuglines = {crlf=ends.crlf, lf=ends.lf, cr=ends.cr,
file=files.target[nextfileno], hunk=nexthunkno}
debug(format("crlf: %(crlf)d lf: %(lf)d cr: %(cr)d\t " ..
"- file: %(file)s hunk: %(hunk)d", debuglines))
end
end
-- state is 'hunkbody' or 'hunkskip'
end
if state == 'hunkskip' then
if match_linerange(line) then
state = 'hunkhead'
elseif startswith(line, "--- ") then
state = 'filenames'
if debugmode and #files.source > 0 then
debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
files.source[nextfileno]))
end
end
-- state is 'hunkskip', 'hunkhead', or 'filenames'
end
local advance
if state == 'filenames' then
if startswith(line, "--- ") then
if array_contains(files.source, nextfileno) then
all_ok = false
warning(format("skipping invalid patch for %s",
files.source[nextfileno+1]))
table.remove(files.source, nextfileno+1)
-- double source filename line is encountered
-- attempt to restart from this second line
end
-- Accept a space as a terminator, like GNU patch does.
-- Breaks patches containing filenames with spaces...
-- FIXME Figure out what does GNU patch do in those cases.
local match = line:match("^--- ([^\t ]+)")
if not match then
all_ok = false
warning(format("skipping invalid filename at line %d", lineno+1))
state = 'header'
else
table.insert(files.source, match)
end
elseif not startswith(line, "+++ ") then
if array_contains(files.source, nextfileno) then
all_ok = false
warning(format("skipping invalid patch with no target for %s",
files.source[nextfileno+1]))
table.remove(files.source, nextfileno+1)
else
-- this should be unreachable
warning("skipping invalid target patch")
end
state = 'header'
else
if array_contains(files.target, nextfileno) then
all_ok = false
warning(format("skipping invalid patch - double target at line %d",
lineno+1))
table.remove(files.source, nextfileno+1)
table.remove(files.target, nextfileno+1)
nextfileno = nextfileno - 1
-- double target filename line is encountered
-- switch back to header state
state = 'header'
else
-- Accept a space as a terminator, like GNU patch does.
-- Breaks patches containing filenames with spaces...
-- FIXME Figure out what does GNU patch do in those cases.
local re_filename = "^%+%+%+ ([^ \t]+)"
local match = line:match(re_filename)
if not match then
all_ok = false
warning(format(
"skipping invalid patch - no target filename at line %d",
lineno+1))
state = 'header'
else
table.insert(files.target, match)
nextfileno = nextfileno + 1
nexthunkno = 0
table.insert(files.hunks, {})
table.insert(files.hunkends, table_copy(lineends))
table.insert(files.fileends, table_copy(lineends))
state = 'hunkhead'
advance = true
end
end
end
-- state is 'filenames', 'header', or ('hunkhead' with advance)
end
if not advance and state == 'hunkhead' then
local m1, m2, m3, m4 = match_linerange(line)
if not m1 then
if not array_contains(files.hunks, nextfileno-1) then
all_ok = false
warning(format("skipping invalid patch with no hunks for file %s",
files.target[nextfileno]))
end
state = 'header'
else
hunkinfo.startsrc = tonumber(m1)
hunkinfo.linessrc = tonumber(m2 or 1)
hunkinfo.starttgt = tonumber(m3)
hunkinfo.linestgt = tonumber(m4 or 1)
hunkinfo.invalid = false
hunkinfo.text = {}
hunkactual.linessrc = 0
hunkactual.linestgt = 0
state = 'hunkbody'
nexthunkno = nexthunkno + 1
end
-- state is 'header' or 'hunkbody'
end
end
if state ~= 'hunkskip' then
warning(format("patch file incomplete - %s", filename))
all_ok = false
-- os.exit(?)
else
-- duplicated message when an eof is reached
if debugmode and #files.source > 0 then
debug(format("- %2d hunks for %s", #files.hunks[nextfileno],
files.source[nextfileno]))
end
end
local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end
info(format("total files: %d total hunks: %d", #files.source, sum))
fp:close()
return files, all_ok
end
local function find_hunk(file, h, hno)
for fuzz=0,2 do
local lineno = h.startsrc
for i=0,#file do
local found = true
local location = lineno
local total = #h.text - fuzz
for l, hline in ipairs(h.text) do
if l > fuzz then
-- todo: \ No newline at the end of file
if startswith(hline, " ") or startswith(hline, "-") then
local line = file[lineno]
lineno = lineno + 1
if not line or #line == 0 then
found = false
break
end
if endlstrip(line) ~= endlstrip(hline:sub(2)) then
found = false
break
end
end
end
end
if found then
local offset = location - h.startsrc - fuzz
if offset ~= 0 then
warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz)))
end
h.startsrc = location
h.starttgt = h.starttgt + offset
for i=1,fuzz do
table.remove(h.text, 1)
table.remove(h.text, #h.text)
end
return true
end
lineno = i
end
end
return false
end
local function load_file(filename)
local fp = assert(io.open(filename))
local file = {}
local readline = file_lines(fp)
while true do
local line = readline()
if not line then break end
table.insert(file, line)
end
fp:close()
return file
end
local function find_hunks(file, hunks)
local matched = true
local lineno = 1
local hno = nil
for hno, h in ipairs(hunks) do
find_hunk(file, h, hno)
end
end
local function check_patched(file, hunks)
local matched = true
local lineno = 1
local hno = nil
local ok, err = pcall(function()
if #file == 0 then
error 'nomatch'
end
for hno, h in ipairs(hunks) do
-- skip to line just before hunk starts
if #file < h.starttgt then
error 'nomatch'
end
lineno = h.starttgt
for _, hline in ipairs(h.text) do
-- todo: \ No newline at the end of file
if not startswith(hline, "-") and not startswith(hline, "\\") then
local line = file[lineno]
lineno = lineno + 1
if #line == 0 then
error 'nomatch'
end
if endlstrip(line) ~= endlstrip(hline:sub(2)) then
warning(format("file is not patched - failed hunk: %d", hno))
error 'nomatch'
end
end
end
end
end)
if err == 'nomatch' then
matched = false
end
-- todo: display failed hunk, i.e. expected/found
return matched
end
local function patch_hunks(srcname, tgtname, hunks)
local src = assert(io.open(srcname, "rb"))
local tgt = assert(io.open(tgtname, "wb"))
local src_readline = file_lines(src)
-- todo: detect linefeeds early - in apply_files routine
-- to handle cases when patch starts right from the first
-- line and no lines are processed. At the moment substituted
-- lineends may not be the same at the start and at the end
-- of patching. Also issue a warning about mixed lineends
local srclineno = 1
local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0}
for hno, h in ipairs(hunks) do
debug(format("processing hunk %d for file %s", hno, tgtname))
-- skip to line just before hunk starts
while srclineno < h.startsrc do
local line = src_readline()
-- Python 'U' mode works only with text files
if endswith(line, "\r\n") then
lineends["\r\n"] = lineends["\r\n"] + 1
elseif endswith(line, "\n") then
lineends["\n"] = lineends["\n"] + 1
elseif endswith(line, "\r") then
lineends["\r"] = lineends["\r"] + 1
end
tgt:write(line)
srclineno = srclineno + 1
end
for _,hline in ipairs(h.text) do
-- todo: check \ No newline at the end of file
if startswith(hline, "-") or startswith(hline, "\\") then
src_readline()
srclineno = srclineno + 1
else
if not startswith(hline, "+") then
src_readline()
srclineno = srclineno + 1
end
local line2write = hline:sub(2)
-- detect if line ends are consistent in source file
local sum = 0
for k,v in pairs(lineends) do if v > 0 then sum=sum+1 end end
if sum == 1 then
local newline
for k,v in pairs(lineends) do if v ~= 0 then newline = k end end
tgt:write(endlstrip(line2write) .. newline)
else -- newlines are mixed or unknown
tgt:write(line2write)
end
end
end
end
for line in src_readline do
tgt:write(line)
end
tgt:close()
src:close()
return true
end
local function strip_dirs(filename, strip)
if strip == nil then return filename end
for i=1,strip do
filename=filename:gsub("^[^/]*/", "")
end
return filename
end
function apply_patch(patch, strip)
local all_ok = true
local total = #patch.source
for fileno, filename in ipairs(patch.source) do
filename = strip_dirs(filename, strip)
local continue
local f2patch = filename
if not exists(f2patch) then
f2patch = strip_dirs(patch.target[fileno], strip)
f2patch = fs.absolute_name(f2patch)
if not exists(f2patch) then --FIX:if f2patch nil
warning(format("source/target file does not exist\n--- %s\n+++ %s",
filename, f2patch))
all_ok = false
continue = true
end
end
if not continue and not isfile(f2patch) then
warning(format("not a file - %s", f2patch))
all_ok = false
continue = true
end
if not continue then
filename = f2patch
info(format("processing %d/%d:\t %s", fileno, total, filename))
-- validate before patching
local hunks = patch.hunks[fileno]
local file = load_file(filename)
local hunkno = 1
local hunk = hunks[hunkno]
local hunkfind = {}
local hunkreplace = {}
local validhunks = 0
local canpatch = false
local hunklineno
local isbreak
local lineno = 0
find_hunks(file, hunks)
for _, line in ipairs(file) do
lineno = lineno + 1
local continue
if not hunk or lineno < hunk.startsrc then
continue = true
elseif lineno == hunk.startsrc then
hunkfind = {}
for _,x in ipairs(hunk.text) do
if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then
hunkfind[#hunkfind+1] = endlstrip(x:sub(2))
end end
hunkreplace = {}
for _,x in ipairs(hunk.text) do
if x:sub(1,1) == ' ' or x:sub(1,1) == '+' then
hunkreplace[#hunkreplace+1] = endlstrip(x:sub(2))
end end
--pprint(hunkreplace)
hunklineno = 1
-- todo \ No newline at end of file
end
-- check hunks in source file
if not continue and lineno < hunk.startsrc + #hunkfind - 1 then
if endlstrip(line) == hunkfind[hunklineno] then
hunklineno = hunklineno + 1
else
debug(format("hunk no.%d doesn't match source file %s",
hunkno, filename))
-- file may be already patched, but check other hunks anyway
hunkno = hunkno + 1
if hunkno <= #hunks then
hunk = hunks[hunkno]
continue = true
else
isbreak = true; break
end
end
end
-- check if processed line is the last line
if not continue and lineno == hunk.startsrc + #hunkfind - 1 then
debug(format("file %s hunk no.%d -- is ready to be patched",
filename, hunkno))
hunkno = hunkno + 1
validhunks = validhunks + 1
if hunkno <= #hunks then
hunk = hunks[hunkno]
else
if validhunks == #hunks then
-- patch file
canpatch = true
isbreak = true; break
end
end
end
end
if not isbreak then
if hunkno <= #hunks then
warning(format("premature end of source file %s at hunk %d",
filename, hunkno))
all_ok = false
end
end
if validhunks < #hunks then
if check_patched(file, hunks) then
warning(format("already patched %s", filename))
else
warning(format("source file is different - %s", filename))
all_ok = false
end
end
if canpatch then
local backupname = filename .. ".orig"
if exists(backupname) then
warning(format("can't backup original file to %s - aborting",
backupname))
all_ok = false
else
assert(os.rename(filename, backupname))
if patch_hunks(backupname, filename, hunks) then
warning(format("successfully patched %s", filename))
assert(os.remove(backupname))
else
warning(format("error patching file %s", filename))
assert(file_copy(filename, filename .. ".invalid"))
warning(format("invalid version is saved to %s",
filename .. ".invalid"))
-- todo: proper rejects
assert(os.rename(backupname, filename))
all_ok = false
end
end
end
end -- if not continue
end -- for
-- todo: check for premature eof
return all_ok
end
You can’t perform that action at this time.
