Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dns/rdataset.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

245 statements  

1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license 

2 

3# Copyright (C) 2001-2017 Nominum, Inc. 

4# 

5# Permission to use, copy, modify, and distribute this software and its 

6# documentation for any purpose with or without fee is hereby granted, 

7# provided that the above copyright notice and this permission notice 

8# appear in all copies. 

9# 

10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES 

11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 

12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR 

13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 

14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 

15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 

16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 

17 

18"""DNS rdatasets (an rdataset is a set of rdatas of a given type and class)""" 

19 

20import dataclasses 

21import io 

22import random 

23import struct 

24from collections.abc import Collection 

25from typing import Any, cast 

26 

27import dns.exception 

28import dns.immutable 

29import dns.name 

30import dns.rdata 

31import dns.rdataclass 

32import dns.rdatatype 

33import dns.set 

34import dns.ttl 

35from dns._render_util import prefixed_length 

36 

37# define SimpleSet here for backwards compatibility 

38SimpleSet = dns.set.Set 

39 

40 

41class DifferingCovers(dns.exception.DNSException): 

42 """An attempt was made to add a DNS SIG/RRSIG whose covered type 

43 is not the same as that of the other rdatas in the rdataset.""" 

44 

45 

46class IncompatibleTypes(dns.exception.DNSException): 

47 """An attempt was made to add DNS RR data of an incompatible type.""" 

48 

49 

50@dataclasses.dataclass(frozen=True) 

51class RdatasetStyle(dns.rdata.RdataStyle): 

52 """Rdataset text styles 

53 

54 An ``RdatasetStyle`` is also a :py:class:`dns.name.NameStyle` and a 

55 :py:class:`dns.rdata.RdataStyle`. See those classes 

56 for a description of their options. 

57 

58 .. attribute:: override_rdclass 

59 

60 A :py:class:`dns.rdataclass.RdataClass` or ``None``. If not ``None``, 

61 use this class instead of the Rdataset's class. 

62 

63 .. attribute:: want_comments 

64 

65 A ``bool``. If ``True``, emit comments for rdata which have them. 

66 The default is ``False``. 

67 

68 .. attribute:: omit_rdclass 

69 

70 A ``bool``. If ``True``, do not print the RdataClass. The default is 

71 ``False``. 

72 

73 .. attribute:: omit_ttl 

74 

75 A ``bool``. If ``True``, do not print the TTL. The default is 

76 ``False``. Use of this option may lose information. 

77 

78 .. attribute:: want_generic 

79 

80 A ``bool``. If ``True``, print RdataClass, RdataType, and Rdatas in 

81 the generic format (the "unknown rdata format"). The default is 

82 ``False``. 

83 

84 .. attribute:: deduplicate_names 

85 

86 A ``bool``. If ``True``, print whitespace instead of the owner name 

87 when the owner name of an RR is the same as the prior RR's owner name. 

88 The default is ``False``. 

89 

90 .. attribute:: first_name_is_duplicate 

91 

92 A ``bool``. If ``True``, consider the first owner name of the rdataset 

93 as a duplicate too, emitting whitespace for it as well. Useful when 

94 emitting a Node of multiple rdatasets and the current rdataset is not 

95 the first. The default is ``False``. 

96 

97 .. attribute:: default_ttl 

98 

99 An ``int`` or ``None``. If ``None`` (the default), there is no default 

100 TTL. If an integer is specified, any TTL matching that value will be 

101 omitted. When emitting a zonefile, a value other than ``None`` will 

102 cause a ``$TTL`` directive to be emitted. 

103 

104 .. attribute:: name_just 

105 

106 An ``int``. The owner name field justification. Negative values are 

107 left-justified, positive values are right-justified. Zero (the default) 

108 means no justification. 

109 

110 .. attribute:: ttl_just 

111 

112 An ``int``. The TTL field justification. Negative values are 

113 left-justified, positive values are right-justified. Zero (the default) 

114 means no justification. 

115 

116 .. attribute:: rdclass_just 

117 

118 An ``int``. The RdataClass field justification. Negative values are 

119 left-justified, positive values are right-justified. Zero (the default) 

120 means no justification. 

121 

122 .. attribute:: rdtype_just 

123 

124 An ``int``. The RdataType field justification. Negative values are 

125 left-justified, positive values are right-justified. Zero (the default) 

126 means no justification. 

127 """ 

