Skip to content

Commit b4814f5

Browse files
authored
Move service_type_name to zeroconf.utils.name (#543)
1 parent 1e3e7df commit b4814f5

2 files changed

Lines changed: 154 additions & 121 deletions

File tree

zeroconf/__init__.py

Lines changed: 1 addition & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
ServiceNameAlreadyRegistered,
109109
)
110110
from .logger import QuietLogger, log
111+
from .utils.name import service_type_name
111112
from .utils.net import ( # noqa # import needed for backwards compat
112113
add_multicast_member,
113114
can_send_to,
@@ -156,127 +157,6 @@
156157
# utility functions
157158

158159

159-
def service_type_name(type_: str, *, strict: bool = True) -> str: # pylint: disable=too-many-branches
160-
"""
161-
Validate a fully qualified service name, instance or subtype. [rfc6763]
162-
163-
Returns fully qualified service name.
164-
165-
Domain names used by mDNS-SD take the following forms:
166-
167-
<sn> . <_tcp|_udp> . local.
168-
<Instance> . <sn> . <_tcp|_udp> . local.
169-
<sub>._sub . <sn> . <_tcp|_udp> . local.
170-
171-
1) must end with 'local.'
172-
173-
This is true because we are implementing mDNS and since the 'm' means
174-
multi-cast, the 'local.' domain is mandatory.
175-
176-
2) local is preceded with either '_udp.' or '_tcp.' unless
177-
strict is False
178-
179-
3) service name <sn> precedes <_tcp|_udp> unless
180-
strict is False
181-
182-
The rules for Service Names [RFC6335] state that they may be no more
183-
than fifteen characters long (not counting the mandatory underscore),
184-
consisting of only letters, digits, and hyphens, must begin and end
185-
with a letter or digit, must not contain consecutive hyphens, and
186-
must contain at least one letter.
187-
188-
The instance name <Instance> and sub type <sub> may be up to 63 bytes.
189-
190-
The portion of the Service Instance Name is a user-
191-
friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It
192-
MUST NOT contain ASCII control characters (byte values 0x00-0x1F and
193-
0x7F) [RFC20] but otherwise is allowed to contain any characters,
194-
without restriction, including spaces, uppercase, lowercase,
195-
punctuation -- including dots -- accented characters, non-Roman text,
196-
and anything else that may be represented using Net-Unicode.
197-
198-
:param type_: Type, SubType or service name to validate
199-
:return: fully qualified service name (eg: _http._tcp.local.)
200-
"""
201-
202-
if type_.endswith((_TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)):
203-
remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.')
204-
trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :]
205-
has_protocol = True
206-
elif strict:
207-
raise BadTypeInNameException(
208-
"Type '%s' must end with '%s' or '%s'"
209-
% (type_, _TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)
210-
)
211-
elif type_.endswith(_LOCAL_TRAILER):
212-
remaining = type_[: -len(_LOCAL_TRAILER)].split('.')
213-
trailer = type_[-len(_LOCAL_TRAILER) + 1 :]
214-
has_protocol = False
215-
else:
216-
raise BadTypeInNameException("Type '%s' must end with '%s'" % (type_, _LOCAL_TRAILER))
217-
218-
if strict or has_protocol:
219-
service_name = remaining.pop()
220-
if not service_name:
221-
raise BadTypeInNameException("No Service name found")
222-
223-
if len(remaining) == 1 and len(remaining[0]) == 0:
224-
raise BadTypeInNameException("Type '%s' must not start with '.'" % type_)
225-
226-
if service_name[0] != '_':
227-
raise BadTypeInNameException("Service name (%s) must start with '_'" % service_name)
228-
229-
test_service_name = service_name[1:]
230-
231-
if len(test_service_name) > 15:
232-
raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % test_service_name)
233-
234-
if '--' in test_service_name:
235-
raise BadTypeInNameException("Service name (%s) must not contain '--'" % test_service_name)
236-
237-
if '-' in (test_service_name[0], test_service_name[-1]):
238-
raise BadTypeInNameException(
239-
"Service name (%s) may not start or end with '-'" % test_service_name
240-
)
241-
242-
if not _HAS_A_TO_Z.search(test_service_name):
243-
raise BadTypeInNameException(
244-
"Service name (%s) must contain at least one letter (eg: 'A-Z')" % test_service_name
245-
)
246-
247-
allowed_characters_re = (
248-
_HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE
249-
)
250-
251-
if not allowed_characters_re.search(test_service_name):
252-
raise BadTypeInNameException(
253-
"Service name (%s) must contain only these characters: "
254-
"A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if strict else ", underscore ('_')")
255-
)
256-
else:
257-
service_name = ''
258-
259-
if remaining and remaining[-1] == '_sub':
260-
remaining.pop()
261-
if len(remaining) == 0 or len(remaining[0]) == 0:
262-
raise BadTypeInNameException("_sub requires a subtype name")
263-
264-
if len(remaining) > 1:
265-
remaining = ['.'.join(remaining)]
266-
267-
if remaining:
268-
length = len(remaining[0].encode('utf-8'))
269-
if length > 63:
270-
raise BadTypeInNameException("Too long: '%s'" % remaining[0])
271-
272-
if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]):
273-
raise BadTypeInNameException(
274-
"Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % remaining[0]
275-
)
276-
277-
return service_name + trailer
278-
279-
280160
def instance_name_from_service_info(info: "ServiceInfo") -> str:
281161
"""Calculate the instance name from the ServiceInfo."""
282162
# This is kind of funky because of the subtype based tests

