Skip to content

Commit 908fb7e

Browse files
committed
test: Use permissions from git in lint-files.py
Instead of using permissions from the local file system, which might depend on the umask, directly check the permissions from git's metadata.
1 parent 48d2e80 commit 908fb7e

File tree

1 file changed

+42
-30
lines changed

1 file changed

+42
-30
lines changed

test/lint/lint-files.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,36 @@
1111
import re
1212
import sys
1313
from subprocess import check_output
14-
from typing import Optional, NoReturn
14+
from typing import Dict, Optional, NoReturn
1515

1616
CMD_TOP_LEVEL = ["git", "rev-parse", "--show-toplevel"]
17-
CMD_ALL_FILES = ["git", "ls-files", "-z", "--full-name"]
18-
CMD_SOURCE_FILES = ["git", "ls-files", "-z", "--full-name", "--", "*.[cC][pP][pP]", "*.[hH]", "*.[pP][yY]", "*.[sS][hH]"]
17+
CMD_ALL_FILES = ["git", "ls-files", "-z", "--full-name", "--stage"]
1918
CMD_SHEBANG_FILES = ["git", "grep", "--full-name", "--line-number", "-I", "^#!"]
2019

20+
ALL_SOURCE_FILENAMES_REGEXP = r"^.*\.(cpp|h|py|sh)$"
2121
ALLOWED_FILENAME_REGEXP = "^[a-zA-Z0-9/_.@][a-zA-Z0-9/_.@-]*$"
2222
ALLOWED_SOURCE_FILENAME_REGEXP = "^[a-z0-9_./-]+$"
2323
ALLOWED_SOURCE_FILENAME_EXCEPTION_REGEXP = (
2424
"^src/(secp256k1/|minisketch/|univalue/|test/fuzz/FuzzedDataProvider.h)"
2525
)
26-
ALLOWED_PERMISSION_NON_EXECUTABLES = 644
27-
ALLOWED_PERMISSION_EXECUTABLES = 755
26+
ALLOWED_PERMISSION_NON_EXECUTABLES = 0o644
27+
ALLOWED_PERMISSION_EXECUTABLES = 0o755
2828
ALLOWED_EXECUTABLE_SHEBANG = {
2929
"py": [b"#!/usr/bin/env python3"],
3030
"sh": [b"#!/usr/bin/env bash", b"#!/bin/sh"],
3131
}
3232

3333

3434
class FileMeta(object):
35-
def __init__(self, file_path: str):
36-
self.file_path = file_path
35+
def __init__(self, file_spec: str):
36+
'''Parse a `git ls files --stage` output line.'''
37+
# 100755 5a150d5f8031fcd75e80a4dd9843afa33655f579 0 ci/test/00_setup_env.sh
38+
meta, self.file_path = file_spec.split('\t', 2)
39+
meta = meta.split()
40+
# The octal file permission of the file. Internally, git only
41+
# keeps an 'executable' bit, so this will always be 0o644 or 0o755.
42+
self.permissions = int(meta[0], 8) & 0o7777
43+
# We don't currently care about the other fields
3744

3845
@property
3946
def extension(self) -> Optional[str]:
@@ -61,20 +68,24 @@ def full_extension(self) -> Optional[str]:
6168
except IndexError:
6269
return None
6370

64-
@property
65-
def permissions(self) -> int:
66-
"""
67-
Returns the octal file permission of the file
68-
"""
69-
return int(oct(os.stat(self.file_path).st_mode)[-3:])
7071

72+
def get_git_file_metadata() -> Dict[str, FileMeta]:
73+
'''
74+
Return a dictionary mapping the name of all files in the repository to git tree metadata.
75+
'''
76+
files_raw = check_output(CMD_ALL_FILES).decode("utf8").rstrip("\0").split("\0")
77+
files = {}
78+
for file_spec in files_raw:
79+
meta = FileMeta(file_spec)
80+
files[meta.file_path] = meta
81+
return files
7182

72-
def check_all_filenames() -> int:
83+
def check_all_filenames(files) -> int:
7384
"""
7485
Checks every file in the repository against an allowed regexp to make sure only lowercase or uppercase
7586
alphanumerics (a-zA-Z0-9), underscores (_), hyphens (-), at (@) and dots (.) are used in repository filenames.
7687
"""
77-
filenames = check_output(CMD_ALL_FILES).decode("utf8").rstrip("\0").split("\0")
88+
filenames = files.keys()
7889
filename_regex = re.compile(ALLOWED_FILENAME_REGEXP)
7990
failed_tests = 0
8091
for filename in filenames:
@@ -86,14 +97,14 @@ def check_all_filenames() -> int:
8697
return failed_tests
8798

8899