128 

129 override_rdclass: dns.rdataclass.RdataClass | None = None 

130 want_comments: bool = False 

131 omit_rdclass: bool = False 

132 omit_ttl: bool = False 

133 want_generic: bool = False 

134 deduplicate_names: bool = False 

135 first_name_is_duplicate: bool = False 

136 default_ttl: int | None = None 

137 name_just: int = 0 

138 ttl_just: int = 0 

139 rdclass_just: int = 0 

140 rdtype_just: int = 0 

141 

142 

143def justify(text: str, amount: int): 

144 if amount == 0: 

145 return text 

146 if amount < 0: 

147 return text.ljust(-1 * amount) 

148 else: 

149 return text.rjust(amount) 

150 

151 

152class Rdataset(dns.set.Set): 

153 """A DNS rdataset.""" 

154 

155 __slots__ = ["rdclass", "rdtype", "covers", "ttl"] 

156 

157 def __init__( 

158 self, 

159 rdclass: dns.rdataclass.RdataClass, 

160 rdtype: dns.rdatatype.RdataType, 

161 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

162 ttl: int = 0, 

163 ): 

164 """Create a new rdataset of the specified class and type. 

165 

166 :param rdclass: The rdataclass. 

167 :type rdclass: :py:class:`dns.rdataclass.RdataClass` 

168 :param rdtype: The rdatatype. 

169 :type rdtype: :py:class:`dns.rdatatype.RdataType` 

170 :param covers: The covered rdatatype. 

171 :type covers: :py:class:`dns.rdatatype.RdataType` 

172 :param ttl: The TTL. 

173 :type ttl: int 

174 """ 

175 

176 super().__init__() 

177 self.rdclass = rdclass 

178 self.rdtype: dns.rdatatype.RdataType = rdtype 

179 self.covers: dns.rdatatype.RdataType = covers 

180 self.ttl = ttl 

181 

182 def _clone(self): 

183 obj = cast(Rdataset, super()._clone()) 

184 obj.rdclass = self.rdclass 

185 obj.rdtype = self.rdtype 

186 obj.covers = self.covers 

187 obj.ttl = self.ttl 

188 return obj 

189 

190 def update_ttl(self, ttl: int) -> None: 

191 """Perform TTL minimization. 

192 

193 Set the TTL of the rdataset to be the lesser of the set's current 

194 TTL or the specified TTL. If the set contains no rdatas, set the TTL 

195 to the specified TTL. 

196 

197 :param ttl: The candidate TTL value. 

198 :type ttl: int or str 

199 """ 

200 ttl = dns.ttl.make(ttl) 

201 if len(self) == 0: 

202 self.ttl = ttl 

203 elif ttl < self.ttl: 

204 self.ttl = ttl 

205 

206 # pylint: disable=arguments-differ,arguments-renamed 

207 def add(self, rd: dns.rdata.Rdata, ttl: int | None = None) -> None: # type: ignore 

208 """Add the specified rdata to the rdataset. 

209 

210 If the optional *ttl* parameter is supplied, then 

211 ``self.update_ttl(ttl)`` will be called prior to adding the rdata. 

212 

213 :param rd: The rdata to add. 

214 :type rd: :py:class:`dns.rdata.Rdata` 

215 :param ttl: The TTL. 

216 :type ttl: int or ``None`` 

217 :raises dns.rdataset.IncompatibleTypes: If the type and class do not 

218 match the type and class of the rdataset. 

219 :raises dns.rdataset.DifferingCovers: If the type is a signature type 

220 and the covered type does not match that of the rdataset. 

221 """ 

222 

223 # 

224 # If we're adding a signature, do some special handling to 

225 # check that the signature covers the same type as the 

226 # other rdatas in this rdataset. If this is the first rdata 