zeroconf/utils/name.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
""" Multicast DNS Service Discovery for Python, v0.14-wmcbrine
2+
Copyright 2003 Paul Scott-Murphy, 2014 William McBrine
3+
4+
This module provides a framework for the use of DNS Service Discovery
5+
using IP multicast.
6+
7+
This library is free software; you can redistribute it and/or
8+
modify it under the terms of the GNU Lesser General Public
9+
License as published by the Free Software Foundation; either
10+
version 2.1 of the License, or (at your option) any later version.
11+
12+
This library is distributed in the hope that it will be useful,
13+
but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+
Lesser General Public License for more details.
16+
17+
You should have received a copy of the GNU Lesser General Public
18+
License along with this library; if not, write to the Free Software
19+
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
20+
USA
21+
"""
22+
23+
from ..const import (
24+
_HAS_ASCII_CONTROL_CHARS,
25+
_HAS_A_TO_Z,
26+
_HAS_ONLY_A_TO_Z_NUM_HYPHEN,
27+
_HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE,
28+
_LOCAL_TRAILER,
29+
_NONTCP_PROTOCOL_LOCAL_TRAILER,
30+
_TCP_PROTOCOL_LOCAL_TRAILER,
31+
)
32+
from ..exceptions import BadTypeInNameException
33+
34+
35+
def service_type_name(type_: str, *, strict: bool = True) -> str: # pylint: disable=too-many-branches
36+
"""
37+
Validate a fully qualified service name, instance or subtype. [rfc6763]
38+
39+
Returns fully qualified service name.
40+
41+
Domain names used by mDNS-SD take the following forms:
42+
43+
<sn> . <_tcp|_udp> . local.
44+
<Instance> . <sn> . <_tcp|_udp> . local.
45+
<sub>._sub . <sn> . <_tcp|_udp> . local.
46+
47+
1) must end with 'local.'
48+
49+
This is true because we are implementing mDNS and since the 'm' means
50+
multi-cast, the 'local.' domain is mandatory.
51+
52+
2) local is preceded with either '_udp.' or '_tcp.' unless
53+
strict is False
54+
55+
3) service name <sn> precedes <_tcp|_udp> unless
56+
strict is False
57+
58+
The rules for Service Names [RFC6335] state that they may be no more
59+
than fifteen characters long (not counting the mandatory underscore),
60+
consisting of only letters, digits, and hyphens, must begin and end
61+
with a letter or digit, must not contain consecutive hyphens, and
62+
must contain at least one letter.
63+
64+
The instance name <Instance> and sub type <sub> may be up to 63 bytes.
65+
66+
The portion of the Service Instance Name is a user-
67+
friendly name consisting of arbitrary Net-Unicode text [RFC5198]. It
68+
MUST NOT contain ASCII control characters (byte values 0x00-0x1F and
69+
0x7F) [RFC20] but otherwise is allowed to contain any characters,
70+
without restriction, including spaces, uppercase, lowercase,
71+
punctuation -- including dots -- accented characters, non-Roman text,
72+
and anything else that may be represented using Net-Unicode.
73+
74+
:param type_: Type, SubType or service name to validate
75+
:return: fully qualified service name (eg: _http._tcp.local.)
76+
"""
77+
78+
if type_.endswith((_TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)):
79+
remaining = type_[: -len(_TCP_PROTOCOL_LOCAL_TRAILER)].split('.')
80+
trailer = type_[-len(_TCP_PROTOCOL_LOCAL_TRAILER) :]
81+
has_protocol = True
82+
elif strict:
83+
raise BadTypeInNameException(
84+
"Type '%s' must end with '%s' or '%s'"
85+
% (type_, _TCP_PROTOCOL_LOCAL_TRAILER, _NONTCP_PROTOCOL_LOCAL_TRAILER)
86+
)
87+
elif type_.endswith(_LOCAL_TRAILER):
88+
remaining = type_[: -len(_LOCAL_TRAILER)].split('.')
89+
trailer = type_[-len(_LOCAL_TRAILER) + 1 :]
90+
has_protocol = False
91+
else:
92+
raise BadTypeInNameException("Type '%s' must end with '%s'" % (type_, _LOCAL_TRAILER))
93+
94+
if strict or has_protocol:
95+
service_name = remaining.pop()
96+
if not service_name:
97+
raise BadTypeInNameException("No Service name found")
98+
99+
if len(remaining) == 1 and len(remaining[0]) == 0:
100+
raise BadTypeInNameException("Type '%s' must not start with '.'" % type_)
101+
102+
if service_name[0] != '_':
103+
raise BadTypeInNameException("Service name (%s) must start with '_'" % service_name)
104+
105+
test_service_name = service_name[1:]
106+
107+
if len(test_service_name) > 15:
108+
raise BadTypeInNameException("Service name (%s) must be <= 15 bytes" % test_service_name)
109+
110+
if '--' in test_service_name:
111+
raise BadTypeInNameException("Service name (%s) must not contain '--'" % test_service_name)
112+
113+
if '-' in (test_service_name[0], test_service_name[-1]):
114+
raise BadTypeInNameException(
115+
"Service name (%s) may not start or end with '-'" % test_service_name
116+
)
117+
118+
if not _HAS_A_TO_Z.search(test_service_name):
119+
raise BadTypeInNameException(
120+
"Service name (%s) must contain at least one letter (eg: 'A-Z')" % test_service_name
121+
)
122+
123+
allowed_characters_re = (
124+
_HAS_ONLY_A_TO_Z_NUM_HYPHEN if strict else _HAS_ONLY_A_TO_Z_NUM_HYPHEN_UNDERSCORE
125+
)
126+
127+
if not allowed_characters_re.search(test_service_name):
128+
raise BadTypeInNameException(
129+
"Service name (%s) must contain only these characters: "
130+
"A-Z, a-z, 0-9, hyphen ('-')%s" % (test_service_name, "" if strict else ", underscore ('_')")
131+
)
132+
else:
133+
service_name = ''
134+
135+
if remaining and remaining[-1] == '_sub':
136+
remaining.pop()
137+
if len(remaining) == 0 or len(remaining[0]) == 0:
138+
raise BadTypeInNameException("_sub requires a subtype name")
139+
140+
if len(remaining) > 1:
141+
remaining = ['.'.join(remaining)]
142+
143+
if remaining:
144+
length = len(remaining[0].encode('utf-8'))
145+
if length > 63:
146+
raise BadTypeInNameException("Too long: '%s'" % remaining[0])
147+
148+
if _HAS_ASCII_CONTROL_CHARS.search(remaining[0]):
149+
raise BadTypeInNameException(
150+
"Ascii control character 0x00-0x1F and 0x7F illegal in '%s'" % remaining[0]
151+
)
152+
153+
return service_name + trailer

0 commit comments

Comments
 (0)