xref: /freebsd/tools/pkgbase/metalog_reader.lua (revision 6ef644f5)
1060a805bSEd Maste#!/usr/libexec/flua
2060a805bSEd Maste
34d846d26SWarner Losh-- SPDX-License-Identifier: BSD-2-Clause
4060a805bSEd Maste--
5060a805bSEd Maste-- Copyright(c) 2020 The FreeBSD Foundation.
6060a805bSEd Maste--
7060a805bSEd Maste-- Redistribution and use in source and binary forms, with or without
8060a805bSEd Maste-- modification, are permitted provided that the following conditions
9060a805bSEd Maste-- are met:
10060a805bSEd Maste-- 1. Redistributions of source code must retain the above copyright
11060a805bSEd Maste--    notice, this list of conditions and the following disclaimer.
12060a805bSEd Maste-- 2. Redistributions in binary form must reproduce the above copyright
13060a805bSEd Maste--    notice, this list of conditions and the following disclaimer in the
14060a805bSEd Maste--    documentation and/or other materials provided with the distribution.
15060a805bSEd Maste--
16060a805bSEd Maste-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17060a805bSEd Maste-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18060a805bSEd Maste-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19060a805bSEd Maste-- ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20060a805bSEd Maste-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21060a805bSEd Maste-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22060a805bSEd Maste-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23060a805bSEd Maste-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24060a805bSEd Maste-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25060a805bSEd Maste-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26060a805bSEd Maste-- SUCH DAMAGE.
27060a805bSEd Maste
28060a805bSEd Maste
29060a805bSEd Mastefunction main(args)
30060a805bSEd Maste	if #args == 0 then usage() end
31060a805bSEd Maste	local filename
32060a805bSEd Maste	local printall, checkonly, pkgonly =
33060a805bSEd Maste	    #args == 1, false, false
34060a805bSEd Maste	local dcount, dsize, fuid, fgid, fid =
35060a805bSEd Maste	    false, false, false, false, false
36060a805bSEd Maste	local verbose = false
37060a805bSEd Maste	local w_notagdirs = false
38060a805bSEd Maste
39060a805bSEd Maste	local i = 1
40060a805bSEd Maste	while i <= #args do
41060a805bSEd Maste		if args[i] == '-h' then
42060a805bSEd Maste			usage(true)
43060a805bSEd Maste		elseif args[i] == '-a' then
44060a805bSEd Maste			printall = true
45060a805bSEd Maste		elseif args[i] == '-c' then
46060a805bSEd Maste			printall = false
47060a805bSEd Maste			checkonly = true
48060a805bSEd Maste		elseif args[i] == '-p' then
49060a805bSEd Maste			printall = false
50060a805bSEd Maste			pkgonly = true
51060a805bSEd Maste			while i < #args do
52060a805bSEd Maste				i = i+1
53060a805bSEd Maste				if args[i] == '-count' then
54060a805bSEd Maste					dcount = true
55060a805bSEd Maste				elseif args[i] == '-size' then
56060a805bSEd Maste					dsize = true
57060a805bSEd Maste				elseif args[i] == '-fsetuid' then
58060a805bSEd Maste					fuid = true
59060a805bSEd Maste				elseif args[i] == '-fsetgid' then
60060a805bSEd Maste					fgid = true
61060a805bSEd Maste				elseif args[i] == '-fsetid' then
62060a805bSEd Maste					fid = true
63060a805bSEd Maste				else
64060a805bSEd Maste					i = i-1
65060a805bSEd Maste					break
66060a805bSEd Maste				end
67060a805bSEd Maste			end
68060a805bSEd Maste		elseif args[i] == '-v' then
69060a805bSEd Maste			verbose = true
70060a805bSEd Maste		elseif args[i] == '-Wcheck-notagdir' then
71060a805bSEd Maste			w_notagdirs = true
72060a805bSEd Maste		elseif args[i]:match('^%-') then
73060a805bSEd Maste			io.stderr:write('Unknown argument '..args[i]..'.\n')
74060a805bSEd Maste			usage()
75060a805bSEd Maste		else
76060a805bSEd Maste			filename = args[i]
77060a805bSEd Maste		end
78060a805bSEd Maste		i = i+1
79060a805bSEd Maste	end
80060a805bSEd Maste
81060a805bSEd Maste	if filename == nil then
82060a805bSEd Maste		io.stderr:write('Missing filename.\n')
83060a805bSEd Maste		usage()
84060a805bSEd Maste	end
85060a805bSEd Maste
86060a805bSEd Maste	local sess = Analysis_session(filename, verbose, w_notagdirs)
87060a805bSEd Maste
88f93d92f4SEd Maste	local errors
89060a805bSEd Maste	if printall then
90060a805bSEd Maste		io.write('--- PACKAGE REPORTS ---\n')
91060a805bSEd Maste		io.write(sess.pkg_report_full())
92060a805bSEd Maste		io.write('--- LINTING REPORTS ---\n')
93f93d92f4SEd Maste		errors = print_lints(sess)
94060a805bSEd Maste	elseif checkonly then
95f93d92f4SEd Maste		errors = print_lints(sess)
96060a805bSEd Maste	elseif pkgonly then
97060a805bSEd Maste		io.write(sess.pkg_report_simple(dcount, dsize, {
98060a805bSEd Maste			fuid and sess.pkg_issetuid or nil,
99060a805bSEd Maste			fgid and sess.pkg_issetgid or nil,
100060a805bSEd Maste			fid and sess.pkg_issetid or nil
101060a805bSEd Maste		}))
102060a805bSEd Maste	else
103060a805bSEd Maste		io.stderr:write('This text should not be displayed.')
104060a805bSEd Maste		usage()
105060a805bSEd Maste	end
106f93d92f4SEd Maste
107f93d92f4SEd Maste	if errors then
108f93d92f4SEd Maste		return 1
109f93d92f4SEd Maste	end
110060a805bSEd Masteend
111060a805bSEd Maste
112060a805bSEd Maste--- @param man boolean
113060a805bSEd Mastefunction usage(man)
114060a805bSEd Maste	local sn = 'Usage: '..arg[0].. ' [-h] [-a | -c | -p [-count] [-size] [-f...]] [-W...] metalog-path \n'
115060a805bSEd Maste	if man then
116060a805bSEd Maste		io.write('\n')
117060a805bSEd Maste		io.write(sn)
118060a805bSEd Maste		io.write(
119060a805bSEd Maste[[
120060a805bSEd Maste
121060a805bSEd MasteThe script reads METALOG file created by pkgbase (make packages) and generates
122060a805bSEd Mastereports about the installed system and issues.  It accepts an mtree file in a
123060a805bSEd Masteformat that's returned by `mtree -c | mtree -C`
124060a805bSEd Maste
125060a805bSEd Maste  Options:
126060a805bSEd Maste  -a         prints all scan results. this is the default option if no option
127060a805bSEd Maste             is provided.
128060a805bSEd Maste  -c         lints the file and gives warnings/errors, including duplication
129060a805bSEd Maste             and conflicting metadata
130060a805bSEd Maste      -Wcheck-notagdir    entries with dir type and no tags will be also
131060a805bSEd Maste                          included the first time they appear
132060a805bSEd Maste  -p         list all package names found in the file as exactly specified by
133060a805bSEd Maste             `tags=package=...`
134060a805bSEd Maste      -count       display the number of files of the package
135060a805bSEd Maste      -size        display the size of the package
136060a805bSEd Maste      -fsetgid     only include packages with setgid files
137060a805bSEd Maste      -fsetuid     only include packages with setuid files
138060a805bSEd Maste      -fsetid      only include packages with setgid or setuid files
139060a805bSEd Maste  -v          verbose mode
140060a805bSEd Maste  -h          help page
141060a805bSEd Maste
142060a805bSEd Maste]])
143060a805bSEd Maste		os.exit()
144060a805bSEd Maste	else
145060a805bSEd Maste		io.stderr:write(sn)
146060a805bSEd Maste		os.exit(1)
147060a805bSEd Maste	end
148060a805bSEd Masteend
149060a805bSEd Maste
150060a805bSEd Maste--- @param sess Analysis_session
151060a805bSEd Mastefunction print_lints(sess)
152060a805bSEd Maste	local dupwarn, duperr = sess.dup_report()
153060a805bSEd Maste	io.write(dupwarn)
154060a805bSEd Maste	io.write(duperr)
155060a805bSEd Maste	local inodewarn, inodeerr = sess.inode_report()
156060a805bSEd Maste	io.write(inodewarn)
157060a805bSEd Maste	io.write(inodeerr)
158f93d92f4SEd Maste	return #duperr > 0 or #inodeerr > 0
159060a805bSEd Masteend
160060a805bSEd Maste
161060a805bSEd Maste--- @param t table
162060a805bSEd Mastefunction sortedPairs(t)
163060a805bSEd Maste	local sortedk = {}
164060a805bSEd Maste	for k in next, t do sortedk[#sortedk+1] = k end
165060a805bSEd Maste	table.sort(sortedk)
166060a805bSEd Maste	local i = 0
167060a805bSEd Maste	return function()
168060a805bSEd Maste		i = i + 1
169060a805bSEd Maste		return sortedk[i], t[sortedk[i]]
170060a805bSEd Maste	end
171060a805bSEd Masteend
172060a805bSEd Maste
173060a805bSEd Maste--- @param t table <T, U>
174060a805bSEd Maste--- @param f function <U -> U>
175060a805bSEd Mastefunction table_map(t, f)
176060a805bSEd Maste	local res = {}
177060a805bSEd Maste	for k, v in pairs(t) do res[k] = f(v) end
178060a805bSEd Maste	return res
179060a805bSEd Masteend
180060a805bSEd Maste
181060a805bSEd Maste--- @class MetalogRow
182060a805bSEd Maste-- a table contaning file's info, from a line content from METALOG file
183060a805bSEd Maste-- all fields in the table are strings
184060a805bSEd Maste-- sample output:
185060a805bSEd Maste--	{
186060a805bSEd Maste--		filename = ./usr/share/man/man3/inet6_rthdr_segments.3.gz
187060a805bSEd Maste--		lineno = 5
188060a805bSEd Maste--		attrs = {
189060a805bSEd Maste--			gname = 'wheel'
190060a805bSEd Maste--			uname = 'root'
191060a805bSEd Maste--			mode = '0444'
192060a805bSEd Maste--			size = '1166'
193060a805bSEd Maste--			time = nil
194060a805bSEd Maste--			type = 'file'
195060a805bSEd Maste--			tags = 'package=clibs,debug'
196060a805bSEd Maste--		}
197060a805bSEd Maste--	}
198060a805bSEd Maste--- @param line string
199060a805bSEd Mastefunction MetalogRow(line, lineno)
200060a805bSEd Maste	local res, attrs = {}, {}
201060a805bSEd Maste	local filename, rest = line:match('^(%S+) (.+)$')
202060a805bSEd Maste	-- mtree file has space escaped as '\\040', not affecting splitting
203060a805bSEd Maste	-- string by space
204060a805bSEd Maste	for attrpair in rest:gmatch('[^ ]+') do
205060a805bSEd Maste		local k, v = attrpair:match('^(.-)=(.+)')
206060a805bSEd Maste		attrs[k] = v
207060a805bSEd Maste	end
208060a805bSEd Maste	res.filename = filename
209060a805bSEd Maste	res.linenum = lineno
210060a805bSEd Maste	res.attrs = attrs
211060a805bSEd Maste	return res
212060a805bSEd Masteend
213060a805bSEd Maste
214060a805bSEd Maste-- check if an array of MetalogRows are equivalent. if not, the first field
215060a805bSEd Maste-- that's different is returned secondly
216060a805bSEd Maste--- @param rows MetalogRow[]
217060a805bSEd Maste--- @param ignore_name boolean
218060a805bSEd Maste--- @param ignore_tags boolean
219060a805bSEd Mastefunction metalogrows_all_equal(rows, ignore_name, ignore_tags)
220060a805bSEd Maste	local __eq = function(l, o)
221060a805bSEd Maste		if not ignore_name and l.filename ~= o.filename then
222060a805bSEd Maste			return false, 'filename'
223060a805bSEd Maste		end
224060a805bSEd Maste		-- ignoring linenum in METALOG file as it's not relavant
225060a805bSEd Maste		for k in pairs(l.attrs) do
226060a805bSEd Maste			if ignore_tags and k == 'tags' then goto continue end
227060a805bSEd Maste			if l.attrs[k] ~= o.attrs[k] and o.attrs[k] ~= nil then
228060a805bSEd Maste				return false, k
229060a805bSEd Maste			end
230060a805bSEd Maste			::continue::
231060a805bSEd Maste		end
232060a805bSEd Maste		return true
233060a805bSEd Maste	end
234060a805bSEd Maste	for _, v in ipairs(rows) do
235060a805bSEd Maste		local bol, offby = __eq(v, rows[1])
236060a805bSEd Maste		if not bol then return false, offby end
237060a805bSEd Maste	end
238060a805bSEd Maste	return true
239060a805bSEd Masteend
240060a805bSEd Maste
241060a805bSEd Maste--- @param tagstr string
242060a805bSEd Mastefunction pkgname_from_tag(tagstr)
243060a805bSEd Maste	local ext, pkgname, pkgend = '', '', ''
244060a805bSEd Maste	for seg in tagstr:gmatch('[^,]+') do
245060a805bSEd Maste		if seg:match('package=') then
246060a805bSEd Maste			pkgname = seg:sub(9)
247060a805bSEd Maste		elseif seg == 'development' or seg == 'profile'
248060a805bSEd Maste			or seg == 'debug' or seg == 'docs' then
249060a805bSEd Maste			pkgend = seg
250060a805bSEd Maste		else
251060a805bSEd Maste			ext = ext == '' and seg or ext..'-'..seg
252060a805bSEd Maste		end
253060a805bSEd Maste	end
254060a805bSEd Maste	pkgname = pkgname
255060a805bSEd Maste		..(ext == '' and '' or '-'..ext)
256060a805bSEd Maste		..(pkgend == '' and '' or '-'..pkgend)
257060a805bSEd Maste	return pkgname
258060a805bSEd Masteend
259060a805bSEd Maste
260060a805bSEd Maste--- @class Analysis_session
261060a805bSEd Maste--- @param metalog string
262060a805bSEd Maste--- @param verbose boolean
263060a805bSEd Maste--- @param w_notagdirs boolean turn on to also check directories
264060a805bSEd Mastefunction Analysis_session(metalog, verbose, w_notagdirs)
265bca4d270SEd Maste	local stage_root = {}
266060a805bSEd Maste	local files = {} -- map<string, MetalogRow[]>
267060a805bSEd Maste	-- set is map<elem, bool>. if bool is true then elem exists
268060a805bSEd Maste	local pkgs = {} -- map<string, set<string>>
269060a805bSEd Maste	----- used to keep track of files not belonging to a pkg. not used so
270060a805bSEd Maste	----- it is commented with -----
271060a805bSEd Maste	-----local nopkg = {} --            set<string>
272060a805bSEd Maste	--- @public
273060a805bSEd Maste	local swarn = {}
274060a805bSEd Maste	--- @public
275060a805bSEd Maste	local serrs = {}
276060a805bSEd Maste
277060a805bSEd Maste	-- returns number of files in package and size of package
278060a805bSEd Maste	-- nil is  returned upon errors
279060a805bSEd Maste	--- @param pkgname string
280060a805bSEd Maste	local function pkg_size(pkgname)
281060a805bSEd Maste		local filecount, sz = 0, 0
282060a805bSEd Maste		for filename in pairs(pkgs[pkgname]) do
283060a805bSEd Maste			local rows = files[filename]
284060a805bSEd Maste			-- normally, there should be only one row per filename
285060a805bSEd Maste			-- if these rows are equal, there should be warning, but it
286060a805bSEd Maste			-- does not affect size counting. if not, it is an error
287060a805bSEd Maste			if #rows > 1 and not metalogrows_all_equal(rows) then
288060a805bSEd Maste				return nil
289060a805bSEd Maste			end
290060a805bSEd Maste			local row = rows[1]
291060a805bSEd Maste			if row.attrs.type == 'file' then
292060a805bSEd Maste				sz = sz + tonumber(row.attrs.size)
293060a805bSEd Maste			end
294060a805bSEd Maste			filecount = filecount + 1
295060a805bSEd Maste		end
296060a805bSEd Maste		return filecount, sz
297060a805bSEd Maste	end
298060a805bSEd Maste
299060a805bSEd Maste	--- @param pkgname string
300060a805bSEd Maste	--- @param mode number
301060a805bSEd Maste	local function pkg_ismode(pkgname, mode)
302060a805bSEd Maste		for filename in pairs(pkgs[pkgname]) do
303060a805bSEd Maste			for _, row in ipairs(files[filename]) do
304060a805bSEd Maste				if tonumber(row.attrs.mode, 8) & mode ~= 0 then
305060a805bSEd Maste					return true
306060a805bSEd Maste				end
307060a805bSEd Maste			end
308060a805bSEd Maste		end
309060a805bSEd Maste		return false
310060a805bSEd Maste	end
311060a805bSEd Maste
312060a805bSEd Maste	--- @param pkgname string
313060a805bSEd Maste	--- @public
314060a805bSEd Maste	local function pkg_issetuid(pkgname)
315060a805bSEd Maste		return pkg_ismode(pkgname, 2048)
316060a805bSEd Maste	end
317060a805bSEd Maste
318060a805bSEd Maste	--- @param pkgname string
319060a805bSEd Maste	--- @public
320060a805bSEd Maste	local function pkg_issetgid(pkgname)
321060a805bSEd Maste		return pkg_ismode(pkgname, 1024)
322060a805bSEd Maste	end
323060a805bSEd Maste
324060a805bSEd Maste	--- @param pkgname string
325060a805bSEd Maste	--- @public
326060a805bSEd Maste	local function pkg_issetid(pkgname)
327060a805bSEd Maste		return pkg_issetuid(pkgname) or pkg_issetgid(pkgname)
328060a805bSEd Maste	end
329060a805bSEd Maste
330060a805bSEd Maste	-- sample return:
331060a805bSEd Maste	-- { [*string]: { count=1, size=2, issetuid=true, issetgid=true } }
332060a805bSEd Maste	local function pkg_report_helper_table()
333060a805bSEd Maste		local res = {}
334060a805bSEd Maste		for pkgname in pairs(pkgs) do
335060a805bSEd Maste			res[pkgname] = {}
336060a805bSEd Maste			res[pkgname].count,
337060a805bSEd Maste			res[pkgname].size = pkg_size(pkgname)
338060a805bSEd Maste			res[pkgname].issetuid = pkg_issetuid(pkgname)
339060a805bSEd Maste			res[pkgname].issetgid = pkg_issetgid(pkgname)
340060a805bSEd Maste		end
341060a805bSEd Maste		return res
342060a805bSEd Maste	end
343060a805bSEd Maste
344060a805bSEd Maste	-- returns a string describing package scan report
345060a805bSEd Maste	--- @public
346060a805bSEd Maste	local function pkg_report_full()
347060a805bSEd Maste		local sb = {}
348060a805bSEd Maste		for pkgname, v in sortedPairs(pkg_report_helper_table()) do
349060a805bSEd Maste			sb[#sb+1] = 'Package '..pkgname..':'
350060a805bSEd Maste			if v.issetuid or v.issetgid then
351060a805bSEd Maste				sb[#sb+1] = ''..table.concat({
352060a805bSEd Maste					v.issetuid and ' setuid' or '',
353060a805bSEd Maste					v.issetgid and ' setgid' or '' }, '')
354060a805bSEd Maste			end
355060a805bSEd Maste			sb[#sb+1] = '\n  number of files: '..(v.count or '?')
356060a805bSEd Maste				..'\n  total size: '..(v.size or '?')
357060a805bSEd Maste			sb[#sb+1] = '\n'
358060a805bSEd Maste		end
359060a805bSEd Maste		return table.concat(sb, '')
360060a805bSEd Maste	end
361060a805bSEd Maste
362060a805bSEd Maste	--- @param have_count boolean
363060a805bSEd Maste	--- @param have_size boolean
364060a805bSEd Maste	--- @param filters function[]
365060a805bSEd Maste	--- @public
366060a805bSEd Maste	-- returns a string describing package size report.
367060a805bSEd Maste	-- sample: "mypackage 2 2048"* if both booleans are true
368060a805bSEd Maste	local function pkg_report_simple(have_count, have_size, filters)
369060a805bSEd Maste		filters = filters or {}
370060a805bSEd Maste		local sb = {}
371060a805bSEd Maste		for pkgname, v in sortedPairs(pkg_report_helper_table()) do
372060a805bSEd Maste			local pred = true
373060a805bSEd Maste			-- doing a foldl to all the function results with (and)
374060a805bSEd Maste			for _, f in pairs(filters) do pred = pred and f(pkgname) end
375060a805bSEd Maste			if pred then
376060a805bSEd Maste				sb[#sb+1] = pkgname..table.concat({
377060a805bSEd Maste					have_count and (' '..(v.count or '?')) or '',
378060a805bSEd Maste					have_size and (' '..(v.size or '?')) or ''}, '')
379060a805bSEd Maste					..'\n'
380060a805bSEd Maste			end
381060a805bSEd Maste		end
382060a805bSEd Maste		return table.concat(sb, '')
383060a805bSEd Maste	end
384060a805bSEd Maste
385060a805bSEd Maste	-- returns a string describing duplicate file warnings,
386060a805bSEd Maste	-- returns a string describing duplicate file errors
387060a805bSEd Maste	--- @public
388060a805bSEd Maste	local function dup_report()
389060a805bSEd Maste		local warn, errs = {}, {}
390060a805bSEd Maste		for filename, rows in sortedPairs(files) do
391060a805bSEd Maste			if #rows == 1 then goto continue end
392060a805bSEd Maste			local iseq, offby = metalogrows_all_equal(rows)
393060a805bSEd Maste			if iseq then -- repeated line, just a warning
3942a3bd087SEd Maste				local dupmsg = filename .. ' ' ..
3952a3bd087SEd Maste				    rows[1].attrs.type ..
3962a3bd087SEd Maste				    ' repeated with same meta: line ' ..
3972a3bd087SEd Maste				    table.concat(table_map(rows, function(e) return e.linenum end), ',')
3982a3bd087SEd Maste				if rows[1].attrs.type == "dir" then
3992a3bd087SEd Maste					if verbose then
4002a3bd087SEd Maste						warn[#warn+1] = 'warning: ' .. dupmsg .. '\n'
4012a3bd087SEd Maste					end
4022a3bd087SEd Maste				else
4030e04dd3bSEd Maste					errs[#errs+1] = 'error: ' .. dupmsg .. '\n'
4042a3bd087SEd Maste				end
405060a805bSEd Maste			elseif not metalogrows_all_equal(rows, false, true) then
406060a805bSEd Maste			-- same filename (possibly different tags), different metadata, an error
407060a805bSEd Maste				errs[#errs+1] = 'error: '..filename
408060a805bSEd Maste					..' exists in multiple locations and with different meta: line '
409060a805bSEd Maste					..table.concat(
410060a805bSEd Maste						table_map(rows, function(e) return e.linenum end), ',')
411060a805bSEd Maste					..'. off by "'..offby..'"'
412060a805bSEd Maste				errs[#errs+1] = '\n'
413060a805bSEd Maste			end
414060a805bSEd Maste			::continue::
415060a805bSEd Maste		end
416060a805bSEd Maste		return table.concat(warn, ''), table.concat(errs, '')
417060a805bSEd Maste	end
418060a805bSEd Maste
419060a805bSEd Maste	-- returns a string describing warnings of found hard links
420060a805bSEd Maste	-- returns a string describing errors of found hard links
421060a805bSEd Maste	--- @public
422060a805bSEd Maste	local function inode_report()
423060a805bSEd Maste		-- obtain inodes of filenames
424060a805bSEd Maste		local attributes = require('lfs').attributes
425060a805bSEd Maste		local inm = {} -- map<number, string[]>
426060a805bSEd Maste		local unstatables = {} -- string[]
427060a805bSEd Maste		for filename in pairs(files) do
428060a805bSEd Maste			-- i only took the first row of a filename,
429060a805bSEd Maste			-- and skip links and folders
430060a805bSEd Maste			if files[filename][1].attrs.type ~= 'file' then
431060a805bSEd Maste				goto continue
432060a805bSEd Maste			end
433bca4d270SEd Maste			local fs = attributes(stage_root .. filename)
434060a805bSEd Maste			if fs == nil then
435060a805bSEd Maste				unstatables[#unstatables+1] = filename
436060a805bSEd Maste				goto continue
437060a805bSEd Maste			end
438060a805bSEd Maste			local inode = fs.ino
439060a805bSEd Maste			inm[inode] = inm[inode] or {}
440bca4d270SEd Maste			table.insert(inm[inode], filename)
441060a805bSEd Maste			::continue::
442060a805bSEd Maste		end
443060a805bSEd Maste
444060a805bSEd Maste		local warn, errs = {}, {}
445060a805bSEd Maste		for _, filenames in pairs(inm) do
446060a805bSEd Maste			if #filenames == 1 then goto continue end
447060a805bSEd Maste			-- i only took the first row of a filename
448060a805bSEd Maste			local rows = table_map(filenames, function(e)
449060a805bSEd Maste				return files[e][1]
450060a805bSEd Maste			end)
451060a805bSEd Maste			local iseq, offby = metalogrows_all_equal(rows, true, true)
452060a805bSEd Maste			if not iseq then
453060a805bSEd Maste				errs[#errs+1] = 'error: '
454060a805bSEd Maste					..'entries point to the same inode but have different meta: '
455060a805bSEd Maste					..table.concat(filenames, ',')..' in line '
456060a805bSEd Maste					..table.concat(
457060a805bSEd Maste						table_map(rows, function(e) return e.linenum end), ',')
458060a805bSEd Maste					..'. off by "'..offby..'"'
459060a805bSEd Maste				errs[#errs+1] = '\n'
460060a805bSEd Maste			end
461060a805bSEd Maste			::continue::
462060a805bSEd Maste		end
463060a805bSEd Maste
464060a805bSEd Maste		if #unstatables > 0 then
465060a805bSEd Maste			warn[#warn+1] = verbose and
466060a805bSEd Maste				'note: skipped checking inodes: '..table.concat(unstatables, ',')..'\n'
467060a805bSEd Maste				or
468060a805bSEd Maste				'note: skipped checking inodes for '..#unstatables..' entries\n'
469060a805bSEd Maste		end
470060a805bSEd Maste
471060a805bSEd Maste		return table.concat(warn, ''), table.concat(errs, '')
472060a805bSEd Maste	end
473060a805bSEd Maste
474bca4d270SEd Maste	-- The METALOG file is assumed to be at the top of the stage directory.
475bca4d270SEd Maste	stage_root = string.gsub(metalog, '/[^/]*$', '/')
476bca4d270SEd Maste
477060a805bSEd Maste	do
478060a805bSEd Maste	local fp, errmsg, errcode = io.open(metalog, 'r')
479060a805bSEd Maste	if fp == nil then
480060a805bSEd Maste		io.stderr:write('cannot open '..metalog..': '..errmsg..': '..errcode..'\n')
481060a805bSEd Maste		os.exit(1)
482060a805bSEd Maste	end
483060a805bSEd Maste
484060a805bSEd Maste	-- scan all lines and put file data into the dictionaries
485060a805bSEd Maste	local firsttimes = {} -- set<string>
486060a805bSEd Maste	local lineno = 0
487060a805bSEd Maste	for line in fp:lines() do
488060a805bSEd Maste		-----local isinpkg = false
489060a805bSEd Maste		lineno = lineno + 1
490eec4f5c0SGordon Bergling		-- skip lines beginning with #
491060a805bSEd Maste		if line:match('^%s*#') then goto continue end
492060a805bSEd Maste		-- skip blank lines
493060a805bSEd Maste		if line:match('^%s*$') then goto continue end
494060a805bSEd Maste
495060a805bSEd Maste		local data = MetalogRow(line, lineno)
496060a805bSEd Maste		-- entries with dir and no tags... ignore for the first time
497060a805bSEd Maste		if not w_notagdirs and
498060a805bSEd Maste			data.attrs.tags == nil and data.attrs.type == 'dir'
499060a805bSEd Maste			and not firsttimes[data.filename] then
500060a805bSEd Maste			firsttimes[data.filename] = true
501060a805bSEd Maste			goto continue
502060a805bSEd Maste		end
503060a805bSEd Maste
504060a805bSEd Maste		files[data.filename] = files[data.filename] or {}
505060a805bSEd Maste		table.insert(files[data.filename], data)
506060a805bSEd Maste
507060a805bSEd Maste		if data.attrs.tags ~= nil then
508060a805bSEd Maste			pkgname = pkgname_from_tag(data.attrs.tags)
509060a805bSEd Maste			pkgs[pkgname] = pkgs[pkgname] or {}
510060a805bSEd Maste			pkgs[pkgname][data.filename] = true
511060a805bSEd Maste			------isinpkg = true
512060a805bSEd Maste		end
513060a805bSEd Maste		-----if not isinpkg then nopkg[data.filename] = true end
514060a805bSEd Maste		::continue::
515060a805bSEd Maste	end
516060a805bSEd Maste
517060a805bSEd Maste	fp:close()
518060a805bSEd Maste	end
519060a805bSEd Maste
520060a805bSEd Maste	return {
521060a805bSEd Maste		warn = swarn,
522060a805bSEd Maste		errs = serrs,
523060a805bSEd Maste		pkg_issetuid = pkg_issetuid,
524060a805bSEd Maste		pkg_issetgid = pkg_issetgid,
525060a805bSEd Maste		pkg_issetid = pkg_issetid,
526060a805bSEd Maste		pkg_report_full = pkg_report_full,
527060a805bSEd Maste		pkg_report_simple = pkg_report_simple,
528060a805bSEd Maste		dup_report = dup_report,
529060a805bSEd Maste		inode_report = inode_report
530060a805bSEd Maste	}
531060a805bSEd Masteend
532060a805bSEd Maste
533f93d92f4SEd Masteos.exit(main(arg))
534