227 # in the set, initialize the covers field. 

228 # 

229 if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype: 

230 raise IncompatibleTypes 

231 if ttl is not None: 

232 self.update_ttl(ttl) 

233 if self.rdtype == dns.rdatatype.RRSIG or self.rdtype == dns.rdatatype.SIG: 

234 covers = rd.covers() 

235 if len(self) == 0 and self.covers == dns.rdatatype.NONE: 

236 self.covers = covers 

237 elif self.covers != covers: 

238 raise DifferingCovers 

239 if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0: 

240 self.clear() 

241 super().add(rd) 

242 

243 def union_update(self, other): 

244 self.update_ttl(other.ttl) 

245 super().union_update(other) 

246 

247 def intersection_update(self, other): 

248 self.update_ttl(other.ttl) 

249 super().intersection_update(other) 

250 

251 def update(self, other): 

252 """Add all rdatas in other to self. 

253 

254 :param other: The rdataset from which to update. 

255 :type other: :py:class:`dns.rdataset.Rdataset` 

256 """ 

257 

258 self.update_ttl(other.ttl) 

259 super().update(other) 

260 

261 def _rdata_repr(self): 

262 def maybe_truncate(s): 

263 if len(s) > 100: 

264 return s[:100] + "..." 

265 return s 

266 

267 return "[" + ", ".join(f"<{maybe_truncate(str(rr))}>" for rr in self) + "]" 

268 

269 def __repr__(self): 

270 if self.covers == 0: 

271 ctext = "" 

272 else: 

273 ctext = "(" + dns.rdatatype.to_text(self.covers) + ")" 

274 return ( 

275 "<DNS " 

276 + dns.rdataclass.to_text(self.rdclass) 

277 + " " 

278 + dns.rdatatype.to_text(self.rdtype) 

279 + ctext 

280 + " rdataset: " 

281 + self._rdata_repr() 

282 + ">" 

283 ) 

284 

285 def __str__(self): 

286 return self.to_text() 

287 

288 def __eq__(self, other): 

289 if not isinstance(other, Rdataset): 

290 return False 

291 if ( 

292 self.rdclass != other.rdclass 

293 or self.rdtype != other.rdtype 

294 or self.covers != other.covers 

295 ): 

296 return False 

297 return super().__eq__(other) 

298 

299 def __ne__(self, other): 

300 return not self.__eq__(other) 

301 

302 def to_text( 

303 self, 

304 name: dns.name.Name | None = None, 

305 origin: dns.name.Name | None = None, 

306 relativize: bool = True, 

307 override_rdclass: dns.rdataclass.RdataClass | None = None, 

308 want_comments: bool = False, 

309 style: RdatasetStyle | None = None, 

310 **kw: Any, 

311 ) -> str: 

312 """Convert the rdataset into DNS zone file format. 

313 

314 See ``dns.name.Name.choose_relativity`` for more information 

315 on how *origin* and *relativize* determine the way names 

316 are emitted. 

317 

318 Any additional keyword arguments are passed on to the rdata 

319 ``to_text()`` method. 

320 

321 :param name: If not ``None``, emit RRs with this as the owner name. 

322 :type name: :py:class:`dns.name.Name` or ``None`` 

323 :param origin: The origin for relative names. 

324 :type origin: :py:class:`dns.name.Name` or ``None`` 

325 :param relativize: If ``True``, names will be relativized to *origin*. 

326 :type relativize: bool 

327 :param override_rdclass: If not ``None``, emit records as if they were 

328 of this class. 

329 :type override_rdclass: :py:class:`dns.rdataclass.RdataClass` or ``None`` 

330 :param want_comments: If ``True``, emit comments for rdata which have 

331 them. The default is ``False``. 

332 :type want_comments: bool 

333 :param style: If specified, overrides the other parameters except 

334 *name*. 

335 :type style: :py:class:`dns.rdataset.RdatasetStyle` or ``None`` 

336 """ 

337 if style is None: 

338 kw = kw.copy() 

339 kw["origin"] = origin 

