Changeset View
Changeset View
Standalone View
Standalone View
usr.sbin/services_mkdb/services_parser.py
- This file was added.
Property | Old Value | New Value |
---|---|---|
File Mode | null | 100755 |
#!/usr/bin/env python3 | |||||
## | |||||
# SPDX-License-Identifier: BSD-2-Clause | |||||
# | |||||
# Copyright (c) 2018 Eric van Gyzen | |||||
# | |||||
# Redistribution and use in source and binary forms, with or without | |||||
# modification, are permitted provided that the following conditions | |||||
# are met: | |||||
# 1. Redistributions of source code must retain the above copyright | |||||
# notice, this list of conditions and the following disclaimer. | |||||
# 2. Redistributions in binary form must reproduce the above copyright | |||||
# notice, this list of conditions and the following disclaimer in the | |||||
# documentation and/or other materials provided with the distribution. | |||||
# | |||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND | |||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE | |||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS | |||||
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |||||
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT | |||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY | |||||
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF | |||||
# SUCH DAMAGE. | |||||
# | |||||
# $FreeBSD$ | |||||
# | |||||
import textwrap | |||||
import xml.etree.ElementTree as ET | |||||
import xml.sax.saxutils | |||||
class Service(object): | |||||
eadler: consider using https://docs.python.org/3/library/functools.html#functools.total_ordering | |||||
vangyzenAuthorUnsubmitted Not Done Inline ActionsThanks. I hadn't heard of that. vangyzen: Thanks. I hadn't heard of that. | |||||
def __init__(self, number, protocol, name, desc, primary): | |||||
self.number = number | |||||
self.protocol = protocol | |||||
self.name = name | |||||
self.key = (number, protocol, name) | |||||
self.description = desc or '' | |||||
self.primary = primary | |||||
def __eq__(self, other): | |||||
return self.key == other.key | |||||
def __ne__(self, other): | |||||
return self.key != other.key | |||||
def __lt__(self, other): | |||||
return self.key < other.key | |||||
def __le__(self, other): | |||||
return self.key <= other.key | |||||
def __gt__(self, other): | |||||
return self.key > other.key | |||||
def __ge__(self, other): | |||||
return self.key >= other.key | |||||
def __hash__(self): | |||||
return hash(self.key) | |||||
def ToXML(self, out): | |||||
record = """ <record> | |||||
<name>{name}</name> | |||||
<protocol>{protocol}</protocol> | |||||
{description} | |||||
<number>{number}</number> | |||||
</record> | |||||
""" | |||||
fields = self.__dict__.copy() | |||||
if fields['description']: | |||||
fields['description'] = \ | |||||
'<description>' + \ | |||||
xml.sax.saxutils.escape(fields['description']) + \ | |||||
'</description>' | |||||
else: | |||||
fields['description'] = '<description/>' | |||||
out.write(record.format(**fields)) | |||||
def parse(infile, logfile): | |||||
services = [] | |||||
tree = ET.parse(infile) | |||||
root = tree.getroot() | |||||
ns = {'a': 'http://www.iana.org/assignments'} | |||||
# Use XPath to find all <record> elements that have | |||||
# <number>, <protocol>, and <name> children. | |||||
records = root.findall( | |||||
'./a:record/a:number/../a:protocol/../a:name/..', | |||||
ns) | |||||
for record in records: | |||||
protocol = record.find('a:protocol', ns).text | |||||
numbers = list(map(int, record.find('a:number', ns).text.split('-'))) | |||||
if len(numbers) > 1: | |||||
assert len(numbers) == 2 | |||||
numbers = range(numbers[0], numbers[1] + 1) | |||||
desc = record.find('a:description', ns).text | |||||
name_elem = record.find('a:name', ns) | |||||
name = name_elem.text.replace(' ', '-') | |||||
primary = name_elem.attrib.get('primary') == '1' | |||||
for number in numbers: | |||||
services.append(Service(number, protocol, name, desc, primary)) | |||||
logfile.write('parsed %d from %s\n' % (len(services), infile)) | |||||
return services | |||||
def prune(logfile, service): | |||||
logfile.write('local.xml service %s/%d/%s can be pruned\n' % | |||||
(service.name, service.number, service.protocol)) | |||||
def generate_etc_services(official, local, headerfile, outfile, logfile): | |||||
by_num_prot = {} | |||||
for service in official: | |||||
num_prot = (service.number, service.protocol) | |||||
by_num_prot.setdefault(num_prot, []).append(service) | |||||
for service in local: | |||||
num_prot = (service.number, service.protocol) | |||||
existing_list = by_num_prot.setdefault(num_prot, []) | |||||
try: | |||||
i = existing_list.index(service) | |||||
existing = existing_list[i] | |||||
if existing.description or not service.description: | |||||
prune(logfile, service) | |||||
else: | |||||
existing.description = service.description | |||||
except ValueError: | |||||
existing_list.append(service) | |||||
outfile.write('#\n') | |||||
Not Done Inline ActionsWhat will mergemaster do if somebody edits this file on an installed system? asomers: What will mergemaster do if somebody edits this file on an installed system? | |||||
Not Done Inline ActionsThe same thing it would do without my changes, and the same thing it does with other files in /etc. I'm not trying to be snarky, but this seems orthogonal to my changes. vangyzen: The same thing it would do without my changes, and the same thing it does with other files in… | |||||
Not Done Inline ActionsThe ongoing pkgbase work is throwing a wrench into things. Mergemaster now blindly overwrites some files in /etc. It would be good to double check that this is not one of those. asomers: The ongoing pkgbase work is throwing a wrench into things. Mergemaster now blindly overwrites… | |||||
outfile.write('# DO NOT EDIT this file in the FreeBSD source tree.\n') | |||||
outfile.write('# See local.xml instead. Feel free to edit this file\n') | |||||
outfile.write('# on an installed system.\n') | |||||
outfile.write(headerfile.read()) | |||||
for num_prot in sorted(by_num_prot): | |||||
service_list = by_num_prot[num_prot] | |||||
names = [] | |||||
desc = '' | |||||
for service in service_list: | |||||
if desc == '' and service.description: | |||||
desc = service.description | |||||
if service.name not in names: | |||||
if service.primary: | |||||
names.insert(0, service.name) | |||||
else: | |||||
names.append(service.name) | |||||
num_prot_str = '%d/%s' % num_prot | |||||
if len(names) > 1: | |||||
aliases = ' ' + ' '.join(names[1:]) | |||||
else: | |||||
aliases = '' | |||||
# A description that simply repeats the name is useless. | |||||
if desc.lower() in names: | |||||
desc = '' | |||||
# If the description is too long, wrap it to multiple lines | |||||
# and print it above the service. | |||||
elif (len(desc) > (45 - len(aliases))) or ('\n' in desc): | |||||
# Collapse multiple spaces into a single space. | |||||
desc = ' '.join(desc.split()) | |||||
for line in textwrap.wrap(desc): | |||||
outfile.write('# ') | |||||
outfile.write(line) | |||||
outfile.write('\n') | |||||
desc = '' | |||||
if desc: | |||||
desc = ' # ' + desc | |||||
if aliases or desc: | |||||
outfile.write('%-20s %-11s%s%s\n' % | |||||
(names[0], num_prot_str, aliases, desc)) | |||||
else: | |||||
# Avoid trailing whitespace. | |||||
outfile.write('%-20s %s\n' % | |||||
(names[0], num_prot_str)) | |||||
# | |||||
# Functions below here were only used in the migration | |||||
# from the manually maintained file to this automated method. | |||||
# | |||||
def parse_etc_services(filename): | |||||
services = [] | |||||
ln = 0 | |||||
for line in open(filename): | |||||
orig_line = line | |||||
try: | |||||
line = line.split('#', 1) | |||||
if not line[0].strip(): | |||||
# entirely comment | |||||
continue | |||||
if len(line) > 1: | |||||
line, desc = line | |||||
else: | |||||
line = line[0] | |||||
desc = '' | |||||
line = line.split() | |||||
assert len(line) >= 2 | |||||
number, protocol = line[1].split('/') | |||||
del line[1] | |||||
names = line | |||||
number = int(number) | |||||
for name in names: | |||||
services.append(Service(number, protocol, name, desc.strip())) | |||||
except: | |||||
print("failed to parse line %d: %s" % (ln, orig_line.rstrip())) | |||||
raise | |||||
ln += 1 | |||||
return services | |||||
def generate_local_xml(official_file_name, old_etc_services, logfile): | |||||
official = parse(official_file_name, logfile) | |||||
local_etc = parse_etc_services(old_etc_services) | |||||
to_print = sorted(set(local_etc) - set(official)) | |||||
local_xml = open('local.xml', 'w') | |||||
local_xml.write('<registry xmlns="http://www.iana.org/assignments"\n') | |||||
local_xml.write(' id="service-names-port-numbers">\n') | |||||
for service in to_print: | |||||
service.ToXML(local_xml) | |||||
local_xml.write('</registry>\n') | |||||
if __name__ == '__main__': | |||||
import sys | |||||
generate_etc_services( | |||||
parse('service-names-port-numbers.xml', sys.stderr), | |||||
parse('local.xml', sys.stderr), | |||||
open('services_header'), | |||||
open('services', 'w'), | |||||
sys.stderr) |
consider using https://docs.python.org/3/library/functools.html#functools.total_ordering