import sys
import codecs
import unicodedata
nline = 0
line = ""
orig_line = ""
warnings = 0
errors = 0
skip_order_check = False
def warning(msg):
global warnings, orig_line, nline
print('%d: warning: %s%s' % (nline, msg, ": \'" + orig_line + "\'" if orig_line else ""))
warnings += 1
def error(msg):
global errors, orig_line, nline
print('%d: error: %s%s' % (nline, msg, ": \'" + orig_line + "\'" if orig_line else ""))
errors += 1
def print_psl(list):
for domain in list:
print(".".join(str(label) for label in reversed(domain)))
def psl_key(s):
if s[0] == '*':
return 0
if s[0] == '!':
return 1
return 2
def check_order(group):
global skip_order_check
try:
if skip_order_check or len(group) < 2:
skip_order_check = False
return
if any(group[0][0] != labels[0] for labels in group):
warning('Domain group TLD is not consistent')
sorted_group = sorted(group, key = lambda labels: (len(labels), psl_key(labels[-1][0]), labels))
if group != sorted_group:
warning('Incorrectly sorted group of domains')
print(" " + str(group))
print(" " + str(sorted_group))
print("Correct sorting would be:")
print_psl(sorted_group)
finally:
del group[:]
def lint_psl(infile):
global orig_line, nline
PSL_FLAG_EXCEPTION = (1<<0)
PSL_FLAG_WILDCARD = (1<<1)
PSL_FLAG_ICANN = (1<<2) PSL_FLAG_PRIVATE = (1<<3) PSL_FLAG_PLAIN = (1<<4)
line2number = {}
line2flag = {}
group = []
section = 0
icann_sections = 0
private_sections = 0
lines = [line.strip('\n') for line in infile]
for line in lines:
nline += 1
stripped = line.strip()
if stripped != line:
line = line.replace('\t','\\t')
line = line.replace('\r','^M')
orig_line = line
warning('Leading/Trailing whitespace')
orig_line = line
line = stripped
if not line:
continue
if line[0:2] == "//":
if section == 0:
if line == "// ===BEGIN ICANN DOMAINS===":
section = PSL_FLAG_ICANN
icann_sections += 1
elif line == "// ===BEGIN PRIVATE DOMAINS===":
section = PSL_FLAG_PRIVATE
private_sections += 1
elif line[3:11] == "===BEGIN":
error('Unexpected begin of unknown section')
elif line[3:9] == "===END":
error('End of section without previous begin')
elif section == PSL_FLAG_ICANN:
if line == "// ===END ICANN DOMAINS===":
section = 0
elif line[3:11] == "===BEGIN":
error('Unexpected begin of section: ')
elif line[3:9] == "===END":
error('Unexpected end of section')
elif section == PSL_FLAG_PRIVATE:
if line == "// ===END PRIVATE DOMAINS===":
section = 0
elif line[3:11] == "===BEGIN":
error('Unexpected begin of section')
elif line[3:9] == "===END":
error('Unexpected end of section')
continue
if section == 0:
error('Rule outside of section')
group.append(list(reversed(line.split('.'))))
try:
if sys.version_info[0] < 3:
line = line.decode('utf-8')
else:
line.encode('utf-8')
except (UnicodeDecodeError, UnicodeEncodeError):
orig_line = None
error('Invalid UTF-8 character')
continue
if unicodedata.normalize("NFKC", line) != line:
error('Rule must be NFKC')
if line != line.lower():
error('Rule must be lowercase')
flags = section
if line[0:2] == '*.':
flags |= PSL_FLAG_WILDCARD
line = line[2:]
if line[0] == '!':
flags |= PSL_FLAG_EXCEPTION
line = line[1:]
else:
flags |= PSL_FLAG_PLAIN
if flags & PSL_FLAG_WILDCARD and flags & PSL_FLAG_EXCEPTION:
error('Combination of wildcard and exception')
continue
labels = line.split('.')
if flags & PSL_FLAG_EXCEPTION and len(labels) > 1:
domain = ".".join(str(label) for label in labels[1:])
if not domain in line2flag:
error('Exception without previous wildcard')
elif not line2flag[domain] & PSL_FLAG_WILDCARD:
error('Exception without previous wildcard')
for label in labels:
if not label:
error('Leading/trailing or multiple dot')
continue
if label[0:4] == 'xn--':
error('Punycode found')
continue
if '--' in label:
error('Double minus found')
continue
for c in label:
if not c.isalnum() and c != '-' and ord(c) < 128:
error('Illegal character')
break
if line in line2flag:
error('Found doublette/ambiguity (previous line was %d)' % line2number[line])
line2number[line] = nline
line2flag[line] = flags
orig_line = None
if section == PSL_FLAG_ICANN:
error('ICANN section not closed')
elif section == PSL_FLAG_PRIVATE:
error('PRIVATE section not closed')
if icann_sections < 1:
warning('No ICANN section found')
elif icann_sections > 1:
warning('%d ICANN sections found' % icann_sections)
if private_sections < 1:
warning('No PRIVATE section found')
elif private_sections > 1:
warning('%d PRIVATE sections found' % private_sections)
def usage():
print('usage: %s PSLfile' % sys.argv[0])
print('or %s - # To read PSL from STDIN' % sys.argv[0])
exit(1)
def main():
if len(sys.argv) < 2:
usage()
with sys.stdin if sys.argv[-1] == '-' else open(sys.argv[-1], 'r', encoding='utf-8', errors="surrogateescape") as infile:
lint_psl(infile)
return errors != 0
if __name__ == '__main__':
sys.exit(main())