340 kw["relativize"] = relativize 

341 kw["override_rdclass"] = override_rdclass 

342 kw["want_comments"] = want_comments 

343 style = RdatasetStyle.from_keywords(kw) 

344 return self.to_styled_text(style, name) 

345 

346 def to_styled_text( 

347 self, style: RdatasetStyle, name: dns.name.Name | None = None 

348 ) -> str: 

349 """Convert the rdataset into styled text format. 

350 

351 See the documentation for :py:class:`dns.rdataset.RdatasetStyle` for a description 

352 of the style parameters. 

353 """ 

354 if name is not None: 

355 if style.deduplicate_names and style.first_name_is_duplicate: 

356 ntext = " " 

357 else: 

358 ntext = f"{name.to_styled_text(style)} " 

359 ntext = justify(ntext, style.name_just) 

360 else: 

361 ntext = "" 

362 s = io.StringIO() 

363 if style.override_rdclass is not None: 

364 rdclass = style.override_rdclass 

365 else: 

366 rdclass = self.rdclass 

367 if style.omit_rdclass: 

368 rdclass_text = "" 

369 elif style.want_generic: 

370 rdclass_text = f"CLASS{rdclass} " 

371 else: 

372 rdclass_text = f"{dns.rdataclass.to_text(rdclass)} " 

373 rdclass_text = justify(rdclass_text, style.rdclass_just) 

374 if style.want_generic: 

375 rdtype_text = f"TYPE{self.rdtype}" 

376 else: 

377 rdtype_text = f"{dns.rdatatype.to_text(self.rdtype)}" 

378 rdtype_text = justify(rdtype_text, style.rdtype_just) 

379 if len(self) == 0: 

380 # 

381 # Empty rdatasets are used for the question section, and in 

382 # some dynamic updates, so we don't need to print out the TTL 

383 # (which is meaningless anyway). 

384 # 

385 s.write(f"{ntext}{rdclass_text}{rdtype_text}\n") 

386 else: 

387 if style.omit_ttl or ( 

388 style.default_ttl is not None and self.ttl == style.default_ttl 

389 ): 

390 ttl = "" 

391 else: 

392 ttl = f"{self.ttl} " 

393 ttl = justify(ttl, style.ttl_just) 

394 for rd in self: 

395 extra = "" 

396 if style.want_comments: 

397 if rd.rdcomment: 

398 extra = f" ;{rd.rdcomment}" 

399 if style.want_generic: 

400 rdata_text = rd.to_generic().to_styled_text(style) 

401 else: 

402 rdata_text = rd.to_styled_text(style) 

403 s.write( 

404 f"{ntext}{ttl}{rdclass_text}{rdtype_text} {rdata_text}{extra}\n" 

405 ) 

406 if style.deduplicate_names: 

407 ntext = " " 

408 ntext = justify(ntext, style.name_just) 

409 

410 # 

411 # We strip off the final \n for the caller's convenience in printing 

412 # 

413 return s.getvalue()[:-1] 

414 

415 def to_wire( 

416 self, 

417 name: dns.name.Name, 

418 file: Any, 

419 compress: dns.name.CompressType | None = None, 

420 origin: dns.name.Name | None = None, 

421 override_rdclass: dns.rdataclass.RdataClass | None = None, 

422 want_shuffle: bool = True, 

423 ) -> int: 

424 """Convert the rdataset to wire format. 

425 

426 :param name: The owner name to use. 

427 :type name: :py:class:`dns.name.Name` 

428 :param file: The file where the name is emitted (typically a 

429 ``BytesIO`` file). 

430 :param compress: The compression table to use. If ``None`` (the 

431 default), names will not be compressed. 

432 :type compress: dict or ``None`` 

433 :param origin: If the name is relative and *origin* is not ``None``, 

434 *origin* will be appended to it. 

435 :type origin: :py:class:`dns.name.Name` or ``None`` 

436 :param override_rdclass: If not ``None``, used as the class instead of 

437 the class of the rdataset. Useful for dynamic update rendering. 

438 :type override_rdclass: int or ``None`` 

439 :param want_shuffle: If ``True``, the order of Rdatas within the 

440 Rdataset will be shuffled before rendering. 

441 :type want_shuffle: bool 

442 

443 :returns: The number of records emitted. 

444 :rtype: int 

445 """ 

