1*060a805bSEd Maste#!/usr/libexec/flua 2*060a805bSEd Maste 3*060a805bSEd Maste-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD 4*060a805bSEd Maste-- 5*060a805bSEd Maste-- Copyright(c) 2020 The FreeBSD Foundation. 6*060a805bSEd Maste-- 7*060a805bSEd Maste-- Redistribution and use in source and binary forms, with or without 8*060a805bSEd Maste-- modification, are permitted provided that the following conditions 9*060a805bSEd Maste-- are met: 10*060a805bSEd Maste-- 1. Redistributions of source code must retain the above copyright 11*060a805bSEd Maste-- notice, this list of conditions and the following disclaimer. 12*060a805bSEd Maste-- 2. Redistributions in binary form must reproduce the above copyright 13*060a805bSEd Maste-- notice, this list of conditions and the following disclaimer in the 14*060a805bSEd Maste-- documentation and/or other materials provided with the distribution. 15*060a805bSEd Maste-- 16*060a805bSEd Maste-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17*060a805bSEd Maste-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18*060a805bSEd Maste-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19*060a805bSEd Maste-- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 20*060a805bSEd Maste-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21*060a805bSEd Maste-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22*060a805bSEd Maste-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23*060a805bSEd Maste-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24*060a805bSEd Maste-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25*060a805bSEd Maste-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26*060a805bSEd Maste-- SUCH DAMAGE. 27*060a805bSEd Maste 28*060a805bSEd Maste-- $FreeBSD$ 29*060a805bSEd Maste 30*060a805bSEd Mastefunction main(args) 31*060a805bSEd Maste if #args == 0 then usage() end 32*060a805bSEd Maste local filename 33*060a805bSEd Maste local printall, checkonly, pkgonly = 34*060a805bSEd Maste #args == 1, false, false 35*060a805bSEd Maste local dcount, dsize, fuid, fgid, fid = 36*060a805bSEd Maste false, false, false, false, false 37*060a805bSEd Maste local verbose = false 38*060a805bSEd Maste local w_notagdirs = false 39*060a805bSEd Maste 40*060a805bSEd Maste local i = 1 41*060a805bSEd Maste while i <= #args do 42*060a805bSEd Maste if args[i] == '-h' then 43*060a805bSEd Maste usage(true) 44*060a805bSEd Maste elseif args[i] == '-a' then 45*060a805bSEd Maste printall = true 46*060a805bSEd Maste elseif args[i] == '-c' then 47*060a805bSEd Maste printall = false 48*060a805bSEd Maste checkonly = true 49*060a805bSEd Maste elseif args[i] == '-p' then 50*060a805bSEd Maste printall = false 51*060a805bSEd Maste pkgonly = true 52*060a805bSEd Maste while i < #args do 53*060a805bSEd Maste i = i+1 54*060a805bSEd Maste if args[i] == '-count' then 55*060a805bSEd Maste dcount = true 56*060a805bSEd Maste elseif args[i] == '-size' then 57*060a805bSEd Maste dsize = true 58*060a805bSEd Maste elseif args[i] == '-fsetuid' then 59*060a805bSEd Maste fuid = true 60*060a805bSEd Maste elseif args[i] == '-fsetgid' then 61*060a805bSEd Maste fgid = true 62*060a805bSEd Maste elseif args[i] == '-fsetid' then 63*060a805bSEd Maste fid = true 64*060a805bSEd Maste else 65*060a805bSEd Maste i = i-1 66*060a805bSEd Maste break 67*060a805bSEd Maste end 68*060a805bSEd Maste end 69*060a805bSEd Maste elseif args[i] == '-v' then 70*060a805bSEd Maste verbose = true 71*060a805bSEd Maste elseif args[i] == '-Wcheck-notagdir' then 72*060a805bSEd Maste w_notagdirs = true 73*060a805bSEd Maste elseif args[i]:match('^%-') then 74*060a805bSEd Maste io.stderr:write('Unknown argument '..args[i]..'.\n') 75*060a805bSEd Maste usage() 76*060a805bSEd Maste else 77*060a805bSEd Maste filename = args[i] 78*060a805bSEd Maste end 79*060a805bSEd Maste i = i+1 80*060a805bSEd Maste end 81*060a805bSEd Maste 82*060a805bSEd Maste if filename == nil then 83*060a805bSEd Maste io.stderr:write('Missing filename.\n') 84*060a805bSEd Maste usage() 85*060a805bSEd Maste end 86*060a805bSEd Maste 87*060a805bSEd Maste local sess = Analysis_session(filename, verbose, w_notagdirs) 88*060a805bSEd Maste 89*060a805bSEd Maste if printall then 90*060a805bSEd Maste io.write('--- PACKAGE REPORTS ---\n') 91*060a805bSEd Maste io.write(sess.pkg_report_full()) 92*060a805bSEd Maste io.write('--- LINTING REPORTS ---\n') 93*060a805bSEd Maste print_lints(sess) 94*060a805bSEd Maste elseif checkonly then 95*060a805bSEd Maste print_lints(sess) 96*060a805bSEd Maste elseif pkgonly then 97*060a805bSEd Maste io.write(sess.pkg_report_simple(dcount, dsize, { 98*060a805bSEd Maste fuid and sess.pkg_issetuid or nil, 99*060a805bSEd Maste fgid and sess.pkg_issetgid or nil, 100*060a805bSEd Maste fid and sess.pkg_issetid or nil 101*060a805bSEd Maste })) 102*060a805bSEd Maste else 103*060a805bSEd Maste io.stderr:write('This text should not be displayed.') 104*060a805bSEd Maste usage() 105*060a805bSEd Maste end 106*060a805bSEd Masteend 107*060a805bSEd Maste 108*060a805bSEd Maste--- @param man boolean 109*060a805bSEd Mastefunction usage(man) 110*060a805bSEd Maste local sn = 'Usage: '..arg[0].. ' [-h] [-a | -c | -p [-count] [-size] [-f...]] [-W...] metalog-path \n' 111*060a805bSEd Maste if man then 112*060a805bSEd Maste io.write('\n') 113*060a805bSEd Maste io.write(sn) 114*060a805bSEd Maste io.write( 115*060a805bSEd Maste[[ 116*060a805bSEd Maste 117*060a805bSEd MasteThe script reads METALOG file created by pkgbase (make packages) and generates 118*060a805bSEd Mastereports about the installed system and issues. It accepts an mtree file in a 119*060a805bSEd Masteformat that's returned by `mtree -c | mtree -C` 120*060a805bSEd Maste 121*060a805bSEd Maste Options: 122*060a805bSEd Maste -a prints all scan results. this is the default option if no option 123*060a805bSEd Maste is provided. 124*060a805bSEd Maste -c lints the file and gives warnings/errors, including duplication 125*060a805bSEd Maste and conflicting metadata 126*060a805bSEd Maste -Wcheck-notagdir entries with dir type and no tags will be also 127*060a805bSEd Maste included the first time they appear 128*060a805bSEd Maste -p list all package names found in the file as exactly specified by 129*060a805bSEd Maste `tags=package=...` 130*060a805bSEd Maste -count display the number of files of the package 131*060a805bSEd Maste -size display the size of the package 132*060a805bSEd Maste -fsetgid only include packages with setgid files 133*060a805bSEd Maste -fsetuid only include packages with setuid files 134*060a805bSEd Maste -fsetid only include packages with setgid or setuid files 135*060a805bSEd Maste -v verbose mode 136*060a805bSEd Maste -h help page 137*060a805bSEd Maste 138*060a805bSEd Maste]]) 139*060a805bSEd Maste os.exit() 140*060a805bSEd Maste else 141*060a805bSEd Maste io.stderr:write(sn) 142*060a805bSEd Maste os.exit(1) 143*060a805bSEd Maste end 144*060a805bSEd Masteend 145*060a805bSEd Maste 146*060a805bSEd Maste--- @param sess Analysis_session 147*060a805bSEd Mastefunction print_lints(sess) 148*060a805bSEd Maste local dupwarn, duperr = sess.dup_report() 149*060a805bSEd Maste io.write(dupwarn) 150*060a805bSEd Maste io.write(duperr) 151*060a805bSEd Maste local inodewarn, inodeerr = sess.inode_report() 152*060a805bSEd Maste io.write(inodewarn) 153*060a805bSEd Maste io.write(inodeerr) 154*060a805bSEd Masteend 155*060a805bSEd Maste 156*060a805bSEd Maste--- @param t table 157*060a805bSEd Mastefunction sortedPairs(t) 158*060a805bSEd Maste local sortedk = {} 159*060a805bSEd Maste for k in next, t do sortedk[#sortedk+1] = k end 160*060a805bSEd Maste table.sort(sortedk) 161*060a805bSEd Maste local i = 0 162*060a805bSEd Maste return function() 163*060a805bSEd Maste i = i + 1 164*060a805bSEd Maste return sortedk[i], t[sortedk[i]] 165*060a805bSEd Maste end 166*060a805bSEd Masteend 167*060a805bSEd Maste 168*060a805bSEd Maste--- @param t table <T, U> 169*060a805bSEd Maste--- @param f function <U -> U> 170*060a805bSEd Mastefunction table_map(t, f) 171*060a805bSEd Maste local res = {} 172*060a805bSEd Maste for k, v in pairs(t) do res[k] = f(v) end 173*060a805bSEd Maste return res 174*060a805bSEd Masteend 175*060a805bSEd Maste 176*060a805bSEd Maste--- @class MetalogRow 177*060a805bSEd Maste-- a table contaning file's info, from a line content from METALOG file 178*060a805bSEd Maste-- all fields in the table are strings 179*060a805bSEd Maste-- sample output: 180*060a805bSEd Maste-- { 181*060a805bSEd Maste-- filename = ./usr/share/man/man3/inet6_rthdr_segments.3.gz 182*060a805bSEd Maste-- lineno = 5 183*060a805bSEd Maste-- attrs = { 184*060a805bSEd Maste-- gname = 'wheel' 185*060a805bSEd Maste-- uname = 'root' 186*060a805bSEd Maste-- mode = '0444' 187*060a805bSEd Maste-- size = '1166' 188*060a805bSEd Maste-- time = nil 189*060a805bSEd Maste-- type = 'file' 190*060a805bSEd Maste-- tags = 'package=clibs,debug' 191*060a805bSEd Maste-- } 192*060a805bSEd Maste-- } 193*060a805bSEd Maste--- @param line string 194*060a805bSEd Mastefunction MetalogRow(line, lineno) 195*060a805bSEd Maste local res, attrs = {}, {} 196*060a805bSEd Maste local filename, rest = line:match('^(%S+) (.+)$') 197*060a805bSEd Maste -- mtree file has space escaped as '\\040', not affecting splitting 198*060a805bSEd Maste -- string by space 199*060a805bSEd Maste for attrpair in rest:gmatch('[^ ]+') do 200*060a805bSEd Maste local k, v = attrpair:match('^(.-)=(.+)') 201*060a805bSEd Maste attrs[k] = v 202*060a805bSEd Maste end 203*060a805bSEd Maste res.filename = filename 204*060a805bSEd Maste res.linenum = lineno 205*060a805bSEd Maste res.attrs = attrs 206*060a805bSEd Maste return res 207*060a805bSEd Masteend 208*060a805bSEd Maste 209*060a805bSEd Maste-- check if an array of MetalogRows are equivalent. if not, the first field 210*060a805bSEd Maste-- that's different is returned secondly 211*060a805bSEd Maste--- @param rows MetalogRow[] 212*060a805bSEd Maste--- @param ignore_name boolean 213*060a805bSEd Maste--- @param ignore_tags boolean 214*060a805bSEd Mastefunction metalogrows_all_equal(rows, ignore_name, ignore_tags) 215*060a805bSEd Maste local __eq = function(l, o) 216*060a805bSEd Maste if not ignore_name and l.filename ~= o.filename then 217*060a805bSEd Maste return false, 'filename' 218*060a805bSEd Maste end 219*060a805bSEd Maste -- ignoring linenum in METALOG file as it's not relavant 220*060a805bSEd Maste for k in pairs(l.attrs) do 221*060a805bSEd Maste if ignore_tags and k == 'tags' then goto continue end 222*060a805bSEd Maste if l.attrs[k] ~= o.attrs[k] and o.attrs[k] ~= nil then 223*060a805bSEd Maste return false, k 224*060a805bSEd Maste end 225*060a805bSEd Maste ::continue:: 226*060a805bSEd Maste end 227*060a805bSEd Maste return true 228*060a805bSEd Maste end 229*060a805bSEd Maste for _, v in ipairs(rows) do 230*060a805bSEd Maste local bol, offby = __eq(v, rows[1]) 231*060a805bSEd Maste if not bol then return false, offby end 232*060a805bSEd Maste end 233*060a805bSEd Maste return true 234*060a805bSEd Masteend 235*060a805bSEd Maste 236*060a805bSEd Maste--- @param tagstr string 237*060a805bSEd Mastefunction pkgname_from_tag(tagstr) 238*060a805bSEd Maste local ext, pkgname, pkgend = '', '', '' 239*060a805bSEd Maste for seg in tagstr:gmatch('[^,]+') do 240*060a805bSEd Maste if seg:match('package=') then 241*060a805bSEd Maste pkgname = seg:sub(9) 242*060a805bSEd Maste elseif seg == 'development' or seg == 'profile' 243*060a805bSEd Maste or seg == 'debug' or seg == 'docs' then 244*060a805bSEd Maste pkgend = seg 245*060a805bSEd Maste else 246*060a805bSEd Maste ext = ext == '' and seg or ext..'-'..seg 247*060a805bSEd Maste end 248*060a805bSEd Maste end 249*060a805bSEd Maste pkgname = pkgname 250*060a805bSEd Maste ..(ext == '' and '' or '-'..ext) 251*060a805bSEd Maste ..(pkgend == '' and '' or '-'..pkgend) 252*060a805bSEd Maste return pkgname 253*060a805bSEd Masteend 254*060a805bSEd Maste 255*060a805bSEd Maste--- @class Analysis_session 256*060a805bSEd Maste--- @param metalog string 257*060a805bSEd Maste--- @param verbose boolean 258*060a805bSEd Maste--- @param w_notagdirs boolean turn on to also check directories 259*060a805bSEd Mastefunction Analysis_session(metalog, verbose, w_notagdirs) 260*060a805bSEd Maste local files = {} -- map<string, MetalogRow[]> 261*060a805bSEd Maste -- set is map<elem, bool>. if bool is true then elem exists 262*060a805bSEd Maste local pkgs = {} -- map<string, set<string>> 263*060a805bSEd Maste ----- used to keep track of files not belonging to a pkg. not used so 264*060a805bSEd Maste ----- it is commented with ----- 265*060a805bSEd Maste -----local nopkg = {} -- set<string> 266*060a805bSEd Maste --- @public 267*060a805bSEd Maste local swarn = {} 268*060a805bSEd Maste --- @public 269*060a805bSEd Maste local serrs = {} 270*060a805bSEd Maste 271*060a805bSEd Maste -- returns number of files in package and size of package 272*060a805bSEd Maste -- nil is returned upon errors 273*060a805bSEd Maste --- @param pkgname string 274*060a805bSEd Maste local function pkg_size(pkgname) 275*060a805bSEd Maste local filecount, sz = 0, 0 276*060a805bSEd Maste for filename in pairs(pkgs[pkgname]) do 277*060a805bSEd Maste local rows = files[filename] 278*060a805bSEd Maste -- normally, there should be only one row per filename 279*060a805bSEd Maste -- if these rows are equal, there should be warning, but it 280*060a805bSEd Maste -- does not affect size counting. if not, it is an error 281*060a805bSEd Maste if #rows > 1 and not metalogrows_all_equal(rows) then 282*060a805bSEd Maste return nil 283*060a805bSEd Maste end 284*060a805bSEd Maste local row = rows[1] 285*060a805bSEd Maste if row.attrs.type == 'file' then 286*060a805bSEd Maste sz = sz + tonumber(row.attrs.size) 287*060a805bSEd Maste end 288*060a805bSEd Maste filecount = filecount + 1 289*060a805bSEd Maste end 290*060a805bSEd Maste return filecount, sz 291*060a805bSEd Maste end 292*060a805bSEd Maste 293*060a805bSEd Maste --- @param pkgname string 294*060a805bSEd Maste --- @param mode number 295*060a805bSEd Maste local function pkg_ismode(pkgname, mode) 296*060a805bSEd Maste for filename in pairs(pkgs[pkgname]) do 297*060a805bSEd Maste for _, row in ipairs(files[filename]) do 298*060a805bSEd Maste if tonumber(row.attrs.mode, 8) & mode ~= 0 then 299*060a805bSEd Maste return true 300*060a805bSEd Maste end 301*060a805bSEd Maste end 302*060a805bSEd Maste end 303*060a805bSEd Maste return false 304*060a805bSEd Maste end 305*060a805bSEd Maste 306*060a805bSEd Maste --- @param pkgname string 307*060a805bSEd Maste --- @public 308*060a805bSEd Maste local function pkg_issetuid(pkgname) 309*060a805bSEd Maste return pkg_ismode(pkgname, 2048) 310*060a805bSEd Maste end 311*060a805bSEd Maste 312*060a805bSEd Maste --- @param pkgname string 313*060a805bSEd Maste --- @public 314*060a805bSEd Maste local function pkg_issetgid(pkgname) 315*060a805bSEd Maste return pkg_ismode(pkgname, 1024) 316*060a805bSEd Maste end 317*060a805bSEd Maste 318*060a805bSEd Maste --- @param pkgname string 319*060a805bSEd Maste --- @public 320*060a805bSEd Maste local function pkg_issetid(pkgname) 321*060a805bSEd Maste return pkg_issetuid(pkgname) or pkg_issetgid(pkgname) 322*060a805bSEd Maste end 323*060a805bSEd Maste 324*060a805bSEd Maste -- sample return: 325*060a805bSEd Maste -- { [*string]: { count=1, size=2, issetuid=true, issetgid=true } } 326*060a805bSEd Maste local function pkg_report_helper_table() 327*060a805bSEd Maste local res = {} 328*060a805bSEd Maste for pkgname in pairs(pkgs) do 329*060a805bSEd Maste res[pkgname] = {} 330*060a805bSEd Maste res[pkgname].count, 331*060a805bSEd Maste res[pkgname].size = pkg_size(pkgname) 332*060a805bSEd Maste res[pkgname].issetuid = pkg_issetuid(pkgname) 333*060a805bSEd Maste res[pkgname].issetgid = pkg_issetgid(pkgname) 334*060a805bSEd Maste end 335*060a805bSEd Maste return res 336*060a805bSEd Maste end 337*060a805bSEd Maste 338*060a805bSEd Maste -- returns a string describing package scan report 339*060a805bSEd Maste --- @public 340*060a805bSEd Maste local function pkg_report_full() 341*060a805bSEd Maste local sb = {} 342*060a805bSEd Maste for pkgname, v in sortedPairs(pkg_report_helper_table()) do 343*060a805bSEd Maste sb[#sb+1] = 'Package '..pkgname..':' 344*060a805bSEd Maste if v.issetuid or v.issetgid then 345*060a805bSEd Maste sb[#sb+1] = ''..table.concat({ 346*060a805bSEd Maste v.issetuid and ' setuid' or '', 347*060a805bSEd Maste v.issetgid and ' setgid' or '' }, '') 348*060a805bSEd Maste end 349*060a805bSEd Maste sb[#sb+1] = '\n number of files: '..(v.count or '?') 350*060a805bSEd Maste ..'\n total size: '..(v.size or '?') 351*060a805bSEd Maste sb[#sb+1] = '\n' 352*060a805bSEd Maste end 353*060a805bSEd Maste return table.concat(sb, '') 354*060a805bSEd Maste end 355*060a805bSEd Maste 356*060a805bSEd Maste --- @param have_count boolean 357*060a805bSEd Maste --- @param have_size boolean 358*060a805bSEd Maste --- @param filters function[] 359*060a805bSEd Maste --- @public 360*060a805bSEd Maste -- returns a string describing package size report. 361*060a805bSEd Maste -- sample: "mypackage 2 2048"* if both booleans are true 362*060a805bSEd Maste local function pkg_report_simple(have_count, have_size, filters) 363*060a805bSEd Maste filters = filters or {} 364*060a805bSEd Maste local sb = {} 365*060a805bSEd Maste for pkgname, v in sortedPairs(pkg_report_helper_table()) do 366*060a805bSEd Maste local pred = true 367*060a805bSEd Maste -- doing a foldl to all the function results with (and) 368*060a805bSEd Maste for _, f in pairs(filters) do pred = pred and f(pkgname) end 369*060a805bSEd Maste if pred then 370*060a805bSEd Maste sb[#sb+1] = pkgname..table.concat({ 371*060a805bSEd Maste have_count and (' '..(v.count or '?')) or '', 372*060a805bSEd Maste have_size and (' '..(v.size or '?')) or ''}, '') 373*060a805bSEd Maste ..'\n' 374*060a805bSEd Maste end 375*060a805bSEd Maste end 376*060a805bSEd Maste return table.concat(sb, '') 377*060a805bSEd Maste end 378*060a805bSEd Maste 379*060a805bSEd Maste -- returns a string describing duplicate file warnings, 380*060a805bSEd Maste -- returns a string describing duplicate file errors 381*060a805bSEd Maste --- @public 382*060a805bSEd Maste local function dup_report() 383*060a805bSEd Maste local warn, errs = {}, {} 384*060a805bSEd Maste for filename, rows in sortedPairs(files) do 385*060a805bSEd Maste if #rows == 1 then goto continue end 386*060a805bSEd Maste local iseq, offby = metalogrows_all_equal(rows) 387*060a805bSEd Maste if iseq then -- repeated line, just a warning 388*060a805bSEd Maste warn[#warn+1] = 'warning: '..filename 389*060a805bSEd Maste ..' repeated with same meta: line ' 390*060a805bSEd Maste ..table.concat( 391*060a805bSEd Maste table_map(rows, function(e) return e.linenum end), ',') 392*060a805bSEd Maste warn[#warn+1] = '\n' 393*060a805bSEd Maste elseif not metalogrows_all_equal(rows, false, true) then 394*060a805bSEd Maste -- same filename (possibly different tags), different metadata, an error 395*060a805bSEd Maste errs[#errs+1] = 'error: '..filename 396*060a805bSEd Maste ..' exists in multiple locations and with different meta: line ' 397*060a805bSEd Maste ..table.concat( 398*060a805bSEd Maste table_map(rows, function(e) return e.linenum end), ',') 399*060a805bSEd Maste ..'. off by "'..offby..'"' 400*060a805bSEd Maste errs[#errs+1] = '\n' 401*060a805bSEd Maste end 402*060a805bSEd Maste ::continue:: 403*060a805bSEd Maste end 404*060a805bSEd Maste return table.concat(warn, ''), table.concat(errs, '') 405*060a805bSEd Maste end 406*060a805bSEd Maste 407*060a805bSEd Maste -- returns a string describing warnings of found hard links 408*060a805bSEd Maste -- returns a string describing errors of found hard links 409*060a805bSEd Maste --- @public 410*060a805bSEd Maste local function inode_report() 411*060a805bSEd Maste -- obtain inodes of filenames 412*060a805bSEd Maste local attributes = require('lfs').attributes 413*060a805bSEd Maste local inm = {} -- map<number, string[]> 414*060a805bSEd Maste local unstatables = {} -- string[] 415*060a805bSEd Maste for filename in pairs(files) do 416*060a805bSEd Maste -- i only took the first row of a filename, 417*060a805bSEd Maste -- and skip links and folders 418*060a805bSEd Maste if files[filename][1].attrs.type ~= 'file' then 419*060a805bSEd Maste goto continue 420*060a805bSEd Maste end 421*060a805bSEd Maste -- make ./xxx become /xxx so that we can stat 422*060a805bSEd Maste filename = filename:sub(2) 423*060a805bSEd Maste local fs = attributes(filename) 424*060a805bSEd Maste if fs == nil then 425*060a805bSEd Maste unstatables[#unstatables+1] = filename 426*060a805bSEd Maste goto continue 427*060a805bSEd Maste end 428*060a805bSEd Maste local inode = fs.ino 429*060a805bSEd Maste inm[inode] = inm[inode] or {} 430*060a805bSEd Maste -- add back the dot prefix 431*060a805bSEd Maste table.insert(inm[inode], '.'..filename) 432*060a805bSEd Maste ::continue:: 433*060a805bSEd Maste end 434*060a805bSEd Maste 435*060a805bSEd Maste local warn, errs = {}, {} 436*060a805bSEd Maste for _, filenames in pairs(inm) do 437*060a805bSEd Maste if #filenames == 1 then goto continue end 438*060a805bSEd Maste -- i only took the first row of a filename 439*060a805bSEd Maste local rows = table_map(filenames, function(e) 440*060a805bSEd Maste return files[e][1] 441*060a805bSEd Maste end) 442*060a805bSEd Maste local iseq, offby = metalogrows_all_equal(rows, true, true) 443*060a805bSEd Maste if not iseq then 444*060a805bSEd Maste errs[#errs+1] = 'error: ' 445*060a805bSEd Maste ..'entries point to the same inode but have different meta: ' 446*060a805bSEd Maste ..table.concat(filenames, ',')..' in line ' 447*060a805bSEd Maste ..table.concat( 448*060a805bSEd Maste table_map(rows, function(e) return e.linenum end), ',') 449*060a805bSEd Maste ..'. off by "'..offby..'"' 450*060a805bSEd Maste errs[#errs+1] = '\n' 451*060a805bSEd Maste end 452*060a805bSEd Maste ::continue:: 453*060a805bSEd Maste end 454*060a805bSEd Maste 455*060a805bSEd Maste if #unstatables > 0 then 456*060a805bSEd Maste warn[#warn+1] = verbose and 457*060a805bSEd Maste 'note: skipped checking inodes: '..table.concat(unstatables, ',')..'\n' 458*060a805bSEd Maste or 459*060a805bSEd Maste 'note: skipped checking inodes for '..#unstatables..' entries\n' 460*060a805bSEd Maste end 461*060a805bSEd Maste 462*060a805bSEd Maste return table.concat(warn, ''), table.concat(errs, '') 463*060a805bSEd Maste end 464*060a805bSEd Maste 465*060a805bSEd Maste do 466*060a805bSEd Maste local fp, errmsg, errcode = io.open(metalog, 'r') 467*060a805bSEd Maste if fp == nil then 468*060a805bSEd Maste io.stderr:write('cannot open '..metalog..': '..errmsg..': '..errcode..'\n') 469*060a805bSEd Maste os.exit(1) 470*060a805bSEd Maste end 471*060a805bSEd Maste 472*060a805bSEd Maste -- scan all lines and put file data into the dictionaries 473*060a805bSEd Maste local firsttimes = {} -- set<string> 474*060a805bSEd Maste local lineno = 0 475*060a805bSEd Maste for line in fp:lines() do 476*060a805bSEd Maste -----local isinpkg = false 477*060a805bSEd Maste lineno = lineno + 1 478*060a805bSEd Maste -- skip lines begining with # 479*060a805bSEd Maste if line:match('^%s*#') then goto continue end 480*060a805bSEd Maste -- skip blank lines 481*060a805bSEd Maste if line:match('^%s*$') then goto continue end 482*060a805bSEd Maste 483*060a805bSEd Maste local data = MetalogRow(line, lineno) 484*060a805bSEd Maste -- entries with dir and no tags... ignore for the first time 485*060a805bSEd Maste if not w_notagdirs and 486*060a805bSEd Maste data.attrs.tags == nil and data.attrs.type == 'dir' 487*060a805bSEd Maste and not firsttimes[data.filename] then 488*060a805bSEd Maste firsttimes[data.filename] = true 489*060a805bSEd Maste goto continue 490*060a805bSEd Maste end 491*060a805bSEd Maste 492*060a805bSEd Maste files[data.filename] = files[data.filename] or {} 493*060a805bSEd Maste table.insert(files[data.filename], data) 494*060a805bSEd Maste 495*060a805bSEd Maste if data.attrs.tags ~= nil then 496*060a805bSEd Maste pkgname = pkgname_from_tag(data.attrs.tags) 497*060a805bSEd Maste pkgs[pkgname] = pkgs[pkgname] or {} 498*060a805bSEd Maste pkgs[pkgname][data.filename] = true 499*060a805bSEd Maste ------isinpkg = true 500*060a805bSEd Maste end 501*060a805bSEd Maste -----if not isinpkg then nopkg[data.filename] = true end 502*060a805bSEd Maste ::continue:: 503*060a805bSEd Maste end 504*060a805bSEd Maste 505*060a805bSEd Maste fp:close() 506*060a805bSEd Maste end 507*060a805bSEd Maste 508*060a805bSEd Maste return { 509*060a805bSEd Maste warn = swarn, 510*060a805bSEd Maste errs = serrs, 511*060a805bSEd Maste pkg_issetuid = pkg_issetuid, 512*060a805bSEd Maste pkg_issetgid = pkg_issetgid, 513*060a805bSEd Maste pkg_issetid = pkg_issetid, 514*060a805bSEd Maste pkg_report_full = pkg_report_full, 515*060a805bSEd Maste pkg_report_simple = pkg_report_simple, 516*060a805bSEd Maste dup_report = dup_report, 517*060a805bSEd Maste inode_report = inode_report 518*060a805bSEd Maste } 519*060a805bSEd Masteend 520*060a805bSEd Maste 521*060a805bSEd Mastemain(arg) 522