89-
def check_source_filenames() -> int:
100+
def check_source_filenames(files) -> int:
90101
"""
91102
Checks only source files (*.cpp, *.h, *.py, *.sh) against a stricter allowed regexp to make sure only lowercase
92103
alphanumerics (a-z0-9), underscores (_), hyphens (-) and dots (.) are used in source code filenames.
93104
94105
Additionally there is an exception regexp for directories or files which are excepted from matching this regexp.
95106
"""
96-
filenames = check_output(CMD_SOURCE_FILES).decode("utf8").rstrip("\0").split("\0")
107+
filenames = [filename for filename in files.keys() if re.match(ALL_SOURCE_FILENAMES_REGEXP, filename, re.IGNORECASE)]
97108
filename_regex = re.compile(ALLOWED_SOURCE_FILENAME_REGEXP)
98109
filename_exception_regex = re.compile(ALLOWED_SOURCE_FILENAME_EXCEPTION_REGEXP)
99110
failed_tests = 0
@@ -106,24 +117,22 @@ def check_source_filenames() -> int:
106117
return failed_tests
107118

108119

109-
def check_all_file_permissions() -> int:
120+
def check_all_file_permissions(files) -> int:
110121
"""
111122
Checks all files in the repository match an allowed executable or non-executable file permission octal.
112123
113124
Additionally checks that for executable files, the file contains a shebang line
114125
"""
115-
filenames = check_output(CMD_ALL_FILES).decode("utf8").rstrip("\0").split("\0")
116126
failed_tests = 0
117-
for filename in filenames:
118-
file_meta = FileMeta(filename)
127+
for filename, file_meta in files.items():
119128
if file_meta.permissions == ALLOWED_PERMISSION_EXECUTABLES:
120129
with open(filename, "rb") as f:
121130
shebang = f.readline().rstrip(b"\n")
122131

123132
# For any file with executable permissions the first line must contain a shebang
124133
if not shebang.startswith(b"#!"):
125134
print(
126-
f"""File "{filename}" has permission {ALLOWED_PERMISSION_EXECUTABLES} (executable) and is thus expected to contain a shebang '#!'. Add shebang or do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES} {filename}" to make it non-executable."""
135+
f"""File "{filename}" has permission {ALLOWED_PERMISSION_EXECUTABLES:03o} (executable) and is thus expected to contain a shebang '#!'. Add shebang or do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES:03o} {filename}" to make it non-executable."""
127136
)
128137
failed_tests += 1
129138

@@ -146,14 +155,14 @@ def check_all_file_permissions() -> int:
146155
continue
147156
else:
148157
print(
149-
f"""File "{filename}" has unexpected permission {file_meta.permissions}. Do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES} {filename}" (if non-executable) or "chmod {ALLOWED_PERMISSION_EXECUTABLES} {filename}" (if executable)."""
158+
f"""File "{filename}" has unexpected permission {file_meta.permissions:03o}. Do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES:03o} {filename}" (if non-executable) or "chmod {ALLOWED_PERMISSION_EXECUTABLES:03o} {filename}" (if executable)."""
150159
)
151160
failed_tests += 1
152161

153162
return failed_tests
154163

155164

156-
def check_shebang_file_permissions() -> int:
165+
def check_shebang_file_permissions(files_meta) -> int:
157166
"""
158167
Checks every file that contains a shebang line to ensure it has an executable permission
159168
"""
@@ -165,7 +174,7 @@ def check_shebang_file_permissions() -> int:
165174

166175
failed_tests = 0
167176
for filename in filenames:
168-
file_meta = FileMeta(filename)
177+
file_meta = files_meta[filename]
169178
if file_meta.permissions != ALLOWED_PERMISSION_EXECUTABLES:
170179
# These file types are typically expected to be sourced and not executed directly
171180
if file_meta.full_extension in ["bash", "init", "openrc", "sh.in"]:
@@ -179,7 +188,7 @@ def check_shebang_file_permissions() -> int:
179188
continue
180189

181190
print(
182-
f"""File "{filename}" contains a shebang line, but has the file permission {file_meta.permissions} instead of the expected executable permission {ALLOWED_PERMISSION_EXECUTABLES}. Do "chmod {ALLOWED_PERMISSION_EXECUTABLES} {filename}" (or remove the shebang line)."""
191+
f"""File "{filename}" contains a shebang line, but has the file permission {file_meta.permissions:03o} instead of the expected executable permission {ALLOWED_PERMISSION_EXECUTABLES:03o}. Do "chmod {ALLOWED_PERMISSION_EXECUTABLES:03o} {filename}" (or remove the shebang line)."""
183192
)
184193
failed_tests += 1
185194
return failed_tests
@@ -188,11 +197,14 @@ def check_shebang_file_permissions() -> int:
188197
def main() -> NoReturn:
189198
root_dir = check_output(CMD_TOP_LEVEL).decode("utf8").strip()
190199
os.chdir(root_dir)
200+
201+
files = get_git_file_metadata()
202+
191203
failed_tests = 0
192-
failed_tests += check_all_filenames()
193-
failed_tests += check_source_filenames()
194-
failed_tests += check_all_file_permissions()
195-
failed_tests += check_shebang_file_permissions()
204+
failed_tests += check_all_filenames(files)
205+
failed_tests += check_source_filenames(files)
206+
failed_tests += check_all_file_permissions(files)
207+
failed_tests += check_shebang_file_permissions(files)
196208

197209
if failed_tests:
198210
print(

0 commit comments

Comments
 (0)