446 

447 if override_rdclass is not None: 

448 rdclass = override_rdclass 

449 want_shuffle = False 

450 else: 

451 rdclass = self.rdclass 

452 if len(self) == 0: 

453 name.to_wire(file, compress, origin) 

454 file.write(struct.pack("!HHIH", self.rdtype, rdclass, 0, 0)) 

455 return 1 

456 else: 

457 l: Rdataset | list[dns.rdata.Rdata] 

458 if want_shuffle: 

459 l = list(self) 

460 random.shuffle(l) 

461 else: 

462 l = self 

463 for rd in l: 

464 name.to_wire(file, compress, origin) 

465 file.write(struct.pack("!HHI", self.rdtype, rdclass, self.ttl)) 

466 with prefixed_length(file, 2): 

467 rd.to_wire(file, compress, origin) 

468 return len(self) 

469 

470 def match( 

471 self, 

472 rdclass: dns.rdataclass.RdataClass, 

473 rdtype: dns.rdatatype.RdataType, 

474 covers: dns.rdatatype.RdataType, 

475 ) -> bool: 

476 """Returns ``True`` if this rdataset matches the specified class, 

477 type, and covers. 

478 """ 

479 if self.rdclass == rdclass and self.rdtype == rdtype and self.covers == covers: 

480 return True 

481 return False 

482 

483 def processing_order(self) -> list[dns.rdata.Rdata]: 

484 """Return rdatas in a valid processing order according to the type's 

485 specification. For example, MX records are in preference order from 

486 lowest to highest preferences, with items of the same preference 

487 shuffled. 

488 

489 For types that do not define a processing order, the rdatas are 

490 simply shuffled. 

491 """ 

492 if len(self) == 0: 

493 return [] 

494 else: 

495 return self[0]._processing_order(iter(self)) # pyright: ignore 

496 

497 

498@dns.immutable.immutable 

499class ImmutableRdataset(Rdataset): # lgtm[py/missing-equals] 

500 """An immutable DNS rdataset.""" 

501 

502 _clone_class = Rdataset 

503 

504 def __init__(self, rdataset: Rdataset): 

505 """Create an immutable rdataset from the specified rdataset.""" 

506 

507 super().__init__( 

508 rdataset.rdclass, rdataset.rdtype, rdataset.covers, rdataset.ttl 

509 ) 

510 self.items = dns.immutable.Dict(rdataset.items) 

511 

512 def update_ttl(self, ttl): 

513 raise TypeError("immutable") 

514 

515 def add(self, rd, ttl=None): # pyright: ignore 

516 raise TypeError("immutable") 

517 

518 def union_update(self, other): 

519 raise TypeError("immutable") 

520 

521 def intersection_update(self, other): 

522 raise TypeError("immutable") 

523 

524 def update(self, other): 

525 raise TypeError("immutable") 

526 

527 def __delitem__(self, i): 

528 raise TypeError("immutable") 

529 

530 # lgtm complains about these not raising ArithmeticError, but there is 

531 # precedent for overrides of these methods in other classes to raise 

532 # TypeError, and it seems like the better exception. 

533 

534 def __ior__(self, other): # lgtm[py/unexpected-raise-in-special-method] 

535 raise TypeError("immutable") 

536 

537 def __iand__(self, other): # lgtm[py/unexpected-raise-in-special-method] 

538 raise TypeError("immutable") 

539 

540 def __iadd__(self, other): # lgtm[py/unexpected-raise-in-special-method] 

541 raise TypeError("immutable") 

542 

543 def __isub__(self, other): # lgtm[py/unexpected-raise-in-special-method] 

544 raise TypeError("immutable") 

545 

546 def clear(self): 

547 raise TypeError("immutable") 

548 

549 def __copy__(self): 

550 return ImmutableRdataset(super().copy()) # pyright: ignore 

551 

552 def copy(self): 

553 return ImmutableRdataset(super().copy()) # pyright: ignore 

554 

555 def union(self, other): 

556 return ImmutableRdataset(super().union(other)) # pyright: ignore 

557 

558 def intersection(self, other): 

559 return ImmutableRdataset(super().intersection(other)) # pyright: ignore 

560 

561 def difference(self, other): 

562 return ImmutableRdataset(super().difference(other)) # pyright: ignore 

563 

564 def symmetric_difference(self, other): 

565 return ImmutableRdataset(super().symmetric_difference(other)) # pyright: ignore 

566 

567 

568def from_text_list( 

569 rdclass: dns.rdataclass.RdataClass | str, 

570 rdtype: dns.rdatatype.RdataType | str, 

571 ttl: int, 

572 text_rdatas: Collection[str], 

573 idna_codec: dns.name.IDNACodec | None = None, 

574 origin: dns.name.Name | None = None, 

575 relativize: bool = True, 

576 relativize_to: dns.name.Name | None = None, 

577) -> Rdataset: 

578 """Create an rdataset with the specified class, type, and TTL, and with 

579 the specified list of rdatas in text format. 

580 

581 :param idna_codec: Specifies the IDNA encoder/decoder. If ``None``, the 

582 default IDNA 2003 encoder/decoder is used. 

583 :type idna_codec: :py:class:`dns.name.IDNACodec` or ``None`` 

584 :param origin: The origin to use for relative names. 

585 :type origin: :py:class:`dns.name.Name` or ``None`` 

586 :param relativize: If ``True``, names will be relativized. 

587 :type relativize: bool 

588 :param relativize_to: The origin to use when relativizing names. If not 

589 set, *origin* is used. 

590 :type relativize_to: :py:class:`dns.name.Name` or ``None`` 

591 

592 :rtype: :py:class:`dns.rdataset.Rdataset` 

593 """ 

594 

595 rdclass = dns.rdataclass.RdataClass.make(rdclass) 

596 rdtype = dns.rdatatype.RdataType.make(rdtype) 

597 r = Rdataset(rdclass, rdtype) 

598 r.update_ttl(ttl) 

599 for t in text_rdatas: 

600 rd = dns.rdata.from_text( 

601 r.rdclass, r.rdtype, t, origin, relativize, relativize_to, idna_codec 

602 ) 

603 r.add(rd) 

604 return r 

605 

606 

607def from_text( 

608 rdclass: dns.rdataclass.RdataClass | str, 

609 rdtype: dns.rdatatype.RdataType | str, 

610 ttl: int, 

611 *text_rdatas: Any, 

612) -> Rdataset: 

613 """Create an rdataset with the specified class, type, and TTL, and with 

614 the specified rdatas in text format. 

615 

616 :rtype: :py:class:`dns.rdataset.Rdataset` 

617 """ 

618 

619 return from_text_list(rdclass, rdtype, ttl, cast(Collection[str], text_rdatas)) 

620 

621 

622def from_rdata_list(ttl: int, rdatas: Collection[dns.rdata.Rdata]) -> Rdataset: 

623 """Create an rdataset with the specified TTL, and with 

624 the specified list of rdata objects. 

625 

626 :rtype: :py:class:`dns.rdataset.Rdataset` 

627 """ 

628 

629 if len(rdatas) == 0: 

630 raise ValueError("rdata list must not be empty") 

631 r = None 

632 for rd in rdatas: 

633 if r is None: 

634 r = Rdataset(rd.rdclass, rd.rdtype) 

635 r.update_ttl(ttl) 

636 r.add(rd) 

637 assert r is not None 

638 return r 

639 

640 

641def from_rdata(ttl: int, *rdatas: Any) -> Rdataset: 

642 """Create an rdataset with the specified TTL, and with 

643 the specified rdata objects. 

644 

645 :rtype: :py:class:`dns.rdataset.Rdataset` 

646 """ 

647 

648 return from_rdata_list(ttl, cast(Collection[dns.rdata.Rdata], rdatas))