From 860264bc3463a3f9635e1cc315bbabcc18e3671b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 9 Nov 2019 15:18:08 +1000 Subject: [PATCH 001/207] frame: Re-jig structure for handling AX.25 2.2 AX.25 2.2 adds support for modulo-128 frames using a 16-bit control field. The only way to know you're dealing with modulo-128 traffic is by remembering that you negotiated it earlier. Thus we need 8-bit and 16-bit versions of the frame structure to handle the enlarged control field in the modulo-128 case. --- aioax25/frame.py | 146 +++++++++++++++++++++-------- tests/test_frame/test_ax25frame.py | 29 ++---- 2 files changed, 115 insertions(+), 60 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 367bd61..09ad12c 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -16,8 +16,6 @@ class AX25Frame(object): Base class for AX.25 frames. """ - POLL_FINAL = 0b00010000 - CONTROL_I_MASK = 0b00000001 CONTROL_I_VAL = 0b00000000 CONTROL_US_MASK = 0b00000011 @@ -49,41 +47,29 @@ def decode(cls, data): if not data: raise ValueError("Insufficient packet data") - # Next should be the control field + # Next should be the control field. Control field + # can be either 8 or 16-bits, we don't know at this point. + # We'll look at the first 8-bits to see if it's a U-frame + # since that has a defined bit pattern we can look for. control = data[0] - data = data[1:] - if (control & cls.CONTROL_I_MASK) == cls.CONTROL_I_VAL: - # This is an I frame - TODO - # return AX25InformationFrame.decode(header, control, data) - return AX25RawFrame( - destination=header.destination, - source=header.source, - repeaters=header.repeaters, - cr=header.cr, - src_cr=header.src_cr, - control=control, - payload=data, - ) - elif (control & cls.CONTROL_US_MASK) == cls.CONTROL_S_VAL: - # This is a S frame - TODO - # return AX25SupervisoryFrame.decode(header, control, data) + if (control & cls.CONTROL_US_MASK) == cls.CONTROL_U_VAL: + # This is a U frame. Data starts after the control byte + # which can only be 8-bits wide. + return AX25UnnumberedFrame.decode(header, control, data[1:]) + else: + # This is either a I or S frame, both of which can either + # have a 8-bit or 16-bit control field. We don't know at + # this point so the only safe answer is to return a raw frame + # and decode it later. return AX25RawFrame( destination=header.destination, source=header.source, repeaters=header.repeaters, cr=header.cr, src_cr=header.src_cr, - control=control, payload=data, ) - elif (control & cls.CONTROL_US_MASK) == cls.CONTROL_U_VAL: - # This is a U frame - return AX25UnnumberedFrame.decode(header, control, data) - else: # pragma: no cover - # This should not happen because all possible bit combinations - # are covered above. - assert False, "How did we get here?" def __init__( self, @@ -109,9 +95,6 @@ def _encode(self): for byte in bytes(self.header): yield byte - # Send the control byte - yield self._control - # Send the payload for byte in self.frame_payload: yield byte @@ -149,14 +132,10 @@ def deadline(self, deadline): def header(self): return self._header - @property - def control(self): - return self._control - @property def frame_payload(self): """ - Return the bytes in the frame payload (following the control byte) + Return the bytes in the frame payload (including the control bytes) """ return b"" @@ -177,6 +156,84 @@ def copy(self, header=None): return clone +class AX258BitFrame(AX25Frame): + """ + Base class for AX.25 frames which have a 8-bit control field. + """ + + POLL_FINAL = 0b00010000 + + def __init__( + self, + destination, + source, + repeaters=None, + cr=False, + src_cr=None, + timestamp=None, + deadline=None, + ): + super(AX258BitFrame, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + src_cr=src_cr, + timestamp=timestamp, + deadline=deadline, + ) + + @property + def control(self): + return self._control + + @property + def frame_payload(self): + """ + Return the bytes in the frame payload (including the control byte) + """ + return bytes([self.control]) + + +class AX2516BitFrame(AX25Frame): + """ + Base class for AX.25 frames which have a 16-bit control field. + """ + + POLL_FINAL = 0b0000000100000000 + + def __init__( + self, + destination, + source, + repeaters=None, + cr=False, + src_cr=None, + timestamp=None, + deadline=None, + ): + super(AX2516BitFrame, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + src_cr=src_cr, + timestamp=timestamp, + deadline=deadline, + ) + + @property + def control(self): + return self._control + + @property + def frame_payload(self): + """ + Return the bytes in the frame payload (including the control bytes) + """ + return bytes([(self.control >> 8) & 0x00FF, self.control & 0x00FF]) + + class AX25RawFrame(AX25Frame): """ A representation of a raw AX.25 frame. @@ -186,7 +243,6 @@ def __init__( self, destination, source, - control, repeaters=None, cr=False, src_cr=None, @@ -195,7 +251,6 @@ def __init__( self._header = AX25FrameHeader( destination, source, repeaters, cr, src_cr ) - self._control = control self._payload = payload or b"" @property @@ -206,7 +261,6 @@ def _copy(self): return self.__class__( destination=self.header.destination, source=self.header.source, - control=self.control, repeaters=self.header.repeaters, cr=self.header.cr, src_cr=self.header.src_cr, @@ -214,7 +268,7 @@ def _copy(self): ) -class AX25UnnumberedFrame(AX25Frame): +class AX25UnnumberedFrame(AX258BitFrame): """ A representation of an un-numbered frame. """ @@ -258,6 +312,8 @@ def __init__( pf=False, cr=False, src_cr=None, + timestamp=None, + deadline=None, ): super(AX25UnnumberedFrame, self).__init__( destination=destination, @@ -265,6 +321,8 @@ def __init__( repeaters=repeaters, cr=cr, src_cr=src_cr, + timestamp=timestamp, + deadline=deadline, ) self._pf = bool(pf) self._modifier = int(modifier) & self.MODIFIER_MASK @@ -360,7 +418,11 @@ def payload(self): @property def frame_payload(self): - return bytearray([self.pid]) + self.payload + return ( + super(AX25UnnumberedInformationFrame, self).frame_payload + + bytearray([self.pid]) + + self.payload + ) def __str__(self): return "%s: PID=0x%02x Payload=%r" % ( @@ -490,7 +552,9 @@ def __init__( @property def frame_payload(self): - return bytes(self._gen_frame_payload()) + return super(AX25FrameRejectFrame, self).frame_payload + bytes( + self._gen_frame_payload() + ) def _gen_frame_payload(self): wxyz = 0 diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 6814c08..8962f14 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -34,13 +34,11 @@ def test_decode_iframe(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "00" # Control byte - "11 22 33 44 55 66 77" # Payload + "00 11 22 33 44 55 66 77" # Payload ) ) assert isinstance(frame, AX25RawFrame), "Did not decode to raw frame" - assert frame.control == 0x00 - hex_cmp(frame.frame_payload, "11 22 33 44 55 66 77") + hex_cmp(frame.frame_payload, "00 11 22 33 44 55 66 77") def test_decode_sframe(): @@ -51,13 +49,11 @@ def test_decode_sframe(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "01" # Control byte - "11 22 33 44 55 66 77" # Payload + "01 11 22 33 44 55 66 77" # Payload ) ) assert isinstance(frame, AX25RawFrame), "Did not decode to raw frame" - assert frame.control == 0x01 - hex_cmp(frame.frame_payload, "11 22 33 44 55 66 77") + hex_cmp(frame.frame_payload, "01 11 22 33 44 55 66 77") def test_decode_uframe(): @@ -75,7 +71,9 @@ def test_decode_uframe(): frame, AX25UnnumberedFrame ), "Did not decode to unnumbered frame" assert frame.modifier == 0xC3 - hex_cmp(frame.frame_payload, "") + + # We should see the control byte as our payload + hex_cmp(frame.frame_payload, "c3") def test_decode_uframe_payload(): @@ -185,8 +183,7 @@ def test_encode_raw(): destination="VK4BWI", source="VK4MSL", cr=True, - control=0x03, - payload=b"\xf0This is a test", + payload=b"\x03\xf0This is a test", ) hex_cmp( bytes(frame), @@ -488,10 +485,7 @@ def test_raw_copy(): Test we can make a copy of a raw frame. """ frame = AX25RawFrame( - destination="VK4BWI", - source="VK4MSL", - control=0xAB, - payload=b"This is a test", + destination="VK4BWI", source="VK4MSL", payload=b"\xabThis is a test" ) framecopy = frame.copy() assert framecopy is not frame @@ -580,10 +574,7 @@ def test_raw_str(): Test we can get a string representation of a raw frame. """ frame = AX25RawFrame( - destination="VK4BWI", - source="VK4MSL", - control=0xAB, - payload=b"This is a test", + destination="VK4BWI", source="VK4MSL", payload=b"\xabThis is a test" ) assert str(frame) == "VK4MSL>VK4BWI" From 9cb3f3a68e572046de490c3e6adbd6270fa5b491 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 9 Nov 2019 17:07:21 +1000 Subject: [PATCH 002/207] frame: Add encoders/decoders for connection-related types. This adds untested code for encoding and decoding various frame types related to connected-mode operation. --- aioax25/frame.py | 604 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 603 insertions(+), 1 deletion(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 09ad12c..1b92dea 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -163,6 +163,18 @@ class AX258BitFrame(AX25Frame): POLL_FINAL = 0b00010000 + # Control field bits + # + # 7 6 5 4 3 2 1 0 + # -------------------------------- + # N(R) | P | N(S) | 0 I Frame + # N(R) |P/F| S S | 0 1 S Frame + # M M M |P/F| M M | 1 1 U Frame + CONTROL_NR_MASK = 0b11100000 + CONTROL_NR_SHIFT = 5 + CONTROL_NS_MASK = 0b00001110 + CONTROL_NS_SHIFT = 1 + def __init__( self, destination, @@ -202,6 +214,19 @@ class AX2516BitFrame(AX25Frame): POLL_FINAL = 0b0000000100000000 + # Control field bits. These are sent least-significant bit first. + # Unnumbered frames _always_ use the 8-bit control format, so here + # we will only see I frames or S frames. + # + # 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + # -------------------------------------------------------------- + # N(R) | P | N(S) | 0 I Frame + # N(R) |P/F| 0 0 0 0 | S S | 0 1 S Frame + CONTROL_NR_MASK = 0b1111111000000000 + CONTROL_NR_SHIFT = 9 + CONTROL_NS_MASK = 0b0000000011111110 + CONTROL_NS_SHIFT = 1 + def __init__( self, destination, @@ -231,7 +256,10 @@ def frame_payload(self): """ Return the bytes in the frame payload (including the control bytes) """ - return bytes([(self.control >> 8) & 0x00FF, self.control & 0x00FF]) + # The control field is sent in LITTLE ENDIAN format so as to avoid + # S frames possibly getting confused with U frames. + control = self.control + return bytes([control & 0x00FF, (control >> 8) & 0x00FF]) class AX25RawFrame(AX25Frame): @@ -282,6 +310,13 @@ def decode(cls, header, control, data): for subclass in ( AX25UnnumberedInformationFrame, AX25FrameRejectFrame, + AX25SetAsyncBalancedModeFrame, + AX25SetAsyncBalancedModeExtendedFrame, + AX25DisconnectFrame, + AX25DisconnectModeFrame, + AX25ExchangeIdentificationFrame, + AX25UnnumberedAcknowledgeFrame, + AX25TestFrame, ): if modifier == subclass.MODIFIER: return subclass.decode(header, control, data) @@ -363,6 +398,371 @@ def _copy(self): ) +class AX25InformationFrameMixin(object): + """ + Common code for AX.25 all information frames + """ + + @classmethod + def decode(cls, header, control, data): + return cls( + destination=header.destination, + source=header.source, + repeaters=header.repeaters, + cr=header.cr, + nr=int((control & cls.CONTROL_NR_MASK) >> cls.CONTROL_NR_SHIFT), + ns=int((control & cls.CONTROL_NS_MASK) >> cls.CONTROL_NS_SHIFT), + pf=bool(control & cls.POLL_FINAL), + payload=data, + ) + + def __init__( + self, + destination, + source, + pid, + nr, + ns, + payload, + repeaters=None, + pf=False, + cr=False, + timestamp=None, + deadline=None, + ): + super(AX25InformationFrameMixin, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + timestamp=timestamp, + deadline=deadline, + ) + self._nr = int(nr) + self._ns = int(ns) + self._pid = int(pid) & 0xFF + self._payload = bytes(payload) + + @property + def pid(self): + return self._pid + + @property + def nr(self): + """ + Return the receive sequence number + """ + return self._nr + + @property + def pf(self): + """ + Return the state of the poll/final bit + """ + return self._pf + + @property + def ns(self): + """ + Return the send sequence number + """ + return self._ns + + @property + def payload(self): + return self._payload + + @property + def frame_payload(self): + return ( + super(AX25InformationFrameMixin, self).frame_payload + + bytearray([self.pid]) + + self.payload + ) + + @property + def _control(self): + """ + Return the value of the control byte. + """ + return ( + ((self.nr << self.CONTROL_NR_SHIFT) & self.CONTROL_NR_MASK) + | (self.POLL_FINAL if self.pf else 0) + | ((self.ns << self.CONTROL_NS_SHIFT) & self.CONTROL_NS_MASK) + | self.CONTROL_I_VAL + ) + + def __str__(self): + return "%s: N(R)=%d P/F=%s N(S)=%d PID=0x%02x Payload=%r" % ( + self.header, + self.nr, + self.pf, + self.ns, + self.pid, + self.payload, + ) + + def _copy(self): + return self.__class__( + destination=self.header.destination, + source=self.header.source, + repeaters=self.header.repeaters, + modifier=self.modifier, + cr=self.header.cr, + pf=self.pf, + pid=self.pid, + nr=self.nr, + ns=self.ns, + payload=self.payload, + ) + + +class AX258BitInformationFrame(AX25InformationFrameMixin, AX258BitFrame): + """ + A representation of an information frame using modulo-8 acknowledgements. + """ + + pass + + +class AX2516BitInformationFrame(AX25InformationFrameMixin, AX2516BitFrame): + """ + A representation of an information frame using modulo-128 acknowledgements. + """ + + pass + + +class AX25SupervisoryFrameMixin(object): + """ + Common code for AX.25 all supervisory frames + """ + + # Supervisory field bits + SUPER_MASK = 0b00001100 + + @classmethod + def decode(cls, header, control): + code = int(control & cls.SUPER_MASK) + return cls.SUBCLASSES[code]( + destination=header.destination, + source=header.source, + repeaters=header.repeaters, + cr=header.cr, + nr=int((control & cls.CONTROL_NR_MASK) >> cls.CONTROL_NR_SHIFT), + pf=bool(control & cls.POLL_FINAL), + ) + + def __init__( + self, + destination, + source, + nr, + code, + repeaters=None, + pf=False, + cr=False, + timestamp=None, + deadline=None, + ): + super(AX25SupervisoryFrameMixin, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + timestamp=timestamp, + deadline=deadline, + ) + self._nr = int(nr) + self._code = int(code) + + @property + def pid(self): + return self._pid + + @property + def nr(self): + """ + Return the receive sequence number + """ + return self._nr + + @property + def pf(self): + """ + Return the state of the poll/final bit + """ + return self._pf + + @property + def code(self): + """ + Return the supervisory control code + """ + return self._code + + @property + def _control(self): + """ + Return the value of the control byte. + """ + return ( + ((self.nr << self.CONTROL_NR_SHIFT) & self.CONTROL_NR_MASK) + | (self.POLL_FINAL if self.pf else 0) + | (self.code & self.SUPER_MASK) + | self.CONTROL_S_VAL + ) + + def __str__(self): + return "%s: N(R)=%d P/F=%s %s" % ( + self.header, + self.nr, + self.pf, + self.__name__, + ) + + def _copy(self): + return self.__class__( + destination=self.header.destination, + source=self.header.source, + repeaters=self.header.repeaters, + cr=self.header.cr, + pf=self.pf, + nr=self.nr, + ) + + +# The 4 types of supervisory frame + + +class AX25ReceiveReadyFrameMixin(AX25SupervisoryFrameMixin): + """ + Receive Ready supervisory frame. + + This frame is sent to indicate readyness to receive more frames. + If pf=True, this is a query being sent asking "are you ready?", otherwise + this is a response saying "I am ready". + """ + + SUPERVISOR_CODE = 0b00000000 + + +class AX25ReceiveNotReadyFrameMixin(AX25SupervisoryFrameMixin): + """ + Receive Not Ready supervisory frame. + + This is the opposite to a RR frame, and indicates we are not yet ready + to receive more traffic and that we may need to re-transmit frames when + we're ready to receive them. + """ + + SUPERVISOR_CODE = 0b00000100 + + +class AX25RejectFrameMixin(AX25SupervisoryFrameMixin): + """ + Reject frame. + + This indicates the indicated frame were not received and need to be re-sent. + All frames prior to the indicated frame are received, everything that + follows must be re-sent. + """ + + SUPERVISOR_CODE = 0b00001000 + + +class AX25SelectiveRejectFrameMixin(AX25SupervisoryFrameMixin): + """ + Selective Reject frame. + + This indicates a specific frame was not received and needs to be re-sent. + There is no requirement to send subsequent frames. + """ + + SUPERVISOR_CODE = 0b00001100 + + +# 8 and 16-bit variants of the above 4 types + + +class AX258BitReceiveReadyFrame(AX25ReceiveReadyFrameMixin, AX258BitFrame): + pass + + +class AX2516BitReceiveReadyFrame(AX25ReceiveReadyFrameMixin, AX2516BitFrame): + pass + + +class AX258BitReceiveNotReadyFrame( + AX25ReceiveNotReadyFrameMixin, AX258BitFrame +): + pass + + +class AX2516BitReceiveNotReadyFrame( + AX25ReceiveNotReadyFrameMixin, AX2516BitFrame +): + pass + + +class AX258BitRejectFrame(AX25RejectFrameMixin, AX258BitFrame): + pass + + +class AX2516BitRejectFrame(AX25RejectFrameMixin, AX2516BitFrame): + pass + + +class AX258BitSelectiveRejectFrame( + AX25SelectiveRejectFrameMixin, AX258BitFrame +): + pass + + +class AX2516BitSelectiveRejectFrame( + AX25SelectiveRejectFrameMixin, AX2516BitFrame +): + pass + + +# 8 and 16-bit variants of the base class + + +class AX258BitSupervisoryFrame(AX25SupervisoryFrameMixin, AX258BitFrame): + SUBCLASSES = dict( + [ + (c.SUPERVISOR_CODE, c) + for c in ( + AX258BitReceiveReadyFrame, + AX258BitReceiveNotReadyFrame, + AX258BitRejectFrame, + AX258BitSelectiveRejectFrame, + ) + ] + ) + + +class AX2516BitSupervisoryFrame(AX25SupervisoryFrameMixin, AX2516BitFrame): + SUBCLASSES = dict( + [ + (c.SUPERVISOR_CODE, c) + for c in ( + AX2516BitReceiveReadyFrame, + AX2516BitReceiveNotReadyFrame, + AX2516BitRejectFrame, + AX2516BitSelectiveRejectFrame, + ) + ] + ) + + +class AX2516BitSupervisoryFrame(AX25SupervisoryFrameMixin, AX2516BitFrame): + pass + + +# Un-numbered frame types + + class AX25UnnumberedInformationFrame(AX25UnnumberedFrame): """ A representation of an un-numbered information frame. @@ -395,6 +795,8 @@ def __init__( pf=False, cr=False, src_cr=None, + timestamp=None, + deadline=None, ): super(AX25UnnumberedInformationFrame, self).__init__( destination=destination, @@ -404,6 +806,8 @@ def __init__( src_cr=src_cr, pf=pf, modifier=self.MODIFIER, + timestamp=timestamp, + deadline=deadline, ) self._pid = int(pid) & 0xFF self._payload = bytes(payload) @@ -628,6 +1032,204 @@ def _copy(self): ) +class AX25BaseUnnumberedFrame(AX25UnnumberedFrame): + """ + Base unnumbered frame sub-class. This is used to provide a common + decode and _copy implementation for basic forms of UI frames without + information fields. + """ + + @classmethod + def decode(cls, header, control, data): + if len(data): + raise ValueError("Frame does not support payload") + + return cls( + destination=header.destination, + source=header.source, + repeaters=header.repeaters, + pf=bool(control & cls.POLL_FINAL), + cr=header.cr, + ) + + def _copy(self): + return self.__class__( + destination=self.header.destination, + source=self.header.source, + repeaters=self.header.repeaters, + cr=self.header.cr, + pf=self.pf, + ) + + +class AX25SetAsyncBalancedModeFrame(AX25BaseUnnumberedFrame): + """ + Set Async Balanced Mode (modulo 8). + + This frame is used to initiate a connection request with the destination + AX.25 node. + """ + + MODIFIER = 0b01101111 + + +class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): + """ + Set Async Balanced Mode Extended (modulo 128). + + This frame is used to initiate a connection request with the destination + AX.25 node, using modulo 128 acknowledgements. + """ + + MODIFIER = 0b00101111 + + +class AX25DisconnectFrame(AX25BaseUnnumberedFrame): + """ + Disconnect frame. + + This frame is used to initiate a disconnection from the other station. + """ + + MODIFIER = 0b01000011 + + +class AX25DisconnectModeFrame(AX25BaseUnnumberedFrame): + """ + Disconnect mode frame. + + This frame is used to indicate to the other station that it is disconnected. + """ + + MODIFIER = 0b00001111 + + +class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): + """ + Exchange Identification frame. + + This frame is used to negotiate TNC features. + """ + + MODIFIER = 0b10101111 + + @classmethod + def decode(cls, header, control, data): + return cls( + destination=header.destination, + source=header.source, + repeaters=header.repeaters, + payload=data, + pf=bool(control & cls.POLL_FINAL), + cr=header.cr, + ) + + def __init__( + self, + destination, + source, + payload, + repeaters=None, + pf=False, + cr=False, + timestamp=None, + deadline=None, + ): + super(AX25ExchangeIdentificationFrame, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + pf=pf, + modifier=self.MODIFIER, + timestamp=timestamp, + deadline=deadline, + ) + self._payload = bytes(payload) + + @property + def payload(self): + return self._payload + + def _copy(self): + return self.__class__( + destination=self.header.destination, + source=self.header.source, + payload=self.payload, + repeaters=self.header.repeaters, + cr=self.header.cr, + pf=self.pf, + ) + + +class AX25UnnumberedAcknowledgeFrame(AX25BaseUnnumberedFrame): + """ + Unnumbered Acknowledge frame. + + This frame is used to acknowledge a SABM/SABME frame. + """ + + MODIFIER = 0b10101111 + + +class AX25TestFrame(AX25UnnumberedFrame): + """ + Test frame. + + This frame is used to initiate an echo request. + """ + + MODIFIER = 0b11100011 + + @classmethod + def decode(cls, header, control, data): + return cls( + destination=header.destination, + source=header.source, + repeaters=header.repeaters, + payload=data, + pf=bool(control & cls.POLL_FINAL), + cr=header.cr, + ) + + def __init__( + self, + destination, + source, + payload, + repeaters=None, + pf=False, + cr=False, + timestamp=None, + deadline=None, + ): + super(AX25TestFrame, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + pf=pf, + modifier=self.MODIFIER, + timestamp=timestamp, + deadline=deadline, + ) + self._payload = bytes(payload) + + @property + def payload(self): + return self._payload + + def _copy(self): + return self.__class__( + destination=self.header.destination, + source=self.header.source, + payload=self.payload, + repeaters=self.header.repeaters, + cr=self.header.cr, + pf=self.pf, + ) + + # Helper classes From 7b79d3b77c467e8c4532fbf253948c244915c811 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 19:32:47 +1000 Subject: [PATCH 003/207] frame: Fix 8-bit supervisory decode Add tests whilst we're at it. --- aioax25/frame.py | 9 ++--- tests/test_frame/test_ax25frame.py | 58 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 1b92dea..0f9b7b9 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -439,6 +439,7 @@ def __init__( deadline=deadline, ) self._nr = int(nr) + self._pf = bool(pf) self._ns = int(ns) self._pid = int(pid) & 0xFF self._payload = bytes(payload) @@ -558,7 +559,6 @@ def __init__( destination, source, nr, - code, repeaters=None, pf=False, cr=False, @@ -574,11 +574,8 @@ def __init__( deadline=deadline, ) self._nr = int(nr) - self._code = int(code) - - @property - def pid(self): - return self._pid + self._code = self.SUPERVISOR_CODE + self._pf = bool(pf) @property def nr(self): diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 8962f14..04e0d16 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -6,6 +6,11 @@ AX25UnnumberedInformationFrame, AX25FrameRejectFrame, AX25UnnumberedFrame, + AX258BitReceiveReadyFrame, + AX2516BitReceiveReadyFrame, + AX258BitSupervisoryFrame, + AX25FrameHeader, + AX258BitRejectFrame, ) from ..hex import from_hex, hex_cmp @@ -605,3 +610,56 @@ def test_ui_tnc2(): payload=b"This is a test", ) assert frame.tnc2 == "VK4MSL>VK4BWI:This is a test" + + +def test_8bs_rr_frame(): + """ + Test we can generate a 8-bit RR supervisory frame + """ + frame = AX258BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=2 + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "41", # Control + ) + + +def test_16bs_rr_frame(): + """ + Test we can generate a 16-bit RR supervisory frame + """ + frame = AX2516BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=46 + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "01 5c", # Control + ) + + +def test_8bs_rej_decode_frame(): + """ + Test we can decode a 8-bit REJ supervisory frame + """ + frame = AX258BitSupervisoryFrame.decode( + header=AX25FrameHeader( + destination="VK4BWI", + source="VK4MSL", + ), + control=0x09, + ) + assert isinstance( + frame, AX258BitRejectFrame + ), "Did not decode to REJ frame" + + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "09", # Control byte + ) From 41778b504c68f098cf8ce99957b627c6b2877dab Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 19:42:30 +1000 Subject: [PATCH 004/207] frame: Add 16-bit supervisory decode test --- aioax25/frame.py | 4 ---- tests/test_frame/test_ax25frame.py | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 0f9b7b9..e0a52a9 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -753,10 +753,6 @@ class AX2516BitSupervisoryFrame(AX25SupervisoryFrameMixin, AX2516BitFrame): ) -class AX2516BitSupervisoryFrame(AX25SupervisoryFrameMixin, AX2516BitFrame): - pass - - # Un-numbered frame types diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 04e0d16..c193d1d 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -11,6 +11,8 @@ AX258BitSupervisoryFrame, AX25FrameHeader, AX258BitRejectFrame, + AX2516BitSupervisoryFrame, + AX2516BitRejectFrame, ) from ..hex import from_hex, hex_cmp @@ -663,3 +665,26 @@ def test_8bs_rej_decode_frame(): "ac 96 68 9a a6 98 e1" # Source "09", # Control byte ) + + +def test_16bs_rej_decode_frame(): + """ + Test we can decode a 16-bit REJ supervisory frame + """ + frame = AX2516BitSupervisoryFrame.decode( + header=AX25FrameHeader( + destination="VK4BWI", + source="VK4MSL", + ), + control=0x0009, + ) + assert isinstance( + frame, AX2516BitRejectFrame + ), "Did not decode to REJ frame" + + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "09 00", # Control bytes + ) From ec18d4163a72061071f4ccefe89cedc0bd7e1075 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 19:45:59 +1000 Subject: [PATCH 005/207] frame: Fix supervisory frame string representation --- aioax25/frame.py | 2 +- tests/test_frame/test_ax25frame.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index e0a52a9..9a746fc 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -615,7 +615,7 @@ def __str__(self): self.header, self.nr, self.pf, - self.__name__, + self.__class__.__name__, ) def _copy(self): diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index c193d1d..df4c57e 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -13,6 +13,7 @@ AX258BitRejectFrame, AX2516BitSupervisoryFrame, AX2516BitRejectFrame, + AX258BitReceiveReadyFrame, ) from ..hex import from_hex, hex_cmp @@ -688,3 +689,17 @@ def test_16bs_rej_decode_frame(): "ac 96 68 9a a6 98 e1" # Source "09 00", # Control bytes ) + + +def test_rr_frame_str(): + """ + Test we can get the string representation of a RR frame. + """ + frame = AX258BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=6 + ) + + eq_( + str(frame), + "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame", + ) From c5d90c011842732e96e7bbbda208fe32e0f8767e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 19:49:17 +1000 Subject: [PATCH 006/207] frame: Add supervisory frame copy test --- tests/test_frame/test_ax25frame.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index df4c57e..6f905cc 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -703,3 +703,21 @@ def test_rr_frame_str(): str(frame), "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame", ) + + +def test_rr_frame_copy(): + """ + Test we can get the string representation of a RR frame. + """ + frame = AX258BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=6 + ) + framecopy = frame.copy() + + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "c1", # Control byte + ) From e16e6867351f0306afbb8dce8fe68310c4a51c94 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:06:23 +1000 Subject: [PATCH 007/207] frame: Fix IFrame copy, add tests --- aioax25/frame.py | 4 +- tests/test_frame/test_ax25frame.py | 115 +++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 9a746fc..abeda1b 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -413,7 +413,8 @@ def decode(cls, header, control, data): nr=int((control & cls.CONTROL_NR_MASK) >> cls.CONTROL_NR_SHIFT), ns=int((control & cls.CONTROL_NS_MASK) >> cls.CONTROL_NS_SHIFT), pf=bool(control & cls.POLL_FINAL), - payload=data, + pid=data[0], + payload=data[1:], ) def __init__( @@ -508,7 +509,6 @@ def _copy(self): destination=self.header.destination, source=self.header.source, repeaters=self.header.repeaters, - modifier=self.modifier, cr=self.header.cr, pf=self.pf, pid=self.pid, diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 6f905cc..062d5d4 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -14,6 +14,8 @@ AX2516BitSupervisoryFrame, AX2516BitRejectFrame, AX258BitReceiveReadyFrame, + AX258BitInformationFrame, + AX2516BitInformationFrame, ) from ..hex import from_hex, hex_cmp @@ -64,6 +66,9 @@ def test_decode_sframe(): hex_cmp(frame.frame_payload, "01 11 22 33 44 55 66 77") +# Unnumbered frame tests + + def test_decode_uframe(): """ Test that a U-frame gets decoded to an unnumbered frame. @@ -615,6 +620,9 @@ def test_ui_tnc2(): assert frame.tnc2 == "VK4MSL>VK4BWI:This is a test" +# Supervisory frame tests + + def test_8bs_rr_frame(): """ Test we can generate a 8-bit RR supervisory frame @@ -666,6 +674,8 @@ def test_8bs_rej_decode_frame(): "ac 96 68 9a a6 98 e1" # Source "09", # Control byte ) + eq_(frame.nr, 0) + eq_(frame.pf, False) def test_16bs_rej_decode_frame(): @@ -689,6 +699,8 @@ def test_16bs_rej_decode_frame(): "ac 96 68 9a a6 98 e1" # Source "09 00", # Control bytes ) + eq_(frame.nr, 0) + eq_(frame.pf, False) def test_rr_frame_str(): @@ -721,3 +733,106 @@ def test_rr_frame_copy(): "ac 96 68 9a a6 98 e1" # Source "c1", # Control byte ) + + +# Information frames + + +def test_8bit_iframe_decode(): + """ + Test we can decode an 8-bit information frame. + """ + frame = AX258BitInformationFrame.decode( + header=AX25FrameHeader( + destination="VK4BWI", + source="VK4MSL", + ), + control=0xD4, + data=b"\xffThis is a test", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "d4" # Control + "ff" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + eq_(frame.nr, 6) + eq_(frame.ns, 2) + eq_(frame.pid, 0xFF) + eq_(frame.payload, b"This is a test") + + +def test_16bit_iframe_decode(): + """ + Test we can decode an 16-bit information frame. + """ + frame = AX2516BitInformationFrame.decode( + header=AX25FrameHeader( + destination="VK4BWI", + source="VK4MSL", + ), + control=0x0D04, + data=b"\xffThis is a test", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "04 0d" # Control + "ff" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + eq_(frame.nr, 6) + eq_(frame.ns, 2) + eq_(frame.pid, 0xFF) + eq_(frame.payload, b"This is a test") + + +def test_iframe_str(): + """ + Test we can get the string representation of an information frame. + """ + frame = AX258BitInformationFrame( + destination="VK4BWI", + source="VK4MSL", + nr=6, + ns=2, + pid=0xFF, + pf=True, + payload=b"Testing 1 2 3", + ) + + eq_( + str(frame), + "VK4MSL>VK4BWI: N(R)=6 P/F=True N(S)=2 PID=0xff " + "Payload=b'Testing 1 2 3'", + ) + + +def test_iframe_copy(): + """ + Test we can get the string representation of an information frame. + """ + frame = AX258BitInformationFrame( + destination="VK4BWI", + source="VK4MSL", + nr=6, + ns=2, + pid=0xFF, + pf=True, + payload=b"Testing 1 2 3", + ) + framecopy = frame.copy() + + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "d4" # Control byte + "ff" # PID + "54 65 73 74 69 6e 67 20" + "31 20 32 20 33", # Payload + ) From 469caf52be19f4694fc32e92827ea46b776a037b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:19:14 +1000 Subject: [PATCH 008/207] frame: Add AX25BaseUnnumberedFrame constructor and tests --- aioax25/frame.py | 20 +++++++++++++++++ tests/test_frame/test_ax25frame.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/aioax25/frame.py b/aioax25/frame.py index abeda1b..ce81f44 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1045,6 +1045,26 @@ def decode(cls, header, control, data): cr=header.cr, ) + def __init__( + self, + destination, + source, + repeaters=None, + pf=False, + cr=False, + timestamp=None, + deadline=None, + ): + super(AX25BaseUnnumberedFrame, self).__init__( + destination=destination, + source=source, + modifier=self.MODIFIER, + repeaters=repeaters, + cr=cr, + timestamp=timestamp, + deadline=deadline, + ) + def _copy(self): return self.__class__( destination=self.header.destination, diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 062d5d4..047de09 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -16,6 +16,7 @@ AX258BitReceiveReadyFrame, AX258BitInformationFrame, AX2516BitInformationFrame, + AX25DisconnectModeFrame, ) from ..hex import from_hex, hex_cmp @@ -493,6 +494,22 @@ def test_encode_frmr_frmr_ctrl(): ) +def test_encode_dm_frame(): + """ + Test we can encode a Disconnect Mode frame. + """ + frame = AX25DisconnectModeFrame( + destination="VK4BWI", + source="VK4MSL", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "0f", # Control + ) + + def test_raw_copy(): """ Test we can make a copy of a raw frame. @@ -530,6 +547,25 @@ def test_u_copy(): ) +def test_dm_copy(): + """ + Test we can make a copy of a Disconnect Mode frame. + """ + frame = AX25DisconnectModeFrame( + destination="VK4BWI", + source="VK4MSL", + ) + framecopy = frame.copy() + assert framecopy is not frame + + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "0f", # Control + ) + + def test_ui_copy(): """ Test we can make a copy of a unnumbered information frame. From 79e87b6616384c4f11565ceede1aa756c7f76b10 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:25:03 +1000 Subject: [PATCH 009/207] frame: Add test for decoding SABM --- tests/test_frame/test_ax25frame.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 047de09..6b48e83 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -17,6 +17,7 @@ AX258BitInformationFrame, AX2516BitInformationFrame, AX25DisconnectModeFrame, + AX25SetAsyncBalancedModeFrame, ) from ..hex import from_hex, hex_cmp @@ -90,6 +91,22 @@ def test_decode_uframe(): hex_cmp(frame.frame_payload, "c3") +def test_decode_sabm(): + """ + Test that a SABM frame is recognised and decoded. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "6f" # Control byte + ) + ) + assert isinstance( + frame, AX25SetAsyncBalancedModeFrame + ), "Did not decode to SABM frame" + + def test_decode_uframe_payload(): """ Test that U-frames other than FRMR and UI are forbidden to have payloads. From 656308f88aa96ad0a08b8a16d9655668f07e0e7a Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:27:02 +1000 Subject: [PATCH 010/207] frame: Add SABM with payload test --- tests/test_frame/test_ax25frame.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 6b48e83..40f86e2 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -107,6 +107,23 @@ def test_decode_sabm(): ), "Did not decode to SABM frame" +def test_decode_sabm_payload(): + """ + Test that a SABM frame forbids payload. + """ + try: + frame = AX25Frame.decode( + from_hex( + 'ac 96 68 84 ae 92 e0' # Destination + 'ac 96 68 9a a6 98 61' # Source + '6f' # Control byte + '11 22 33 44 55' # Payload + ) + ) + assert False, 'This should not have worked' + except ValueError as e: + eq_(str(e), 'Frame does not support payload') + def test_decode_uframe_payload(): """ Test that U-frames other than FRMR and UI are forbidden to have payloads. From e997021d62ae29c809b01bc2dccf307766051ce6 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:30:08 +1000 Subject: [PATCH 011/207] frame: Fix test frame encoding, add tests --- aioax25/frame.py | 4 ++++ tests/test_frame/test_ax25frame.py | 37 +++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index ce81f44..bd47696 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1232,6 +1232,10 @@ def __init__( def payload(self): return self._payload + @property + def frame_payload(self): + return super(AX25TestFrame, self).frame_payload + bytes(self.payload) + def _copy(self): return self.__class__( destination=self.header.destination, diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 40f86e2..dfd0b3e 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -18,6 +18,7 @@ AX2516BitInformationFrame, AX25DisconnectModeFrame, AX25SetAsyncBalancedModeFrame, + AX25TestFrame, ) from ..hex import from_hex, hex_cmp @@ -113,16 +114,17 @@ def test_decode_sabm_payload(): """ try: frame = AX25Frame.decode( - from_hex( - 'ac 96 68 84 ae 92 e0' # Destination - 'ac 96 68 9a a6 98 61' # Source - '6f' # Control byte - '11 22 33 44 55' # Payload - ) + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "6f" # Control byte + "11 22 33 44 55" # Payload + ) ) - assert False, 'This should not have worked' + assert False, "This should not have worked" except ValueError as e: - eq_(str(e), 'Frame does not support payload') + eq_(str(e), "Frame does not support payload") + def test_decode_uframe_payload(): """ @@ -306,6 +308,25 @@ def test_encode_ui(): ) +def test_encode_test(): + """ + Test that we can encode a TEST frame. + """ + frame = AX25TestFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + payload=b"This is a test", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e3" # Control + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + + def test_encode_pf(): """ Test we can set the PF bit on a frame. From 558a16e2a938cf43d09c7e3ae7650aa8b301ab51 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:41:05 +1000 Subject: [PATCH 012/207] frame: Fix pass thorough of timestamp/deadline in AX25RawFrame --- aioax25/frame.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index bd47696..98ac952 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -275,9 +275,17 @@ def __init__( cr=False, src_cr=None, payload=None, + timestamp=None, + deadline=None, ): - self._header = AX25FrameHeader( - destination, source, repeaters, cr, src_cr + super(AX25RawFrame, self).__init__( + destination=destination, + source=source, + repeaters=repeaters, + cr=cr, + src_cr=src_cr, + timestamp=timestamp, + deadline=deadline, ) self._payload = payload or b"" From d17ccb05e8dff09f9f8058f628f806f263d39429 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:42:49 +1000 Subject: [PATCH 013/207] frame: Exclude some lines from coverage --- aioax25/frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 98ac952..6763c50 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -133,11 +133,11 @@ def header(self): return self._header @property - def frame_payload(self): + def frame_payload(self): # pragma: no cover """ Return the bytes in the frame payload (including the control bytes) """ - return b"" + raise NotImplementedError("To be implemented in sub-class") @property def tnc2(self): From 286e2cfe0fe72873a88469aa7825ed1d8a1cd1ac Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 20:43:00 +1000 Subject: [PATCH 014/207] frame: Add test cases for base class. --- tests/test_frame/test_ax25frame.py | 93 ++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index dfd0b3e..34065c4 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -22,6 +22,8 @@ ) from ..hex import from_hex, hex_cmp +# Basic frame operations + def test_decode_incomplete(): """ @@ -69,6 +71,60 @@ def test_decode_sframe(): hex_cmp(frame.frame_payload, "01 11 22 33 44 55 66 77") +def test_frame_timestamp(): + """ + Test that the timestamp property is set from constructor. + """ + frame = AX25RawFrame( + destination="VK4BWI", source="VK4MSL", timestamp=11223344 + ) + eq_(frame.timestamp, 11223344) + + +def test_frame_deadline(): + """ + Test that the deadline property is set from constructor. + """ + frame = AX25RawFrame( + destination="VK4BWI", source="VK4MSL", deadline=11223344 + ) + eq_(frame.deadline, 11223344) + + +def test_frame_deadline_ro_if_set_constructor(): + """ + Test that the deadline property is read-only once set by contructor + """ + frame = AX25RawFrame( + destination="VK4BWI", source="VK4MSL", deadline=11223344 + ) + try: + frame.deadline = 99887766 + except ValueError as e: + eq_(str(e), "Deadline may not be changed after being set") + + eq_(frame.deadline, 11223344) + + +def test_frame_deadline_ro_if_set(): + """ + Test that the deadline property is read-only once set after constructor + """ + frame = AX25RawFrame( + destination="VK4BWI", + source="VK4MSL", + ) + + frame.deadline = 44556677 + + try: + frame.deadline = 99887766 + except ValueError as e: + eq_(str(e), "Deadline may not be changed after being set") + + eq_(frame.deadline, 44556677) + + # Unnumbered frame tests @@ -327,6 +383,43 @@ def test_encode_test(): ) +def test_decode_test(): + """ + Test that we can decode a TEST frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e3" # Control + "31 32 33 34 35 36 37 38 39 2e 2e 2e" # Payload + ) + ) + assert isinstance(frame, AX25TestFrame) + eq_(frame.payload, b"123456789...") + + +def test_copy_test(): + """ + Test that we can copy a TEST frame. + """ + frame = AX25TestFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + payload=b"This is a test", + ) + framecopy = frame.copy() + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e3" # Control + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + + def test_encode_pf(): """ Test we can set the PF bit on a frame. From 59bac2eb74a72de4a33f12d87f7446be171c56c1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 11 Nov 2019 21:21:38 +1000 Subject: [PATCH 015/207] frame: Implement XID encode/decode and tests --- aioax25/frame.py | 123 ++++++++++++++- tests/test_frame/test_ax25frame.py | 243 ++++++++++++++++++++++++++--- 2 files changed, 334 insertions(+), 32 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 6763c50..78d10fe 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1134,13 +1134,100 @@ class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): MODIFIER = 0b10101111 + class AX25XIDParameter(object): + """ + Representation of a single XID parameter. + """ + + @classmethod + def decode(cls, data): + """ + Decode the parameter value given, return the parameter and the + remaining data. + """ + if len(data) < 2: + raise ValueError("Insufficient data for parameter") + + pi = data[0] + pl = data[1] + data = data[2:] + + if pl > 0: + if len(data) < pl: + raise ValueError("Parameter is truncated") + pv = data[0:pl] + data = data[pl:] + else: + pv = None + + return (cls(pi=pi, pv=pv), data) + + def __init__(self, pi, pv): + """ + Create a new XID parameter + """ + self._pi = pi + self._pv = pv + + @property + def pi(self): + """ + Return the Parameter Identifier + """ + return self._pi + + @property + def pv(self): + """ + Return the Parameter Value + """ + return self._pv + + def __bytes__(self): + """ + Return the encoded parameter value. + """ + pv = self.pv + param = bytes([self.pi]) + if pv is None: + param += bytes([0]) + else: + param += bytes([len(pv)]) + pv + + return param + + def copy(self): + """ + Return a copy of this parameter. + """ + return self.__class__(pi=self.pi, pv=self.pv) + @classmethod def decode(cls, header, control, data): + if len(data) < 4: + raise ValueError("Truncated XID header") + + fi = data[0] + gi = data[1] + # Yep, GL is big-endian, just for a change! + gl = (data[2] << 8) | data[3] + data = data[4:] + + if len(data) != gl: + raise ValueError("Truncated XID data") + + parameters = [] + while data: + (param, data) = cls.AX25XIDParameter.decode(data) + parameters.append(param) + return cls( destination=header.destination, source=header.source, repeaters=header.repeaters, - payload=data, + fi=fi, + gi=gi, + parameters=parameters, pf=bool(control & cls.POLL_FINAL), cr=header.cr, ) @@ -1149,7 +1236,9 @@ def __init__( self, destination, source, - payload, + fi, + gi, + parameters, repeaters=None, pf=False, cr=False, @@ -1166,17 +1255,39 @@ def __init__( timestamp=timestamp, deadline=deadline, ) - self._payload = bytes(payload) + self._fi = int(fi) + self._gi = int(gi) + self._parameters = list(parameters) @property - def payload(self): - return self._payload + def fi(self): + return self._fi + + @property + def gi(self): + return self._gi + + @property + def parameters(self): + return self._parameters + + @property + def frame_payload(self): + parameters = b"".join([bytes(param) for param in self.parameters]) + gl = len(parameters) + return ( + super(AX25ExchangeIdentificationFrame, self).frame_payload + + bytes([self.fi, self.gi, (gl >> 8) & 0xFF, gl & 0xFF]) + + parameters + ) def _copy(self): return self.__class__( destination=self.header.destination, source=self.header.source, - payload=self.payload, + fi=self.fi, + gi=self.gi, + parameters=[p.copy() for p in self.parameters], repeaters=self.header.repeaters, cr=self.header.cr, pf=self.pf, diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 34065c4..05d306d 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -19,6 +19,7 @@ AX25DisconnectModeFrame, AX25SetAsyncBalancedModeFrame, AX25TestFrame, + AX25ExchangeIdentificationFrame, ) from ..hex import from_hex, hex_cmp @@ -78,7 +79,7 @@ def test_frame_timestamp(): frame = AX25RawFrame( destination="VK4BWI", source="VK4MSL", timestamp=11223344 ) - eq_(frame.timestamp, 11223344) + assert frame.timestamp == 11223344 def test_frame_deadline(): @@ -88,7 +89,7 @@ def test_frame_deadline(): frame = AX25RawFrame( destination="VK4BWI", source="VK4MSL", deadline=11223344 ) - eq_(frame.deadline, 11223344) + assert frame.deadline == 11223344 def test_frame_deadline_ro_if_set_constructor(): @@ -101,9 +102,9 @@ def test_frame_deadline_ro_if_set_constructor(): try: frame.deadline = 99887766 except ValueError as e: - eq_(str(e), "Deadline may not be changed after being set") + assert str(e) == "Deadline may not be changed after being set" - eq_(frame.deadline, 11223344) + assert frame.deadline == 11223344 def test_frame_deadline_ro_if_set(): @@ -120,9 +121,9 @@ def test_frame_deadline_ro_if_set(): try: frame.deadline = 99887766 except ValueError as e: - eq_(str(e), "Deadline may not be changed after being set") + assert str(e) == "Deadline may not be changed after being set" - eq_(frame.deadline, 44556677) + assert frame.deadline == 44556677 # Unnumbered frame tests @@ -179,7 +180,7 @@ def test_decode_sabm_payload(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Frame does not support payload") + assert str(e) == "Frame does not support payload" def test_decode_uframe_payload(): @@ -396,7 +397,7 @@ def test_decode_test(): ) ) assert isinstance(frame, AX25TestFrame) - eq_(frame.payload, b"123456789...") + assert frame.payload == b"123456789..." def test_copy_test(): @@ -420,6 +421,198 @@ def test_copy_test(): ) +def test_encode_xid(): + """ + Test that we can encode a XID frame. + """ + frame = AX25ExchangeIdentificationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + fi=0x82, + gi=0x80, + parameters=[ + AX25ExchangeIdentificationFrame.AX25XIDParameter( + pi=0x12, pv=bytes([0x34, 0x56]) + ), + AX25ExchangeIdentificationFrame.AX25XIDParameter( + pi=0x34, pv=None + ), + ], + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # Format indicator + "80" # Group Ident + "00 06" # Group length + # First parameter + "12" # Parameter ID + "02" # Length + "34 56" # Value + # Second parameter + "34" # Parameter ID + "00", # Length (no value) + ) + + +def test_decode_xid(): + """ + Test that we can decode a XID frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 0c" # GL + # Some parameters + "01 01 aa" + "02 01 bb" + "03 02 11 22" + "04 00" + ) + ) + assert isinstance(frame, AX25ExchangeIdentificationFrame) + assert frame.fi == 0x82 + assert frame.gi == 0x80 + assert len(frame.parameters) == 4 + assert frame.parameters[0].pi == 0x01 + assert frame.parameters[0].pv == b"\xaa" + assert frame.parameters[1].pi == 0x02 + assert frame.parameters[1].pv == b"\xbb" + assert frame.parameters[2].pi == 0x03 + assert frame.parameters[2].pv == b"\x11\x22" + assert frame.parameters[3].pi == 0x04 + assert frame.parameters[3].pv is None + + +def test_decode_xid_truncated_header(): + """ + Test that decoding a XID with truncated header fails. + """ + try: + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00" # Incomplete GL + ) + ) + assert False, "This should not have worked" + except ValueError as e: + assert str(e) == "Truncated XID header" + + +def test_decode_xid_truncated_payload(): + """ + Test that decoding a XID with truncated payload fails. + """ + try: + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 05" # GL + "11" # Incomplete payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + assert str(e) == "Truncated XID data" + + +def test_decode_xid_truncated_param_header(): + """ + Test that decoding a XID with truncated parameter header fails. + """ + try: + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 01" # GL + "11" # Incomplete payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + assert str(e) == "Insufficient data for parameter" + + +def test_decode_xid_truncated_param_value(): + """ + Test that decoding a XID with truncated parameter value fails. + """ + try: + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 04" # GL + "11 06 22 33" # Incomplete payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + assert str(e) == "Parameter is truncated" + + +def test_copy_xid(): + """ + Test that we can copy a XID frame. + """ + frame = AX25ExchangeIdentificationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + fi=0x82, + gi=0x80, + parameters=[ + AX25ExchangeIdentificationFrame.AX25XIDParameter( + pi=0x12, pv=bytes([0x34, 0x56]) + ), + AX25ExchangeIdentificationFrame.AX25XIDParameter( + pi=0x34, pv=None + ), + ], + ) + framecopy = frame.copy() + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # Format indicator + "80" # Group Ident + "00 06" # Group length + # First parameter + "12" # Parameter ID + "02" # Length + "34 56" # Value + # Second parameter + "34" # Parameter ID + "00", # Length (no value) + ) + + def test_encode_pf(): """ Test we can set the PF bit on a frame. @@ -858,8 +1051,8 @@ def test_8bs_rej_decode_frame(): "ac 96 68 9a a6 98 e1" # Source "09", # Control byte ) - eq_(frame.nr, 0) - eq_(frame.pf, False) + assert frame.nr == 0 + assert frame.pf == False def test_16bs_rej_decode_frame(): @@ -883,8 +1076,8 @@ def test_16bs_rej_decode_frame(): "ac 96 68 9a a6 98 e1" # Source "09 00", # Control bytes ) - eq_(frame.nr, 0) - eq_(frame.pf, False) + assert frame.nr == 0 + assert frame.pf == False def test_rr_frame_str(): @@ -895,9 +1088,8 @@ def test_rr_frame_str(): destination="VK4BWI", source="VK4MSL", nr=6 ) - eq_( - str(frame), - "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame", + assert str(frame) == ( + "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame" ) @@ -942,10 +1134,10 @@ def test_8bit_iframe_decode(): "ff" # PID "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload ) - eq_(frame.nr, 6) - eq_(frame.ns, 2) - eq_(frame.pid, 0xFF) - eq_(frame.payload, b"This is a test") + assert frame.nr == 6 + assert frame.ns == 2 + assert frame.pid == 0xFF + assert frame.payload == b"This is a test" def test_16bit_iframe_decode(): @@ -968,10 +1160,10 @@ def test_16bit_iframe_decode(): "ff" # PID "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload ) - eq_(frame.nr, 6) - eq_(frame.ns, 2) - eq_(frame.pid, 0xFF) - eq_(frame.payload, b"This is a test") + assert frame.nr == 6 + assert frame.ns == 2 + assert frame.pid == 0xFF + assert frame.payload == b"This is a test" def test_iframe_str(): @@ -988,10 +1180,9 @@ def test_iframe_str(): payload=b"Testing 1 2 3", ) - eq_( - str(frame), + assert str(frame) == ( "VK4MSL>VK4BWI: N(R)=6 P/F=True N(S)=2 PID=0xff " - "Payload=b'Testing 1 2 3'", + "Payload=b'Testing 1 2 3'" ) From 594ac772436dfae3c0c2537be1ecb48c01726eab Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 16 Nov 2019 14:10:43 +1000 Subject: [PATCH 016/207] frame: Fix UA modifier --- aioax25/frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 78d10fe..fe7c0d4 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1301,7 +1301,7 @@ class AX25UnnumberedAcknowledgeFrame(AX25BaseUnnumberedFrame): This frame is used to acknowledge a SABM/SABME frame. """ - MODIFIER = 0b10101111 + MODIFIER = 0b01100011 class AX25TestFrame(AX25UnnumberedFrame): From 534dfdcb9b892ab1c34eb694dbfca2be79e9d30e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 16 Nov 2019 14:12:06 +1000 Subject: [PATCH 017/207] frame: Re-work frame decoding. Put the majority of the frame logic in the top-level `AX25Frame.decode` method for convenience. We can rely on the fact that the least-significant two bits of a control field are all we need to identify I, S and U frames. Further more, the least significant byte is sent first, so there's sufficient detail at this point to identify the U frames up-front, and with some help, I and S frames too. --- aioax25/frame.py | 120 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index fe7c0d4..4b64c39 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -16,6 +16,7 @@ class AX25Frame(object): Base class for AX.25 frames. """ + # The following are the same for 8 and 16-bit control fields. CONTROL_I_MASK = 0b00000001 CONTROL_I_VAL = 0b00000000 CONTROL_US_MASK = 0b00000011 @@ -39,11 +40,18 @@ class AX25Frame(object): PID_ESCAPE = 0xFF @classmethod - def decode(cls, data): + def decode(cls, data, modulo128=None): """ Decode a single AX.25 frame from the given data. """ - (header, data) = AX25FrameHeader.decode(data) + if isinstance(data, AX25Frame): + # We were given a previously decoded frame. + header = data.header + data = data.frame_payload + else: + # We were given raw data. + (header, data) = AX25FrameHeader.decode(bytes(data)) + if not data: raise ValueError("Insufficient packet data") @@ -59,17 +67,49 @@ def decode(cls, data): return AX25UnnumberedFrame.decode(header, control, data[1:]) else: # This is either a I or S frame, both of which can either - # have a 8-bit or 16-bit control field. We don't know at - # this point so the only safe answer is to return a raw frame - # and decode it later. - return AX25RawFrame( - destination=header.destination, - source=header.source, - repeaters=header.repeaters, - cr=header.cr, - src_cr=header.src_cr, - payload=data, - ) + # have a 8-bit or 16-bit control field. + if modulo128 is True: + # And the caller has told us it's a 16-bit field, so let's + # decode the rest of it! + if len(data) < 2: + raise ValueError("Insufficient packet data") + control |= data[1] << 8 + + # Discard the control field from the data payload as we + # have decoded it now. + data = data[2:] + + # We'll use these classes + InformationFrame = AX2516BitInformationFrame + SupervisoryFrame = AX2516BitSupervisoryFrame + elif modulo128 is False: + # Caller has told us it's an 8-bit field, so already decoded. + data = data[1:] + + # We'll use these classes + InformationFrame = AX258BitInformationFrame + SupervisoryFrame = AX258BitSupervisoryFrame + else: + # We don't know at this point so the only safe answer is to + # return a raw frame and decode it later. + return AX25RawFrame( + destination=header.destination, + source=header.source, + repeaters=header.repeaters, + cr=header.cr, + src_cr=header.src_cr, + payload=data, + ) + + # We've got the full control field and payload now. + if (control & cls.CONTROL_I_MASK) == cls.CONTROL_I_VAL: + # This is an I frame. + return InformationFrame.decode(header, control, data) + elif (control & cls.CONTROL_US_MASK) == cls.CONTROL_S_VAL: + # This is a S frame. + return SupervisoryFrame.decode(header, control, data) + + assert False, "Unrecognised control field value: 0x%04x" % control def __init__( self, @@ -311,23 +351,25 @@ class AX25UnnumberedFrame(AX258BitFrame): MODIFIER_MASK = 0b11101111 + SUBCLASSES = {} + + @classmethod + def register(cls, subclass): + """ + Register a sub-class of UnnumberedFrame with the decoder. + """ + assert ( + subclass.MODIFIER not in cls.SUBCLASSES + ), "Duplicate registration" + cls.SUBCLASSES[subclass.MODIFIER] = subclass + @classmethod def decode(cls, header, control, data): # Decode based on the control field modifier = control & cls.MODIFIER_MASK - for subclass in ( - AX25UnnumberedInformationFrame, - AX25FrameRejectFrame, - AX25SetAsyncBalancedModeFrame, - AX25SetAsyncBalancedModeExtendedFrame, - AX25DisconnectFrame, - AX25DisconnectModeFrame, - AX25ExchangeIdentificationFrame, - AX25UnnumberedAcknowledgeFrame, - AX25TestFrame, - ): - if modifier == subclass.MODIFIER: - return subclass.decode(header, control, data) + subclass = cls.SUBCLASSES.get(modifier, None) + if subclass is not None: + return subclass.decode(header, control, data) # If we're still here, clearly this is a plain U frame. if data: @@ -865,6 +907,9 @@ def get_tnc2(self, charset="latin1", errors="strict"): ) +AX25UnnumberedFrame.register(AX25UnnumberedInformationFrame) + + class AX25FrameRejectFrame(AX25UnnumberedFrame): """ A representation of a Frame Reject (FRMR) frame. @@ -1033,6 +1078,9 @@ def _copy(self): ) +AX25UnnumberedFrame.register(AX25FrameRejectFrame) + + class AX25BaseUnnumberedFrame(AX25UnnumberedFrame): """ Base unnumbered frame sub-class. This is used to provide a common @@ -1094,6 +1142,9 @@ class AX25SetAsyncBalancedModeFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b01101111 +AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeFrame) + + class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): """ Set Async Balanced Mode Extended (modulo 128). @@ -1105,6 +1156,9 @@ class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b00101111 +AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeExtendedFrame) + + class AX25DisconnectFrame(AX25BaseUnnumberedFrame): """ Disconnect frame. @@ -1115,6 +1169,9 @@ class AX25DisconnectFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b01000011 +AX25UnnumberedFrame.register(AX25DisconnectFrame) + + class AX25DisconnectModeFrame(AX25BaseUnnumberedFrame): """ Disconnect mode frame. @@ -1125,6 +1182,9 @@ class AX25DisconnectModeFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b00001111 +AX25UnnumberedFrame.register(AX25DisconnectModeFrame) + + class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): """ Exchange Identification frame. @@ -1294,6 +1354,9 @@ def _copy(self): ) +AX25UnnumberedFrame.register(AX25ExchangeIdentificationFrame) + + class AX25UnnumberedAcknowledgeFrame(AX25BaseUnnumberedFrame): """ Unnumbered Acknowledge frame. @@ -1304,6 +1367,9 @@ class AX25UnnumberedAcknowledgeFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b01100011 +AX25UnnumberedFrame.register(AX25UnnumberedAcknowledgeFrame) + + class AX25TestFrame(AX25UnnumberedFrame): """ Test frame. @@ -1366,6 +1432,8 @@ def _copy(self): ) +AX25UnnumberedFrame.register(AX25TestFrame) + # Helper classes From 7c6f75ea9ad32dd28123d31652a9e76de076cd47 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 16 Nov 2019 14:15:01 +1000 Subject: [PATCH 018/207] frame: Document the module a little bit. --- aioax25/frame.py | 61 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 4b64c39..d107d53 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1,7 +1,44 @@ #!/usr/bin/env python3 """ -AX.25 framing +AX.25 framing. This module defines encoders and decoders for all frame types +used in version 2.2 of the AX.25 standard. + +AX25Frame is the base class, and provides an abstract interface for interacting +with all frame types. AX.25 identifies the specific type of frame from bits in +the control field which can be 8 or 16-bits wide. + +The only way to know if it's 16-bits is in witnessing the initial connection +being made between two stations which means it's impossible for a stateless +decoder to fully decode an arbitrary AX.25 frame. + +Thankfully the control field is sent little-endian format, so the first byte we +encounter is the least significant bits -- which is sufficient to identify whether +the frame is an I, S or U frame: the least significant two bits carry this +information. + +For this reason there are 3 sub-classes of this top-level class: + + - AX25RawFrame: This is used when we only need to decode the initial AX.25 + addressing header to know whether we need to worry about the frame + further. + + - AX258BitFrame: This is used for AX.25 frames with an 8-bit control field, + which is anything sent by an AX.25 v2.0 station, any un-numbered frame, + or any I or S frame where modulo-8 frame numbering is used. + + - AX2516BitFrame: This is used for AX.25 v2.2 stations where modulo-128 + frame numbering has been negotiated by both parties. + +Decoding is done by calling the AX25Frame.decode class method. This takes two +parameters: + + - data: either raw bytes or an AX25Frame class. The latter form is useful when + you've previously decoded a frame as a AX25RawFrame and need to further + dissect it as either a AX258BitFrame or AX2516BitFrame sub-class. + + - modulo128: by default is None, but if set to a boolean, will decode I or S + frames accordingly instead of just returning AX25RawFrame. """ import re @@ -304,7 +341,14 @@ def frame_payload(self): class AX25RawFrame(AX25Frame): """ - A representation of a raw AX.25 frame. + A representation of a raw AX.25 frame. This class is intended to capture + partially decoded frame data in the case where we don't know whether a control + field is 8 or 16-bits wide. + + It may be fed to the AX25Frame.decode function again with modulo128=False for + known 8-bit frames, or modulo128=True for known 16-bit frames. For digipeating + applications, often no further dissection is necessary and so the frame can be + used as-is. """ def __init__( @@ -346,7 +390,11 @@ def _copy(self): class AX25UnnumberedFrame(AX258BitFrame): """ - A representation of an un-numbered frame. + A representation of an un-numbered frame. U frames are used for all + sorts of in-band signalling as well as for connectionless data transfer + via UI frames (see AX25UnnumberedInformationFrame). + + All U frames have an 8-bit control field. """ MODIFIER_MASK = 0b11101111 @@ -365,6 +413,13 @@ def register(cls, subclass): @classmethod def decode(cls, header, control, data): + """ + Decode an unnumbered frame which has been partially decoded by + AX25Frame.decode. This inspects the value of the modifier bits + in the control field (see AX.25 2.2 spec 4.3.3) and passes the + arguments given to the appropriate sub-class. + """ + # Decode based on the control field modifier = control & cls.MODIFIER_MASK subclass = cls.SUBCLASSES.get(modifier, None) From eac390ea9a95842c85bfcb13c9b7fdbf1cabf86c Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 16 Nov 2019 14:50:27 +1000 Subject: [PATCH 019/207] frame: Tweak coverage, add further checks. --- aioax25/frame.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index d107d53..7ba5052 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -143,10 +143,14 @@ def decode(cls, data, modulo128=None): # This is an I frame. return InformationFrame.decode(header, control, data) elif (control & cls.CONTROL_US_MASK) == cls.CONTROL_S_VAL: - # This is a S frame. - return SupervisoryFrame.decode(header, control, data) - - assert False, "Unrecognised control field value: 0x%04x" % control + # This is a S frame. No payload expected + if len(data): + raise ValueError( + "Supervisory frames do not " "support payloads." + ) + return SupervisoryFrame.decode(header, control) + else: # pragma: no cover + assert False, "Unrecognised control field: 0x%04x" % control def __init__( self, From 151bc84dd254abd463b05b8c3b8698f3a4675544 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 16 Nov 2019 14:50:45 +1000 Subject: [PATCH 020/207] frame tests: Re-work test cases. --- tests/test_frame/test_ax25frame.py | 180 +++++++++++++++++++---------- 1 file changed, 118 insertions(+), 62 deletions(-) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 05d306d..1312a2e 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -72,6 +72,22 @@ def test_decode_sframe(): hex_cmp(frame.frame_payload, "01 11 22 33 44 55 66 77") +def test_decode_rawframe(): + """ + Test that we can decode an AX25RawFrame. + """ + rawframe = AX25RawFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + payload=b"\x03\xf0This is a test", + ) + frame = AX25Frame.decode(rawframe) + assert isinstance(frame, AX25UnnumberedInformationFrame) + assert frame.pid == 0xF0 + assert frame.payload == b"This is a test" + + def test_frame_timestamp(): """ Test that the timestamp property is set from constructor. @@ -633,6 +649,7 @@ def test_encode_pf(): "f0" # PID "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload ) + assert frame.control == 0x13 def test_encode_frmr_w(): @@ -1000,57 +1017,106 @@ def test_ui_tnc2(): # Supervisory frame tests +def test_sframe_payload_reject(): + """ + Test payloads are forbidden for S-frames + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "41" # Control + "31 32 33 34 35" # Payload + ), + modulo128=False, + ) + assert False, "Should not have worked" + except ValueError as e: + assert str(e) == "Supervisory frames do not support payloads." + + +def test_16bs_truncated_reject(): + """ + Test that 16-bit S-frames with truncated control fields are rejected. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "01" # Control (LSB only) + ), + modulo128=True, + ) + assert False, "Should not have worked" + except ValueError as e: + assert str(e) == "Insufficient packet data" + + def test_8bs_rr_frame(): """ Test we can generate a 8-bit RR supervisory frame """ - frame = AX258BitReceiveReadyFrame( - destination="VK4BWI", source="VK4MSL", nr=2 - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "41", # Control + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "41" # Control + ), + modulo128=False, ) + assert isinstance(frame, AX258BitReceiveReadyFrame) + assert frame.nr == 2 def test_16bs_rr_frame(): """ Test we can generate a 16-bit RR supervisory frame """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "01 5c" # Control + ), + modulo128=True, + ) + assert isinstance(frame, AX2516BitReceiveReadyFrame) + assert frame.nr == 46 + + +def test_16bs_rr_encode(): + """ + Test we can encode a 16-bit RR supervisory frame + """ frame = AX2516BitReceiveReadyFrame( - destination="VK4BWI", source="VK4MSL", nr=46 + destination="VK4BWI", source="VK4MSL", nr=46, pf=True ) hex_cmp( bytes(frame), "ac 96 68 84 ae 92 60" # Destination "ac 96 68 9a a6 98 e1" # Source - "01 5c", # Control + "01 5d", # Control ) + assert frame.control == 0x5D01 def test_8bs_rej_decode_frame(): """ Test we can decode a 8-bit REJ supervisory frame """ - frame = AX258BitSupervisoryFrame.decode( - header=AX25FrameHeader( - destination="VK4BWI", - source="VK4MSL", + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "09" # Control byte ), - control=0x09, + modulo128=False, ) assert isinstance( frame, AX258BitRejectFrame ), "Did not decode to REJ frame" - - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "09", # Control byte - ) assert frame.nr == 0 assert frame.pf == False @@ -1059,23 +1125,17 @@ def test_16bs_rej_decode_frame(): """ Test we can decode a 16-bit REJ supervisory frame """ - frame = AX2516BitSupervisoryFrame.decode( - header=AX25FrameHeader( - destination="VK4BWI", - source="VK4MSL", + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "09 00" # Control bytes ), - control=0x0009, + modulo128=True, ) assert isinstance( frame, AX2516BitRejectFrame ), "Did not decode to REJ frame" - - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "09 00", # Control bytes - ) assert frame.nr == 0 assert frame.pf == False @@ -1118,22 +1178,20 @@ def test_8bit_iframe_decode(): """ Test we can decode an 8-bit information frame. """ - frame = AX258BitInformationFrame.decode( - header=AX25FrameHeader( - destination="VK4BWI", - source="VK4MSL", + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "d4" # Control + "ff" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74" # Payload ), - control=0xD4, - data=b"\xffThis is a test", - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "d4" # Control - "ff" # PID - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + modulo128=False, ) + + assert isinstance( + frame, AX258BitInformationFrame + ), "Did not decode to 8-bit I-Frame" assert frame.nr == 6 assert frame.ns == 2 assert frame.pid == 0xFF @@ -1144,22 +1202,20 @@ def test_16bit_iframe_decode(): """ Test we can decode an 16-bit information frame. """ - frame = AX2516BitInformationFrame.decode( - header=AX25FrameHeader( - destination="VK4BWI", - source="VK4MSL", + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "04 0d" # Control + "ff" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74" # Payload ), - control=0x0D04, - data=b"\xffThis is a test", - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "04 0d" # Control - "ff" # PID - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + modulo128=True, ) + + assert isinstance( + frame, AX2516BitInformationFrame + ), "Did not decode to 16-bit I-Frame" assert frame.nr == 6 assert frame.ns == 2 assert frame.pid == 0xFF From 244eda689409d725c11e2d10b8300d81fe77ac2f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 13:14:57 +1000 Subject: [PATCH 021/207] frame: Make sure we handle AX.25 1.x frames properly We're unlikely to see these, I think most people will have "ugraded" to 2.x by now, and if not, they should be encouraged to do so, but at the very least we can at least ensure we digipeat such frames properly. --- aioax25/frame.py | 22 +++++++++++++--- tests/test_frame/test_ax25frameheader.py | 33 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 7ba5052..a61344f 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1523,16 +1523,24 @@ def decode(cls, data): source=addresses[1], repeaters=addresses[2:], cr=addresses[0].ch, - src_cr=addresses[1].ch, + # Legacy AX.25 1.x stations set the C bits identically + legacy=addresses[0].ch is addresses[1].ch, ), data, ) def __init__( - self, destination, source, repeaters=None, cr=False, src_cr=None + self, + destination, + source, + repeaters=None, + cr=False, + src_cr=None, + legacy=False, ): self._cr = bool(cr) self._src_cr = src_cr + self._legacy = bool(legacy) self._destination = AX25Address.decode(destination) self._source = AX25Address.decode(source) self._repeaters = AX25Path(*(repeaters or [])) @@ -1608,8 +1616,12 @@ def src_cr(self): Command/Response bit in the source address. """ if self._src_cr is None: - return not self.cr + if self.legacy: + return self.cr # AX.25 1.x: the C/R bits are identical + else: + return not self.cr # AX.25 2.x: the C/R bits are opposite else: + # We were given an explicit value. return self._src_cr @property @@ -1628,6 +1640,10 @@ def tnc2(self): (",%s" % self._repeaters) if self._repeaters else "", ) + @property + def legacy(self): + return self._legacy + class AX25Path(Sequence): """ diff --git a/tests/test_frame/test_ax25frameheader.py b/tests/test_frame/test_ax25frameheader.py index b99e3e0..85ba848 100644 --- a/tests/test_frame/test_ax25frameheader.py +++ b/tests/test_frame/test_ax25frameheader.py @@ -34,6 +34,39 @@ def test_decode_no_digis(): assert data == b"frame data goes here" +def test_decode_legacy(): + """ + Test we can decode an AX.25 1.x frame. + """ + (header, data) = AX25FrameHeader.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 e1" # Source + ) + + b"frame data goes here" # Frame data + ) + assert header.legacy + assert header.cr + eq_(header.destination, AX25Address("VK4BWI", ch=True)) + eq_(header.source, AX25Address("VK4MSL", extension=True, ch=True)) + eq_(len(header.repeaters), 0) + eq_(data, b"frame data goes here") + + +def test_encode_legacy(): + """ + Test we can encode an AX.25 1.x frame. + """ + header = AX25FrameHeader( + destination="VK4BWI", source="VK4MSL", cr=False, legacy=True + ) + hex_cmp( + bytes(header), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 61", # Source + ) + + def test_decode_with_1digi(): """ Test we can decode an AX.25 frame with one digipeater. From 78ee141c8159d2d6e6d8c3c404d5d2ab7d06e27f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 13:32:35 +1000 Subject: [PATCH 022/207] frame: Add AX25Address.decode SSID override --- aioax25/frame.py | 10 ++++++---- tests/test_frame/test_ax25address.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index a61344f..4d3d96f 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1724,7 +1724,7 @@ class AX25Address(object): CALL_RE = re.compile(r"^([0-9A-Z]+)(?:-([0-9]{1,2}))?(\*?)$") @classmethod - def decode(cls, data): + def decode(cls, data, ssid=None): """ Decode an AX.25 address from a frame. """ @@ -1748,10 +1748,12 @@ def decode(cls, data): match = cls.CALL_RE.match(data.upper()) if not match: raise ValueError("Not a valid SSID: %s" % data) + + if ssid is None: + ssid = int(match.group(2) or 0) + return cls( - callsign=match.group(1), - ssid=int(match.group(2) or 0), - ch=match.group(3) == "*", + callsign=match.group(1), ssid=ssid, ch=match.group(3) == "*" ) elif isinstance(data, AX25Address): # Clone factory diff --git a/tests/test_frame/test_ax25address.py b/tests/test_frame/test_ax25address.py index a2b2968..e196c70 100644 --- a/tests/test_frame/test_ax25address.py +++ b/tests/test_frame/test_ax25address.py @@ -109,6 +109,18 @@ def test_decode_str_ssid(): assert addr._ssid == 12 +def test_decode_str_override_ssid(): + """ + Test that we can override the SSID in a string. + """ + addr = AX25Address.decode( + "VK4MSL-12", + # This will override the -12 above + ssid=9, + ) + eq_(addr._ssid, 9) + + def test_decode_str_ch(): """ Test that we can decode the C/H bit in a string. From ca23f5e6cf7d0a690d54a2272538cd7ed2256e31 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 14:58:04 +1000 Subject: [PATCH 023/207] frame: Set defaults for C/R and P/F bits. --- aioax25/frame.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 4d3d96f..3006ff5 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1147,6 +1147,10 @@ class AX25BaseUnnumberedFrame(AX25UnnumberedFrame): information fields. """ + # Defaults for PF, CR fields + PF = False + CR = False + @classmethod def decode(cls, header, control, data): if len(data): @@ -1165,11 +1169,16 @@ def __init__( destination, source, repeaters=None, - pf=False, - cr=False, + pf=None, + cr=None, timestamp=None, deadline=None, ): + if pf is None: + pf = self.PF + if cr is None: + cr = self.CR + super(AX25BaseUnnumberedFrame, self).__init__( destination=destination, source=source, @@ -1199,6 +1208,7 @@ class AX25SetAsyncBalancedModeFrame(AX25BaseUnnumberedFrame): """ MODIFIER = 0b01101111 + CR = True AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeFrame) @@ -1213,6 +1223,7 @@ class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): """ MODIFIER = 0b00101111 + CR = True AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeExtendedFrame) @@ -1226,6 +1237,7 @@ class AX25DisconnectFrame(AX25BaseUnnumberedFrame): """ MODIFIER = 0b01000011 + CR = True AX25UnnumberedFrame.register(AX25DisconnectFrame) @@ -1239,6 +1251,7 @@ class AX25DisconnectModeFrame(AX25BaseUnnumberedFrame): """ MODIFIER = 0b00001111 + CR = False AX25UnnumberedFrame.register(AX25DisconnectModeFrame) @@ -1424,6 +1437,7 @@ class AX25UnnumberedAcknowledgeFrame(AX25BaseUnnumberedFrame): """ MODIFIER = 0b01100011 + CR = False AX25UnnumberedFrame.register(AX25UnnumberedAcknowledgeFrame) From cc0e2ffea3595c33a474dcc05e56660ca8e8d457 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 18:03:37 +1000 Subject: [PATCH 024/207] version: Add enumeration for AX.25 protocol versions. --- aioax25/version.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 aioax25/version.py diff --git a/aioax25/version.py b/aioax25/version.py new file mode 100644 index 0000000..808d0c1 --- /dev/null +++ b/aioax25/version.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +""" +AX.25 Version enumeration. This is used to record whether a station is +using a known version of AX.25. +""" + +import enum + + +class AX25Version(enum.Enum): + UNKNOWN = '0.0' # The version is not known + AX25_10 = '1.x' # AX.25 1.x in use + AX25_20 = '2.0' # AX.25 2.0 in use + AX25_22 = '2.2' # AX.25 2.2 in use From 15eae67d0fce282de9d85221c2cef787ff1cdc03 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 18:23:00 +1000 Subject: [PATCH 025/207] station, peer: Commit work-in-progress implementation The concept here is that `AX25Station` represents the local AX.25 node implemented by the Python application, while `AX25Peer` represents a remote AX.25 TNC. `AX25Station` basically routes according to the source AX.25 address to an appropriate `AX25Peer`. Some garbage collection is implemented to ensure memory isn't exhausted. --- aioax25/peer.py | 631 +++++++++++++++++++++++++++++++++++++++++++++ aioax25/station.py | 192 ++++++++++++++ 2 files changed, 823 insertions(+) create mode 100644 aioax25/peer.py create mode 100644 aioax25/station.py diff --git a/aioax25/peer.py b/aioax25/peer.py new file mode 100644 index 0000000..51d4b35 --- /dev/null +++ b/aioax25/peer.py @@ -0,0 +1,631 @@ +#!/usr/bin/env python3 + +""" +AX.25 Station Peer interface. + +This is used as the "proxy" for interacting with a remote AX.25 station. +""" + +import logging +from .signal import Signal +import weakref +import enum + +from .frame import AX25Frame, AX25Address, AX25SetAsyncBalancedModeFrame, \ + AX25SetAsyncBalancedModeExtendedFrame, \ + AX25ExchangeIdentificationFrame, AX25UnnumberedAcknowledgeFrame, \ + AX25TestFrame, AX25DisconnectFrame, AX25DisconnectModeFrame, \ + AX25FrameRejectFrame, AX25RawFrame, \ + AX2516BitInformationFrame, AX258BitInformationFrame, \ + AX258BitReceiveReadyFrame, AX2516BitReceiveReadyFrame, \ + AX258BitReceiveNotReadyFrame, AX2516BitReceiveNotReadyFrame, \ + AX258BitRejectFrame, AX2516BitRejectFrame, \ + AX258BitSelectiveRejectFrame, AX2516BitSelectiveRejectFrame + + +class AX25Peer(object): + """ + This class is a proxy representation of the remote AX.25 peer that may be + connected to this station. The factory for these objects is the + AX25Station's getpeer method. + """ + + class AX25PeerState(enum.Enum): + # DISCONNECTED: No connection has been established + DISCONNECTED = 0 + + # Awaiting response to XID request + NEGOTIATING = 1 + + # Awaiting response to SABM(E) request + CONNECTING = 2 + + # Connection is established + CONNECTED = 3 + + # Awaiting response to DISC request + DISCONNECTING = 4 + + # FRMR condition entered + FRMR = 5 + + + def __init__(self, station, destination, repeaters, max_ifield, max_retries, + max_outstanding_mod8, max_outstanding_mod128, rr_delay, rr_interval, + rnr_interval, idle_timeout, protocol, log, loop, reply_path=None, + locked_path=False): + """ + Create a peer context for the station named by 'address'. + """ + self._station = weakref.ref(station) + self._repeaters = repeaters + self._reply_path = reply_path + self._destination = destination + self._idle_timeout = idle_timeout + self._max_ifield = max_ifield + self._max_retries = max_retries + self._max_outstanding_mod8 = max_outstanding_mod8 + self._max_outstanding_mod128 = max_outstanding_mod128 + self._rr_delay = rr_delay + self._rr_interval = rr_interval + self._rnr_interval = rnr_interval + self._locked_path = bool(locked_path) + self._protocol = protocol + self._log = log + self._loop = loop + + # Internal state (see AX.25 2.2 spec 4.2.4) + self._state = self.AX25PeerState.DISCONNECTED + self._max_outstanding = None # Decided when SABM(E) received + self._modulo = None # Set when SABM(E) received + self._connected = False # Set to true on SABM UA + self._send_state = 0 # AKA V(S) + self._send_seq = 0 # AKA N(S) + self._recv_state = 0 # AKA V(R) + self._recv_seq = 0 # AKA N(R) + self._ack_state = 0 # AKA V(A) + self._local_busy = False # Local end busy, respond to + # RR and I-frames with RNR. + self._peer_busy = False # Peer busy, await RR. + + # Time of last sent RNR notification + self._last_rnr_sent = 0 + + # Classes to use for I and S frames + self._IFrameClass = None + self._RRFrameClass = None + self._RNRFrameClass = None + self._REJFrameClass = None + self._SREJFrameClass = None + + # Timeouts + self._idle_timeout_handle = None + self._rr_notification_timeout_handle = None + + # Unacknowledged I-frames to be ACKed. This dictionary maps the N(R) + # of the frame with a tuple of the form (pid, payload). + self._pending_iframes = {} + + # Data pending to be sent. This will be a queue of frames represented + # as tuples of the form (pid, payload). Each payload is assumed to be + # no bigger than max_ifield. + self._pending_data = [] + + # Link quality measurement: + # rx_path_count is incremented each time a frame is received via a + # given AX.25 digipeater path. + self._rx_path_count = {} + # tx_path_score is incremented for each transmitted frame which is ACKed + # and decremented each time a frame is REJected. + self._tx_path_score = {} + + # Handling of TEST frames + self._testframe_handler = None + + # Signals: + + # Fired when an I-frame is received + self.received_information = Signal() + + # Fired when the connection state changes + self.connect_state_changed = Signal() + + # Kick off the idle timer + self._reset_idle_timeout() + + @property + def reply_path(self): + """ + Return the digipeater path to use when contacting this station. + """ + if self._reply_path is None: + # No specific path set, are we locked to a given path? + if self._locked_path: + # Use the one we were given + return self._repeaters + + # Enumerate all possible paths and select the "best" path + all_paths = list(self._tx_path_score.items()) \ + + [(path, 0) for path in self._rx_path_count.keys()] + all_paths.sort(key=lambda p : p[0]) + best_path = all_paths[-1][0] + + # Use this until we have reason to change + self._reply_path = AX25Path(*best_path) + + return self._reply_path + + def weight_path(self, path, weight, relative=True): + """ + Adjust the weight of a digipeater path used to reach this station. + If relative is True, this increments by weight; otherwise it overrides + the weight given. + """ + path = tuple(reversed(AX25Path(*(path or [])).reply)) + + if relative: + weight += self._tx_path_score.get(path, 0) + self._tx_path_score[path] = weight + + def _cancel_idle_timeout(self): + """ + Cancel the idle timeout handle + """ + if self._idle_timeout_handle is not None: + self._idle_timeout_handle.cancel() + self._idle_timeout_handle = None + + def _reset_idle_timeout(self): + """ + Reset the idle timeout handle + """ + self._cancel_idle_timeout() + self._idle_timeout_handle = self._loop.call_later( + self._idle_timeout, self._cleanup) + + def _cleanup(self): + """ + Clean up the instance of this peer as the activity has expired. + """ + if self._state != self.AX25PeerState.DISCONNECTED: + self._log.warning('Disconnecting peer due to inactivity') + self._send_dm() + + self._station()._drop_peer(self) + + # Cancel other timers + self._cancel_rr_notification() + + def _on_receive(self, frame): + """ + Handle an incoming AX.25 frame from this peer. + """ + # Kick off the idle timer + self._reset_idle_timeout() + + if not self._locked_path: + # Increment the received frame count + path = tuple(reversed(frame.header.repeaters.reply)) + self._rx_path_count[path] = self._rx_path_count.get(path, 0) + 1 + + # AX.25 2.2 sect 6.3.1: "The originating TNC sending a SABM(E) command + # ignores and discards any frames except SABM, DISC, UA and DM frames + # from the distant TNC." + if (self._state == self.AX25PeerState.CONNECTING) and \ + not isinstance(frame, ( \ + AX25SetAsyncBalancedModeFrame, # SABM + AX25SetAsyncBalancedModeExtendedFrame, # SABME + AX25DisconnectFrame, # DISC + AX25UnnumberedAcknowledgeFrame, # UA + AX25DisconnectModeFrame)): # DM + self._log.debug('Dropping frame due to pending SABM UA: %s', frame) + return + + # AX.25 2.0 sect 2.4.5: "After sending the FRMR frame, the sending DXE + # will enter the frame reject condition. This condition is cleared when + # the DXE that sent the FRMR frame receives a SABM or DISC command, or + # a DM response frame. Any other command received while the DXE is in + # the frame reject state will cause another FRMR to be sent out with + # the same information field as originally sent." + if (self._state == self.AX25PeerState.FRMR) and \ + not isinstance(frame, ( \ + AX25SetAsyncBalancedModeFrame, # SABM + AX25DisconnectFrame)): # DISC + self._log.debug('Dropping frame due to FRMR: %s', frame) + return + + if isinstance(frame, AX25TestFrame): + # TEST frame + return self._on_receive_test(frame) + elif isinstance(frame, (AX25SetAsyncBalancedModeFrame, + AX25SetAsyncBalancedModeExtendedFrame)): + # SABM or SABME frame + return self._on_receive_sabm(frame) + elif isinstance(frame, AX25DisconnectFrame): + # DISC frame + return self._on_receive_disc() + elif isinstance(frame, AX25DisconnectModeFrame): + # DM frame + return self._on_receive_dm() + elif isinstance(frame, AX25ExchangeIdentificationFrame): + # XID frame + return self._on_receive_xid(frame) + elif isinstance(frame, AX25RawFrame): + # This is either an I or S frame. We should know enough now to + # decode it properly. + if self._state == self.AX25PeerState.CONNECTED: + # A connection is in progress, we can decode this + frame = AX25Frame.decode(frame, modulo128=(self._modulo == 128)) + if isinstance(frame, AX25InformationFrameMixin): + # This is an I-frame + return self._on_receive_iframe(frame) + elif isinstance(frame, AX25SupervisoryFrameMixin): + # This is an S-frame + return self._on_receive_sframe(frame) + else: # pragma: no cover + self._log.warning('Dropping unrecognised frame: %s', frame) + else: + # No connection in progress, send a DM. + return self._send_dm() + + def _on_receive_iframe(self, frame): + """ + Handle an incoming I-frame + """ + # Cancel a pending RR notification frame. + self._cancel_rr_notification() + + # AX.25 2.2 spec 6.4.2.2: "If the TNC is in a busy condition, it + # ignores any I frames without reporting this condition, other than + # repeating the indication of the busy condition." + if self._local_busy: + self._log.warning( + 'Dropping I-frame during busy condition: %s', frame + ) + self._send_rnr_notification() + return + + # AX.25 2.2 spec 6.4.2.1: "If a TNC receives a valid I frame (one whose + # send sequence number equals the receiver's receive state variable) and + # is not in the busy condition,…" + if frame.ns != self._recv_seq: + # TODO: should we send a REJ/SREJ after a time-out? + return + + # "…it accepts the received I frame, + # increments its receive state variable, and acts in one of the following + # manners:…" + self._recv_state = (self._recv_state + 1) % self._modulo + + # "a) If it has an I frame to send, that I frame may be sent with the + # transmitted N(R) equal to its receive state V(R) (thus acknowledging + # the received frame)." + # + # We need to also be mindful of the number of outstanding frames here! + if len(self._pending_data) and (len(self._pending_iframes) \ + < self._max_outstanding): + return self._send_next_iframe() + + # "b) If there are no outstanding I frames, the receiving TNC sends + # an RR frame with N(R) equal to V(R)." + self._schedule_rr_notification() + + def _on_receive_sframe(self, frame): + """ + Handle a S-frame from the peer. + """ + # TODO + pass + + def _on_receive_test(self, frame): + self._log.debug('Received TEST response: %s', frame) + if self._testframe_handler: + self._testframe_handler._on_receive(frame) + + def _on_receive_sabm(self, frame): + modulo128 = isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) + self._log.debug('Received SABM(E): %s (extended=%s)', frame, modulo128) + if modulo128: + # If we don't know the protocol of the peer, we can safely assume + # AX.25 2.2 now. + if self._protocol == AX25Version.UNKNOWN: + self._protocol = AX25Version.AX25_22 + + # Make sure both ends are enabled for AX.25 2.2 + if self._station().protocol != AX25Version.AX25_22: + # We are not in AX.25 2.2 mode. + # + # "A TNC that uses a version of AX.25 prior to v2.2 responds + # with a FRMR". W bit indicates the control field was not + # understood. + self._log.warning( + 'Sending FRMR as we are not in AX.25 2.2 mode' + ) + return self._send_frmr(frame, w=True) + + if self._protocol != AX25Version.AX25_22: + # The other station is not in AX.25 2.2 mode. + # + # "If the TNC is not capable of accepting a SABME, it + # responds with a DM frame". + self._log.warning( + 'Sending DM as peer is not in AX.25 2.2 mode' + ) + return self._send_dm() + + # Set up the connection state + self._init_connection(modulo128) + + # Send a UA and set ourselves as connected + self._set_conn_state(self.AX25PeerState.CONNECTED) + self._send_ua() + + def _init_connection(self, modulo128): + """ + Initialise the AX.25 connection. + """ + if modulo128: + # Set the maximum outstanding frames variable + self._max_outstanding = self._max_outstanding_mod128 + + # Initialise the modulo value + self._modulo = 128 + + # Set the classes to use for I and S frames for modulo128 ops. + self._IFrameClass = AX2516BitInformationFrame + self._RRFrameClass = AX2516BitReceiveReadyFrame + self._RNRFrameClass = AX2516BitReceiveNotReadyFrame + self._REJFrameClass = AX2516BitRejectFrame + self._SREJFrameClass = AX2516BitSelectiveRejectFrame + else: + # Set the maximum outstanding frames variable + self._max_outstanding = self._max_outstanding_mod8 + + # Initialise the modulo value + self._modulo = 8 + + # Set the classes to use for I and S frames for modulo8 ops. + self._IFrameClass = AX258BitInformationFrame + self._RRFrameClass = AX258BitReceiveReadyFrame + self._RNRFrameClass = AX258BitReceiveNotReadyFrame + self._REJFrameClass = AX258BitRejectFrame + self._SREJFrameClass = AX258BitSelectiveRejectFrame + + # Reset state variables + self._reset_connection_state() + + def _reset_connection_state(self): + # Reset our state + self._send_state = 0 # AKA V(S) + self._send_seq = 0 # AKA N(S) + self._recv_state = 0 # AKA V(R) + self._recv_seq = 0 # AKA N(R) + self._ack_state = 0 # AKA V(A) + + # Unacknowledged I-frames to be ACKed + self._pending_iframes = {} + + # Data pending to be sent. + self._pending_data = [] + + def _set_conn_state(self, state): + if self._state == state: + # Nothing to do + return + + # Update the state + self._log.info('Connection state change: %s→%s', + self._state, state) + self._state = state + + # Notify any observers that the state changed + self.connect_state_changed.emit( + station=self._station(), + peer=self, + state=self._state) + + def _on_disconnect(self): + """ + Clean up the connection. + """ + # Send a UA and set ourselves as disconnected + self._set_conn_state(self.AX25PeerState.DISCONNECTED) + + # Reset our state + self._reset_connection_state() + + # Data pending to be sent. + self._pending_data = [] + + def _on_receive_disc(self): + """ + Handle a disconnect request from this peer. + """ + # Send a UA and set ourselves as disconnected + self._log.info('Received DISC from peer') + self._on_disconnect() + self._send_ua() + + def _on_receive_dm(self): + """ + Handle a disconnect request from this peer. + """ + # Set ourselves as disconnected + self._log.info('Received DM from peer') + self._on_disconnect() + + def _on_receive_xid(self, frame): + """ + Handle a request to negotiate parameters. + """ + if self._station().protocol != AX25Version.AX25_22: + # Not supporting this in AX.25 2.0 mode + self._log.warning( + 'Received XID from peer, we are not in AX.25 2.2 mode' + ) + return self._send_frmr(frame, w=True) + if self._state in ( + self.AX25PeerState.CONNECTING, + self.AX25PeerState.DISCONNECTING + ): + # AX.25 2.2 sect 4.3.3.7: "A station receiving an XID command + # returns an XID response unless a UA response to a mode setting + # command is awaiting transmission, or a FRMR condition exists". + self._log.warning( + 'UA is pending, dropping received XID' + ) + return + + # TODO: figure out XID and send an appropriate response. + self._log.error('TODO: implement XID') + return self._send_frmr(frame, w=True) + + def _send_dm(self): + """ + Send a DM frame to the remote station. + """ + self._log.debug('Sending DM') + self._transmit_frame( + AX25DisconnectModeFrame( + destination=self._address, + source=self._station().address, + repeaters=self.reply_path + ) + ) + + def _send_ua(self): + """ + Send a UA frame to the remote station. + """ + self._log.debug('Sending UA') + self._transmit_frame( + AX25UnnumberedAcknowledgeFrame( + destination=self._address, + source=self._station().address, + repeaters=self.reply_path + ) + ) + + def _send_frmr(self, frame, w=False, x=False, y=False, z=False): + """ + Send a FRMR frame to the remote station. + """ + self._log.debug('Sending FRMR in reply to %s', frame) + + # AX.25 2.0 sect 2.4.5: "After sending the FRMR frame, the sending DXE + # will enter the frame reject condition. This condition is cleared when + # the DXE that sent the FRMR frame receives a SABM or DISC command, or + # a DM response frame. Any other command received while the DXE is in + # the frame reject state will cause another FRMR to be sent out with + # the same information field as originally sent." + self._set_conn_state(self.AX25PeerState.FRMR) + + # See https://www.tapr.org/pub_ax25.html + self._transmit_frame( + AX25FrameRejectFrame( + destination=self._address, + source=self._station().address, + repeaters=self.reply_path, + w=w, x=x, y=y, z=z, + vr=self._recv_state, + vs=self._send_state, + frmr_cr=frame.header.cr, + frmr_control=frame.control + ) + ) + + def _cancel_rr_notification(self): + """ + Cancel transmission of an RR + """ + if self._rr_notification_timeout_handle is not None: + self._rr_notification_timeout_handle.cancel() + self._rr_notification_timeout_handle = None + + def _schedule_rr_notification(self): + """ + Schedule a RR notification frame to be sent. + """ + self._cancel_rr_notification() + self._rr_notification_timeout_handle = \ + self._loop.call_later(self._send_rr_notification) + + def _send_rr_notification(self): + """ + Send a RR notification frame + """ + self._cancel_rr_notification() + self._transmit_frame( + self._RRFrameClass( + destination=self._address, + source=self._station().address, + repeaters=self.reply_path, + pf=False, nr=self._recv_state + ) + ) + + def _send_rnr_notification(self): + """ + Send a RNR notification if the RNR interval has elapsed. + """ + now = self._loop.time() + if (now - self._last_rnr_sent) > self._rnr_interval: + self._transmit_frame( + self._RNRFrameClass( + destination=self._address, + source=self._station().address, + repeaters=self.reply_path, + nr=self._recv_seq, + pf=False + ) + ) + self._last_rnr_sent = now + + def _send_next_iframe(self): + """ + Send the next I-frame, if there aren't too many frames pending. + """ + if not len(self._pending_data) or (len(self._pending_iframes) \ + >= self._max_outstanding): + self._log.debug('Waiting for pending I-frames to be ACKed') + return + + # AX.25 2.2 spec 6.4.1: "Whenever a TNC has an I frame to transmit, + # it sends the I frame with the N(S) of the control field equal to + # its current send state variable V(S)…" + ns = self._send_state + assert ns not in self._pending_iframes, 'Duplicate N(S) pending' + + # Retrieve the next pending I-frame payload + (pid, payload) = self._pending_data.pop(0) + self._pending_iframes[ns] = (pid, payload) + + # Send it + self._transmit_iframe(ns) + + # "After the I frame is sent, the send state variable is incremented + # by one." + self._send_state = (self._send_state + 1) \ + % self._modulo + + def _transmit_iframe(self, ns): + """ + Transmit the I-frame identified by the given N(S) parameter. + """ + (pid, payload) = self._pending_iframes[ns] + self._transmit_frame( + self._IFrameClass( + destination=self._address, + source=self._station().address, + repeaters=self.reply_path, + nr=self._recv_seq, + ns=ns, + pf=False, + pid=pid, payload=payload + ) + ) + + def _transmit_frame(self, frame): + # Kick off the idle timer + self._reset_idle_timeout() + return self._station()._interface().transmit(frame) diff --git a/aioax25/station.py b/aioax25/station.py new file mode 100644 index 0000000..14dca43 --- /dev/null +++ b/aioax25/station.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 + +""" +AX.25 Station interface. + +This implements the base-level AX.25 logic for a station listening at a given +SSID. +""" + + +import logging +import asyncio +from .signal import Signal +import weakref + +from .frame import AX25Address, AX25Path, AX25TestFrame, \ + AX25SetAsynchronousBalancedModeFrame, \ + AX25SetAsynchronousBalancedModeExtendedFrame, \ + AX25ExchangeIdentificationFrame + +from .peer import AX25Peer +from .version import AX25Version + + +class AX25Station(object): + """ + The AX25Station class represents the station on the AX.25 network + implemented by the caller of this library. Notably, it provides + hooks for handling incoming connections, and methods for making + connections to other AX.25 stations. + + To be able to participate as a connected-mode station, create an instance + of AX25Station, referencing an instance of AX25Interface as the interface + parameter; then call the attach method. + """ + + def __init__(self, interface, + # Station call-sign and SSID + callsign, ssid=None, + # Parameters (AX.25 2.2 sect 6.7.2) + max_ifield=256, # aka N1 + max_retries=10, # aka N2, value from figure 4.5 + # k value, for mod128 and mod8 connections + max_outstanding_mod8=7, + max_outstanding_mod128=127, + # Timer parameters + idle_timeout=900.0, # Idle timeout before we "forget" peers + rr_delay=10.0, # Delay between I-frame and RR + rr_interval=30.0, # Poll interval when peer in busy state + rnr_interval=10.0, # Delay between RNRs when busy + # Protocol version to use for our station + protocol=AX25Version.AX25_22, + # IOLoop and logging + log=None, loop=None): + + if log is None: + log = logging.getLogger(self.__class__.__module__) + + if loop is None: + loop = asyncio.get_event_loop() + + # Ensure we are running a supported version of AX.25 + protocol = AX25Version(protocol) + if protocol not in (AX25Version.AX25_20, AX25Version.AX25_22): + raise ValueError('%r not a supported AX.25 protocol version'\ + % protocol) + + # Configuration parameters + self._address = AX25Address.decode(callsign, ssid) + self._interface = weakref.ref(interface) + self._protocol = protocol + self._idle_timeout = idle_timeout + self._max_ifield = max_ifield + self._max_retries = max_retries + self._max_outstanding_mod8 = max_outstanding_mod8 + self._max_outstanding_mod128 = max_outstanding_mod128 + self._rr_delay = rr_delay + self._rr_interval = rr_interval + self._rnr_interval = rnr_interval + self._log = log + self._loop = loop + + # Remote station handlers + self._peers = {} + + # Signal emitted when a SABM(E) is received + self.connection_request = Signal() + + @property + def address(self): + """ + Return the source address of this station. + """ + return self._address + + @property + def protocol(self): + """ + Return the protocol version of this station. + """ + return self._protocol + + def attach(self): + """ + Connect the station to the interface. + """ + interface = self._interface() + interface.bind(self._on_receive, + callsign=self.address.callsign, + ssid=self.address.ssid, + regex=False) + + def detach(self): + """ + Disconnect from the interface. + """ + interface = self._interface() + interface.unbind(self._on_receive, + callsign=self.address.callsign, + ssid=self.address.ssid, + regex=False) + + def getpeer(self, callsign, ssid=None, repeaters=None, + create=True, **kwargs): + """ + Retrieve an instance of a peer context. This creates the peer + object if it doesn't already exist unless create is set to False + (in which case it will raise KeyError). + """ + address = AX25Address.decode(callsign, ssid).normalised + try: + return self._peers[address] + except KeyError: + if not create: + raise + pass + + # Not there, so set some defaults, then create + kwargs.setdefault('max_ifield', self._max_ifield) + kwargs.setdefault('max_retries', self._max_retries) + kwargs.setdefault('max_outstanding_mod8', self._max_outstanding_mod8) + kwargs.setdefault('max_outstanding_mod128', self._max_outstanding_mod128) + kwargs.setdefault('rr_delay', self._rr_delay) + kwargs.setdefault('rr_interval', self._rr_interval) + kwargs.setdefault('rnr_interval', self._rnr_interval) + kwargs.setdefault('idle_timeout', self._idle_timeout) + kwargs.setdefault('protocol', AX25Version.UNKNOWN) + peer = AX25Peer(self, address, + repeaters=AX25Path(*(repeaters or [])), + log=self._log.getChild('peer.%s' % address), + loop=self._loop, + **kwargs) + self._peers[address] = peer + return peer + + def _drop_peer(self, peer): + """ + Drop a peer. This is called by the peer when its idle timeout expires. + """ + self._peers.pop(peer.address, None) + + def _on_receive(self, frame, **kwargs): + """ + Handling of incoming messages. + """ + if frame.header.cr: + # This is a command frame + if isinstance(frame, AX25TestFrame): + # A TEST request frame, context not required + return self._on_receive_test(frame) + + # If we're still here, then we don't handle unsolicited frames + # of this type, so pass it to a handler if we have one. + peer = self.getpeer(frame.header.source, + repeaters=frame.header.repeaters.reply) + peer._on_receive(frame) + + def _on_receive_test(self, frame): + """ + Handle a TEST frame. + """ + # The frame is a test request. + interface = self._interface() + interface.transmit( + AX25TestFrame( + destination=frame.header.source, + source=self.address, + repeaters=frame.header.repeaters.reply, + payload=frame.payload, + cr=False + ) + ) From 4709af1b1ed2c8f3257d4ce9c9a44591a69d8077 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 18:44:53 +1000 Subject: [PATCH 026/207] station: Tweak error on unsupported protocol --- aioax25/station.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioax25/station.py b/aioax25/station.py index 14dca43..63520df 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -63,7 +63,7 @@ def __init__(self, interface, protocol = AX25Version(protocol) if protocol not in (AX25Version.AX25_20, AX25Version.AX25_22): raise ValueError('%r not a supported AX.25 protocol version'\ - % protocol) + % protocol.value) # Configuration parameters self._address = AX25Address.decode(callsign, ssid) From 3078eb8dd91c7b6264bf4f9f0dd99b501e83c7df Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 18:45:08 +1000 Subject: [PATCH 027/207] station: Clean up imports --- aioax25/station.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aioax25/station.py b/aioax25/station.py index 63520df..96b164d 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -13,10 +13,7 @@ from .signal import Signal import weakref -from .frame import AX25Address, AX25Path, AX25TestFrame, \ - AX25SetAsynchronousBalancedModeFrame, \ - AX25SetAsynchronousBalancedModeExtendedFrame, \ - AX25ExchangeIdentificationFrame +from .frame import AX25Address, AX25Path, AX25TestFrame from .peer import AX25Peer from .version import AX25Version From f46a300076e720b00064f6c19e3222acd8529d96 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 18:45:30 +1000 Subject: [PATCH 028/207] station: Initial test cases --- tests/test_station/__init__.py | 0 tests/test_station/test_station.py | 55 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/test_station/__init__.py create mode 100644 tests/test_station/test_station.py diff --git a/tests/test_station/__init__.py b/tests/test_station/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_station/test_station.py b/tests/test_station/test_station.py new file mode 100644 index 0000000..2d1ca22 --- /dev/null +++ b/tests/test_station/test_station.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +from aioax25.station import AX25Station +from aioax25.peer import AX25Peer +from aioax25.version import AX25Version + +from nose.tools import eq_ + + +def test_constructor_log(): + """ + Test the AX25Constructor uses the log given. + """ + class DummyInterface(object): + pass + + class DummyLogger(object): + pass + + log = DummyLogger() + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL', ssid=3, + log=log) + assert station._log is log + +def test_constructor_loop(): + """ + Test the AX25Constructor uses the IO loop given. + """ + class DummyInterface(object): + pass + + class DummyLoop(object): + pass + + loop = DummyLoop() + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL', ssid=3, + loop=loop) + assert station._loop is loop + +def test_constructor_protocol(): + """ + Test the AX25Constructor validates the protocol + """ + try: + class DummyInterface(object): + pass + + AX25Station(interface=DummyInterface(), callsign='VK4MSL', ssid=3, + protocol=AX25Version.AX25_10) + assert False, 'Should not have worked' + except ValueError as e: + eq_(str(e), + "'1.x' not a supported AX.25 protocol version") From 415827cd82490e5c17ae9895e516bc1b92c70e08 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 19:02:42 +1000 Subject: [PATCH 029/207] station: Normalise the given call-sign --- aioax25/station.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioax25/station.py b/aioax25/station.py index 96b164d..69e3226 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -63,7 +63,7 @@ def __init__(self, interface, % protocol.value) # Configuration parameters - self._address = AX25Address.decode(callsign, ssid) + self._address = AX25Address.decode(callsign, ssid).normalised self._interface = weakref.ref(interface) self._protocol = protocol self._idle_timeout = idle_timeout From 64caadcb37f498d052ede2ff298974a161c602f1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 19:03:01 +1000 Subject: [PATCH 030/207] station tests: Split cases up, add attach/detach checks --- tests/test_station/test_attach_detach.py | 49 +++++++++++++++++++ .../{test_station.py => test_constructor.py} | 12 +---- tests/test_station/test_properties.py | 24 +++++++++ 3 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 tests/test_station/test_attach_detach.py rename tests/test_station/{test_station.py => test_constructor.py} (86%) create mode 100644 tests/test_station/test_properties.py diff --git a/tests/test_station/test_attach_detach.py b/tests/test_station/test_attach_detach.py new file mode 100644 index 0000000..9d916a1 --- /dev/null +++ b/tests/test_station/test_attach_detach.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +from aioax25.station import AX25Station + +from nose.tools import eq_ +from ..mocks import DummyInterface + + +def test_attach(): + """ + Test attach binds the station to the interface. + """ + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL-5') + station.attach() + + eq_(len(interface.bind_calls), 1) + eq_(len(interface.unbind_calls), 0) + eq_(len(interface.transmit_calls), 0) + + (args, kwargs) = interface.bind_calls.pop() + eq_(args, (station._on_receive,)) + eq_(set(kwargs.keys()), set([ + 'callsign', 'ssid', 'regex' + ])) + eq_(kwargs['callsign'], 'VK4MSL') + eq_(kwargs['ssid'], 5) + eq_(kwargs['regex'], False) + +def test_detach(): + """ + Test attach unbinds the station to the interface. + """ + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL-5') + station.detach() + + eq_(len(interface.bind_calls), 0) + eq_(len(interface.unbind_calls), 1) + eq_(len(interface.transmit_calls), 0) + + (args, kwargs) = interface.unbind_calls.pop() + eq_(args, (station._on_receive,)) + eq_(set(kwargs.keys()), set([ + 'callsign', 'ssid', 'regex' + ])) + eq_(kwargs['callsign'], 'VK4MSL') + eq_(kwargs['ssid'], 5) + eq_(kwargs['regex'], False) diff --git a/tests/test_station/test_station.py b/tests/test_station/test_constructor.py similarity index 86% rename from tests/test_station/test_station.py rename to tests/test_station/test_constructor.py index 2d1ca22..df8e024 100644 --- a/tests/test_station/test_station.py +++ b/tests/test_station/test_constructor.py @@ -1,19 +1,17 @@ #!/usr/bin/env python3 from aioax25.station import AX25Station -from aioax25.peer import AX25Peer from aioax25.version import AX25Version +from aioax25.frame import AX25Address from nose.tools import eq_ +from ..mocks import DummyInterface def test_constructor_log(): """ Test the AX25Constructor uses the log given. """ - class DummyInterface(object): - pass - class DummyLogger(object): pass @@ -27,9 +25,6 @@ def test_constructor_loop(): """ Test the AX25Constructor uses the IO loop given. """ - class DummyInterface(object): - pass - class DummyLoop(object): pass @@ -44,9 +39,6 @@ def test_constructor_protocol(): Test the AX25Constructor validates the protocol """ try: - class DummyInterface(object): - pass - AX25Station(interface=DummyInterface(), callsign='VK4MSL', ssid=3, protocol=AX25Version.AX25_10) assert False, 'Should not have worked' diff --git a/tests/test_station/test_properties.py b/tests/test_station/test_properties.py new file mode 100644 index 0000000..2c19f92 --- /dev/null +++ b/tests/test_station/test_properties.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from aioax25.station import AX25Station +from aioax25.version import AX25Version +from aioax25.frame import AX25Address + +from nose.tools import eq_ +from ..mocks import DummyInterface + + +def test_address(): + """ + Test the address of the station is set from the constructor. + """ + station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') + eq_(station.address, AX25Address(callsign='VK4MSL', ssid=5)) + +def test_protocol(): + """ + Test the protocol of the station is set from the constructor. + """ + station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5', + protocol=AX25Version.AX25_20) + eq_(station.protocol, AX25Version.AX25_20) From 58eac8b4b837ccc6da98debad586e364c30e6dba Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 19:11:23 +1000 Subject: [PATCH 031/207] test cases: Add mocks library --- tests/mocks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/mocks.py diff --git a/tests/mocks.py b/tests/mocks.py new file mode 100644 index 0000000..740eb45 --- /dev/null +++ b/tests/mocks.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +class DummyInterface(object): + def __init__(self): + self.bind_calls = [] + self.unbind_calls = [] + self.transmit_calls = [] + + def bind(self, *args, **kwargs): + self.bind_calls.append((args, kwargs)) + + def unbind(self, *args, **kwargs): + self.unbind_calls.append((args, kwargs)) + + def transmit(self, *args, **kwargs): + self.transmit_calls.append((args, kwargs)) From d78627e826ae70d66bb9428cf22abfd0d7c5c6d0 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 17 Nov 2019 19:11:40 +1000 Subject: [PATCH 032/207] station: Add peer factory tests --- tests/test_station/test_getpeer.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/test_station/test_getpeer.py diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py new file mode 100644 index 0000000..d5a1242 --- /dev/null +++ b/tests/test_station/test_getpeer.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +from aioax25.station import AX25Station +from aioax25.peer import AX25Peer + +from nose.tools import eq_ +from ..mocks import DummyInterface + + +def test_unknown_peer_nocreate_keyerror(): + """ + Test fetching an unknown peer with create=False raises KeyError + """ + station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') + try: + station.getpeer('VK4BWI', create=False) + assert False, 'Should not have worked' + except KeyError as e: + eq_(str(e), 'AX25Address(callsign=VK4BWI, ssid=0, '\ + 'ch=False, res0=True, res1=True, extension=False)') + +def test_unknown_peer_create_instance(): + """ + Test fetching an unknown peer with create=True generates peer + """ + station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') + peer = station.getpeer('VK4BWI', create=True) + assert isinstance(peer, AX25Peer) From 4185ba457691f946bf0e75bf8a926cbe16fe27ee Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 20:57:32 +1000 Subject: [PATCH 033/207] station: Add existing peer retrieval test. --- tests/mocks.py | 11 +++++++++++ tests/test_station/test_getpeer.py | 19 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/mocks.py b/tests/mocks.py index 740eb45..5b13a67 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -14,3 +14,14 @@ def unbind(self, *args, **kwargs): def transmit(self, *args, **kwargs): self.transmit_calls.append((args, kwargs)) + + +class DummyPeer(object): + def __init__(self, address): + self.address_read = False + self._address = address + + @property + def address(self): + self.address_read = True + return self._address diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py index d5a1242..2340b41 100644 --- a/tests/test_station/test_getpeer.py +++ b/tests/test_station/test_getpeer.py @@ -2,9 +2,10 @@ from aioax25.station import AX25Station from aioax25.peer import AX25Peer +from aioax25.frame import AX25Address from nose.tools import eq_ -from ..mocks import DummyInterface +from ..mocks import DummyInterface, DummyPeer def test_unknown_peer_nocreate_keyerror(): @@ -19,6 +20,7 @@ def test_unknown_peer_nocreate_keyerror(): eq_(str(e), 'AX25Address(callsign=VK4BWI, ssid=0, '\ 'ch=False, res0=True, res1=True, extension=False)') + def test_unknown_peer_create_instance(): """ Test fetching an unknown peer with create=True generates peer @@ -26,3 +28,18 @@ def test_unknown_peer_create_instance(): station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') peer = station.getpeer('VK4BWI', create=True) assert isinstance(peer, AX25Peer) + + +def test_known_peer_fetch_instance(): + """ + Test fetching an known peer returns that known peer + """ + station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') + mypeer = DummyPeer(AX25Address('VK4BWI')) + + # Inject the peer + station._peers[mypeer._address] = mypeer + + # Retrieve the peer instance + peer = station.getpeer('VK4BWI') + assert peer is mypeer From 585457ede361dcd0b26bf2ba4dee365da8e56bd6 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 20:57:49 +1000 Subject: [PATCH 034/207] station: Add _drop_peer test --- tests/test_station/test_droppeer.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/test_station/test_droppeer.py diff --git a/tests/test_station/test_droppeer.py b/tests/test_station/test_droppeer.py new file mode 100644 index 0000000..64ac4d7 --- /dev/null +++ b/tests/test_station/test_droppeer.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from aioax25.station import AX25Station +from aioax25.frame import AX25Address + +from nose.tools import eq_ +from ..mocks import DummyInterface, DummyPeer + + +def test_known_peer_fetch_instance(): + """ + Test calling _drop_peer removes the peer + """ + station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') + mypeer = DummyPeer(AX25Address('VK4BWI')) + + # Inject the peer + station._peers[mypeer._address] = mypeer + + # Drop the peer + peer = station._drop_peer(mypeer) + assert mypeer._address not in station._peers + assert mypeer.address_read From 2eb493b4ddfc9e56ebdf01f969564c0203b5af6e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 21:23:57 +1000 Subject: [PATCH 035/207] peer: Fix address property/constructor argument --- aioax25/peer.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 51d4b35..aaa80a2 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -50,7 +50,7 @@ class AX25PeerState(enum.Enum): FRMR = 5 - def __init__(self, station, destination, repeaters, max_ifield, max_retries, + def __init__(self, station, address, repeaters, max_ifield, max_retries, max_outstanding_mod8, max_outstanding_mod128, rr_delay, rr_interval, rnr_interval, idle_timeout, protocol, log, loop, reply_path=None, locked_path=False): @@ -60,7 +60,7 @@ def __init__(self, station, destination, repeaters, max_ifield, max_retries, self._station = weakref.ref(station) self._repeaters = repeaters self._reply_path = reply_path - self._destination = destination + self._address = address self._idle_timeout = idle_timeout self._max_ifield = max_ifield self._max_retries = max_retries @@ -133,6 +133,13 @@ def __init__(self, station, destination, repeaters, max_ifield, max_retries, # Kick off the idle timer self._reset_idle_timeout() + @property + def address(self): + """ + Return the peer's AX.25 address + """ + return self._address + @property def reply_path(self): """ From 5bd8a2bcbd2b4daac0d495f8648cfbf6402a62ae Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 21:24:28 +1000 Subject: [PATCH 036/207] station: Add debug messages for receive tracing --- aioax25/station.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aioax25/station.py b/aioax25/station.py index 69e3226..bea91f6 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -162,6 +162,7 @@ def _on_receive(self, frame, **kwargs): """ if frame.header.cr: # This is a command frame + self._log.debug('Checking command frame sub-class: %s', frame) if isinstance(frame, AX25TestFrame): # A TEST request frame, context not required return self._on_receive_test(frame) @@ -170,6 +171,7 @@ def _on_receive(self, frame, **kwargs): # of this type, so pass it to a handler if we have one. peer = self.getpeer(frame.header.source, repeaters=frame.header.repeaters.reply) + self._log.debug('Passing frame to peer %s: %s', peer.address, frame) peer._on_receive(frame) def _on_receive_test(self, frame): @@ -177,6 +179,7 @@ def _on_receive_test(self, frame): Handle a TEST frame. """ # The frame is a test request. + self._log.debug('Responding to test frame: %s', frame) interface = self._interface() interface.transmit( AX25TestFrame( From 01708b86e107c0c80b8b9aef66e12cbde0ce57b8 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 21:24:42 +1000 Subject: [PATCH 037/207] test mocks: Add receive stub to dummy peer class --- tests/mocks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index 5b13a67..4a54a33 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -21,7 +21,12 @@ def __init__(self, address): self.address_read = False self._address = address + self.on_receive_calls = [] + @property def address(self): self.address_read = True return self._address + + def _on_receive(self, *args, **kwargs): + self.on_receive_calls.append((args, kwargs)) From 62fcb9787221bc606bd0cdcf74e0105771bcab1d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 21:28:24 +1000 Subject: [PATCH 038/207] station: Add test cases for received frames. --- tests/test_station/test_receive.py | 127 +++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/test_station/test_receive.py diff --git a/tests/test_station/test_receive.py b/tests/test_station/test_receive.py new file mode 100644 index 0000000..07735b3 --- /dev/null +++ b/tests/test_station/test_receive.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +from aioax25.station import AX25Station +from aioax25.frame import AX25Address, AX25TestFrame, \ + AX25UnnumberedInformationFrame + +from nose.tools import eq_ +from ..mocks import DummyInterface, DummyPeer + + +def test_testframe_cmd_echo(): + """ + Test passing a test frame with CR=True triggers a reply frame. + """ + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL-5') + + # Pass in a frame + station._on_receive(frame=AX25TestFrame( + destination='VK4MSL-5', + source='VK4MSL-7', + cr=True, + payload=b'This is a test frame' + )) + + # There should be no peers + eq_(station._peers, {}) + + # There should be a reply queued up + eq_(interface.bind_calls, []) + eq_(interface.unbind_calls, []) + eq_(len(interface.transmit_calls), 1) + + (tx_call_args, tx_call_kwargs) = interface.transmit_calls.pop() + eq_(tx_call_kwargs, {}) + eq_(len(tx_call_args), 1) + frame = tx_call_args[0] + + # The reply should have the source/destination swapped and the + # CR bit cleared. + assert isinstance(frame, AX25TestFrame), 'Not a test frame' + eq_(frame.header.cr, False) + eq_(frame.header.destination, AX25Address('VK4MSL', ssid=7)) + eq_(frame.header.source, AX25Address('VK4MSL', ssid=5)) + eq_(frame.payload, b'This is a test frame') + + +def test_route_testframe_reply(): + """ + Test passing a test frame reply routes to the appropriate AX25Peer instance. + """ + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL-5') + + # Stub out _on_test_frame + def stub_on_test_frame(*args, **kwargs): + assert False, 'Should not have been called' + station._on_test_frame = stub_on_test_frame + + # Inject a couple of peers + peer1 = DummyPeer(AX25Address('VK4MSL', ssid=7)) + peer2 = DummyPeer(AX25Address('VK4BWI', ssid=7)) + station._peers[peer1._address] = peer1 + station._peers[peer2._address] = peer2 + + # Pass in the message + txframe = AX25TestFrame( + destination='VK4MSL-5', + source='VK4MSL-7', + cr=False, + payload=b'This is a test frame' + ) + station._on_receive(frame=txframe) + + # There should be no replies queued + eq_(interface.bind_calls, []) + eq_(interface.unbind_calls, []) + eq_(interface.transmit_calls, []) + + # This should have gone to peer1, not peer2 + eq_(peer2.on_receive_calls, []) + eq_(len(peer1.on_receive_calls), 1) + (rx_call_args, rx_call_kwargs) = peer1.on_receive_calls.pop() + eq_(rx_call_kwargs, {}) + eq_(len(rx_call_args), 1) + assert rx_call_args[0] is txframe + + +def test_route_incoming_msg(): + """ + Test passing a frame routes to the appropriate AX25Peer instance. + """ + interface = DummyInterface() + station = AX25Station(interface=interface, callsign='VK4MSL-5') + + # Stub out _on_test_frame + def stub_on_test_frame(*args, **kwargs): + assert False, 'Should not have been called' + station._on_test_frame = stub_on_test_frame + + # Inject a couple of peers + peer1 = DummyPeer(AX25Address('VK4MSL', ssid=7)) + peer2 = DummyPeer(AX25Address('VK4BWI', ssid=7)) + station._peers[peer1._address] = peer1 + station._peers[peer2._address] = peer2 + + # Pass in the message + txframe = AX25UnnumberedInformationFrame( + destination='VK4MSL-5', + source='VK4BWI-7', + cr=True, pid=0xab, + payload=b'This is a test frame' + ) + station._on_receive(frame=txframe) + + # There should be no replies queued + eq_(interface.bind_calls, []) + eq_(interface.unbind_calls, []) + eq_(interface.transmit_calls, []) + + # This should have gone to peer2, not peer1 + eq_(peer1.on_receive_calls, []) + eq_(len(peer2.on_receive_calls), 1) + (rx_call_args, rx_call_kwargs) = peer2.on_receive_calls.pop() + eq_(rx_call_kwargs, {}) + eq_(len(rx_call_args), 1) + assert rx_call_args[0] is txframe From 2262bf0446b78266caae16dd5a148b412f666b0b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 21:30:41 +1000 Subject: [PATCH 039/207] peer: Use address property --- aioax25/peer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index aaa80a2..c5315d3 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -494,7 +494,7 @@ def _send_dm(self): self._log.debug('Sending DM') self._transmit_frame( AX25DisconnectModeFrame( - destination=self._address, + destination=self.address, source=self._station().address, repeaters=self.reply_path ) @@ -507,7 +507,7 @@ def _send_ua(self): self._log.debug('Sending UA') self._transmit_frame( AX25UnnumberedAcknowledgeFrame( - destination=self._address, + destination=self.address, source=self._station().address, repeaters=self.reply_path ) @@ -530,7 +530,7 @@ def _send_frmr(self, frame, w=False, x=False, y=False, z=False): # See https://www.tapr.org/pub_ax25.html self._transmit_frame( AX25FrameRejectFrame( - destination=self._address, + destination=self.address, source=self._station().address, repeaters=self.reply_path, w=w, x=x, y=y, z=z, @@ -564,7 +564,7 @@ def _send_rr_notification(self): self._cancel_rr_notification() self._transmit_frame( self._RRFrameClass( - destination=self._address, + destination=self.address, source=self._station().address, repeaters=self.reply_path, pf=False, nr=self._recv_state @@ -579,7 +579,7 @@ def _send_rnr_notification(self): if (now - self._last_rnr_sent) > self._rnr_interval: self._transmit_frame( self._RNRFrameClass( - destination=self._address, + destination=self.address, source=self._station().address, repeaters=self.reply_path, nr=self._recv_seq, @@ -622,7 +622,7 @@ def _transmit_iframe(self, ns): (pid, payload) = self._pending_iframes[ns] self._transmit_frame( self._IFrameClass( - destination=self._address, + destination=self.address, source=self._station().address, repeaters=self.reply_path, nr=self._recv_seq, From 7b1c7cc010261922d03f9a234ba1124274266a61 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 21:47:31 +1000 Subject: [PATCH 040/207] Fix issues spotted by pyflakes. --- aioax25/peer.py | 308 +++++++++++++---------- aioax25/station.py | 129 +++++----- aioax25/version.py | 8 +- tests/mocks.py | 1 + tests/test_frame/test_ax25address.py | 2 +- tests/test_frame/test_ax25frame.py | 14 +- tests/test_frame/test_ax25frameheader.py | 8 +- tests/test_station/test_attach_detach.py | 25 +- tests/test_station/test_constructor.py | 28 ++- tests/test_station/test_droppeer.py | 7 +- tests/test_station/test_getpeer.py | 23 +- tests/test_station/test_properties.py | 12 +- tests/test_station/test_receive.py | 64 ++--- 13 files changed, 356 insertions(+), 273 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index c5315d3..61405e0 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -6,21 +6,36 @@ This is used as the "proxy" for interacting with a remote AX.25 station. """ -import logging from .signal import Signal import weakref import enum -from .frame import AX25Frame, AX25Address, AX25SetAsyncBalancedModeFrame, \ - AX25SetAsyncBalancedModeExtendedFrame, \ - AX25ExchangeIdentificationFrame, AX25UnnumberedAcknowledgeFrame, \ - AX25TestFrame, AX25DisconnectFrame, AX25DisconnectModeFrame, \ - AX25FrameRejectFrame, AX25RawFrame, \ - AX2516BitInformationFrame, AX258BitInformationFrame, \ - AX258BitReceiveReadyFrame, AX2516BitReceiveReadyFrame, \ - AX258BitReceiveNotReadyFrame, AX2516BitReceiveNotReadyFrame, \ - AX258BitRejectFrame, AX2516BitRejectFrame, \ - AX258BitSelectiveRejectFrame, AX2516BitSelectiveRejectFrame +from .version import AX25Version +from .frame import ( + AX25Frame, + AX25Path, + AX25SetAsyncBalancedModeFrame, + AX25SetAsyncBalancedModeExtendedFrame, + AX25ExchangeIdentificationFrame, + AX25UnnumberedAcknowledgeFrame, + AX25TestFrame, + AX25DisconnectFrame, + AX25DisconnectModeFrame, + AX25FrameRejectFrame, + AX25RawFrame, + AX2516BitInformationFrame, + AX258BitInformationFrame, + AX258BitReceiveReadyFrame, + AX2516BitReceiveReadyFrame, + AX258BitReceiveNotReadyFrame, + AX2516BitReceiveNotReadyFrame, + AX258BitRejectFrame, + AX2516BitRejectFrame, + AX258BitSelectiveRejectFrame, + AX2516BitSelectiveRejectFrame, + AX25InformationFrameMixin, + AX25SupervisoryFrameMixin, +) class AX25Peer(object): @@ -49,11 +64,25 @@ class AX25PeerState(enum.Enum): # FRMR condition entered FRMR = 5 - - def __init__(self, station, address, repeaters, max_ifield, max_retries, - max_outstanding_mod8, max_outstanding_mod128, rr_delay, rr_interval, - rnr_interval, idle_timeout, protocol, log, loop, reply_path=None, - locked_path=False): + def __init__( + self, + station, + address, + repeaters, + max_ifield, + max_retries, + max_outstanding_mod8, + max_outstanding_mod128, + rr_delay, + rr_interval, + rnr_interval, + idle_timeout, + protocol, + log, + loop, + reply_path=None, + locked_path=False, + ): """ Create a peer context for the station named by 'address'. """ @@ -76,17 +105,17 @@ def __init__(self, station, address, repeaters, max_ifield, max_retries, # Internal state (see AX.25 2.2 spec 4.2.4) self._state = self.AX25PeerState.DISCONNECTED - self._max_outstanding = None # Decided when SABM(E) received - self._modulo = None # Set when SABM(E) received - self._connected = False # Set to true on SABM UA - self._send_state = 0 # AKA V(S) - self._send_seq = 0 # AKA N(S) - self._recv_state = 0 # AKA V(R) - self._recv_seq = 0 # AKA N(R) - self._ack_state = 0 # AKA V(A) - self._local_busy = False # Local end busy, respond to - # RR and I-frames with RNR. - self._peer_busy = False # Peer busy, await RR. + self._max_outstanding = None # Decided when SABM(E) received + self._modulo = None # Set when SABM(E) received + self._connected = False # Set to true on SABM UA + self._send_state = 0 # AKA V(S) + self._send_seq = 0 # AKA N(S) + self._recv_state = 0 # AKA V(R) + self._recv_seq = 0 # AKA N(R) + self._ack_state = 0 # AKA V(A) + self._local_busy = False # Local end busy, respond to + # RR and I-frames with RNR. + self._peer_busy = False # Peer busy, await RR. # Time of last sent RNR notification self._last_rnr_sent = 0 @@ -152,9 +181,10 @@ def reply_path(self): return self._repeaters # Enumerate all possible paths and select the "best" path - all_paths = list(self._tx_path_score.items()) \ - + [(path, 0) for path in self._rx_path_count.keys()] - all_paths.sort(key=lambda p : p[0]) + all_paths = list(self._tx_path_score.items()) + [ + (path, 0) for path in self._rx_path_count.keys() + ] + all_paths.sort(key=lambda p: p[0]) best_path = all_paths[-1][0] # Use this until we have reason to change @@ -188,14 +218,15 @@ def _reset_idle_timeout(self): """ self._cancel_idle_timeout() self._idle_timeout_handle = self._loop.call_later( - self._idle_timeout, self._cleanup) + self._idle_timeout, self._cleanup + ) def _cleanup(self): """ Clean up the instance of this peer as the activity has expired. """ if self._state != self.AX25PeerState.DISCONNECTED: - self._log.warning('Disconnecting peer due to inactivity') + self._log.warning("Disconnecting peer due to inactivity") self._send_dm() self._station()._drop_peer(self) @@ -218,14 +249,19 @@ def _on_receive(self, frame): # AX.25 2.2 sect 6.3.1: "The originating TNC sending a SABM(E) command # ignores and discards any frames except SABM, DISC, UA and DM frames # from the distant TNC." - if (self._state == self.AX25PeerState.CONNECTING) and \ - not isinstance(frame, ( \ - AX25SetAsyncBalancedModeFrame, # SABM - AX25SetAsyncBalancedModeExtendedFrame, # SABME - AX25DisconnectFrame, # DISC - AX25UnnumberedAcknowledgeFrame, # UA - AX25DisconnectModeFrame)): # DM - self._log.debug('Dropping frame due to pending SABM UA: %s', frame) + if (self._state == self.AX25PeerState.CONNECTING) and not isinstance( + frame, + ( + AX25SetAsyncBalancedModeFrame, # SABM + AX25SetAsyncBalancedModeExtendedFrame, # SABME + AX25DisconnectFrame, # DISC + AX25UnnumberedAcknowledgeFrame, # UA + AX25DisconnectModeFrame, + ), + ): # DM + self._log.debug( + "Dropping frame due to pending SABM UA: %s", frame + ) return # AX.25 2.0 sect 2.4.5: "After sending the FRMR frame, the sending DXE @@ -234,18 +270,23 @@ def _on_receive(self, frame): # a DM response frame. Any other command received while the DXE is in # the frame reject state will cause another FRMR to be sent out with # the same information field as originally sent." - if (self._state == self.AX25PeerState.FRMR) and \ - not isinstance(frame, ( \ - AX25SetAsyncBalancedModeFrame, # SABM - AX25DisconnectFrame)): # DISC - self._log.debug('Dropping frame due to FRMR: %s', frame) + if (self._state == self.AX25PeerState.FRMR) and not isinstance( + frame, + (AX25SetAsyncBalancedModeFrame, AX25DisconnectFrame), # SABM + ): # DISC + self._log.debug("Dropping frame due to FRMR: %s", frame) return if isinstance(frame, AX25TestFrame): # TEST frame return self._on_receive_test(frame) - elif isinstance(frame, (AX25SetAsyncBalancedModeFrame, - AX25SetAsyncBalancedModeExtendedFrame)): + elif isinstance( + frame, + ( + AX25SetAsyncBalancedModeFrame, + AX25SetAsyncBalancedModeExtendedFrame, + ), + ): # SABM or SABME frame return self._on_receive_sabm(frame) elif isinstance(frame, AX25DisconnectFrame): @@ -262,15 +303,19 @@ def _on_receive(self, frame): # decode it properly. if self._state == self.AX25PeerState.CONNECTED: # A connection is in progress, we can decode this - frame = AX25Frame.decode(frame, modulo128=(self._modulo == 128)) + frame = AX25Frame.decode( + frame, modulo128=(self._modulo == 128) + ) if isinstance(frame, AX25InformationFrameMixin): # This is an I-frame return self._on_receive_iframe(frame) elif isinstance(frame, AX25SupervisoryFrameMixin): # This is an S-frame return self._on_receive_sframe(frame) - else: # pragma: no cover - self._log.warning('Dropping unrecognised frame: %s', frame) + else: # pragma: no cover + self._log.warning( + "Dropping unrecognised frame: %s", frame + ) else: # No connection in progress, send a DM. return self._send_dm() @@ -287,7 +332,7 @@ def _on_receive_iframe(self, frame): # repeating the indication of the busy condition." if self._local_busy: self._log.warning( - 'Dropping I-frame during busy condition: %s', frame + "Dropping I-frame during busy condition: %s", frame ) self._send_rnr_notification() return @@ -309,8 +354,9 @@ def _on_receive_iframe(self, frame): # the received frame)." # # We need to also be mindful of the number of outstanding frames here! - if len(self._pending_data) and (len(self._pending_iframes) \ - < self._max_outstanding): + if len(self._pending_data) and ( + len(self._pending_iframes) < self._max_outstanding + ): return self._send_next_iframe() # "b) If there are no outstanding I frames, the receiving TNC sends @@ -325,13 +371,15 @@ def _on_receive_sframe(self, frame): pass def _on_receive_test(self, frame): - self._log.debug('Received TEST response: %s', frame) + self._log.debug("Received TEST response: %s", frame) if self._testframe_handler: self._testframe_handler._on_receive(frame) def _on_receive_sabm(self, frame): modulo128 = isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) - self._log.debug('Received SABM(E): %s (extended=%s)', frame, modulo128) + self._log.debug( + "Received SABM(E): %s (extended=%s)", frame, modulo128 + ) if modulo128: # If we don't know the protocol of the peer, we can safely assume # AX.25 2.2 now. @@ -346,7 +394,7 @@ def _on_receive_sabm(self, frame): # with a FRMR". W bit indicates the control field was not # understood. self._log.warning( - 'Sending FRMR as we are not in AX.25 2.2 mode' + "Sending FRMR as we are not in AX.25 2.2 mode" ) return self._send_frmr(frame, w=True) @@ -356,7 +404,7 @@ def _on_receive_sabm(self, frame): # "If the TNC is not capable of accepting a SABME, it # responds with a DM frame". self._log.warning( - 'Sending DM as peer is not in AX.25 2.2 mode' + "Sending DM as peer is not in AX.25 2.2 mode" ) return self._send_dm() @@ -403,11 +451,11 @@ def _init_connection(self, modulo128): def _reset_connection_state(self): # Reset our state - self._send_state = 0 # AKA V(S) - self._send_seq = 0 # AKA N(S) - self._recv_state = 0 # AKA V(R) - self._recv_seq = 0 # AKA N(R) - self._ack_state = 0 # AKA V(A) + self._send_state = 0 # AKA V(S) + self._send_seq = 0 # AKA N(S) + self._recv_state = 0 # AKA V(R) + self._recv_seq = 0 # AKA N(R) + self._ack_state = 0 # AKA V(A) # Unacknowledged I-frames to be ACKed self._pending_iframes = {} @@ -421,15 +469,13 @@ def _set_conn_state(self, state): return # Update the state - self._log.info('Connection state change: %s→%s', - self._state, state) + self._log.info("Connection state change: %s→%s", self._state, state) self._state = state # Notify any observers that the state changed self.connect_state_changed.emit( - station=self._station(), - peer=self, - state=self._state) + station=self._station(), peer=self, state=self._state + ) def _on_disconnect(self): """ @@ -449,7 +495,7 @@ def _on_receive_disc(self): Handle a disconnect request from this peer. """ # Send a UA and set ourselves as disconnected - self._log.info('Received DISC from peer') + self._log.info("Received DISC from peer") self._on_disconnect() self._send_ua() @@ -458,7 +504,7 @@ def _on_receive_dm(self): Handle a disconnect request from this peer. """ # Set ourselves as disconnected - self._log.info('Received DM from peer') + self._log.info("Received DM from peer") self._on_disconnect() def _on_receive_xid(self, frame): @@ -468,56 +514,54 @@ def _on_receive_xid(self, frame): if self._station().protocol != AX25Version.AX25_22: # Not supporting this in AX.25 2.0 mode self._log.warning( - 'Received XID from peer, we are not in AX.25 2.2 mode' + "Received XID from peer, we are not in AX.25 2.2 mode" ) return self._send_frmr(frame, w=True) if self._state in ( - self.AX25PeerState.CONNECTING, - self.AX25PeerState.DISCONNECTING + self.AX25PeerState.CONNECTING, + self.AX25PeerState.DISCONNECTING, ): # AX.25 2.2 sect 4.3.3.7: "A station receiving an XID command # returns an XID response unless a UA response to a mode setting # command is awaiting transmission, or a FRMR condition exists". - self._log.warning( - 'UA is pending, dropping received XID' - ) + self._log.warning("UA is pending, dropping received XID") return # TODO: figure out XID and send an appropriate response. - self._log.error('TODO: implement XID') + self._log.error("TODO: implement XID") return self._send_frmr(frame, w=True) def _send_dm(self): """ Send a DM frame to the remote station. """ - self._log.debug('Sending DM') + self._log.debug("Sending DM") self._transmit_frame( - AX25DisconnectModeFrame( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path - ) + AX25DisconnectModeFrame( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + ) ) def _send_ua(self): """ Send a UA frame to the remote station. """ - self._log.debug('Sending UA') + self._log.debug("Sending UA") self._transmit_frame( - AX25UnnumberedAcknowledgeFrame( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path - ) + AX25UnnumberedAcknowledgeFrame( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + ) ) def _send_frmr(self, frame, w=False, x=False, y=False, z=False): """ Send a FRMR frame to the remote station. """ - self._log.debug('Sending FRMR in reply to %s', frame) + self._log.debug("Sending FRMR in reply to %s", frame) # AX.25 2.0 sect 2.4.5: "After sending the FRMR frame, the sending DXE # will enter the frame reject condition. This condition is cleared when @@ -529,16 +573,19 @@ def _send_frmr(self, frame, w=False, x=False, y=False, z=False): # See https://www.tapr.org/pub_ax25.html self._transmit_frame( - AX25FrameRejectFrame( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path, - w=w, x=x, y=y, z=z, - vr=self._recv_state, - vs=self._send_state, - frmr_cr=frame.header.cr, - frmr_control=frame.control - ) + AX25FrameRejectFrame( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + w=w, + x=x, + y=y, + z=z, + vr=self._recv_state, + vs=self._send_state, + frmr_cr=frame.header.cr, + frmr_control=frame.control, + ) ) def _cancel_rr_notification(self): @@ -554,8 +601,9 @@ def _schedule_rr_notification(self): Schedule a RR notification frame to be sent. """ self._cancel_rr_notification() - self._rr_notification_timeout_handle = \ - self._loop.call_later(self._send_rr_notification) + self._rr_notification_timeout_handle = self._loop.call_later( + self._send_rr_notification + ) def _send_rr_notification(self): """ @@ -563,12 +611,13 @@ def _send_rr_notification(self): """ self._cancel_rr_notification() self._transmit_frame( - self._RRFrameClass( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path, - pf=False, nr=self._recv_state - ) + self._RRFrameClass( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + pf=False, + nr=self._recv_state, + ) ) def _send_rnr_notification(self): @@ -578,13 +627,13 @@ def _send_rnr_notification(self): now = self._loop.time() if (now - self._last_rnr_sent) > self._rnr_interval: self._transmit_frame( - self._RNRFrameClass( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path, - nr=self._recv_seq, - pf=False - ) + self._RNRFrameClass( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + nr=self._recv_seq, + pf=False, + ) ) self._last_rnr_sent = now @@ -592,16 +641,17 @@ def _send_next_iframe(self): """ Send the next I-frame, if there aren't too many frames pending. """ - if not len(self._pending_data) or (len(self._pending_iframes) \ - >= self._max_outstanding): - self._log.debug('Waiting for pending I-frames to be ACKed') + if not len(self._pending_data) or ( + len(self._pending_iframes) >= self._max_outstanding + ): + self._log.debug("Waiting for pending I-frames to be ACKed") return # AX.25 2.2 spec 6.4.1: "Whenever a TNC has an I frame to transmit, # it sends the I frame with the N(S) of the control field equal to # its current send state variable V(S)…" ns = self._send_state - assert ns not in self._pending_iframes, 'Duplicate N(S) pending' + assert ns not in self._pending_iframes, "Duplicate N(S) pending" # Retrieve the next pending I-frame payload (pid, payload) = self._pending_data.pop(0) @@ -612,8 +662,7 @@ def _send_next_iframe(self): # "After the I frame is sent, the send state variable is incremented # by one." - self._send_state = (self._send_state + 1) \ - % self._modulo + self._send_state = (self._send_state + 1) % self._modulo def _transmit_iframe(self, ns): """ @@ -621,15 +670,16 @@ def _transmit_iframe(self, ns): """ (pid, payload) = self._pending_iframes[ns] self._transmit_frame( - self._IFrameClass( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path, - nr=self._recv_seq, - ns=ns, - pf=False, - pid=pid, payload=payload - ) + self._IFrameClass( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + nr=self._recv_seq, + ns=ns, + pf=False, + pid=pid, + payload=payload, + ) ) def _transmit_frame(self, frame): diff --git a/aioax25/station.py b/aioax25/station.py index bea91f6..688eff1 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -31,24 +31,29 @@ class AX25Station(object): parameter; then call the attach method. """ - def __init__(self, interface, - # Station call-sign and SSID - callsign, ssid=None, - # Parameters (AX.25 2.2 sect 6.7.2) - max_ifield=256, # aka N1 - max_retries=10, # aka N2, value from figure 4.5 - # k value, for mod128 and mod8 connections - max_outstanding_mod8=7, - max_outstanding_mod128=127, - # Timer parameters - idle_timeout=900.0, # Idle timeout before we "forget" peers - rr_delay=10.0, # Delay between I-frame and RR - rr_interval=30.0, # Poll interval when peer in busy state - rnr_interval=10.0, # Delay between RNRs when busy - # Protocol version to use for our station - protocol=AX25Version.AX25_22, - # IOLoop and logging - log=None, loop=None): + def __init__( + self, + interface, + # Station call-sign and SSID + callsign, + ssid=None, + # Parameters (AX.25 2.2 sect 6.7.2) + max_ifield=256, # aka N1 + max_retries=10, # aka N2, value from figure 4.5 + # k value, for mod128 and mod8 connections + max_outstanding_mod8=7, + max_outstanding_mod128=127, + # Timer parameters + idle_timeout=900.0, # Idle timeout before we "forget" peers + rr_delay=10.0, # Delay between I-frame and RR + rr_interval=30.0, # Poll interval when peer in busy state + rnr_interval=10.0, # Delay between RNRs when busy + # Protocol version to use for our station + protocol=AX25Version.AX25_22, + # IOLoop and logging + log=None, + loop=None, + ): if log is None: log = logging.getLogger(self.__class__.__module__) @@ -59,8 +64,9 @@ def __init__(self, interface, # Ensure we are running a supported version of AX.25 protocol = AX25Version(protocol) if protocol not in (AX25Version.AX25_20, AX25Version.AX25_22): - raise ValueError('%r not a supported AX.25 protocol version'\ - % protocol.value) + raise ValueError( + "%r not a supported AX.25 protocol version" % protocol.value + ) # Configuration parameters self._address = AX25Address.decode(callsign, ssid).normalised @@ -102,23 +108,28 @@ def attach(self): Connect the station to the interface. """ interface = self._interface() - interface.bind(self._on_receive, - callsign=self.address.callsign, - ssid=self.address.ssid, - regex=False) + interface.bind( + self._on_receive, + callsign=self.address.callsign, + ssid=self.address.ssid, + regex=False, + ) def detach(self): """ Disconnect from the interface. """ interface = self._interface() - interface.unbind(self._on_receive, - callsign=self.address.callsign, - ssid=self.address.ssid, - regex=False) + interface.unbind( + self._on_receive, + callsign=self.address.callsign, + ssid=self.address.ssid, + regex=False, + ) - def getpeer(self, callsign, ssid=None, repeaters=None, - create=True, **kwargs): + def getpeer( + self, callsign, ssid=None, repeaters=None, create=True, **kwargs + ): """ Retrieve an instance of a peer context. This creates the peer object if it doesn't already exist unless create is set to False @@ -133,20 +144,25 @@ def getpeer(self, callsign, ssid=None, repeaters=None, pass # Not there, so set some defaults, then create - kwargs.setdefault('max_ifield', self._max_ifield) - kwargs.setdefault('max_retries', self._max_retries) - kwargs.setdefault('max_outstanding_mod8', self._max_outstanding_mod8) - kwargs.setdefault('max_outstanding_mod128', self._max_outstanding_mod128) - kwargs.setdefault('rr_delay', self._rr_delay) - kwargs.setdefault('rr_interval', self._rr_interval) - kwargs.setdefault('rnr_interval', self._rnr_interval) - kwargs.setdefault('idle_timeout', self._idle_timeout) - kwargs.setdefault('protocol', AX25Version.UNKNOWN) - peer = AX25Peer(self, address, - repeaters=AX25Path(*(repeaters or [])), - log=self._log.getChild('peer.%s' % address), - loop=self._loop, - **kwargs) + kwargs.setdefault("max_ifield", self._max_ifield) + kwargs.setdefault("max_retries", self._max_retries) + kwargs.setdefault("max_outstanding_mod8", self._max_outstanding_mod8) + kwargs.setdefault( + "max_outstanding_mod128", self._max_outstanding_mod128 + ) + kwargs.setdefault("rr_delay", self._rr_delay) + kwargs.setdefault("rr_interval", self._rr_interval) + kwargs.setdefault("rnr_interval", self._rnr_interval) + kwargs.setdefault("idle_timeout", self._idle_timeout) + kwargs.setdefault("protocol", AX25Version.UNKNOWN) + peer = AX25Peer( + self, + address, + repeaters=AX25Path(*(repeaters or [])), + log=self._log.getChild("peer.%s" % address), + loop=self._loop, + **kwargs + ) self._peers[address] = peer return peer @@ -162,16 +178,17 @@ def _on_receive(self, frame, **kwargs): """ if frame.header.cr: # This is a command frame - self._log.debug('Checking command frame sub-class: %s', frame) + self._log.debug("Checking command frame sub-class: %s", frame) if isinstance(frame, AX25TestFrame): # A TEST request frame, context not required return self._on_receive_test(frame) # If we're still here, then we don't handle unsolicited frames # of this type, so pass it to a handler if we have one. - peer = self.getpeer(frame.header.source, - repeaters=frame.header.repeaters.reply) - self._log.debug('Passing frame to peer %s: %s', peer.address, frame) + peer = self.getpeer( + frame.header.source, repeaters=frame.header.repeaters.reply + ) + self._log.debug("Passing frame to peer %s: %s", peer.address, frame) peer._on_receive(frame) def _on_receive_test(self, frame): @@ -179,14 +196,14 @@ def _on_receive_test(self, frame): Handle a TEST frame. """ # The frame is a test request. - self._log.debug('Responding to test frame: %s', frame) + self._log.debug("Responding to test frame: %s", frame) interface = self._interface() interface.transmit( - AX25TestFrame( - destination=frame.header.source, - source=self.address, - repeaters=frame.header.repeaters.reply, - payload=frame.payload, - cr=False - ) + AX25TestFrame( + destination=frame.header.source, + source=self.address, + repeaters=frame.header.repeaters.reply, + payload=frame.payload, + cr=False, + ) ) diff --git a/aioax25/version.py b/aioax25/version.py index 808d0c1..1493388 100644 --- a/aioax25/version.py +++ b/aioax25/version.py @@ -9,7 +9,7 @@ class AX25Version(enum.Enum): - UNKNOWN = '0.0' # The version is not known - AX25_10 = '1.x' # AX.25 1.x in use - AX25_20 = '2.0' # AX.25 2.0 in use - AX25_22 = '2.2' # AX.25 2.2 in use + UNKNOWN = "0.0" # The version is not known + AX25_10 = "1.x" # AX.25 1.x in use + AX25_20 = "2.0" # AX.25 2.0 in use + AX25_22 = "2.2" # AX.25 2.2 in use diff --git a/tests/mocks.py b/tests/mocks.py index 4a54a33..ad0ba2e 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 + class DummyInterface(object): def __init__(self): self.bind_calls = [] diff --git a/tests/test_frame/test_ax25address.py b/tests/test_frame/test_ax25address.py index e196c70..9952ec0 100644 --- a/tests/test_frame/test_ax25address.py +++ b/tests/test_frame/test_ax25address.py @@ -118,7 +118,7 @@ def test_decode_str_override_ssid(): # This will override the -12 above ssid=9, ) - eq_(addr._ssid, 9) + assert addr._ssid == 9 def test_decode_str_ch(): diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 1312a2e..8eaeaab 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -8,12 +8,8 @@ AX25UnnumberedFrame, AX258BitReceiveReadyFrame, AX2516BitReceiveReadyFrame, - AX258BitSupervisoryFrame, - AX25FrameHeader, AX258BitRejectFrame, - AX2516BitSupervisoryFrame, AX2516BitRejectFrame, - AX258BitReceiveReadyFrame, AX258BitInformationFrame, AX2516BitInformationFrame, AX25DisconnectModeFrame, @@ -186,7 +182,7 @@ def test_decode_sabm_payload(): Test that a SABM frame forbids payload. """ try: - frame = AX25Frame.decode( + AX25Frame.decode( from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source @@ -512,7 +508,7 @@ def test_decode_xid_truncated_header(): Test that decoding a XID with truncated header fails. """ try: - frame = AX25Frame.decode( + AX25Frame.decode( from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source @@ -532,7 +528,7 @@ def test_decode_xid_truncated_payload(): Test that decoding a XID with truncated payload fails. """ try: - frame = AX25Frame.decode( + AX25Frame.decode( from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source @@ -553,7 +549,7 @@ def test_decode_xid_truncated_param_header(): Test that decoding a XID with truncated parameter header fails. """ try: - frame = AX25Frame.decode( + AX25Frame.decode( from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source @@ -574,7 +570,7 @@ def test_decode_xid_truncated_param_value(): Test that decoding a XID with truncated parameter value fails. """ try: - frame = AX25Frame.decode( + AX25Frame.decode( from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source diff --git a/tests/test_frame/test_ax25frameheader.py b/tests/test_frame/test_ax25frameheader.py index 85ba848..1723486 100644 --- a/tests/test_frame/test_ax25frameheader.py +++ b/tests/test_frame/test_ax25frameheader.py @@ -47,10 +47,10 @@ def test_decode_legacy(): ) assert header.legacy assert header.cr - eq_(header.destination, AX25Address("VK4BWI", ch=True)) - eq_(header.source, AX25Address("VK4MSL", extension=True, ch=True)) - eq_(len(header.repeaters), 0) - eq_(data, b"frame data goes here") + assert header.destination == AX25Address("VK4BWI", ch=True) + assert header.source == AX25Address("VK4MSL", extension=True, ch=True) + assert len(header.repeaters) == 0 + assert data == b"frame data goes here" def test_encode_legacy(): diff --git a/tests/test_station/test_attach_detach.py b/tests/test_station/test_attach_detach.py index 9d916a1..8713f76 100644 --- a/tests/test_station/test_attach_detach.py +++ b/tests/test_station/test_attach_detach.py @@ -11,7 +11,7 @@ def test_attach(): Test attach binds the station to the interface. """ interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL-5') + station = AX25Station(interface=interface, callsign="VK4MSL-5") station.attach() eq_(len(interface.bind_calls), 1) @@ -20,19 +20,18 @@ def test_attach(): (args, kwargs) = interface.bind_calls.pop() eq_(args, (station._on_receive,)) - eq_(set(kwargs.keys()), set([ - 'callsign', 'ssid', 'regex' - ])) - eq_(kwargs['callsign'], 'VK4MSL') - eq_(kwargs['ssid'], 5) - eq_(kwargs['regex'], False) + eq_(set(kwargs.keys()), set(["callsign", "ssid", "regex"])) + eq_(kwargs["callsign"], "VK4MSL") + eq_(kwargs["ssid"], 5) + eq_(kwargs["regex"], False) + def test_detach(): """ Test attach unbinds the station to the interface. """ interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL-5') + station = AX25Station(interface=interface, callsign="VK4MSL-5") station.detach() eq_(len(interface.bind_calls), 0) @@ -41,9 +40,7 @@ def test_detach(): (args, kwargs) = interface.unbind_calls.pop() eq_(args, (station._on_receive,)) - eq_(set(kwargs.keys()), set([ - 'callsign', 'ssid', 'regex' - ])) - eq_(kwargs['callsign'], 'VK4MSL') - eq_(kwargs['ssid'], 5) - eq_(kwargs['regex'], False) + eq_(set(kwargs.keys()), set(["callsign", "ssid", "regex"])) + eq_(kwargs["callsign"], "VK4MSL") + eq_(kwargs["ssid"], 5) + eq_(kwargs["regex"], False) diff --git a/tests/test_station/test_constructor.py b/tests/test_station/test_constructor.py index df8e024..6ea9c0b 100644 --- a/tests/test_station/test_constructor.py +++ b/tests/test_station/test_constructor.py @@ -2,7 +2,6 @@ from aioax25.station import AX25Station from aioax25.version import AX25Version -from aioax25.frame import AX25Address from nose.tools import eq_ from ..mocks import DummyInterface @@ -12,36 +11,45 @@ def test_constructor_log(): """ Test the AX25Constructor uses the log given. """ + class DummyLogger(object): pass log = DummyLogger() interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL', ssid=3, - log=log) + station = AX25Station( + interface=interface, callsign="VK4MSL", ssid=3, log=log + ) assert station._log is log + def test_constructor_loop(): """ Test the AX25Constructor uses the IO loop given. """ + class DummyLoop(object): pass loop = DummyLoop() interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL', ssid=3, - loop=loop) + station = AX25Station( + interface=interface, callsign="VK4MSL", ssid=3, loop=loop + ) assert station._loop is loop + def test_constructor_protocol(): """ Test the AX25Constructor validates the protocol """ try: - AX25Station(interface=DummyInterface(), callsign='VK4MSL', ssid=3, - protocol=AX25Version.AX25_10) - assert False, 'Should not have worked' + AX25Station( + interface=DummyInterface(), + callsign="VK4MSL", + ssid=3, + protocol=AX25Version.AX25_10, + ) + assert False, "Should not have worked" except ValueError as e: - eq_(str(e), - "'1.x' not a supported AX.25 protocol version") + eq_(str(e), "'1.x' not a supported AX.25 protocol version") diff --git a/tests/test_station/test_droppeer.py b/tests/test_station/test_droppeer.py index 64ac4d7..6e70d0e 100644 --- a/tests/test_station/test_droppeer.py +++ b/tests/test_station/test_droppeer.py @@ -3,7 +3,6 @@ from aioax25.station import AX25Station from aioax25.frame import AX25Address -from nose.tools import eq_ from ..mocks import DummyInterface, DummyPeer @@ -11,13 +10,13 @@ def test_known_peer_fetch_instance(): """ Test calling _drop_peer removes the peer """ - station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') - mypeer = DummyPeer(AX25Address('VK4BWI')) + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") + mypeer = DummyPeer(AX25Address("VK4BWI")) # Inject the peer station._peers[mypeer._address] = mypeer # Drop the peer - peer = station._drop_peer(mypeer) + station._drop_peer(mypeer) assert mypeer._address not in station._peers assert mypeer.address_read diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py index 2340b41..731a8db 100644 --- a/tests/test_station/test_getpeer.py +++ b/tests/test_station/test_getpeer.py @@ -12,21 +12,24 @@ def test_unknown_peer_nocreate_keyerror(): """ Test fetching an unknown peer with create=False raises KeyError """ - station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") try: - station.getpeer('VK4BWI', create=False) - assert False, 'Should not have worked' + station.getpeer("VK4BWI", create=False) + assert False, "Should not have worked" except KeyError as e: - eq_(str(e), 'AX25Address(callsign=VK4BWI, ssid=0, '\ - 'ch=False, res0=True, res1=True, extension=False)') + eq_( + str(e), + "AX25Address(callsign=VK4BWI, ssid=0, " + "ch=False, res0=True, res1=True, extension=False)", + ) def test_unknown_peer_create_instance(): """ Test fetching an unknown peer with create=True generates peer """ - station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') - peer = station.getpeer('VK4BWI', create=True) + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") + peer = station.getpeer("VK4BWI", create=True) assert isinstance(peer, AX25Peer) @@ -34,12 +37,12 @@ def test_known_peer_fetch_instance(): """ Test fetching an known peer returns that known peer """ - station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') - mypeer = DummyPeer(AX25Address('VK4BWI')) + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") + mypeer = DummyPeer(AX25Address("VK4BWI")) # Inject the peer station._peers[mypeer._address] = mypeer # Retrieve the peer instance - peer = station.getpeer('VK4BWI') + peer = station.getpeer("VK4BWI") assert peer is mypeer diff --git a/tests/test_station/test_properties.py b/tests/test_station/test_properties.py index 2c19f92..21de4ac 100644 --- a/tests/test_station/test_properties.py +++ b/tests/test_station/test_properties.py @@ -12,13 +12,17 @@ def test_address(): """ Test the address of the station is set from the constructor. """ - station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5') - eq_(station.address, AX25Address(callsign='VK4MSL', ssid=5)) + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") + eq_(station.address, AX25Address(callsign="VK4MSL", ssid=5)) + def test_protocol(): """ Test the protocol of the station is set from the constructor. """ - station = AX25Station(interface=DummyInterface(), callsign='VK4MSL-5', - protocol=AX25Version.AX25_20) + station = AX25Station( + interface=DummyInterface(), + callsign="VK4MSL-5", + protocol=AX25Version.AX25_20, + ) eq_(station.protocol, AX25Version.AX25_20) diff --git a/tests/test_station/test_receive.py b/tests/test_station/test_receive.py index 07735b3..34dbec6 100644 --- a/tests/test_station/test_receive.py +++ b/tests/test_station/test_receive.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 from aioax25.station import AX25Station -from aioax25.frame import AX25Address, AX25TestFrame, \ - AX25UnnumberedInformationFrame +from aioax25.frame import ( + AX25Address, + AX25TestFrame, + AX25UnnumberedInformationFrame, +) from nose.tools import eq_ from ..mocks import DummyInterface, DummyPeer @@ -13,15 +16,17 @@ def test_testframe_cmd_echo(): Test passing a test frame with CR=True triggers a reply frame. """ interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL-5') + station = AX25Station(interface=interface, callsign="VK4MSL-5") # Pass in a frame - station._on_receive(frame=AX25TestFrame( - destination='VK4MSL-5', - source='VK4MSL-7', - cr=True, - payload=b'This is a test frame' - )) + station._on_receive( + frame=AX25TestFrame( + destination="VK4MSL-5", + source="VK4MSL-7", + cr=True, + payload=b"This is a test frame", + ) + ) # There should be no peers eq_(station._peers, {}) @@ -38,11 +43,11 @@ def test_testframe_cmd_echo(): # The reply should have the source/destination swapped and the # CR bit cleared. - assert isinstance(frame, AX25TestFrame), 'Not a test frame' + assert isinstance(frame, AX25TestFrame), "Not a test frame" eq_(frame.header.cr, False) - eq_(frame.header.destination, AX25Address('VK4MSL', ssid=7)) - eq_(frame.header.source, AX25Address('VK4MSL', ssid=5)) - eq_(frame.payload, b'This is a test frame') + eq_(frame.header.destination, AX25Address("VK4MSL", ssid=7)) + eq_(frame.header.source, AX25Address("VK4MSL", ssid=5)) + eq_(frame.payload, b"This is a test frame") def test_route_testframe_reply(): @@ -50,25 +55,26 @@ def test_route_testframe_reply(): Test passing a test frame reply routes to the appropriate AX25Peer instance. """ interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL-5') + station = AX25Station(interface=interface, callsign="VK4MSL-5") # Stub out _on_test_frame def stub_on_test_frame(*args, **kwargs): - assert False, 'Should not have been called' + assert False, "Should not have been called" + station._on_test_frame = stub_on_test_frame # Inject a couple of peers - peer1 = DummyPeer(AX25Address('VK4MSL', ssid=7)) - peer2 = DummyPeer(AX25Address('VK4BWI', ssid=7)) + peer1 = DummyPeer(AX25Address("VK4MSL", ssid=7)) + peer2 = DummyPeer(AX25Address("VK4BWI", ssid=7)) station._peers[peer1._address] = peer1 station._peers[peer2._address] = peer2 # Pass in the message txframe = AX25TestFrame( - destination='VK4MSL-5', - source='VK4MSL-7', + destination="VK4MSL-5", + source="VK4MSL-7", cr=False, - payload=b'This is a test frame' + payload=b"This is a test frame", ) station._on_receive(frame=txframe) @@ -91,25 +97,27 @@ def test_route_incoming_msg(): Test passing a frame routes to the appropriate AX25Peer instance. """ interface = DummyInterface() - station = AX25Station(interface=interface, callsign='VK4MSL-5') + station = AX25Station(interface=interface, callsign="VK4MSL-5") # Stub out _on_test_frame def stub_on_test_frame(*args, **kwargs): - assert False, 'Should not have been called' + assert False, "Should not have been called" + station._on_test_frame = stub_on_test_frame # Inject a couple of peers - peer1 = DummyPeer(AX25Address('VK4MSL', ssid=7)) - peer2 = DummyPeer(AX25Address('VK4BWI', ssid=7)) + peer1 = DummyPeer(AX25Address("VK4MSL", ssid=7)) + peer2 = DummyPeer(AX25Address("VK4BWI", ssid=7)) station._peers[peer1._address] = peer1 station._peers[peer2._address] = peer2 # Pass in the message txframe = AX25UnnumberedInformationFrame( - destination='VK4MSL-5', - source='VK4BWI-7', - cr=True, pid=0xab, - payload=b'This is a test frame' + destination="VK4MSL-5", + source="VK4BWI-7", + cr=True, + pid=0xAB, + payload=b"This is a test frame", ) station._on_receive(frame=txframe) From ea8a925582daf4dd0ba62f02fcbb6e5b085e9c87 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 22:16:53 +1000 Subject: [PATCH 041/207] peer: Fix RR frame scheduling --- aioax25/peer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 61405e0..29806f2 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -602,7 +602,7 @@ def _schedule_rr_notification(self): """ self._cancel_rr_notification() self._rr_notification_timeout_handle = self._loop.call_later( - self._send_rr_notification + self._rr_delay, self._send_rr_notification ) def _send_rr_notification(self): From d7a1fc0f6a7b06066a716037ddd87d1adb6b3c63 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 21 Nov 2019 22:17:06 +1000 Subject: [PATCH 042/207] peer: Implement ping method and handler --- aioax25/peer.py | 154 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 29806f2..bc908ec 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -204,6 +204,22 @@ def weight_path(self, path, weight, relative=True): weight += self._tx_path_score.get(path, 0) self._tx_path_score[path] = weight + def ping(self, payload=None, timeout=30.0, callback=None): + """ + Ping the peer and wait for a response. + """ + if self._testframe_handler is not None: + raise RuntimeError("Existing ping request in progress") + + handler = AX25PeerTestHandler(self, bytes(payload or b""), timeout) + handler.done_sig.connect(self._on_test_done) + + if callback is not None: + handler.done_sig.connect(callback) + + handler._go() + return + def _cancel_idle_timeout(self): """ Cancel the idle timeout handle @@ -372,8 +388,23 @@ def _on_receive_sframe(self, frame): def _on_receive_test(self, frame): self._log.debug("Received TEST response: %s", frame) - if self._testframe_handler: - self._testframe_handler._on_receive(frame) + if not self._testframe_handler: + return + + handler = self._testframe_handler() + if not handler: + return + + self.handler._on_receive(frame) + + def _on_test_done(self, handler, **kwargs): + if not self._testframe_handler: + return + + if handler is not self._testframe_handler(): + return + + self._testframe_handler = None def _on_receive_sabm(self, frame): modulo128 = isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) @@ -682,7 +713,122 @@ def _transmit_iframe(self, ns): ) ) - def _transmit_frame(self, frame): + def _transmit_frame(self, frame, callback=None): # Kick off the idle timer self._reset_idle_timeout() - return self._station()._interface().transmit(frame) + return self._station()._interface().transmit(frame, callback=None) + + +class AX25PeerTestHandler(object): + """ + This class is used to manage the sending of a TEST frame to the peer + station and receiving the peer reply. Round-trip time is made available + in case the calling application needs it. + """ + + def __init__(self, peer, payload, timeout): + self._peer = peer + self._timeout = timeout + self._timeout_handle = None + + # Create the frame to send + self._tx_frame = AX25TestFrame( + destination=peer.address, + source=peer._station().address, + repeaters=self.reply_path, + payload=payload, + cr=True, + ) + + # Store the received frame here + self._rx_frame = None + + # Time of transmission + self._tx_time = None + + # Time of reception + self._rx_time = None + + # Flag indicating we are done + self._done = False + + # Signal on "done" or time-out. + self.done_sig = Signal() + + @property + def peer(self): + """ + Return the peer being pinged + """ + return self._peer + + @property + def tx_time(self): + """ + Return the transmit time, as measured by the IO loop + """ + return self._tx_time + + @property + def tx_frame(self): + """ + Return the transmitted frame + """ + return self._tx_frame + + @property + def rx_time(self): + """ + Return the receive time, as measured by the IO loop + """ + return self._rx_time + + @property + def rx_frame(self): + """ + Return the received frame + """ + return self._rx_frame + + def _go(self): + """ + Start the transmission. + """ + self.peer._transmit_frame(self.tx_frame, callback=self._transmit_done) + + # Start the time-out timer + self._timeout_handle = self._peer._loop.call_later( + self._timeout, self._on_timeout + ) + + def _transmit_done(self, *args, **kwargs): + """ + Note the time that the transmission took place. + """ + self._tx_time = self._peer._loop.time() + + def _on_receive(self, frame, **kwargs): + """ + Process the incoming frame. + """ + if self._done: + # We are done + return + + # Mark ourselves done + self._done = True + + # Stop the clock! + self._timeout_handle.cancel() + self._rx_time = self._peer._loop.time() + + # Stash the result and notify the caller + self._rx_frame = frame + self.done_sig.emit(handler=self) + + def _on_timeout(self): + """ + Process a time-out + """ + self._done = True + self.done_sig.emit(handler=self) From 953fd549dabbe9d2dab5772144318465c9268742 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 11:20:22 +1000 Subject: [PATCH 043/207] frame: Expand on XID parsing/generation It'd be nice to handle integer XID values, but ISO-8885 uses "f***ed up endian", that is to say, it's sometimes little-endian (e.g. Classes of Procedures) and sometimes big-endian (e.g. I-Field Length). --- aioax25/frame.py | 172 ++++++++++++++++++----------- tests/test_frame/test_ax25frame.py | 100 +++++++++++++---- 2 files changed, 186 insertions(+), 86 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 3006ff5..892d872 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -43,6 +43,7 @@ import re import time +import enum from collections.abc import Sequence # Frame type classes @@ -1257,82 +1258,123 @@ class AX25DisconnectModeFrame(AX25BaseUnnumberedFrame): AX25UnnumberedFrame.register(AX25DisconnectModeFrame) -class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): +class AX25XIDParameterIdentifier(enum.Enum): + """ + Known values of PI in XID frames. """ - Exchange Identification frame. - This frame is used to negotiate TNC features. + # Negotiates half/full duplex operation + ClassesOfProcedure = 2 + + # Selects between REJ, SREJ or both + HDLCOptionalFunctions = 3 + + # Sets the outgoing I field length in bits (not bytes) + IFieldLengthTransmit = 5 + + # Sets the incoming I field length in bits (not bytes) + IFieldLengthReceive = 6 + + # Sets the number of outstanding I-frames (k) + WindowSizeReceive = 8 + + # Sets the duration of the Wait For Acknowledge (T1) timer + AcknowledgeTimer = 9 + + # Sets the retry count (N1) + Retries = 10 + + def __int__(self): + """ + Return the value of the PI. + """ + return self.value + + +class AX25XIDParameter(object): + """ + Representation of a single XID parameter. """ - MODIFIER = 0b10101111 + @classmethod + def decode(cls, data): + """ + Decode the parameter value given, return the parameter and the + remaining data. + """ + if len(data) < 2: + raise ValueError("Insufficient data for parameter") + + pi = data[0] + pl = data[1] + data = data[2:] + + if pl > 0: + if len(data) < pl: + raise ValueError("Parameter is truncated") + pv = data[0:pl] + data = data[pl:] + else: + pv = None + + return (cls(pi=pi, pv=pv), data) - class AX25XIDParameter(object): + def __init__(self, pi, pv): """ - Representation of a single XID parameter. + Create a new XID parameter """ + try: + pi = AX25XIDParameterIdentifier(pi) + except ValueError: + # Pass through the PI as given. + pass - @classmethod - def decode(cls, data): - """ - Decode the parameter value given, return the parameter and the - remaining data. - """ - if len(data) < 2: - raise ValueError("Insufficient data for parameter") + self._pi = pi + self._pv = pv - pi = data[0] - pl = data[1] - data = data[2:] + @property + def pi(self): + """ + Return the Parameter Identifier + """ + return self._pi - if pl > 0: - if len(data) < pl: - raise ValueError("Parameter is truncated") - pv = data[0:pl] - data = data[pl:] - else: - pv = None - - return (cls(pi=pi, pv=pv), data) - - def __init__(self, pi, pv): - """ - Create a new XID parameter - """ - self._pi = pi - self._pv = pv - - @property - def pi(self): - """ - Return the Parameter Identifier - """ - return self._pi - - @property - def pv(self): - """ - Return the Parameter Value - """ - return self._pv - - def __bytes__(self): - """ - Return the encoded parameter value. - """ - pv = self.pv - param = bytes([self.pi]) - if pv is None: - param += bytes([0]) - else: - param += bytes([len(pv)]) + pv + @property + def pv(self): + """ + Return the Parameter Value + """ + return self._pv + + def __bytes__(self): + """ + Return the encoded parameter value. + """ + pv = self.pv + param = bytes([int(self.pi)]) + + if pv is None: + param += bytes([0]) + else: + param += bytes([len(pv)]) + pv - return param + return param - def copy(self): - """ - Return a copy of this parameter. - """ - return self.__class__(pi=self.pi, pv=self.pv) + def copy(self): + """ + Return a copy of this parameter. + """ + return self.__class__(pi=self.pi, pv=self.pv) + + +class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): + """ + Exchange Identification frame. + + This frame is used to negotiate TNC features. + """ + + MODIFIER = 0b10101111 @classmethod def decode(cls, header, control, data): @@ -1350,7 +1392,7 @@ def decode(cls, header, control, data): parameters = [] while data: - (param, data) = cls.AX25XIDParameter.decode(data) + (param, data) = AX25XIDParameter.decode(data) parameters.append(param) return cls( diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 8eaeaab..4a70a50 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -16,6 +16,8 @@ AX25SetAsyncBalancedModeFrame, AX25TestFrame, AX25ExchangeIdentificationFrame, + AX25XIDParameter, + AX25XIDParameterIdentifier, ) from ..hex import from_hex, hex_cmp @@ -444,12 +446,13 @@ def test_encode_xid(): fi=0x82, gi=0x80, parameters=[ - AX25ExchangeIdentificationFrame.AX25XIDParameter( - pi=0x12, pv=bytes([0x34, 0x56]) - ), - AX25ExchangeIdentificationFrame.AX25XIDParameter( - pi=0x34, pv=None + AX25XIDParameter( + # Should be encoded to bytes for us + pi=AX25XIDParameterIdentifier.IFieldLengthTransmit, + pv=bytes([0x08, 0x00]), ), + AX25XIDParameter(pi=0x12, pv=bytes([0x34, 0x56])), + AX25XIDParameter(pi=0x34, pv=None), ], ) hex_cmp( @@ -459,12 +462,16 @@ def test_encode_xid(): "af" # Control "82" # Format indicator "80" # Group Ident - "00 06" # Group length + "00 0a" # Group length # First parameter + "05" # Parameter ID + "02" # Length + "08 00" # Value + # Second parameter "12" # Parameter ID "02" # Length "34 56" # Value - # Second parameter + # Third parameter "34" # Parameter ID "00", # Length (no value) ) @@ -493,14 +500,69 @@ def test_decode_xid(): assert frame.fi == 0x82 assert frame.gi == 0x80 assert len(frame.parameters) == 4 - assert frame.parameters[0].pi == 0x01 - assert frame.parameters[0].pv == b"\xaa" - assert frame.parameters[1].pi == 0x02 - assert frame.parameters[1].pv == b"\xbb" - assert frame.parameters[2].pi == 0x03 - assert frame.parameters[2].pv == b"\x11\x22" - assert frame.parameters[3].pi == 0x04 - assert frame.parameters[3].pv is None + + param = frame.parameters[0] + assert param.pi == 0x01 + assert param.pv == b"\xaa" + + param = frame.parameters[1] + assert param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure + assert param.pv == b"\xbb" + + param = frame.parameters[2] + assert param.pi == AX25XIDParameterIdentifier.HDLCOptionalFunctions + assert param.pv == b"\x11\x22" + + param = frame.parameters[3] + assert param.pi == 0x04 + assert param.pv is None + + +def test_decode_xid_fig46(): + """ + Test that we can decode the XID example from AX.25 2.2 figure 4.6. + """ + frame = AX25Frame.decode( + from_hex( + "9c 94 6e a0 40 40 e0" # Destination + "9c 6e 98 8a 9a 40 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 17" # GL + "02 02 00 20" + "03 03 86 a8 02" + "06 02 04 00" + "08 01 02" + "09 02 10 00" + "0a 01 03" + ) + ) + assert len(frame.parameters) == 6 + + param = frame.parameters[0] + assert param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure + assert param.pv == b"\x00\x20" + + param = frame.parameters[1] + assert param.pi == AX25XIDParameterIdentifier.HDLCOptionalFunctions + assert param.pv == b"\x86\xa8\x02" + + param = frame.parameters[2] + assert param.pi == AX25XIDParameterIdentifier.IFieldLengthReceive + assert param.pv == b"\x04\x00" + + param = frame.parameters[3] + assert param.pi == AX25XIDParameterIdentifier.WindowSizeReceive + assert param.pv == b"\x02" + + param = frame.parameters[4] + assert param.pi == AX25XIDParameterIdentifier.AcknowledgeTimer + assert param.pv == b"\x10\x00" + + param = frame.parameters[5] + assert param.pi == AX25XIDParameterIdentifier.Retries + assert param.pv == b"\x03" def test_decode_xid_truncated_header(): @@ -597,12 +659,8 @@ def test_copy_xid(): fi=0x82, gi=0x80, parameters=[ - AX25ExchangeIdentificationFrame.AX25XIDParameter( - pi=0x12, pv=bytes([0x34, 0x56]) - ), - AX25ExchangeIdentificationFrame.AX25XIDParameter( - pi=0x34, pv=None - ), + AX25XIDParameter(pi=0x12, pv=bytes([0x34, 0x56])), + AX25XIDParameter(pi=0x34, pv=None), ], ) framecopy = frame.copy() From 72dd5d0dce7c5cdd0c46dea8c3bf674e7cbb959c Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 12:36:32 +1000 Subject: [PATCH 044/207] uint: Add unsigned integer encoding/decoding routines I do this a lot all over the place, so consoliate it here. --- aioax25/uint.py | 44 +++++++++++++++++++++++++++++++++++++++ tests/test_uint.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 aioax25/uint.py create mode 100644 tests/test_uint.py diff --git a/aioax25/uint.py b/aioax25/uint.py new file mode 100644 index 0000000..cad8a1b --- /dev/null +++ b/aioax25/uint.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +""" +Unsigned Integer encoding and decoding routines. + +Both endians of integer are used, and sometimes the fields are odd sizes like +the 24-bit fields in XID HDLC Optional Function fields. This allows encoding +or decoding of any integer, of any length, in either endianness. +""" + + +def encode(value, length=None, big_endian=False): + """ + Encode the given unsigned integer value as bytes, optionally of a given + length. + """ + + output = bytearray() + while (value != 0) if (length is None) else (length > 0): + output += bytes([value & 0xff]) + value >>= 8 + if length is not None: + length -= 1 + + if not output: + # No output, so return a null byte + output += b'\x00' + + if big_endian: + output.reverse() + + return bytes(output) + + +def decode(value, big_endian=False): + """ + Decode the given bytes as an unsigned integer. + """ + + output = 0 + for byte in (value if big_endian else reversed(value)): + output <<= 8 + output |= byte + return output diff --git a/tests/test_uint.py b/tests/test_uint.py new file mode 100644 index 0000000..3d5260e --- /dev/null +++ b/tests/test_uint.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 + +from aioax25.uint import encode, decode +from nose.tools import eq_ +from .hex import from_hex, hex_cmp + +def test_encode_zero(): + """ + Test encode generates at least one byte if given a zero. + """ + hex_cmp(encode(0), b'\x00') + +def test_encode_le_nolen(): + """ + Test encode represents a little-endian integer in as few bytes as needed. + """ + hex_cmp(encode(0x12345, big_endian=False), + from_hex('45 23 01')) + +def test_encode_le_len(): + """ + Test encode will generate an integer of the required size. + """ + hex_cmp(encode(0x12345, big_endian=False, length=4), + from_hex('45 23 01 00')) + +def test_encode_le_truncate(): + """ + Test encode will truncate an integer to the required size. + """ + hex_cmp(encode(0x123456789a, big_endian=False, length=4), + from_hex('9a 78 56 34')) + +def test_encode_be(): + """ + Test we can encode big-endian integers. + """ + hex_cmp(encode(0x11223344, big_endian=True), + from_hex('11 22 33 44')) + +def test_decode_be(): + """ + Test we can decode big-endian integers. + """ + eq_(decode(from_hex('11 22 33'), big_endian=True), 0x112233) + +def test_decode_le(): + """ + Test we can decode little-endian integers. + """ + eq_(decode(from_hex('11 22 33'), big_endian=False), 0x332211) From 1d1ff34c05faba0ee2398ed6763cf62bfe7a1eb4 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 12:49:36 +1000 Subject: [PATCH 045/207] frame: Use uint encode/decode for multi-byte fields. --- aioax25/frame.py | 11 ++++++----- aioax25/uint.py | 6 +++--- tests/test_uint.py | 30 +++++++++++++++++++----------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 892d872..bbf1561 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -46,6 +46,8 @@ import enum from collections.abc import Sequence +from . import uint + # Frame type classes @@ -340,8 +342,7 @@ def frame_payload(self): """ # The control field is sent in LITTLE ENDIAN format so as to avoid # S frames possibly getting confused with U frames. - control = self.control - return bytes([control & 0x00FF, (control >> 8) & 0x00FF]) + return uint.encode(self.control, big_endian=False, length=2) class AX25RawFrame(AX25Frame): @@ -1384,7 +1385,7 @@ def decode(cls, header, control, data): fi = data[0] gi = data[1] # Yep, GL is big-endian, just for a change! - gl = (data[2] << 8) | data[3] + gl = uint.decode(data[2:4], big_endian=True) data = data[4:] if len(data) != gl: @@ -1448,10 +1449,10 @@ def parameters(self): @property def frame_payload(self): parameters = b"".join([bytes(param) for param in self.parameters]) - gl = len(parameters) return ( super(AX25ExchangeIdentificationFrame, self).frame_payload - + bytes([self.fi, self.gi, (gl >> 8) & 0xFF, gl & 0xFF]) + + bytes([self.fi, self.gi]) + + uint.encode(len(parameters), length=2, big_endian=True) + parameters ) diff --git a/aioax25/uint.py b/aioax25/uint.py index cad8a1b..c384d4b 100644 --- a/aioax25/uint.py +++ b/aioax25/uint.py @@ -17,14 +17,14 @@ def encode(value, length=None, big_endian=False): output = bytearray() while (value != 0) if (length is None) else (length > 0): - output += bytes([value & 0xff]) + output += bytes([value & 0xFF]) value >>= 8 if length is not None: length -= 1 if not output: # No output, so return a null byte - output += b'\x00' + output += b"\x00" if big_endian: output.reverse() @@ -38,7 +38,7 @@ def decode(value, big_endian=False): """ output = 0 - for byte in (value if big_endian else reversed(value)): + for byte in value if big_endian else reversed(value): output <<= 8 output |= byte return output diff --git a/tests/test_uint.py b/tests/test_uint.py index 3d5260e..46252d0 100644 --- a/tests/test_uint.py +++ b/tests/test_uint.py @@ -4,48 +4,56 @@ from nose.tools import eq_ from .hex import from_hex, hex_cmp + def test_encode_zero(): """ Test encode generates at least one byte if given a zero. """ - hex_cmp(encode(0), b'\x00') + hex_cmp(encode(0), b"\x00") + def test_encode_le_nolen(): """ Test encode represents a little-endian integer in as few bytes as needed. """ - hex_cmp(encode(0x12345, big_endian=False), - from_hex('45 23 01')) + hex_cmp(encode(0x12345, big_endian=False), from_hex("45 23 01")) + def test_encode_le_len(): """ Test encode will generate an integer of the required size. """ - hex_cmp(encode(0x12345, big_endian=False, length=4), - from_hex('45 23 01 00')) + hex_cmp( + encode(0x12345, big_endian=False, length=4), from_hex("45 23 01 00") + ) + def test_encode_le_truncate(): """ Test encode will truncate an integer to the required size. """ - hex_cmp(encode(0x123456789a, big_endian=False, length=4), - from_hex('9a 78 56 34')) + hex_cmp( + encode(0x123456789A, big_endian=False, length=4), + from_hex("9a 78 56 34"), + ) + def test_encode_be(): """ Test we can encode big-endian integers. """ - hex_cmp(encode(0x11223344, big_endian=True), - from_hex('11 22 33 44')) + hex_cmp(encode(0x11223344, big_endian=True), from_hex("11 22 33 44")) + def test_decode_be(): """ Test we can decode big-endian integers. """ - eq_(decode(from_hex('11 22 33'), big_endian=True), 0x112233) + eq_(decode(from_hex("11 22 33"), big_endian=True), 0x112233) + def test_decode_le(): """ Test we can decode little-endian integers. """ - eq_(decode(from_hex('11 22 33'), big_endian=False), 0x332211) + eq_(decode(from_hex("11 22 33"), big_endian=False), 0x332211) From bfec94181d161e4aa6edd3dced58cd6b4b207815 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 14:36:20 +1000 Subject: [PATCH 046/207] frame: Define various types of XID parameter --- aioax25/frame.py | 472 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 466 insertions(+), 6 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index bbf1561..820c718 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1276,7 +1276,10 @@ class AX25XIDParameterIdentifier(enum.Enum): # Sets the incoming I field length in bits (not bytes) IFieldLengthReceive = 6 - # Sets the number of outstanding I-frames (k) + # Sets the outgoing number of outstanding I-frames (k) + WindowSizeTransmit = 7 + + # Sets the incoming number of outstanding I-frames (k) WindowSizeReceive = 8 # Sets the duration of the Wait For Acknowledge (T1) timer @@ -1297,6 +1300,16 @@ class AX25XIDParameter(object): Representation of a single XID parameter. """ + PARAMETERS = {} + + @classmethod + def register(cls, subclass): + """ + Register a sub-class of XID parameter. + """ + assert subclass.PI not in cls.PARAMETERS + cls.PARAMETERS[subclass.PI] = subclass + @classmethod def decode(cls, data): """ @@ -1318,9 +1331,16 @@ def decode(cls, data): else: pv = None - return (cls(pi=pi, pv=pv), data) + try: + # Hand to the sub-class to decode + param = cls.PARAMETERS[AX25XIDParameterIdentifier(pi)].decode(pv) + except (KeyError, ValueError): + # Not recognised, so return a base class + param = AX25XIDRawParameter(pi=pi, pv=pv) - def __init__(self, pi, pv): + return (param, data) + + def __init__(self, pi): """ Create a new XID parameter """ @@ -1331,7 +1351,6 @@ def __init__(self, pi, pv): pass self._pi = pi - self._pv = pv @property def pi(self): @@ -1341,11 +1360,12 @@ def pi(self): return self._pi @property - def pv(self): + def pv(self): # pragma: no cover """ Return the Parameter Value """ - return self._pv + raise NotImplementedError('To be implemented in %s' \ + % self.__class__.__name__) def __bytes__(self): """ @@ -1361,6 +1381,35 @@ def __bytes__(self): return param + def copy(self): # pragma: no cover + """ + Return a copy of this parameter. + """ + raise NotImplementedError('To be implemented in %s' \ + % self.__class__.__name__) + + +class AX25XIDRawParameter(AX25XIDParameter): + """ + Representation of a single XID parameter that we don't recognise. + """ + + def __init__(self, pi, pv): + """ + Create a new XID parameter + """ + if pv is not None: + pv = bytes(pv) + self._pv = pv + super(AX25XIDRawParameter, self).__init__(pi=pi) + + @property + def pv(self): + """ + Return the Parameter Value + """ + return self._pv + def copy(self): """ Return a copy of this parameter. @@ -1368,6 +1417,417 @@ def copy(self): return self.__class__(pi=self.pi, pv=self.pv) +class AX25XIDClassOfProceduresParameter(AX25XIDParameter): + """ + Class of Procedures XID parameter. This parameter is used to negotiate + half or full duplex communications between two TNCs. + """ + PI = AX25XIDParameterIdentifier.ClassesOfProcedure + + # Bit fields for this parameter: + BALANCED_ABM = 0b0000000000000001 # Should be 1 + UNBALANCED_NRM_PRI = 0b0000000000000010 # Should be 0 + UNBALANCED_NRM_SEC = 0b0000000000000100 # Should be 0 + UNBALANCED_ARM_PRI = 0b0000000000001000 # Should be 0 + UNBALANCED_ARM_SEC = 0b0000000000010000 # Should be 0 + HALF_DUPLEX = 0b0000000000100000 # Should oppose FULL_DUPLEX + FULL_DUPLEX = 0b0000000001000000 # Should oppose HALF_DUPLEX + RESERVED_MASK = 0b1111111110000000 # Should be all zeros + RESERVED_POS = 7 + + @classmethod + def decode(cls, pv): + # Decode the PV + pv = uint.decode(pv, big_endian=False) + return cls( + full_duplex=bool(pv & cls.FULL_DUPLEX), + half_duplex=bool(pv & cls.HALF_DUPLEX), + unbalanced_nrm_pri=bool(pv & cls.UNBALANCED_NRM_PRI), + unbalanced_nrm_sec=bool(pv & cls.UNBALANCED_NRM_SEC), + unbalanced_arm_pri=bool(pv & cls.UNBALANCED_ARM_PRI), + unbalanced_arm_sec=bool(pv & cls.UNBALANCED_ARM_SEC), + balanced_abm=bool(pv & cls.BALANCED_ABM), + reserved=((pv & cls.RESERVED_MASK) >> cls.RESERVED_POS) + ) + + def __init__(self, full_duplex=False, half_duplex=False, + unbalanced_nrm_pri=False, unbalanced_nrm_sec=False, + unbalanced_arm_pri=False, unbalanced_arm_sec=False, + balanced_abm=True, reserved=0): + """ + Create a Class Of Procedures XID parameter. The defaults are set + so that at most, only half_duplex or full_duplex should need setting. + """ + self._half_duplex = half_duplex + self._full_duplex = full_duplex + self._unbalanced_nrm_pri = unbalanced_nrm_pri + self._unbalanced_nrm_sec = unbalanced_nrm_sec + self._unbalanced_arm_pri = unbalanced_arm_pri + self._unbalanced_arm_sec = unbalanced_arm_sec + self._balanced_abm = balanced_abm + self._reserved = reserved + super(AX25XIDClassOfProceduresParameter, self).__init__(pi=self.PI) + + @property + def pv(self): + # We reproduce all bits as given, even if the combination is invalid + # Value is encoded in little-endian format as two bytes. + return uint.encode( + ( ((self.reserved << self.RESERVED_POS) + & self.RESERVED_MASK) + | ((self.full_duplex and self.FULL_DUPLEX) or 0) + | ((self.half_duplex and self.HALF_DUPLEX) or 0) + | ((self.unbalanced_nrm_pri and self.UNBALANCED_NRM_PRI) or 0) + | ((self.unbalanced_nrm_sec and self.UNBALANCED_NRM_SEC) or 0) + | ((self.unbalanced_arm_pri and self.UNBALANCED_ARM_PRI) or 0) + | ((self.unbalanced_arm_sec and self.UNBALANCED_ARM_SEC) or 0) + | ((self.balanced_abm and self.BALANCED_ABM) or 0)), + big_endian=False, length=2 + ) + + @property + def half_duplex(self): + return self._half_duplex + + @property + def full_duplex(self): + return self._full_duplex + + @property + def unbalanced_nrm_pri(self): + return self._unbalanced_nrm_pri + + @property + def unbalanced_nrm_sec(self): + return self._unbalanced_nrm_sec + + @property + def unbalanced_arm_pri(self): + return self._unbalanced_arm_pri + + @property + def unbalanced_arm_sec(self): + return self._unbalanced_arm_sec + + @property + def balanced_abm(self): + return self._balanced_abm + + @property + def reserved(self): + return self._reserved + + def copy(self): + return self.__class__( + half_duplex=self.half_duplex, + full_duplex=self.full_duplex, + unbalanced_nrm_pri=self.unbalanced_nrm_pri, + unbalanced_nrm_sec=self.unbalanced_nrm_sec, + unbalanced_arm_pri=self.unbalanced_arm_pri, + unbalanced_arm_sec=self.unbalanced_arm_sec, + reserved=self.reserved + ) +AX25XIDParameter.register(AX25XIDClassOfProceduresParameter) + + +class AX25XIDHDLCOptionalFunctionsParameter(AX25XIDParameter): + """ + HDLC Optional Functions XID parameter. This parameter is used to negotiate + what optional features of the HDLC specification will be used to + synchronise communications. + """ + PI = AX25XIDParameterIdentifier.HDLCOptionalFunctions + + # Bit fields for this parameter: + RESERVED1 = 0b000000000000000000000001 # Should be 0 + REJ = 0b000000000000000000000010 # Negotiable + SREJ = 0b000000000000000000000100 # Negotiable + UI = 0b000000000000000000001000 # Should be 0 + SIM_RIM = 0b000000000000000000010000 # Should be 0 + UP = 0b000000000000000000100000 # Should be 0 + BASIC_ADDR = 0b000000000000000001000000 # Should be 1 + EXTD_ADDR = 0b000000000000000010000000 # Should be 0 + DELETE_I_RESP = 0b000000000000000100000000 # Should be 0 + DELETE_I_CMD = 0b000000000000001000000000 # Should be 0 + MODULO8 = 0b000000000000010000000000 # Negotiable + MODULO128 = 0b000000000000100000000000 # Negotiable + RSET = 0b000000000001000000000000 # Should be 0 + TEST = 0b000000000010000000000000 # Should be 1 + RD = 0b000000000100000000000000 # Should be 0 + FCS16 = 0b000000001000000000000000 # Should be 1 + FCS32 = 0b000000010000000000000000 # Should be 0 + SYNC_TX = 0b000000100000000000000000 # Should be 1 + START_STOP_TX = 0b000001000000000000000000 # Should be 0 + START_STOP_FLOW_CTL = 0b000010000000000000000000 # Should be 0 + START_STOP_TRANSP = 0b000100000000000000000000 # Should be 0 + SREJ_MULTIFRAME = 0b001000000000000000000000 # Should be 0 + RESERVED2_MASK = 0b110000000000000000000000 # Should be 00 + RESERVED2_POS = 22 + + @classmethod + def decode(cls, pv): + # Decode the PV + pv = uint.decode(pv, big_endian=False) + return cls( + modulo128=pv & cls.MODULO128, + modulo8=pv & cls.MODULO8, + srej=pv & cls.SREJ, + rej=pv & cls.REJ, + srej_multiframe=pv & cls.SREJ_MULTIFRAME, + start_stop_transp=pv & cls.START_STOP_TRANSP, + start_stop_flow_ctl=pv & cls.START_STOP_FLOW_CTL, + start_stop_tx=pv & cls.START_STOP_TX, + sync_tx=pv & cls.SYNC_TX, + fcs32=pv & cls.FCS32, + fcs16=pv & cls.FCS16, + rd=pv & cls.RD, + test=pv & cls.TEST, + rset=pv & cls.RSET, + delete_i_cmd=pv & cls.DELETE_I_CMD, + delete_i_resp=pv & cls.DELETE_I_RESP, + extd_addr=pv & cls.EXTD_ADDR, + basic_addr=pv & cls.BASIC_ADDR, + up=pv & cls.UP, + sim_rim=pv & cls.SIM_RIM, + ui=pv & cls.UI, + reserved2=(pv & cls.RESERVED2_MASK) >> cls.RESERVED2_POS, + reserved1=pv & cls.RESERVED1 + ) + + def __init__(self, modulo128=False, modulo8=False, srej=False, rej=False, + srej_multiframe=False, start_stop_transp=False, + start_stop_flow_ctl=False, start_stop_tx=False, sync_tx=True, + fcs32=False, fcs16=True, rd=False, test=True, rset=False, + delete_i_cmd=False, delete_i_resp=False, extd_addr=True, + basic_addr=False, up=False, sim_rim=False, ui=False, + reserved2=0, reserved1=False): + """ + HDLC Optional Features XID parameter. The defaults are set + so that at most, only srej, rej, modulo8 and/or modulo128 need setting. + """ + self._modulo128 = modulo128 + self._modulo8 = modulo8 + self._srej = srej + self._rej = rej + self._srej_multiframe = srej_multiframe + self._start_stop_transp = start_stop_transp + self._start_stop_flow_ctl = start_stop_flow_ctl + self._start_stop_tx = start_stop_tx + self._sync_tx = sync_tx + self._fcs32 = fcs32 + self._fcs16 = fcs16 + self._rd = rd + self._test = test + self._rset = rset + self._delete_i_cmd = delete_i_cmd + self._delete_i_resp = delete_i_resp + self._extd_addr = extd_addr + self._basic_addr = basic_addr + self._up = up + self._sim_rim = sim_rim + self._ui = ui + self._reserved2 = reserved2 + self._reserved1 = reserved1 + + super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__(pi=self.PI) + + @property + def pv(self): + # We reproduce all bits as given, even if the combination is invalid + return uint.encode( + ( ((self.reserved2 << self.RESERVED2_POS) + & self.RESERVED2_MASK) + | ((self.modulo128 and self.MODULO128) or 0) + | ((self.modulo8 and self.MODULO8) or 0) + | ((self.srej and self.SREJ) or 0) + | ((self.rej and self.REJ) or 0) + | ((self.srej_multiframe and self.SREJ_MULTIFRAME) or 0) + | ((self.start_stop_transp and self.START_STOP_TRANSP) or 0) + | ((self.start_stop_flow_ctl and self.START_STOP_FLOW_CTL) or 0) + | ((self.start_stop_tx and self.START_STOP_TX) or 0) + | ((self.sync_tx and self.SYNC_TX) or 0) + | ((self.fcs32 and self.FCS32) or 0) + | ((self.fcs16 and self.FCS16) or 0) + | ((self.rd and self.RD) or 0) + | ((self.test and self.TEST) or 0) + | ((self.rset and self.RSET) or 0) + | ((self.delete_i_cmd and self.DELETE_I_CMD) or 0) + | ((self.delete_i_resp and self.DELETE_I_RESP) or 0) + | ((self.extd_addr and self.EXTD_ADDR) or 0) + | ((self.basic_addr and self.BASIC_ADDR) or 0) + | ((self.up and self.UP) or 0) + | ((self.sim_rim and self.SIM_RIM) or 0) + | ((self.ui and self.UI) or 0) + | ((self.reserved1 and self.RESERVED1) or 0)), + big_endian=False, length=3 + ) + + @property + def modulo128(self): + return self._modulo128 + + @property + def modulo8(self): + return self._modulo8 + + @property + def srej(self): + return self._srej + + @property + def rej(self): + return self._rej + + @property + def srej_multiframe(self): + return self._srej_multiframe + + @property + def start_stop_transp(self): + return self._start_stop_transp + + @property + def start_stop_flow_ctl(self): + return self._start_stop_flow_ctl + + @property + def start_stop_tx(self): + return self._start_stop_tx + + @property + def sync_tx(self): + return self._sync_tx + + @property + def fcs32(self): + return self._fcs32 + + @property + def fcs16(self): + return self._fcs16 + + @property + def rd(self): + return self._rd + + @property + def test(self): + return self._test + + @property + def rset(self): + return self._rset + + @property + def delete_i_cmd(self): + return self._delete_i_cmd + + @property + def delete_i_resp(self): + return self._delete_i_resp + + @property + def extd_addr(self): + return self._extd_addr + + @property + def basic_addr(self): + return self._basic_addr + + @property + def up(self): + return self._up + + @property + def sim_rim(self): + return self._sim_rim + + @property + def ui(self): + return self._ui + + @property + def reserved2(self): + return self._reserved2 + + @property + def reserved1(self): + return self._reserved1 + + def copy(self): + return self.__class__( + modulo128=self.modulo128, modulo8=self.modulo8, + srej=self.srej, rej=self.rej, + srej_multiframe=self.srej_multiframe, + start_stop_transp=self.start_stop_transp, + start_stop_flow_ctl=self.start_stop_flow_ctl, + start_stop_tx=self.start_stop_tx, sync_tx=self.sync_tx, + fcs32=self.fcs32, fcs16=self.fcs16, rd=self.rd, test=self.test, + rset=self.rset, delete_i_cmd=self.delete_i_cmd, + delete_i_resp=self.delete_i_resp, extd_addr=self.extd_addr, + basic_addr=self.basic_addr, up=self.up, sim_rim=self.sim_rim, + ui=self.ui, reserved2=self.reserved2, reserved1=self.reserved1 + ) +AX25XIDParameter.register(AX25XIDHDLCOptionalFunctionsParameter) + + +class AX25XIDBigEndianParameter(AX25XIDParameter): + """ + Base class for all big-endian parameters (field lengths, window sizes, ACK + timers, retries). + """ + LENGTH = None + + @classmethod + def decode(cls, pv): + return cls(value=uint.decode(pv, big_endian=True)) + + def __init__(self, value): + """ + Create a big-endian integer parameter. + """ + self._value = value + + super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__(pi=self.PI) + + @property + def pv(self): + return uint.encode(self.value, big_endian=True, length=self.LENGTH) + + @property + def value(self): + return self._value + + def copy(self): + return self.__class__(value=self.value) + + +class AX25XIDIFieldLengthTransmitParameter(AX25XIDBigEndianParameter): + PI = AX25XIDParameterIdentifier.IFieldLengthTransmit + + +class AX25XIDIFieldLengthReceiveParameter(AX25XIDBigEndianParameter): + PI = AX25XIDParameterIdentifier.WindowSizeReceive + + +class AX25XIDWindowSizeTransmitParameter(AX25XIDBigEndianParameter): + PI = AX25XIDParameterIdentifier.IFieldLengthTransmit + LENGTH = 1 + + +class AX25XIDWindowSizeReceiveParameter(AX25XIDBigEndianParameter): + PI = AX25XIDParameterIdentifier.WindowSizeReceive + LENGTH = 1 + + +class AX25XIDAcknowledgeTimerParameter(AX25XIDBigEndianParameter): + PI = AX25XIDParameterIdentifier.AcknowledgeTimer + + +class AX25XIDRetriesParameter(AX25XIDBigEndianParameter): + PI = AX25XIDParameterIdentifier.Retries + + class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): """ Exchange Identification frame. From 0fda48871da7834411897942be71c302e8f44b26 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 14:37:24 +1000 Subject: [PATCH 047/207] frame tests: Split test suites up by frame type. --- aioax25/frame.py | 320 ++++++----- tests/test_frame/test_ax25frame.py | 859 ----------------------------- 2 files changed, 189 insertions(+), 990 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 820c718..f2b80ec 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1360,12 +1360,13 @@ def pi(self): return self._pi @property - def pv(self): # pragma: no cover + def pv(self): # pragma: no cover """ Return the Parameter Value """ - raise NotImplementedError('To be implemented in %s' \ - % self.__class__.__name__) + raise NotImplementedError( + "To be implemented in %s" % self.__class__.__name__ + ) def __bytes__(self): """ @@ -1381,12 +1382,13 @@ def __bytes__(self): return param - def copy(self): # pragma: no cover + def copy(self): # pragma: no cover """ Return a copy of this parameter. """ - raise NotImplementedError('To be implemented in %s' \ - % self.__class__.__name__) + raise NotImplementedError( + "To be implemented in %s" % self.__class__.__name__ + ) class AX25XIDRawParameter(AX25XIDParameter): @@ -1422,18 +1424,19 @@ class AX25XIDClassOfProceduresParameter(AX25XIDParameter): Class of Procedures XID parameter. This parameter is used to negotiate half or full duplex communications between two TNCs. """ + PI = AX25XIDParameterIdentifier.ClassesOfProcedure # Bit fields for this parameter: - BALANCED_ABM = 0b0000000000000001 # Should be 1 - UNBALANCED_NRM_PRI = 0b0000000000000010 # Should be 0 - UNBALANCED_NRM_SEC = 0b0000000000000100 # Should be 0 - UNBALANCED_ARM_PRI = 0b0000000000001000 # Should be 0 - UNBALANCED_ARM_SEC = 0b0000000000010000 # Should be 0 - HALF_DUPLEX = 0b0000000000100000 # Should oppose FULL_DUPLEX - FULL_DUPLEX = 0b0000000001000000 # Should oppose HALF_DUPLEX - RESERVED_MASK = 0b1111111110000000 # Should be all zeros - RESERVED_POS = 7 + BALANCED_ABM = 0b0000000000000001 # Should be 1 + UNBALANCED_NRM_PRI = 0b0000000000000010 # Should be 0 + UNBALANCED_NRM_SEC = 0b0000000000000100 # Should be 0 + UNBALANCED_ARM_PRI = 0b0000000000001000 # Should be 0 + UNBALANCED_ARM_SEC = 0b0000000000010000 # Should be 0 + HALF_DUPLEX = 0b0000000000100000 # Should oppose FULL_DUPLEX + FULL_DUPLEX = 0b0000000001000000 # Should oppose HALF_DUPLEX + RESERVED_MASK = 0b1111111110000000 # Should be all zeros + RESERVED_POS = 7 @classmethod def decode(cls, pv): @@ -1447,13 +1450,20 @@ def decode(cls, pv): unbalanced_arm_pri=bool(pv & cls.UNBALANCED_ARM_PRI), unbalanced_arm_sec=bool(pv & cls.UNBALANCED_ARM_SEC), balanced_abm=bool(pv & cls.BALANCED_ABM), - reserved=((pv & cls.RESERVED_MASK) >> cls.RESERVED_POS) + reserved=((pv & cls.RESERVED_MASK) >> cls.RESERVED_POS), ) - def __init__(self, full_duplex=False, half_duplex=False, - unbalanced_nrm_pri=False, unbalanced_nrm_sec=False, - unbalanced_arm_pri=False, unbalanced_arm_sec=False, - balanced_abm=True, reserved=0): + def __init__( + self, + full_duplex=False, + half_duplex=False, + unbalanced_nrm_pri=False, + unbalanced_nrm_sec=False, + unbalanced_arm_pri=False, + unbalanced_arm_sec=False, + balanced_abm=True, + reserved=0, + ): """ Create a Class Of Procedures XID parameter. The defaults are set so that at most, only half_duplex or full_duplex should need setting. @@ -1462,7 +1472,7 @@ def __init__(self, full_duplex=False, half_duplex=False, self._full_duplex = full_duplex self._unbalanced_nrm_pri = unbalanced_nrm_pri self._unbalanced_nrm_sec = unbalanced_nrm_sec - self._unbalanced_arm_pri = unbalanced_arm_pri + self._unbalanced_arm_pri = unbalanced_arm_pri self._unbalanced_arm_sec = unbalanced_arm_sec self._balanced_abm = balanced_abm self._reserved = reserved @@ -1473,16 +1483,18 @@ def pv(self): # We reproduce all bits as given, even if the combination is invalid # Value is encoded in little-endian format as two bytes. return uint.encode( - ( ((self.reserved << self.RESERVED_POS) - & self.RESERVED_MASK) - | ((self.full_duplex and self.FULL_DUPLEX) or 0) - | ((self.half_duplex and self.HALF_DUPLEX) or 0) - | ((self.unbalanced_nrm_pri and self.UNBALANCED_NRM_PRI) or 0) - | ((self.unbalanced_nrm_sec and self.UNBALANCED_NRM_SEC) or 0) - | ((self.unbalanced_arm_pri and self.UNBALANCED_ARM_PRI) or 0) - | ((self.unbalanced_arm_sec and self.UNBALANCED_ARM_SEC) or 0) - | ((self.balanced_abm and self.BALANCED_ABM) or 0)), - big_endian=False, length=2 + ( + ((self.reserved << self.RESERVED_POS) & self.RESERVED_MASK) + | ((self.full_duplex and self.FULL_DUPLEX) or 0) + | ((self.half_duplex and self.HALF_DUPLEX) or 0) + | ((self.unbalanced_nrm_pri and self.UNBALANCED_NRM_PRI) or 0) + | ((self.unbalanced_nrm_sec and self.UNBALANCED_NRM_SEC) or 0) + | ((self.unbalanced_arm_pri and self.UNBALANCED_ARM_PRI) or 0) + | ((self.unbalanced_arm_sec and self.UNBALANCED_ARM_SEC) or 0) + | ((self.balanced_abm and self.BALANCED_ABM) or 0) + ), + big_endian=False, + length=2, ) @property @@ -1519,14 +1531,16 @@ def reserved(self): def copy(self): return self.__class__( - half_duplex=self.half_duplex, - full_duplex=self.full_duplex, - unbalanced_nrm_pri=self.unbalanced_nrm_pri, - unbalanced_nrm_sec=self.unbalanced_nrm_sec, - unbalanced_arm_pri=self.unbalanced_arm_pri, - unbalanced_arm_sec=self.unbalanced_arm_sec, - reserved=self.reserved + half_duplex=self.half_duplex, + full_duplex=self.full_duplex, + unbalanced_nrm_pri=self.unbalanced_nrm_pri, + unbalanced_nrm_sec=self.unbalanced_nrm_sec, + unbalanced_arm_pri=self.unbalanced_arm_pri, + unbalanced_arm_sec=self.unbalanced_arm_sec, + reserved=self.reserved, ) + + AX25XIDParameter.register(AX25XIDClassOfProceduresParameter) @@ -1536,71 +1550,91 @@ class AX25XIDHDLCOptionalFunctionsParameter(AX25XIDParameter): what optional features of the HDLC specification will be used to synchronise communications. """ + PI = AX25XIDParameterIdentifier.HDLCOptionalFunctions # Bit fields for this parameter: - RESERVED1 = 0b000000000000000000000001 # Should be 0 - REJ = 0b000000000000000000000010 # Negotiable - SREJ = 0b000000000000000000000100 # Negotiable - UI = 0b000000000000000000001000 # Should be 0 - SIM_RIM = 0b000000000000000000010000 # Should be 0 - UP = 0b000000000000000000100000 # Should be 0 - BASIC_ADDR = 0b000000000000000001000000 # Should be 1 - EXTD_ADDR = 0b000000000000000010000000 # Should be 0 - DELETE_I_RESP = 0b000000000000000100000000 # Should be 0 - DELETE_I_CMD = 0b000000000000001000000000 # Should be 0 - MODULO8 = 0b000000000000010000000000 # Negotiable - MODULO128 = 0b000000000000100000000000 # Negotiable - RSET = 0b000000000001000000000000 # Should be 0 - TEST = 0b000000000010000000000000 # Should be 1 - RD = 0b000000000100000000000000 # Should be 0 - FCS16 = 0b000000001000000000000000 # Should be 1 - FCS32 = 0b000000010000000000000000 # Should be 0 - SYNC_TX = 0b000000100000000000000000 # Should be 1 - START_STOP_TX = 0b000001000000000000000000 # Should be 0 - START_STOP_FLOW_CTL = 0b000010000000000000000000 # Should be 0 - START_STOP_TRANSP = 0b000100000000000000000000 # Should be 0 - SREJ_MULTIFRAME = 0b001000000000000000000000 # Should be 0 - RESERVED2_MASK = 0b110000000000000000000000 # Should be 00 - RESERVED2_POS = 22 + RESERVED1 = 0b000000000000000000000001 # Should be 0 + REJ = 0b000000000000000000000010 # Negotiable + SREJ = 0b000000000000000000000100 # Negotiable + UI = 0b000000000000000000001000 # Should be 0 + SIM_RIM = 0b000000000000000000010000 # Should be 0 + UP = 0b000000000000000000100000 # Should be 0 + BASIC_ADDR = 0b000000000000000001000000 # Should be 1 + EXTD_ADDR = 0b000000000000000010000000 # Should be 0 + DELETE_I_RESP = 0b000000000000000100000000 # Should be 0 + DELETE_I_CMD = 0b000000000000001000000000 # Should be 0 + MODULO8 = 0b000000000000010000000000 # Negotiable + MODULO128 = 0b000000000000100000000000 # Negotiable + RSET = 0b000000000001000000000000 # Should be 0 + TEST = 0b000000000010000000000000 # Should be 1 + RD = 0b000000000100000000000000 # Should be 0 + FCS16 = 0b000000001000000000000000 # Should be 1 + FCS32 = 0b000000010000000000000000 # Should be 0 + SYNC_TX = 0b000000100000000000000000 # Should be 1 + START_STOP_TX = 0b000001000000000000000000 # Should be 0 + START_STOP_FLOW_CTL = 0b000010000000000000000000 # Should be 0 + START_STOP_TRANSP = 0b000100000000000000000000 # Should be 0 + SREJ_MULTIFRAME = 0b001000000000000000000000 # Should be 0 + RESERVED2_MASK = 0b110000000000000000000000 # Should be 00 + RESERVED2_POS = 22 @classmethod def decode(cls, pv): # Decode the PV pv = uint.decode(pv, big_endian=False) return cls( - modulo128=pv & cls.MODULO128, - modulo8=pv & cls.MODULO8, - srej=pv & cls.SREJ, - rej=pv & cls.REJ, - srej_multiframe=pv & cls.SREJ_MULTIFRAME, - start_stop_transp=pv & cls.START_STOP_TRANSP, - start_stop_flow_ctl=pv & cls.START_STOP_FLOW_CTL, - start_stop_tx=pv & cls.START_STOP_TX, - sync_tx=pv & cls.SYNC_TX, - fcs32=pv & cls.FCS32, - fcs16=pv & cls.FCS16, - rd=pv & cls.RD, - test=pv & cls.TEST, - rset=pv & cls.RSET, - delete_i_cmd=pv & cls.DELETE_I_CMD, - delete_i_resp=pv & cls.DELETE_I_RESP, - extd_addr=pv & cls.EXTD_ADDR, - basic_addr=pv & cls.BASIC_ADDR, - up=pv & cls.UP, - sim_rim=pv & cls.SIM_RIM, - ui=pv & cls.UI, - reserved2=(pv & cls.RESERVED2_MASK) >> cls.RESERVED2_POS, - reserved1=pv & cls.RESERVED1 + modulo128=pv & cls.MODULO128, + modulo8=pv & cls.MODULO8, + srej=pv & cls.SREJ, + rej=pv & cls.REJ, + srej_multiframe=pv & cls.SREJ_MULTIFRAME, + start_stop_transp=pv & cls.START_STOP_TRANSP, + start_stop_flow_ctl=pv & cls.START_STOP_FLOW_CTL, + start_stop_tx=pv & cls.START_STOP_TX, + sync_tx=pv & cls.SYNC_TX, + fcs32=pv & cls.FCS32, + fcs16=pv & cls.FCS16, + rd=pv & cls.RD, + test=pv & cls.TEST, + rset=pv & cls.RSET, + delete_i_cmd=pv & cls.DELETE_I_CMD, + delete_i_resp=pv & cls.DELETE_I_RESP, + extd_addr=pv & cls.EXTD_ADDR, + basic_addr=pv & cls.BASIC_ADDR, + up=pv & cls.UP, + sim_rim=pv & cls.SIM_RIM, + ui=pv & cls.UI, + reserved2=(pv & cls.RESERVED2_MASK) >> cls.RESERVED2_POS, + reserved1=pv & cls.RESERVED1, ) - def __init__(self, modulo128=False, modulo8=False, srej=False, rej=False, - srej_multiframe=False, start_stop_transp=False, - start_stop_flow_ctl=False, start_stop_tx=False, sync_tx=True, - fcs32=False, fcs16=True, rd=False, test=True, rset=False, - delete_i_cmd=False, delete_i_resp=False, extd_addr=True, - basic_addr=False, up=False, sim_rim=False, ui=False, - reserved2=0, reserved1=False): + def __init__( + self, + modulo128=False, + modulo8=False, + srej=False, + rej=False, + srej_multiframe=False, + start_stop_transp=False, + start_stop_flow_ctl=False, + start_stop_tx=False, + sync_tx=True, + fcs32=False, + fcs16=True, + rd=False, + test=True, + rset=False, + delete_i_cmd=False, + delete_i_resp=False, + extd_addr=True, + basic_addr=False, + up=False, + sim_rim=False, + ui=False, + reserved2=0, + reserved1=False, + ): """ HDLC Optional Features XID parameter. The defaults are set so that at most, only srej, rej, modulo8 and/or modulo128 need setting. @@ -1629,38 +1663,45 @@ def __init__(self, modulo128=False, modulo8=False, srej=False, rej=False, self._reserved2 = reserved2 self._reserved1 = reserved1 - super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__(pi=self.PI) + super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__( + pi=self.PI + ) @property def pv(self): # We reproduce all bits as given, even if the combination is invalid return uint.encode( - ( ((self.reserved2 << self.RESERVED2_POS) - & self.RESERVED2_MASK) - | ((self.modulo128 and self.MODULO128) or 0) - | ((self.modulo8 and self.MODULO8) or 0) - | ((self.srej and self.SREJ) or 0) - | ((self.rej and self.REJ) or 0) - | ((self.srej_multiframe and self.SREJ_MULTIFRAME) or 0) - | ((self.start_stop_transp and self.START_STOP_TRANSP) or 0) - | ((self.start_stop_flow_ctl and self.START_STOP_FLOW_CTL) or 0) - | ((self.start_stop_tx and self.START_STOP_TX) or 0) - | ((self.sync_tx and self.SYNC_TX) or 0) - | ((self.fcs32 and self.FCS32) or 0) - | ((self.fcs16 and self.FCS16) or 0) - | ((self.rd and self.RD) or 0) - | ((self.test and self.TEST) or 0) - | ((self.rset and self.RSET) or 0) - | ((self.delete_i_cmd and self.DELETE_I_CMD) or 0) - | ((self.delete_i_resp and self.DELETE_I_RESP) or 0) - | ((self.extd_addr and self.EXTD_ADDR) or 0) - | ((self.basic_addr and self.BASIC_ADDR) or 0) - | ((self.up and self.UP) or 0) - | ((self.sim_rim and self.SIM_RIM) or 0) - | ((self.ui and self.UI) or 0) - | ((self.reserved1 and self.RESERVED1) or 0)), - big_endian=False, length=3 - ) + ( + ((self.reserved2 << self.RESERVED2_POS) & self.RESERVED2_MASK) + | ((self.modulo128 and self.MODULO128) or 0) + | ((self.modulo8 and self.MODULO8) or 0) + | ((self.srej and self.SREJ) or 0) + | ((self.rej and self.REJ) or 0) + | ((self.srej_multiframe and self.SREJ_MULTIFRAME) or 0) + | ((self.start_stop_transp and self.START_STOP_TRANSP) or 0) + | ( + (self.start_stop_flow_ctl and self.START_STOP_FLOW_CTL) + or 0 + ) + | ((self.start_stop_tx and self.START_STOP_TX) or 0) + | ((self.sync_tx and self.SYNC_TX) or 0) + | ((self.fcs32 and self.FCS32) or 0) + | ((self.fcs16 and self.FCS16) or 0) + | ((self.rd and self.RD) or 0) + | ((self.test and self.TEST) or 0) + | ((self.rset and self.RSET) or 0) + | ((self.delete_i_cmd and self.DELETE_I_CMD) or 0) + | ((self.delete_i_resp and self.DELETE_I_RESP) or 0) + | ((self.extd_addr and self.EXTD_ADDR) or 0) + | ((self.basic_addr and self.BASIC_ADDR) or 0) + | ((self.up and self.UP) or 0) + | ((self.sim_rim and self.SIM_RIM) or 0) + | ((self.ui and self.UI) or 0) + | ((self.reserved1 and self.RESERVED1) or 0) + ), + big_endian=False, + length=3, + ) @property def modulo128(self): @@ -1756,18 +1797,32 @@ def reserved1(self): def copy(self): return self.__class__( - modulo128=self.modulo128, modulo8=self.modulo8, - srej=self.srej, rej=self.rej, - srej_multiframe=self.srej_multiframe, - start_stop_transp=self.start_stop_transp, - start_stop_flow_ctl=self.start_stop_flow_ctl, - start_stop_tx=self.start_stop_tx, sync_tx=self.sync_tx, - fcs32=self.fcs32, fcs16=self.fcs16, rd=self.rd, test=self.test, - rset=self.rset, delete_i_cmd=self.delete_i_cmd, - delete_i_resp=self.delete_i_resp, extd_addr=self.extd_addr, - basic_addr=self.basic_addr, up=self.up, sim_rim=self.sim_rim, - ui=self.ui, reserved2=self.reserved2, reserved1=self.reserved1 + modulo128=self.modulo128, + modulo8=self.modulo8, + srej=self.srej, + rej=self.rej, + srej_multiframe=self.srej_multiframe, + start_stop_transp=self.start_stop_transp, + start_stop_flow_ctl=self.start_stop_flow_ctl, + start_stop_tx=self.start_stop_tx, + sync_tx=self.sync_tx, + fcs32=self.fcs32, + fcs16=self.fcs16, + rd=self.rd, + test=self.test, + rset=self.rset, + delete_i_cmd=self.delete_i_cmd, + delete_i_resp=self.delete_i_resp, + extd_addr=self.extd_addr, + basic_addr=self.basic_addr, + up=self.up, + sim_rim=self.sim_rim, + ui=self.ui, + reserved2=self.reserved2, + reserved1=self.reserved1, ) + + AX25XIDParameter.register(AX25XIDHDLCOptionalFunctionsParameter) @@ -1776,6 +1831,7 @@ class AX25XIDBigEndianParameter(AX25XIDParameter): Base class for all big-endian parameters (field lengths, window sizes, ACK timers, retries). """ + LENGTH = None @classmethod @@ -1788,7 +1844,9 @@ def __init__(self, value): """ self._value = value - super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__(pi=self.PI) + super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__( + pi=self.PI + ) @property def pv(self): diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 4a70a50..40e6396 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -4,20 +4,12 @@ AX25Frame, AX25RawFrame, AX25UnnumberedInformationFrame, - AX25FrameRejectFrame, - AX25UnnumberedFrame, AX258BitReceiveReadyFrame, AX2516BitReceiveReadyFrame, AX258BitRejectFrame, AX2516BitRejectFrame, AX258BitInformationFrame, AX2516BitInformationFrame, - AX25DisconnectModeFrame, - AX25SetAsyncBalancedModeFrame, - AX25TestFrame, - AX25ExchangeIdentificationFrame, - AX25XIDParameter, - AX25XIDParameterIdentifier, ) from ..hex import from_hex, hex_cmp @@ -140,161 +132,6 @@ def test_frame_deadline_ro_if_set(): assert frame.deadline == 44556677 -# Unnumbered frame tests - - -def test_decode_uframe(): - """ - Test that a U-frame gets decoded to an unnumbered frame. - """ - frame = AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "c3" # Control byte - ) - ) - assert isinstance( - frame, AX25UnnumberedFrame - ), "Did not decode to unnumbered frame" - assert frame.modifier == 0xC3 - - # We should see the control byte as our payload - hex_cmp(frame.frame_payload, "c3") - - -def test_decode_sabm(): - """ - Test that a SABM frame is recognised and decoded. - """ - frame = AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "6f" # Control byte - ) - ) - assert isinstance( - frame, AX25SetAsyncBalancedModeFrame - ), "Did not decode to SABM frame" - - -def test_decode_sabm_payload(): - """ - Test that a SABM frame forbids payload. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "6f" # Control byte - "11 22 33 44 55" # Payload - ) - ) - assert False, "This should not have worked" - except ValueError as e: - assert str(e) == "Frame does not support payload" - - -def test_decode_uframe_payload(): - """ - Test that U-frames other than FRMR and UI are forbidden to have payloads. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "c3 11 22 33" # Control byte - ) - ) - assert False, "Should not have worked" - except ValueError as e: - assert str(e) == ( - "Unnumbered frames (other than UI and " - "FRMR) do not have payloads" - ) - - -def test_decode_frmr(): - """ - Test that a FRMR gets decoded to a frame reject frame. - """ - frame = AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "87" # Control byte - "11 22 33" # Payload - ) - ) - assert isinstance( - frame, AX25FrameRejectFrame - ), "Did not decode to FRMR frame" - assert frame.modifier == 0x87 - assert frame.w == True - assert frame.x == False - assert frame.y == False - assert frame.z == False - assert frame.vr == 1 - assert frame.frmr_cr == False - assert frame.vs == 1 - - -def test_decode_frmr_len(): - """ - Test that a FRMR must have 3 byte payload. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "87" # Control byte - "11 22" # Payload - ) - ) - assert False, "Should not have worked" - except ValueError as e: - assert str(e) == "Payload of FRMR must be 3 bytes" - - -def test_decode_ui(): - """ - Test that a UI gets decoded to an unnumbered information frame. - """ - frame = AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "03 11 22 33" # Control byte - ) - ) - assert isinstance( - frame, AX25UnnumberedInformationFrame - ), "Did not decode to UI frame" - assert frame.pid == 0x11 - hex_cmp(frame.payload, "22 33") - - -def test_decode_ui_len(): - """ - Test that a UI must have at least one byte payload. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "03" # Control byte - ) - ) - assert False, "Should not have worked" - except ValueError as e: - assert str(e) == "Payload of UI must be at least one byte" - - def test_encode_raw(): """ Test that we can encode a raw frame. @@ -315,613 +152,6 @@ def test_encode_raw(): ) -def test_encode_uframe(): - """ - Test that we can encode a U-frame. - """ - frame = AX25UnnumberedFrame( - destination="VK4BWI", source="VK4MSL", modifier=0xE7, cr=True - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "e7", # Control - ) - - -def test_encode_frmr(): - """ - Test that we can encode a FRMR. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=True, - x=False, - y=True, - z=False, - vr=1, - frmr_cr=False, - vs=2, - frmr_control=0xAA, - cr=True, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "87" # Control - "05" # W/X/Y/Z - "24" # VR/CR/VS - "aa", # FRMR Control - ) - - -def test_encode_ui(): - """ - Test that we can encode a UI frame. - """ - frame = AX25UnnumberedInformationFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - pid=0xF0, - payload=b"This is a test", - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "03" # Control - "f0" # PID - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload - ) - - -def test_encode_test(): - """ - Test that we can encode a TEST frame. - """ - frame = AX25TestFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - payload=b"This is a test", - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "e3" # Control - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload - ) - - -def test_decode_test(): - """ - Test that we can decode a TEST frame. - """ - frame = AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "e3" # Control - "31 32 33 34 35 36 37 38 39 2e 2e 2e" # Payload - ) - ) - assert isinstance(frame, AX25TestFrame) - assert frame.payload == b"123456789..." - - -def test_copy_test(): - """ - Test that we can copy a TEST frame. - """ - frame = AX25TestFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - payload=b"This is a test", - ) - framecopy = frame.copy() - assert framecopy is not frame - hex_cmp( - bytes(framecopy), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "e3" # Control - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload - ) - - -def test_encode_xid(): - """ - Test that we can encode a XID frame. - """ - frame = AX25ExchangeIdentificationFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - fi=0x82, - gi=0x80, - parameters=[ - AX25XIDParameter( - # Should be encoded to bytes for us - pi=AX25XIDParameterIdentifier.IFieldLengthTransmit, - pv=bytes([0x08, 0x00]), - ), - AX25XIDParameter(pi=0x12, pv=bytes([0x34, 0x56])), - AX25XIDParameter(pi=0x34, pv=None), - ], - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # Format indicator - "80" # Group Ident - "00 0a" # Group length - # First parameter - "05" # Parameter ID - "02" # Length - "08 00" # Value - # Second parameter - "12" # Parameter ID - "02" # Length - "34 56" # Value - # Third parameter - "34" # Parameter ID - "00", # Length (no value) - ) - - -def test_decode_xid(): - """ - Test that we can decode a XID frame. - """ - frame = AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # FI - "80" # GI - "00 0c" # GL - # Some parameters - "01 01 aa" - "02 01 bb" - "03 02 11 22" - "04 00" - ) - ) - assert isinstance(frame, AX25ExchangeIdentificationFrame) - assert frame.fi == 0x82 - assert frame.gi == 0x80 - assert len(frame.parameters) == 4 - - param = frame.parameters[0] - assert param.pi == 0x01 - assert param.pv == b"\xaa" - - param = frame.parameters[1] - assert param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure - assert param.pv == b"\xbb" - - param = frame.parameters[2] - assert param.pi == AX25XIDParameterIdentifier.HDLCOptionalFunctions - assert param.pv == b"\x11\x22" - - param = frame.parameters[3] - assert param.pi == 0x04 - assert param.pv is None - - -def test_decode_xid_fig46(): - """ - Test that we can decode the XID example from AX.25 2.2 figure 4.6. - """ - frame = AX25Frame.decode( - from_hex( - "9c 94 6e a0 40 40 e0" # Destination - "9c 6e 98 8a 9a 40 61" # Source - "af" # Control - "82" # FI - "80" # GI - "00 17" # GL - "02 02 00 20" - "03 03 86 a8 02" - "06 02 04 00" - "08 01 02" - "09 02 10 00" - "0a 01 03" - ) - ) - assert len(frame.parameters) == 6 - - param = frame.parameters[0] - assert param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure - assert param.pv == b"\x00\x20" - - param = frame.parameters[1] - assert param.pi == AX25XIDParameterIdentifier.HDLCOptionalFunctions - assert param.pv == b"\x86\xa8\x02" - - param = frame.parameters[2] - assert param.pi == AX25XIDParameterIdentifier.IFieldLengthReceive - assert param.pv == b"\x04\x00" - - param = frame.parameters[3] - assert param.pi == AX25XIDParameterIdentifier.WindowSizeReceive - assert param.pv == b"\x02" - - param = frame.parameters[4] - assert param.pi == AX25XIDParameterIdentifier.AcknowledgeTimer - assert param.pv == b"\x10\x00" - - param = frame.parameters[5] - assert param.pi == AX25XIDParameterIdentifier.Retries - assert param.pv == b"\x03" - - -def test_decode_xid_truncated_header(): - """ - Test that decoding a XID with truncated header fails. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # FI - "80" # GI - "00" # Incomplete GL - ) - ) - assert False, "This should not have worked" - except ValueError as e: - assert str(e) == "Truncated XID header" - - -def test_decode_xid_truncated_payload(): - """ - Test that decoding a XID with truncated payload fails. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # FI - "80" # GI - "00 05" # GL - "11" # Incomplete payload - ) - ) - assert False, "This should not have worked" - except ValueError as e: - assert str(e) == "Truncated XID data" - - -def test_decode_xid_truncated_param_header(): - """ - Test that decoding a XID with truncated parameter header fails. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # FI - "80" # GI - "00 01" # GL - "11" # Incomplete payload - ) - ) - assert False, "This should not have worked" - except ValueError as e: - assert str(e) == "Insufficient data for parameter" - - -def test_decode_xid_truncated_param_value(): - """ - Test that decoding a XID with truncated parameter value fails. - """ - try: - AX25Frame.decode( - from_hex( - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # FI - "80" # GI - "00 04" # GL - "11 06 22 33" # Incomplete payload - ) - ) - assert False, "This should not have worked" - except ValueError as e: - assert str(e) == "Parameter is truncated" - - -def test_copy_xid(): - """ - Test that we can copy a XID frame. - """ - frame = AX25ExchangeIdentificationFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - fi=0x82, - gi=0x80, - parameters=[ - AX25XIDParameter(pi=0x12, pv=bytes([0x34, 0x56])), - AX25XIDParameter(pi=0x34, pv=None), - ], - ) - framecopy = frame.copy() - assert framecopy is not frame - hex_cmp( - bytes(framecopy), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "af" # Control - "82" # Format indicator - "80" # Group Ident - "00 06" # Group length - # First parameter - "12" # Parameter ID - "02" # Length - "34 56" # Value - # Second parameter - "34" # Parameter ID - "00", # Length (no value) - ) - - -def test_encode_pf(): - """ - Test we can set the PF bit on a frame. - """ - frame = AX25UnnumberedInformationFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - pf=True, - pid=0xF0, - payload=b"This is a test", - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "13" # Control - "f0" # PID - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload - ) - assert frame.control == 0x13 - - -def test_encode_frmr_w(): - """ - Test we can set the W bit on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=True, - x=False, - y=False, - z=False, - vr=0, - vs=0, - frmr_control=0, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "01 00 00", # FRMR data - ) - - -def test_encode_frmr_x(): - """ - Test we can set the X bit on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=True, - y=False, - z=False, - vr=0, - vs=0, - frmr_control=0, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "02 00 00", # FRMR data - ) - - -def test_encode_frmr_y(): - """ - Test we can set the Y bit on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=True, - z=False, - vr=0, - vs=0, - frmr_control=0, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "04 00 00", # FRMR data - ) - - -def test_encode_frmr_z(): - """ - Test we can set the Z bit on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=False, - z=True, - vr=0, - vs=0, - frmr_control=0, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "08 00 00", # FRMR data - ) - - -def test_encode_frmr_cr(): - """ - Test we can set the CR bit on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=False, - z=False, - vr=0, - vs=0, - frmr_control=0, - frmr_cr=True, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "00 10 00", # FRMR data - ) - - -def test_encode_frmr_vr(): - """ - Test we can set the V(R) field on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=False, - z=False, - vr=5, - vs=0, - frmr_control=0, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "00 a0 00", # FRMR data - ) - - -def test_encode_frmr_vs(): - """ - Test we can set the V(S) field on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=False, - z=False, - vr=0, - vs=5, - frmr_control=0, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "00 0a 00", # FRMR data - ) - - -def test_encode_frmr_frmr_ctrl(): - """ - Test we can set the FRMR Control field on a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=False, - z=False, - vr=0, - vs=0, - frmr_control=0x55, - frmr_cr=False, - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "00 00 55", # FRMR data - ) - - -def test_encode_dm_frame(): - """ - Test we can encode a Disconnect Mode frame. - """ - frame = AX25DisconnectModeFrame( - destination="VK4BWI", - source="VK4MSL", - ) - hex_cmp( - bytes(frame), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "0f", # Control - ) - - def test_raw_copy(): """ Test we can make a copy of a raw frame. @@ -941,95 +171,6 @@ def test_raw_copy(): ) -def test_u_copy(): - """ - Test we can make a copy of a unnumbered frame. - """ - frame = AX25UnnumberedFrame( - destination="VK4BWI", source="VK4MSL", modifier=0x43 # Disconnect - ) - framecopy = frame.copy() - assert framecopy is not frame - - hex_cmp( - bytes(framecopy), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "43", # Control - ) - - -def test_dm_copy(): - """ - Test we can make a copy of a Disconnect Mode frame. - """ - frame = AX25DisconnectModeFrame( - destination="VK4BWI", - source="VK4MSL", - ) - framecopy = frame.copy() - assert framecopy is not frame - - hex_cmp( - bytes(framecopy), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "0f", # Control - ) - - -def test_ui_copy(): - """ - Test we can make a copy of a unnumbered information frame. - """ - frame = AX25UnnumberedInformationFrame( - destination="VK4BWI", - source="VK4MSL", - cr=True, - pid=0xF0, - payload=b"This is a test", - ) - framecopy = frame.copy() - assert framecopy is not frame - - hex_cmp( - bytes(framecopy), - "ac 96 68 84 ae 92 e0" # Destination - "ac 96 68 9a a6 98 61" # Source - "03" # Control - "f0" # PID - "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload - ) - - -def test_frmr_copy(): - """ - Test we can copy a FRMR frame. - """ - frame = AX25FrameRejectFrame( - destination="VK4BWI", - source="VK4MSL", - w=False, - x=False, - y=False, - z=False, - vr=0, - vs=0, - frmr_control=0x55, - frmr_cr=False, - ) - framecopy = frame.copy() - - assert framecopy is not frame - hex_cmp( - bytes(framecopy), - "ac 96 68 84 ae 92 60" # Destination - "ac 96 68 9a a6 98 e1" # Source - "87" # Control - "00 00 55", # FRMR data - ) - - def test_raw_str(): """ Test we can get a string representation of a raw frame. From 5daec31ec5f9b0a75b20e154af3786429569fc08 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 15:36:55 +1000 Subject: [PATCH 048/207] frame: Fix HDLC Optional Functions parsing, add tests --- aioax25/frame.py | 48 +-- tests/test_frame/test_iframe.py | 106 ++++++ tests/test_frame/test_sframe.py | 167 +++++++++ tests/test_frame/test_uframe.py | 629 ++++++++++++++++++++++++++++++++ tests/test_frame/test_xid.py | 402 ++++++++++++++++++++ 5 files changed, 1328 insertions(+), 24 deletions(-) create mode 100644 tests/test_frame/test_iframe.py create mode 100644 tests/test_frame/test_sframe.py create mode 100644 tests/test_frame/test_uframe.py create mode 100644 tests/test_frame/test_xid.py diff --git a/aioax25/frame.py b/aioax25/frame.py index f2b80ec..3392c2e 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1441,7 +1441,7 @@ class AX25XIDClassOfProceduresParameter(AX25XIDParameter): @classmethod def decode(cls, pv): # Decode the PV - pv = uint.decode(pv, big_endian=False) + pv = uint.decode(pv, big_endian=True) return cls( full_duplex=bool(pv & cls.FULL_DUPLEX), half_duplex=bool(pv & cls.HALF_DUPLEX), @@ -1493,7 +1493,7 @@ def pv(self): | ((self.unbalanced_arm_sec and self.UNBALANCED_ARM_SEC) or 0) | ((self.balanced_abm and self.BALANCED_ABM) or 0) ), - big_endian=False, + big_endian=True, length=2, ) @@ -1584,29 +1584,29 @@ def decode(cls, pv): # Decode the PV pv = uint.decode(pv, big_endian=False) return cls( - modulo128=pv & cls.MODULO128, - modulo8=pv & cls.MODULO8, - srej=pv & cls.SREJ, - rej=pv & cls.REJ, - srej_multiframe=pv & cls.SREJ_MULTIFRAME, - start_stop_transp=pv & cls.START_STOP_TRANSP, - start_stop_flow_ctl=pv & cls.START_STOP_FLOW_CTL, - start_stop_tx=pv & cls.START_STOP_TX, - sync_tx=pv & cls.SYNC_TX, - fcs32=pv & cls.FCS32, - fcs16=pv & cls.FCS16, - rd=pv & cls.RD, - test=pv & cls.TEST, - rset=pv & cls.RSET, - delete_i_cmd=pv & cls.DELETE_I_CMD, - delete_i_resp=pv & cls.DELETE_I_RESP, - extd_addr=pv & cls.EXTD_ADDR, - basic_addr=pv & cls.BASIC_ADDR, - up=pv & cls.UP, - sim_rim=pv & cls.SIM_RIM, - ui=pv & cls.UI, + modulo128=bool(pv & cls.MODULO128), + modulo8=bool(pv & cls.MODULO8), + srej=bool(pv & cls.SREJ), + rej=bool(pv & cls.REJ), + srej_multiframe=bool(pv & cls.SREJ_MULTIFRAME), + start_stop_transp=bool(pv & cls.START_STOP_TRANSP), + start_stop_flow_ctl=bool(pv & cls.START_STOP_FLOW_CTL), + start_stop_tx=bool(pv & cls.START_STOP_TX), + sync_tx=bool(pv & cls.SYNC_TX), + fcs32=bool(pv & cls.FCS32), + fcs16=bool(pv & cls.FCS16), + rd=bool(pv & cls.RD), + test=bool(pv & cls.TEST), + rset=bool(pv & cls.RSET), + delete_i_cmd=bool(pv & cls.DELETE_I_CMD), + delete_i_resp=bool(pv & cls.DELETE_I_RESP), + extd_addr=bool(pv & cls.EXTD_ADDR), + basic_addr=bool(pv & cls.BASIC_ADDR), + up=bool(pv & cls.UP), + sim_rim=bool(pv & cls.SIM_RIM), + ui=bool(pv & cls.UI), reserved2=(pv & cls.RESERVED2_MASK) >> cls.RESERVED2_POS, - reserved1=pv & cls.RESERVED1, + reserved1=bool(pv & cls.RESERVED1), ) def __init__( diff --git a/tests/test_frame/test_iframe.py b/tests/test_frame/test_iframe.py new file mode 100644 index 0000000..5a0b3c0 --- /dev/null +++ b/tests/test_frame/test_iframe.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +from aioax25.frame import ( + AX25Frame, + AX258BitInformationFrame, + AX2516BitInformationFrame, +) + +from nose.tools import eq_ +from ..hex import from_hex, hex_cmp + + +def test_8bit_iframe_decode(): + """ + Test we can decode an 8-bit information frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "d4" # Control + "ff" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74" # Payload + ), + modulo128=False, + ) + + assert isinstance( + frame, AX258BitInformationFrame + ), "Did not decode to 8-bit I-Frame" + eq_(frame.nr, 6) + eq_(frame.ns, 2) + eq_(frame.pid, 0xFF) + eq_(frame.payload, b"This is a test") + + +def test_16bit_iframe_decode(): + """ + Test we can decode an 16-bit information frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "04 0d" # Control + "ff" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74" # Payload + ), + modulo128=True, + ) + + assert isinstance( + frame, AX2516BitInformationFrame + ), "Did not decode to 16-bit I-Frame" + eq_(frame.nr, 6) + eq_(frame.ns, 2) + eq_(frame.pid, 0xFF) + eq_(frame.payload, b"This is a test") + + +def test_iframe_str(): + """ + Test we can get the string representation of an information frame. + """ + frame = AX258BitInformationFrame( + destination="VK4BWI", + source="VK4MSL", + nr=6, + ns=2, + pid=0xFF, + pf=True, + payload=b"Testing 1 2 3", + ) + + eq_( + str(frame), + "VK4MSL>VK4BWI: N(R)=6 P/F=True N(S)=2 PID=0xff " + "Payload=b'Testing 1 2 3'", + ) + + +def test_iframe_copy(): + """ + Test we can get the string representation of an information frame. + """ + frame = AX258BitInformationFrame( + destination="VK4BWI", + source="VK4MSL", + nr=6, + ns=2, + pid=0xFF, + pf=True, + payload=b"Testing 1 2 3", + ) + framecopy = frame.copy() + + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "d4" # Control byte + "ff" # PID + "54 65 73 74 69 6e 67 20" + "31 20 32 20 33", # Payload + ) diff --git a/tests/test_frame/test_sframe.py b/tests/test_frame/test_sframe.py new file mode 100644 index 0000000..68d6a78 --- /dev/null +++ b/tests/test_frame/test_sframe.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 + +from aioax25.frame import ( + AX25Frame, + AX258BitReceiveReadyFrame, + AX2516BitReceiveReadyFrame, + AX258BitRejectFrame, + AX2516BitRejectFrame, +) + +from nose.tools import eq_ +from ..hex import from_hex, hex_cmp + + +def test_sframe_payload_reject(): + """ + Test payloads are forbidden for S-frames + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "41" # Control + "31 32 33 34 35" # Payload + ), + modulo128=False, + ) + assert False, "Should not have worked" + except ValueError as e: + eq_(str(e), "Supervisory frames do not support payloads.") + + +def test_16bs_truncated_reject(): + """ + Test that 16-bit S-frames with truncated control fields are rejected. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "01" # Control (LSB only) + ), + modulo128=True, + ) + assert False, "Should not have worked" + except ValueError as e: + eq_(str(e), "Insufficient packet data") + + +def test_8bs_rr_frame(): + """ + Test we can generate a 8-bit RR supervisory frame + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "41" # Control + ), + modulo128=False, + ) + assert isinstance(frame, AX258BitReceiveReadyFrame) + eq_(frame.nr, 2) + + +def test_16bs_rr_frame(): + """ + Test we can generate a 16-bit RR supervisory frame + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "01 5c" # Control + ), + modulo128=True, + ) + assert isinstance(frame, AX2516BitReceiveReadyFrame) + eq_(frame.nr, 46) + + +def test_16bs_rr_encode(): + """ + Test we can encode a 16-bit RR supervisory frame + """ + frame = AX2516BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=46, pf=True + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "01 5d", # Control + ) + eq_(frame.control, 0x5D01) + + +def test_8bs_rej_decode_frame(): + """ + Test we can decode a 8-bit REJ supervisory frame + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "09" # Control byte + ), + modulo128=False, + ) + assert isinstance( + frame, AX258BitRejectFrame + ), "Did not decode to REJ frame" + eq_(frame.nr, 0) + eq_(frame.pf, False) + + +def test_16bs_rej_decode_frame(): + """ + Test we can decode a 16-bit REJ supervisory frame + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "09 00" # Control bytes + ), + modulo128=True, + ) + assert isinstance( + frame, AX2516BitRejectFrame + ), "Did not decode to REJ frame" + eq_(frame.nr, 0) + eq_(frame.pf, False) + + +def test_rr_frame_str(): + """ + Test we can get the string representation of a RR frame. + """ + frame = AX258BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=6 + ) + + eq_( + str(frame), + "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame", + ) + + +def test_rr_frame_copy(): + """ + Test we can get the string representation of a RR frame. + """ + frame = AX258BitReceiveReadyFrame( + destination="VK4BWI", source="VK4MSL", nr=6 + ) + framecopy = frame.copy() + + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "c1", # Control byte + ) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py new file mode 100644 index 0000000..0b31911 --- /dev/null +++ b/tests/test_frame/test_uframe.py @@ -0,0 +1,629 @@ +#!/usr/bin/env python3 + +from aioax25.frame import ( + AX25Frame, + AX25UnnumberedInformationFrame, + AX25FrameRejectFrame, + AX25UnnumberedFrame, + AX25DisconnectModeFrame, + AX25SetAsyncBalancedModeFrame, + AX25TestFrame, +) + +from nose.tools import eq_ +from ..hex import from_hex, hex_cmp + + +def test_decode_uframe(): + """ + Test that a U-frame gets decoded to an unnumbered frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "c3" # Control byte + ) + ) + assert isinstance( + frame, AX25UnnumberedFrame + ), "Did not decode to unnumbered frame" + eq_(frame.modifier, 0xC3) + + # We should see the control byte as our payload + hex_cmp(frame.frame_payload, "c3") + + +def test_decode_sabm(): + """ + Test that a SABM frame is recognised and decoded. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "6f" # Control byte + ) + ) + assert isinstance( + frame, AX25SetAsyncBalancedModeFrame + ), "Did not decode to SABM frame" + + +def test_decode_sabm_payload(): + """ + Test that a SABM frame forbids payload. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "6f" # Control byte + "11 22 33 44 55" # Payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + eq_(str(e), "Frame does not support payload") + + +def test_decode_uframe_payload(): + """ + Test that U-frames other than FRMR and UI are forbidden to have payloads. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "c3 11 22 33" # Control byte + ) + ) + assert False, "Should not have worked" + except ValueError as e: + eq_( + str(e), + "Unnumbered frames (other than UI and " + "FRMR) do not have payloads", + ) + + +def test_decode_frmr(): + """ + Test that a FRMR gets decoded to a frame reject frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "87" # Control byte + "11 22 33" # Payload + ) + ) + assert isinstance( + frame, AX25FrameRejectFrame + ), "Did not decode to FRMR frame" + eq_(frame.modifier, 0x87) + eq_(frame.w, True) + eq_(frame.x, False) + eq_(frame.y, False) + eq_(frame.z, False) + eq_(frame.vr, 1) + eq_(frame.frmr_cr, False) + eq_(frame.vs, 1) + + +def test_decode_frmr_len(): + """ + Test that a FRMR must have 3 byte payload. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "87" # Control byte + "11 22" # Payload + ) + ) + assert False, "Should not have worked" + except ValueError as e: + eq_(str(e), "Payload of FRMR must be 3 bytes") + + +def test_decode_ui(): + """ + Test that a UI gets decoded to an unnumbered information frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "03 11 22 33" # Control byte + ) + ) + assert isinstance( + frame, AX25UnnumberedInformationFrame + ), "Did not decode to UI frame" + eq_(frame.pid, 0x11) + hex_cmp(frame.payload, "22 33") + + +def test_decode_ui_len(): + """ + Test that a UI must have at least one byte payload. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "03" # Control byte + ) + ) + assert False, "Should not have worked" + except ValueError as e: + eq_(str(e), "Payload of UI must be at least one byte") + + +def test_encode_uframe(): + """ + Test that we can encode a U-frame. + """ + frame = AX25UnnumberedFrame( + destination="VK4BWI", source="VK4MSL", modifier=0xE7, cr=True + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e7", # Control + ) + + +def test_encode_frmr(): + """ + Test that we can encode a FRMR. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=True, + x=False, + y=True, + z=False, + vr=1, + frmr_cr=False, + vs=2, + frmr_control=0xAA, + cr=True, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "87" # Control + "05" # W/X/Y/Z + "24" # VR/CR/VS + "aa", # FRMR Control + ) + + +def test_encode_ui(): + """ + Test that we can encode a UI frame. + """ + frame = AX25UnnumberedInformationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + pid=0xF0, + payload=b"This is a test", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "03" # Control + "f0" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + + +def test_encode_test(): + """ + Test that we can encode a TEST frame. + """ + frame = AX25TestFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + payload=b"This is a test", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e3" # Control + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + + +def test_decode_test(): + """ + Test that we can decode a TEST frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e3" # Control + "31 32 33 34 35 36 37 38 39 2e 2e 2e" # Payload + ) + ) + assert isinstance(frame, AX25TestFrame) + eq_(frame.payload, b"123456789...") + + +def test_copy_test(): + """ + Test that we can copy a TEST frame. + """ + frame = AX25TestFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + payload=b"This is a test", + ) + framecopy = frame.copy() + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "e3" # Control + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + + +def test_encode_pf(): + """ + Test we can set the PF bit on a frame. + """ + frame = AX25UnnumberedInformationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + pf=True, + pid=0xF0, + payload=b"This is a test", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "13" # Control + "f0" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + eq_(frame.control, 0x13) + + +def test_encode_frmr_w(): + """ + Test we can set the W bit on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=True, + x=False, + y=False, + z=False, + vr=0, + vs=0, + frmr_control=0, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "01 00 00", # FRMR data + ) + + +def test_encode_frmr_x(): + """ + Test we can set the X bit on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=True, + y=False, + z=False, + vr=0, + vs=0, + frmr_control=0, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "02 00 00", # FRMR data + ) + + +def test_encode_frmr_y(): + """ + Test we can set the Y bit on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=True, + z=False, + vr=0, + vs=0, + frmr_control=0, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "04 00 00", # FRMR data + ) + + +def test_encode_frmr_z(): + """ + Test we can set the Z bit on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=False, + z=True, + vr=0, + vs=0, + frmr_control=0, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "08 00 00", # FRMR data + ) + + +def test_encode_frmr_cr(): + """ + Test we can set the CR bit on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=False, + z=False, + vr=0, + vs=0, + frmr_control=0, + frmr_cr=True, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "00 10 00", # FRMR data + ) + + +def test_encode_frmr_vr(): + """ + Test we can set the V(R) field on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=False, + z=False, + vr=5, + vs=0, + frmr_control=0, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "00 a0 00", # FRMR data + ) + + +def test_encode_frmr_vs(): + """ + Test we can set the V(S) field on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=False, + z=False, + vr=0, + vs=5, + frmr_control=0, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "00 0a 00", # FRMR data + ) + + +def test_encode_frmr_frmr_ctrl(): + """ + Test we can set the FRMR Control field on a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=False, + z=False, + vr=0, + vs=0, + frmr_control=0x55, + frmr_cr=False, + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "00 00 55", # FRMR data + ) + + +def test_encode_dm_frame(): + """ + Test we can encode a Disconnect Mode frame. + """ + frame = AX25DisconnectModeFrame( + destination="VK4BWI", + source="VK4MSL", + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "0f", # Control + ) + + +def test_u_copy(): + """ + Test we can make a copy of a unnumbered frame. + """ + frame = AX25UnnumberedFrame( + destination="VK4BWI", source="VK4MSL", modifier=0x43 # Disconnect + ) + framecopy = frame.copy() + assert framecopy is not frame + + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "43", # Control + ) + + +def test_dm_copy(): + """ + Test we can make a copy of a Disconnect Mode frame. + """ + frame = AX25DisconnectModeFrame( + destination="VK4BWI", + source="VK4MSL", + ) + framecopy = frame.copy() + assert framecopy is not frame + + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "0f", # Control + ) + + +def test_ui_copy(): + """ + Test we can make a copy of a unnumbered information frame. + """ + frame = AX25UnnumberedInformationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + pid=0xF0, + payload=b"This is a test", + ) + framecopy = frame.copy() + assert framecopy is not frame + + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "03" # Control + "f0" # PID + "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload + ) + + +def test_frmr_copy(): + """ + Test we can copy a FRMR frame. + """ + frame = AX25FrameRejectFrame( + destination="VK4BWI", + source="VK4MSL", + w=False, + x=False, + y=False, + z=False, + vr=0, + vs=0, + frmr_control=0x55, + frmr_cr=False, + ) + framecopy = frame.copy() + + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 60" # Destination + "ac 96 68 9a a6 98 e1" # Source + "87" # Control + "00 00 55", # FRMR data + ) + + +def test_ui_str(): + """ + Test we can get a string representation of a UI frame. + """ + frame = AX25UnnumberedInformationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + pid=0xF0, + payload=b"This is a test", + ) + eq_(str(frame), "VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'") diff --git a/tests/test_frame/test_xid.py b/tests/test_frame/test_xid.py new file mode 100644 index 0000000..e3f2f4d --- /dev/null +++ b/tests/test_frame/test_xid.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 + +from aioax25.frame import ( + AX25Frame, + AX25ExchangeIdentificationFrame, + AX25XIDRawParameter, + AX25XIDParameterIdentifier, + AX25XIDClassOfProceduresParameter, + AX25XIDHDLCOptionalFunctionsParameter, +) + +from nose.tools import eq_ +from ..hex import from_hex, hex_cmp + + +def test_encode_xid(): + """ + Test that we can encode a XID frame. + """ + frame = AX25ExchangeIdentificationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + fi=0x82, + gi=0x80, + parameters=[ + AX25XIDRawParameter(pi=0x12, pv=bytes([0x34, 0x56])), + AX25XIDRawParameter(pi=0x34, pv=None), + ], + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # Format indicator + "80" # Group Ident + "00 06" # Group length + # First parameter + "12" # Parameter ID + "02" # Length + "34 56" # Value + # Second parameter + "34" # Parameter ID + "00", # Length (no value) + ) + + +def test_decode_xid(): + """ + Test that we can decode a XID frame. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 0c" # GL + # Some parameters + "11 01 aa" + "12 01 bb" + "13 02 11 22" + "14 00" + ) + ) + assert isinstance(frame, AX25ExchangeIdentificationFrame) + eq_(frame.fi, 0x82) + eq_(frame.gi, 0x80) + eq_(len(frame.parameters), 4) + + param = frame.parameters[0] + eq_(param.pi, 0x11) + eq_(param.pv, b"\xaa") + + param = frame.parameters[1] + eq_(param.pi, 0x12) + eq_(param.pv, b"\xbb") + + param = frame.parameters[2] + eq_(param.pi, 0x13) + eq_(param.pv, b"\x11\x22") + + param = frame.parameters[3] + eq_(param.pi, 0x14) + assert param.pv is None + + +def test_decode_xid_fig46(): + """ + Test that we can decode the XID example from AX.25 2.2 figure 4.6. + """ + frame = AX25Frame.decode( + from_hex( + "9c 94 6e a0 40 40 e0" # Destination + "9c 6e 98 8a 9a 40 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 17" # GL + "02 02 00 20" + "03 03 86 a8 02" + "06 02 04 00" + "08 01 02" + "09 02 10 00" + "0a 01 03" + ) + ) + eq_(len(frame.parameters), 6) + + param = frame.parameters[0] + eq_(param.pi, AX25XIDParameterIdentifier.ClassesOfProcedure) + eq_(param.pv, b"\x00\x20") + + param = frame.parameters[1] + eq_(param.pi, AX25XIDParameterIdentifier.HDLCOptionalFunctions) + eq_(param.pv, b"\x86\xa8\x02") + + param = frame.parameters[2] + eq_(param.pi, AX25XIDParameterIdentifier.IFieldLengthReceive) + eq_(param.pv, b"\x04\x00") + + param = frame.parameters[3] + eq_(param.pi, AX25XIDParameterIdentifier.WindowSizeReceive) + eq_(param.pv, b"\x02") + + param = frame.parameters[4] + eq_(param.pi, AX25XIDParameterIdentifier.AcknowledgeTimer) + eq_(param.pv, b"\x10\x00") + + param = frame.parameters[5] + eq_(param.pi, AX25XIDParameterIdentifier.Retries) + eq_(param.pv, b"\x03") + + +def test_decode_xid_truncated_header(): + """ + Test that decoding a XID with truncated header fails. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00" # Incomplete GL + ) + ) + assert False, "This should not have worked" + except ValueError as e: + eq_(str(e), "Truncated XID header") + + +def test_decode_xid_truncated_payload(): + """ + Test that decoding a XID with truncated payload fails. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 05" # GL + "11" # Incomplete payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + eq_(str(e), "Truncated XID data") + + +def test_decode_xid_truncated_param_header(): + """ + Test that decoding a XID with truncated parameter header fails. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 01" # GL + "11" # Incomplete payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + eq_(str(e), "Insufficient data for parameter") + + +def test_decode_xid_truncated_param_value(): + """ + Test that decoding a XID with truncated parameter value fails. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # FI + "80" # GI + "00 04" # GL + "11 06 22 33" # Incomplete payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + eq_(str(e), "Parameter is truncated") + + +def test_copy_xid(): + """ + Test that we can copy a XID frame. + """ + frame = AX25ExchangeIdentificationFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + fi=0x82, + gi=0x80, + parameters=[ + AX25XIDRawParameter(pi=0x12, pv=bytes([0x34, 0x56])), + AX25XIDRawParameter(pi=0x34, pv=None), + ], + ) + framecopy = frame.copy() + assert framecopy is not frame + hex_cmp( + bytes(framecopy), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "af" # Control + "82" # Format indicator + "80" # Group Ident + "00 06" # Group length + # First parameter + "12" # Parameter ID + "02" # Length + "34 56" # Value + # Second parameter + "34" # Parameter ID + "00", # Length (no value) + ) + + +def test_decode_cop_param(): + """ + Test we can decode a Class Of Procedures parameter. + """ + param = AX25XIDClassOfProceduresParameter.decode(from_hex("80 20")) + eq_(param.half_duplex, True) + eq_(param.full_duplex, False) + eq_(param.unbalanced_nrm_pri, False) + eq_(param.unbalanced_nrm_sec, False) + eq_(param.unbalanced_arm_pri, False) + eq_(param.unbalanced_arm_sec, False) + eq_(param.reserved, 256) + + +def test_copy_cop_param(): + """ + Test we can copy a Class Of Procedures parameter. + """ + param = AX25XIDClassOfProceduresParameter( + full_duplex=False, + half_duplex=True, + reserved=193, # See that it is preserved + ) + copyparam = param.copy() + assert param is not copyparam + + # Ensure all parameters match + eq_(param.full_duplex, copyparam.full_duplex) + eq_(param.half_duplex, copyparam.half_duplex) + eq_(param.unbalanced_nrm_pri, copyparam.unbalanced_nrm_pri) + eq_(param.unbalanced_nrm_sec, copyparam.unbalanced_nrm_sec) + eq_(param.unbalanced_arm_pri, copyparam.unbalanced_arm_pri) + eq_(param.unbalanced_arm_sec, copyparam.unbalanced_arm_sec) + eq_(param.balanced_abm, copyparam.balanced_abm) + eq_(param.reserved, copyparam.reserved) + + +def test_encode_cop_param(): + """ + Test we can encode a Class Of Procedures parameter. + """ + param = AX25XIDClassOfProceduresParameter( + reserved=232, # 15-7 = 232 + full_duplex=True, # 6 = 1 + half_duplex=False, # 5 = 0 + unbalanced_nrm_pri=True, # 1 = 1 + ) + + # Expecting: + # 0111 0100 0100 0011 + + hex_cmp(param.pv, from_hex("74 43")) + + +def test_decode_hdlcfunc_param(): + """ + Test we can decode a HDLC Optional Functions parameter. + """ + param = AX25XIDHDLCOptionalFunctionsParameter.decode(from_hex("86 a8 82")) + # Specifically called out in the example (AX.25 2.2 spec Figure 4.6) + eq_(param.srej, True) + eq_(param.rej, True) + eq_(param.extd_addr, True) + eq_(param.fcs16, True) + eq_(param.modulo128, True) + eq_(param.sync_tx, True) + # Changed by us to test round-tripping + eq_(param.reserved2, 2) + # Modulo128 is on, so we expect this off + eq_(param.modulo8, False) + # Expected defaults + eq_(param.srej_multiframe, False) + eq_(param.start_stop_transp, False) + eq_(param.start_stop_flow_ctl, False) + eq_(param.sync_tx, True) + eq_(param.fcs32, False) + eq_(param.rd, False) + eq_(param.test, True) + eq_(param.rset, False) + eq_(param.delete_i_cmd, False) + eq_(param.delete_i_resp, False) + eq_(param.basic_addr, False) + eq_(param.up, False) + eq_(param.sim_rim, False) + eq_(param.ui, False) + eq_(param.reserved1, False) + + +def test_copy_hdlcfunc_param(): + """ + Test we can copy a HDLC Optional Functions parameter. + """ + param = AX25XIDHDLCOptionalFunctionsParameter( + modulo128=False, + modulo8=True, + rej=True, + srej=False, + rset=True, + test=False, + fcs32=True, + reserved1=True, + reserved2=1, + ) + copyparam = param.copy() + assert param is not copyparam + + # Ensure all parameters match + eq_(param.modulo128, copyparam.modulo128) + eq_(param.modulo8, copyparam.modulo8) + eq_(param.srej, copyparam.srej) + eq_(param.rej, copyparam.rej) + eq_(param.srej_multiframe, copyparam.srej_multiframe) + eq_(param.start_stop_transp, copyparam.start_stop_transp) + eq_(param.start_stop_flow_ctl, copyparam.start_stop_flow_ctl) + eq_(param.start_stop_tx, copyparam.start_stop_tx) + eq_(param.sync_tx, copyparam.sync_tx) + eq_(param.fcs32, copyparam.fcs32) + eq_(param.fcs16, copyparam.fcs16) + eq_(param.rd, copyparam.rd) + eq_(param.test, copyparam.test) + eq_(param.rset, copyparam.rset) + eq_(param.delete_i_cmd, copyparam.delete_i_cmd) + eq_(param.delete_i_resp, copyparam.delete_i_resp) + eq_(param.extd_addr, copyparam.extd_addr) + eq_(param.basic_addr, copyparam.basic_addr) + eq_(param.up, copyparam.up) + eq_(param.sim_rim, copyparam.sim_rim) + eq_(param.ui, copyparam.ui) + eq_(param.reserved2, copyparam.reserved2) + eq_(param.reserved1, copyparam.reserved1) + + +def test_encode_hdlcfunc_param(): + """ + Test we can encode a HDLC Optional Functions parameter. + """ + param = AX25XIDHDLCOptionalFunctionsParameter( + modulo128=True, + modulo8=False, + srej=True, + rej=False, + # Some atypical values + ui=True, + fcs32=True, + reserved2=2, + ) + + hex_cmp(param.pv, from_hex("8c a8 83")) From a377c812db4c9682c62cc7ff7c28c192286bace1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 15:48:34 +1000 Subject: [PATCH 049/207] frame: Fix XID HDLC Optional Features encoding/decoding --- aioax25/frame.py | 8 +++----- tests/test_frame/test_xid.py | 33 ++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 3392c2e..d952ac7 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1844,9 +1844,7 @@ def __init__(self, value): """ self._value = value - super(AX25XIDHDLCOptionalFunctionsParameter, self).__init__( - pi=self.PI - ) + super(AX25XIDBigEndianParameter, self).__init__(pi=self.PI) @property def pv(self): @@ -1865,11 +1863,11 @@ class AX25XIDIFieldLengthTransmitParameter(AX25XIDBigEndianParameter): class AX25XIDIFieldLengthReceiveParameter(AX25XIDBigEndianParameter): - PI = AX25XIDParameterIdentifier.WindowSizeReceive + PI = AX25XIDParameterIdentifier.IFieldLengthReceive class AX25XIDWindowSizeTransmitParameter(AX25XIDBigEndianParameter): - PI = AX25XIDParameterIdentifier.IFieldLengthTransmit + PI = AX25XIDParameterIdentifier.WindowSizeTransmit LENGTH = 1 diff --git a/tests/test_frame/test_xid.py b/tests/test_frame/test_xid.py index e3f2f4d..a423d59 100644 --- a/tests/test_frame/test_xid.py +++ b/tests/test_frame/test_xid.py @@ -7,6 +7,8 @@ AX25XIDParameterIdentifier, AX25XIDClassOfProceduresParameter, AX25XIDHDLCOptionalFunctionsParameter, + AX25XIDIFieldLengthReceiveParameter, + AX25XIDRetriesParameter, ) from nose.tools import eq_ @@ -24,6 +26,14 @@ def test_encode_xid(): fi=0x82, gi=0x80, parameters=[ + # Typical parameters we'd expect to see + AX25XIDClassOfProceduresParameter(half_duplex=True), + AX25XIDHDLCOptionalFunctionsParameter( + srej=True, rej=True, modulo128=True + ), + AX25XIDIFieldLengthReceiveParameter(1024), + AX25XIDRetriesParameter(5), + # Arbitrary parameters for testing AX25XIDRawParameter(pi=0x12, pv=bytes([0x34, 0x56])), AX25XIDRawParameter(pi=0x34, pv=None), ], @@ -35,14 +45,23 @@ def test_encode_xid(): "af" # Control "82" # Format indicator "80" # Group Ident - "00 06" # Group length - # First parameter - "12" # Parameter ID + "00 16" # Group length + # First parameter: CoP + "02" # Parameter ID "02" # Length - "34 56" # Value - # Second parameter - "34" # Parameter ID - "00", # Length (no value) + "00 21" # Value + # Second parameter: HDLC Optional Functions + "03" # Parameter ID + "03" # Length + "86 a8 02" # Value + # Third parameter: I field receive size + "06" "02" "04 00" + # Fourth parameter: retries + "0a" "01" "05" + # Fifth parameter: custom + "12" "02" "34 56" + # Sixth parameter: custom, no length set + "34" "00", ) From 9694d2570b56c3ea30eb4ca3a16b55a9d1380d76 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 15:54:11 +1000 Subject: [PATCH 050/207] frame: Add test cases for other XID parameters --- aioax25/frame.py | 18 ++++++++++++++++++ tests/test_frame/test_xid.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/aioax25/frame.py b/aioax25/frame.py index d952ac7..3dacc89 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1862,28 +1862,46 @@ class AX25XIDIFieldLengthTransmitParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.IFieldLengthTransmit +AX25XIDParameter.register(AX25XIDIFieldLengthTransmitParameter) + + class AX25XIDIFieldLengthReceiveParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.IFieldLengthReceive +AX25XIDParameter.register(AX25XIDIFieldLengthReceiveParameter) + + class AX25XIDWindowSizeTransmitParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.WindowSizeTransmit LENGTH = 1 +AX25XIDParameter.register(AX25XIDWindowSizeTransmitParameter) + + class AX25XIDWindowSizeReceiveParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.WindowSizeReceive LENGTH = 1 +AX25XIDParameter.register(AX25XIDWindowSizeReceiveParameter) + + class AX25XIDAcknowledgeTimerParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.AcknowledgeTimer +AX25XIDParameter.register(AX25XIDAcknowledgeTimerParameter) + + class AX25XIDRetriesParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.Retries +AX25XIDParameter.register(AX25XIDRetriesParameter) + + class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): """ Exchange Identification frame. diff --git a/tests/test_frame/test_xid.py b/tests/test_frame/test_xid.py index a423d59..3e59721 100644 --- a/tests/test_frame/test_xid.py +++ b/tests/test_frame/test_xid.py @@ -419,3 +419,32 @@ def test_encode_hdlcfunc_param(): ) hex_cmp(param.pv, from_hex("8c a8 83")) + + +def test_encode_retries_param(): + """ + Test we can encode a Retries parameter. + """ + param = AX25XIDRetriesParameter(96) + + hex_cmp(param.pv, from_hex("60")) + + +def test_decode_retries_param(): + """ + Test we can decode a Retries parameter. + """ + param = AX25XIDRetriesParameter.decode(from_hex("10")) + eq_(param.value, 16) + + +def test_copy_retries_param(): + """ + Test we can copy a Retries parameter. + """ + param = AX25XIDRetriesParameter(38) + copyparam = param.copy() + assert param is not copyparam + + # Ensure all parameters match + eq_(param.value, copyparam.value) From e5b98ed145c89cf8f34a01de42469df9a5bae0b8 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 16:19:14 +1000 Subject: [PATCH 051/207] frame: Define defaults for XIDs --- aioax25/frame.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/aioax25/frame.py b/aioax25/frame.py index 3dacc89..d0d3f5d 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1543,6 +1543,13 @@ def copy(self): AX25XIDParameter.register(AX25XIDClassOfProceduresParameter) +# From AX.25 2.2 spec section 6.3.2: +AX25_20_DEFAULT_XID_COP = AX25XIDClassOfProceduresParameter( + half_duplex=True, full_duplex=False +) +AX25_22_DEFAULT_XID_COP = AX25XIDClassOfProceduresParameter( + half_duplex=True, full_duplex=False +) class AX25XIDHDLCOptionalFunctionsParameter(AX25XIDParameter): """ @@ -1825,6 +1832,14 @@ def copy(self): AX25XIDParameter.register(AX25XIDHDLCOptionalFunctionsParameter) +# From AX.25 2.2 spec section 6.3.2: +AX25_20_DEFAULT_XID_HDLCOPTFUNC = AX25XIDHDLCOptionalFunctionsParameter( + rej=True, srej=False, modulo8=True, modulo128=False +) +AX25_22_DEFAULT_XID_HDLCOPTFUNC = AX25XIDHDLCOptionalFunctionsParameter( + rej=False, srej=True, modulo8=True, modulo128=False +) + class AX25XIDBigEndianParameter(AX25XIDParameter): """ @@ -1871,6 +1886,10 @@ class AX25XIDIFieldLengthReceiveParameter(AX25XIDBigEndianParameter): AX25XIDParameter.register(AX25XIDIFieldLengthReceiveParameter) +# From AX.25 2.2 spec section 6.3.2: +AX25_20_DEFAULT_XID_IFIELDRX = AX25XIDIFieldLengthReceiveParameter(2048) +AX25_22_DEFAULT_XID_IFIELDRX = AX25XIDIFieldLengthReceiveParameter(2048) + class AX25XIDWindowSizeTransmitParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.WindowSizeTransmit @@ -1887,6 +1906,10 @@ class AX25XIDWindowSizeReceiveParameter(AX25XIDBigEndianParameter): AX25XIDParameter.register(AX25XIDWindowSizeReceiveParameter) +# From AX.25 2.2 spec section 6.3.2: +AX25_20_DEFAULT_XID_WINDOWSZRX = AX25XIDIFieldLengthReceiveParameter(7) +AX25_22_DEFAULT_XID_WINDOWSZRX = AX25XIDIFieldLengthReceiveParameter(7) + class AX25XIDAcknowledgeTimerParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.AcknowledgeTimer @@ -1894,6 +1917,10 @@ class AX25XIDAcknowledgeTimerParameter(AX25XIDBigEndianParameter): AX25XIDParameter.register(AX25XIDAcknowledgeTimerParameter) +# From AX.25 2.2 spec section 6.3.2: +AX25_20_DEFAULT_XID_ACKTIMER = AX25XIDIFieldLengthReceiveParameter(3000) +AX25_22_DEFAULT_XID_ACKTIMER = AX25XIDIFieldLengthReceiveParameter(3000) + class AX25XIDRetriesParameter(AX25XIDBigEndianParameter): PI = AX25XIDParameterIdentifier.Retries @@ -1901,6 +1928,10 @@ class AX25XIDRetriesParameter(AX25XIDBigEndianParameter): AX25XIDParameter.register(AX25XIDRetriesParameter) +# From AX.25 2.2 spec section 6.3.2: +AX25_20_DEFAULT_XID_RETRIES = AX25XIDIFieldLengthReceiveParameter(10) +AX25_22_DEFAULT_XID_RETRIES = AX25XIDIFieldLengthReceiveParameter(10) + class AX25ExchangeIdentificationFrame(AX25UnnumberedFrame): """ From 28d6735923de613c6da52b581e3b560381edc570 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 16:29:27 +1000 Subject: [PATCH 052/207] frame: Define expected FI and GI for XID. --- aioax25/frame.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index d0d3f5d..ad0711c 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1545,12 +1545,13 @@ def copy(self): # From AX.25 2.2 spec section 6.3.2: AX25_20_DEFAULT_XID_COP = AX25XIDClassOfProceduresParameter( - half_duplex=True, full_duplex=False + half_duplex=True, full_duplex=False ) AX25_22_DEFAULT_XID_COP = AX25XIDClassOfProceduresParameter( - half_duplex=True, full_duplex=False + half_duplex=True, full_duplex=False ) + class AX25XIDHDLCOptionalFunctionsParameter(AX25XIDParameter): """ HDLC Optional Functions XID parameter. This parameter is used to negotiate @@ -1834,10 +1835,10 @@ def copy(self): # From AX.25 2.2 spec section 6.3.2: AX25_20_DEFAULT_XID_HDLCOPTFUNC = AX25XIDHDLCOptionalFunctionsParameter( - rej=True, srej=False, modulo8=True, modulo128=False + rej=True, srej=False, modulo8=True, modulo128=False ) AX25_22_DEFAULT_XID_HDLCOPTFUNC = AX25XIDHDLCOptionalFunctionsParameter( - rej=False, srej=True, modulo8=True, modulo128=False + rej=False, srej=True, modulo8=True, modulo128=False ) @@ -1972,13 +1973,17 @@ def decode(cls, header, control, data): cr=header.cr, ) + # AX.25 2.2 sect 4.3.3.7 defines the following values for FI and GI: + FI = 0x82 + GI = 0x80 + def __init__( self, destination, source, - fi, - gi, parameters, + fi=FI, + gi=GI, repeaters=None, pf=False, cr=False, From 427dcdcf8b5accb6cdae2ee02126f116e7af5476 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 17:00:25 +1000 Subject: [PATCH 053/207] peer, station: Add parameters, implement XID handling --- aioax25/peer.py | 228 ++++++++++++++++++++++++++++++++++++++++++++- aioax25/station.py | 20 +++- 2 files changed, 244 insertions(+), 4 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index bc908ec..6e142fc 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -35,6 +35,27 @@ AX2516BitSelectiveRejectFrame, AX25InformationFrameMixin, AX25SupervisoryFrameMixin, + AX25XIDParameterIdentifier, + AX25XIDClassOfProceduresParameter, + AX25XIDHDLCOptionalFunctionsParameter, + AX25XIDIFieldLengthTransmitParameter, + AX25XIDIFieldLengthReceiveParameter, + AX25XIDWindowSizeTransmitParameter, + AX25XIDWindowSizeReceiveParameter, + AX25XIDAcknowledgeTimerParameter, + AX25XIDRetriesParameter, + AX25_20_DEFAULT_XID_COP, + AX25_22_DEFAULT_XID_COP, + AX25_20_DEFAULT_XID_HDLCOPTFUNC, + AX25_22_DEFAULT_XID_HDLCOPTFUNC, + AX25_20_DEFAULT_XID_IFIELDRX, + AX25_22_DEFAULT_XID_IFIELDRX, + AX25_20_DEFAULT_XID_WINDOWSZRX, + AX25_22_DEFAULT_XID_WINDOWSZRX, + AX25_20_DEFAULT_XID_ACKTIMER, + AX25_22_DEFAULT_XID_ACKTIMER, + AX25_20_DEFAULT_XID_RETRIES, + AX25_22_DEFAULT_XID_RETRIES, ) @@ -45,6 +66,21 @@ class AX25Peer(object): AX25Station's getpeer method. """ + class AX25RejectMode(enum.Enum): + IMPLICIT = "implicit" + SELECTIVE = "selective" + SELECTIVE_RR = "selective_rr" + + # Value precedence: + _PRECEDENCE = {"selective_rr": 2, "selective": 1, "implicit": 0} + + @property + def precedence(self): + """ + Get the precedence of this mode. + """ + return self._PRECEDENCE[self.value] + class AX25PeerState(enum.Enum): # DISCONNECTED: No connection has been established DISCONNECTED = 0 @@ -70,16 +106,20 @@ def __init__( address, repeaters, max_ifield, + max_ifield_rx, max_retries, max_outstanding_mod8, max_outstanding_mod128, rr_delay, rr_interval, rnr_interval, + ack_timeout, idle_timeout, protocol, + modulo128, log, loop, + reject_mode, reply_path=None, locked_path=False, ): @@ -90,9 +130,13 @@ def __init__( self._repeaters = repeaters self._reply_path = reply_path self._address = address + self._ack_timeout = ack_timeout self._idle_timeout = idle_timeout + self._reject_mode = reject_mode self._max_ifield = max_ifield + self._max_ifield_rx = max_ifield_rx self._max_retries = max_retries + self._modulo128 = modulo128 self._max_outstanding_mod8 = max_outstanding_mod8 self._max_outstanding_mod128 = max_outstanding_mod128 self._rr_delay = rr_delay @@ -105,6 +149,7 @@ def __init__( # Internal state (see AX.25 2.2 spec 4.2.4) self._state = self.AX25PeerState.DISCONNECTED + self._reject_mode = None self._max_outstanding = None # Decided when SABM(E) received self._modulo = None # Set when SABM(E) received self._connected = False # Set to true on SABM UA @@ -548,6 +593,7 @@ def _on_receive_xid(self, frame): "Received XID from peer, we are not in AX.25 2.2 mode" ) return self._send_frmr(frame, w=True) + if self._state in ( self.AX25PeerState.CONNECTING, self.AX25PeerState.DISCONNECTING, @@ -558,9 +604,185 @@ def _on_receive_xid(self, frame): self._log.warning("UA is pending, dropping received XID") return - # TODO: figure out XID and send an appropriate response. - self._log.error("TODO: implement XID") - return self._send_frmr(frame, w=True) + # We have received an XID, AX.25 2.0 and earlier stations do not know + # this frame, so clearly this is at least AX.25 2.2. + if self._protocol == AX25Version.UNKNOWN: + self._protocol = AX25Version.AX25_22 + + # Don't process the contents of the frame unless FI and GI match. + if (frame.fi == frame.FI) and (frame.gi == frame.GI): + for param in frame.parameters: + if param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure: + self._process_xid_cop(param) + elif ( + param.pi + == AX25XIDParameterIdentifier.HDLCOptionalFunctions + ): + self._process_xid_hdlcoptfunc(param) + elif ( + param.pi == AX25XIDParameterIdentifier.IFieldLengthReceive + ): + self._process_xid_ifieldlenrx(param) + elif param.pi == AX25XIDParameterIdentifier.WindowSizeReceive: + self._process_xid_winszrx(param) + elif param.pi == AX25XIDParameterIdentifier.AcknowledgeTimer: + self._process_xid_acktimer(param) + elif param.pi == AX25XIDParameterIdentifier.Retries: + self._process_xid_retrycounter(param) + + if frame.header.cr: + # Other station is requesting negotiation, send response. + self._send_xid(cr=False) + + def _process_xid_cop(self, param): + if param.pv is None: + # We were told to use defaults. This is from a XID frame, + # so assume AX.25 2.2 defaults. + self._log.debug("XID: Assuming default Classes of Procedure") + param = AX25_22_DEFAULT_XID_COP + + # Ensure we don't confuse ourselves if the station sets both + # full-duplex and half-duplex bits. Half-duplex is always a + # safe choice in case of such confusion. + self._full_duplex = ( + self._full_duplex + and param.full_duplex + and (not param.half_duplex) + ) + self._log.debug("XID: Setting full-duplex: %s", self._full_duplex) + + def _process_xid_hdlcoptfunc(self, param): + if param.pv is None: + # We were told to use defaults. This is from a XID frame, + # so assume AX.25 2.2 defaults. + self._log.debug("XID: Assuming default HDLC Optional Features") + param = AX25_22_DEFAULT_XID_HDLCOPTFUNC + + # Negotiable parts of this parameter are: + # - SREJ/REJ bits + if param.srej and param.rej: + reject_mode = self.AX25RejectMode.SELECTIVE_RR + elif param.srej: + reject_mode = self.AX25RejectMode.SELECTIVE + else: + # Technically this means also the invalid SREJ=0 REJ=0, + # we'll assume they meant REJ=1 in that case. + reject_mode = self.AX25RejectMode.IMPLICIT + + if self._reject_mode.precedence > reject_mode: + self._reject_mode = reject_mode + self._log.debug("XID: Set reject mode: %s", self._reject_mode.value) + + if self._modulo128 and (not param.modulo128): + self._modulo128 = False + self._log.debug("XID: Set modulo128 mode: %s", self._modulo128) + + def _process_xid_ifieldlenrx(self, param): + if param.pv is None: + # We were told to use defaults. This is from a XID frame, + # so assume AX.25 2.2 defaults. + self._log.debug("XID: Assuming default I-Field Receive Length") + param = AX25_22_DEFAULT_XID_IFIELDRX + + self._max_ifield = min([self._max_ifield, param.value]) + self._log.debug( + "XID: Setting I-Field Receive Length: %d", self._max_ifield + ) + + def _process_xid_winszrx(self, param): + if param.pv is None: + # We were told to use defaults. This is from a XID frame, + # so assume AX.25 2.2 defaults. + self._log.debug("XID: Assuming default Window Size Receive") + param = AX25_22_DEFAULT_XID_WINDOWSZRX + + self._max_outstanding = min( + [ + ( + self._max_outstanding_mod128 + if self._modulo128 + else self._max_outstanding_mod8 + ), + param.value, + ] + ) + self._log.debug( + "XID: Setting Window Size Receive: %d", self._max_outstanding + ) + + def _process_xid_acktimer(self, param): + if param.pv is None: + # We were told to use defaults. This is from a XID frame, + # so assume AX.25 2.2 defaults. + self._log.debug("XID: Assuming default ACK timer") + param = AX25_22_DEFAULT_XID_ACKTIMER + + self._ack_timeout = ( + max([self._ack_timeout * 1000, param.value]) / 1000 + ) + self._log.debug( + "XID: Setting ACK timeout: %.3f sec", self._ack_timeout + ) + + def _process_xid_retrycounter(self, param): + if param.pv is None: + # We were told to use defaults. This is from a XID frame, + # so assume AX.25 2.2 defaults. + self._log.debug("XID: Assuming default retry limit") + param = AX25_22_DEFAULT_XID_ACKTIMER + + self._max_retries = max([self._max_retries, param.value]) + self._log.debug("XID: Setting retry limit: %d", self._max_retries) + + def _send_xid(self, cr): + self._transmit_frame( + AX25ExchangeIdentificationFrame( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + parameters=[ + # TODO: do we want to support full-duplex? + # what changes? + AX25XIDClassOfProceduresParameter( + half_duplex=True, full_duplex=False + ), + AX25XIDHDLCOptionalFunctionsParameter( + rej=( + self._reject_mode + in ( + self.AX25RejectMode.IMPLICIT, + self.AX25RejectMode.SELECTIVE_RR, + ) + ), + srej=( + self._reject_mode + in ( + self.AX25RejectMode.SELECTIVE, + self.AX25RejectMode.SELECTIVE_RR, + ) + ), + modulo8=(not self._modulo128), + modulo128=(self._modulo128), + ), + AX25XIDIFieldLengthTransmitParameter( + self._max_ifield * 8 + ), + AX25XIDIFieldLengthReceiveParameter( + self._max_ifield_rx * 8 + ), + AX25XIDWindowSizeTransmitParameter(self._max_outstanding), + AX25XIDWindowSizeReceiveParameter( + self._max_outstanding_mod128 + if self._modulus128 + else self._max_outstanding_mod8 + ), + AX25XIDAcknowledgeTimerParameter( + int(self._ack_timeout * 1000) + ), + ], + cr=cr, + ) + ) def _send_dm(self): """ diff --git a/aioax25/station.py b/aioax25/station.py index 688eff1..2af2891 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -37,13 +37,23 @@ def __init__( # Station call-sign and SSID callsign, ssid=None, + # Classes of Procedures options + full_duplex=False, + # HDLC Optional Functions + modulo128=False, # Whether to use Mod128 by default + reject_mode=AX25Peer.AX25RejectMode.SELECTIVE_RR, + # What reject mode to use? # Parameters (AX.25 2.2 sect 6.7.2) max_ifield=256, # aka N1 + max_ifield_rx=256, # the N1 we advertise in XIDs max_retries=10, # aka N2, value from figure 4.5 - # k value, for mod128 and mod8 connections + # k value, for mod128 and mod8 connections, this sets the + # advertised window size in XID. Peer station sets actual + # value used here. max_outstanding_mod8=7, max_outstanding_mod128=127, # Timer parameters + ack_timeout=3.0, # Acknowledge timeout (aka T1) idle_timeout=900.0, # Idle timeout before we "forget" peers rr_delay=10.0, # Delay between I-frame and RR rr_interval=30.0, # Poll interval when peer in busy state @@ -72,8 +82,12 @@ def __init__( self._address = AX25Address.decode(callsign, ssid).normalised self._interface = weakref.ref(interface) self._protocol = protocol + self._ack_timeout = ack_timeout self._idle_timeout = idle_timeout + self._reject_mode = AX25Peer.AX25RejectMode(reject_mode) + self._modulo128 = modulo128 self._max_ifield = max_ifield + self._max_ifield_rx = max_ifield_rx self._max_retries = max_retries self._max_outstanding_mod8 = max_outstanding_mod8 self._max_outstanding_mod128 = max_outstanding_mod128 @@ -144,7 +158,10 @@ def getpeer( pass # Not there, so set some defaults, then create + kwargs.setdefault("reject_mode", self._reject_mode) + kwargs.setdefault("modulo128", self._modulo128) kwargs.setdefault("max_ifield", self._max_ifield) + kwargs.setdefault("max_ifield_rx", self._max_ifield_rx) kwargs.setdefault("max_retries", self._max_retries) kwargs.setdefault("max_outstanding_mod8", self._max_outstanding_mod8) kwargs.setdefault( @@ -153,6 +170,7 @@ def getpeer( kwargs.setdefault("rr_delay", self._rr_delay) kwargs.setdefault("rr_interval", self._rr_interval) kwargs.setdefault("rnr_interval", self._rnr_interval) + kwargs.setdefault("ack_timeout", self._ack_timeout) kwargs.setdefault("idle_timeout", self._idle_timeout) kwargs.setdefault("protocol", AX25Version.UNKNOWN) peer = AX25Peer( From a14d80a251eb27583952f09d88c79adc4dc6d7d4 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 22:31:57 +1000 Subject: [PATCH 054/207] peer: Implement more connection logic --- aioax25/peer.py | 499 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 439 insertions(+), 60 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 6e142fc..36a0f57 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -7,6 +7,7 @@ """ from .signal import Signal + import weakref import enum @@ -100,6 +101,9 @@ class AX25PeerState(enum.Enum): # FRMR condition entered FRMR = 5 + # Incomming connection, awaiting our UA or DM response + INCOMING_CONNECTION = 6 + def __init__( self, station, @@ -152,7 +156,9 @@ def __init__( self._reject_mode = None self._max_outstanding = None # Decided when SABM(E) received self._modulo = None # Set when SABM(E) received + self._negotiated = False # Set to True after XID negotiation self._connected = False # Set to true on SABM UA + self._last_act = 0 # Time of last activity self._send_state = 0 # AKA V(S) self._send_seq = 0 # AKA N(S) self._recv_state = 0 # AKA V(R) @@ -173,6 +179,7 @@ def __init__( self._SREJFrameClass = None # Timeouts + self._incoming_connect_timeout_handle = None self._idle_timeout_handle = None self._rr_notification_timeout_handle = None @@ -193,8 +200,12 @@ def __init__( # and decremented each time a frame is REJected. self._tx_path_score = {} - # Handling of TEST frames + # Handling of various incoming frames self._testframe_handler = None + self._xidframe_handler = None + self._uaframe_handler = None + self._dmframe_handler = None + self._frmrframe_handler = None # Signals: @@ -263,7 +274,46 @@ def ping(self, payload=None, timeout=30.0, callback=None): handler.done_sig.connect(callback) handler._go() - return + return handler + + def connect(self): + """ + Connect to the remote node. + """ + if self._state in self.AX25PeerState.DISCONNECTED: + handler = AX25PeerConnectionHandler(self) + handler.done_sig.connect(self._on_connect_response) + handler._go() + + def accept(self): + """ + Accept an incoming connection from the peer. + """ + if self._state in self.AX25PeerState.INCOMING_CONNECTION: + self._log.info("Accepting incoming connection") + # Send a UA and set ourselves as connected + self._stop_incoming_connect_timer() + self._set_conn_state(self.AX25PeerState.CONNECTED) + self._send_ua() + + def reject(self): + """ + Reject an incoming connection from the peer. + """ + if self._state in self.AX25PeerState.INCOMING_CONNECTION: + self._log.info("Rejecting incoming connection") + # Send a DM and set ourselves as disconnected + self._stop_incoming_connect_timer() + self._set_conn_state(self.AX25PeerState.DISCONNECTED) + self._send_dm() + + def disconnect(self): + """ + Disconnect from the remote node. + """ + if self._state == self.AX25PeerState.CONNECTED: + self._uaframe_handler = self._on_disconnect + self._send_disc() def _cancel_idle_timeout(self): """ @@ -302,6 +352,9 @@ def _on_receive(self, frame): # Kick off the idle timer self._reset_idle_timeout() + # Update the last activity timestamp + self._last_act = self._loop.time() + if not self._locked_path: # Increment the received frame count path = tuple(reversed(frame.header.repeaters.reply)) @@ -341,6 +394,12 @@ def _on_receive(self, frame): if isinstance(frame, AX25TestFrame): # TEST frame return self._on_receive_test(frame) + elif isinstance(frame, AX25FrameRejectFrame): + # FRMR frame + return self._on_receive_frmr() + elif isinstance(frame, AX25UnnumberedAcknowledgeFrame): + # UA frame + return self._on_receive_ua() elif isinstance( frame, ( @@ -451,12 +510,20 @@ def _on_test_done(self, handler, **kwargs): self._testframe_handler = None + def _on_receive_frmr(self, frame): + # We just received a FRMR. + self._log.warning("Received FRMR from peer: %s", frame) + if self._frmrframe_handler is not None: + # Pass to handler + self._frmrframe_handler(frame) + else: + # No handler, send DM to recover + self._send_dm() + def _on_receive_sabm(self, frame): - modulo128 = isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) - self._log.debug( - "Received SABM(E): %s (extended=%s)", frame, modulo128 - ) - if modulo128: + extended = isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) + self._log.info("Received SABM%s", "E" if extended else "") + if extended: # If we don't know the protocol of the peer, we can safely assume # AX.25 2.2 now. if self._protocol == AX25Version.UNKNOWN: @@ -485,17 +552,89 @@ def _on_receive_sabm(self, frame): return self._send_dm() # Set up the connection state - self._init_connection(modulo128) + self._init_connection(extended) - # Send a UA and set ourselves as connected - self._set_conn_state(self.AX25PeerState.CONNECTED) - self._send_ua() + # Set the incoming connection state, and emit a signal via the + # station's 'connection_request' signal. + self._set_conn_state(self.AX25PeerState.INCOMING_CONNECTION) + self._start_incoming_connect_timer() + self._station().connection_request.emit(peer=self) + + def _start_incoming_connect_timer(self): + self._incoming_connect_timeout_handle = self._loop.call_later( + self._ack_timeout, self._on_incoming_connect_timeout + ) + + def _stop_incoming_connect_timer(self): + if self._incoming_connect_timeout_handle is not None: + self._incoming_connect_timeout_handle.cancel() + + self._incoming_connect_timeout_handle = None + + def _on_incoming_connect_timeout(self): + if self._state == self.AX25PeerState.INCOMING_CONNECTION: + self._incoming_connect_timeout_handle = None + self.reject() + + def _on_connect_response(self, response, **kwargs): + # Handle the connection result. + if response == "ack": + # We're in. + self._set_conn_state(self.AX25PeerState.CONNECTED) + else: + # Didn't work + self._set_conn_state(self.AX25PeerState.DISCONNECTED) + + def _negotiate(self, callback): + """ + Undertake negotiation with the peer station. + """ + # Sanity check, don't call this if we know the station won't take it. + if self._protocol not in (AX25Version.UNKNOWN, AX25Version.AX25_22): + raise RuntimeError( + "%s does not support negotiation" % self._protocol.value + ) + + handler = AX25PeerNegotiationHandler(self) + handler.done_sig.connect(self._on_negotiate_result) + handler.done_sig.connect(callback) + + handler._go() + self._set_conn_state(self.AX25PeerState.NEGOTIATING) + return + + def _on_negotiate_result(self, response, **kwargs): + """ + Handle the negotiation response. + """ + if response in ("frmr", "dm"): + # Other station did not like this. + self._log.info( + "Failed XID negotiation, loading AX.25 2.0 defaults" + ) + self._process_xid_cop(AX25_20_DEFAULT_XID_COP) + self._process_xid_hdlcoptfunc(AX25_20_DEFAULT_XID_HDLCOPTFUNC) + self._process_xid_ifieldlenrx(AX25_20_DEFAULT_XID_IFIELDRX) + self._process_xid_winszrx(AX25_20_DEFAULT_XID_WINDOWSZRX) + self._process_xid_acktimer(AX25_20_DEFAULT_XID_ACKTIMER) + self._process_xid_retrycounter(AX25_20_DEFAULT_XID_RETRIES) + if self._protocol in (AX25Version.UNKNOWN, AX25Version.AX25_22): + self._log.info("Downgrading to AX.25 2.0 due to failed XID") + self._protocol = AX25Version.AX25_20 + self._modulo128 = False + elif self._protocol != AX25Version.AX25_22: + # Clearly this station understands AX.25 2.2 + self._log.info("Upgrading to AX.25 2.2 due to successful XID") + self._protocol = AX25Version.AX25_22 + + self._negotiated = True + self._set_conn_state(self.AX25PeerState.DISCONNECTED) - def _init_connection(self, modulo128): + def _init_connection(self, extended): """ Initialise the AX.25 connection. """ - if modulo128: + if extended: # Set the maximum outstanding frames variable self._max_outstanding = self._max_outstanding_mod128 @@ -557,6 +696,7 @@ def _on_disconnect(self): """ Clean up the connection. """ + self._log.info("Disconnected from peer") # Send a UA and set ourselves as disconnected self._set_conn_state(self.AX25PeerState.DISCONNECTED) @@ -572,21 +712,37 @@ def _on_receive_disc(self): """ # Send a UA and set ourselves as disconnected self._log.info("Received DISC from peer") - self._on_disconnect() self._send_ua() + self._on_disconnect() + + def _on_receive_ua(self): + """ + Handle a Un-numbered Acknowledge from the peer + """ + self._log.info("Received UA from peer") + if self._uaframe_handler: + self._uaframe_handler() def _on_receive_dm(self): """ Handle a disconnect request from this peer. """ - # Set ourselves as disconnected - self._log.info("Received DM from peer") - self._on_disconnect() + if self._state in ( + self.AX25PeerState.CONNECTED, + self.AX25PeerState.CONNECTING, + ): + # Set ourselves as disconnected + self._log.info("Received DM from peer") + self._on_disconnect() + elif self._dmframe_handler: + self._dmframe_handler() + self._dmframe_handler = None def _on_receive_xid(self, frame): """ Handle a request to negotiate parameters. """ + self._log.info("Received XID from peer") if self._station().protocol != AX25Version.AX25_22: # Not supporting this in AX.25 2.0 mode self._log.warning( @@ -611,28 +767,64 @@ def _on_receive_xid(self, frame): # Don't process the contents of the frame unless FI and GI match. if (frame.fi == frame.FI) and (frame.gi == frame.GI): - for param in frame.parameters: - if param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure: - self._process_xid_cop(param) - elif ( - param.pi - == AX25XIDParameterIdentifier.HDLCOptionalFunctions - ): - self._process_xid_hdlcoptfunc(param) - elif ( - param.pi == AX25XIDParameterIdentifier.IFieldLengthReceive - ): - self._process_xid_ifieldlenrx(param) - elif param.pi == AX25XIDParameterIdentifier.WindowSizeReceive: - self._process_xid_winszrx(param) - elif param.pi == AX25XIDParameterIdentifier.AcknowledgeTimer: - self._process_xid_acktimer(param) - elif param.pi == AX25XIDParameterIdentifier.Retries: - self._process_xid_retrycounter(param) + # Key these by PI + params = dict([(p.pi, p) for p in frame.parameters]) + + # Process the parameters in this order + + self._process_xid_cop( + params.get( + AX25XIDParameterIdentifier.ClassesOfProcedure, + AX25_22_DEFAULT_XID_COP, + ) + ) + + self._process_xid_hdlcoptfunc( + params.get( + AX25XIDParameterIdentifier.HDLCOptionalFunctions, + AX25_22_DEFAULT_XID_HDLCOPTFUNC, + ) + ) + + self._process_xid_ifieldlenrx( + params.get( + AX25XIDParameterIdentifier.IFieldLengthReceive, + AX25_22_DEFAULT_XID_IFIELDRX, + ) + ) + + self._process_xid_winszrx( + params.get( + AX25XIDParameterIdentifier.WindowSizeReceive, + AX25_22_DEFAULT_XID_WINDOWSZRX, + ) + ) + + self._process_xid_acktimer( + params.get( + AX25XIDParameterIdentifier.AcknowledgeTimer, + AX25_22_DEFAULT_XID_ACKTIMER, + ) + ) + + self._process_xid_retrycounter( + params.get( + AX25XIDParameterIdentifier.Retries, + AX25_22_DEFAULT_XID_RETRIES, + ) + ) if frame.header.cr: # Other station is requesting negotiation, send response. self._send_xid(cr=False) + elif self._xidframe_handler is not None: + # This is a reply to our XID + self._xidframe_handler(frame) + self._xidframe_handler = None + + # Having received the XID, we consider ourselves having negotiated + # parameters. Future connections will skip this step. + self._negotiated = True def _process_xid_cop(self, param): if param.pv is None: @@ -669,11 +861,17 @@ def _process_xid_hdlcoptfunc(self, param): # we'll assume they meant REJ=1 in that case. reject_mode = self.AX25RejectMode.IMPLICIT - if self._reject_mode.precedence > reject_mode: + # We take the option with the lowest precedence + if self._reject_mode.precedence > reject_mode.precedence: self._reject_mode = reject_mode self._log.debug("XID: Set reject mode: %s", self._reject_mode.value) - if self._modulo128 and (not param.modulo128): + # - Modulo 8/128: again, unless the station positively says + # "I support modulo 128", use modulo 8. + # The remote station is meant to set either modulo128 *OR* modulo8. + # If we have it enabled our end, and they either have the modulo8 + # bit set, or the modulo128 bit clear, then fail back to modulo8. + if self._modulo128 and ((not param.modulo128) or param.modulo8): self._modulo128 = False self._log.debug("XID: Set modulo128 mode: %s", self._modulo128) @@ -729,11 +927,31 @@ def _process_xid_retrycounter(self, param): # We were told to use defaults. This is from a XID frame, # so assume AX.25 2.2 defaults. self._log.debug("XID: Assuming default retry limit") - param = AX25_22_DEFAULT_XID_ACKTIMER + param = AX25_22_DEFAULT_XID_RETRIES self._max_retries = max([self._max_retries, param.value]) self._log.debug("XID: Setting retry limit: %d", self._max_retries) + def _send_sabm(self): + """ + Send a SABM(E) frame to the remote station. + """ + self._log.info("Sending SABM%s", "E" if self._modulo128 else "") + SABMClass = ( + AX25SetAsyncBalancedModeExtendedFrame + if self._modulo128 + else AX25SetAsyncBalancedModeFrame + ) + + self._transmit_frame( + SABMClass( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + ) + ) + self._set_conn_state(self.AX25PeerState.CONNECTING) + def _send_xid(self, cr): self._transmit_frame( AX25ExchangeIdentificationFrame( @@ -741,10 +959,9 @@ def _send_xid(self, cr): source=self._station().address, repeaters=self.reply_path, parameters=[ - # TODO: do we want to support full-duplex? - # what changes? AX25XIDClassOfProceduresParameter( - half_duplex=True, full_duplex=False + half_duplex=not self._full_duplex, + full_duplex=self._full_duplex, ), AX25XIDHDLCOptionalFunctionsParameter( rej=( @@ -779,6 +996,7 @@ def _send_xid(self, cr): AX25XIDAcknowledgeTimerParameter( int(self._ack_timeout * 1000) ), + AX25XIDRetriesParameter(self._max_retries), ], cr=cr, ) @@ -788,7 +1006,7 @@ def _send_dm(self): """ Send a DM frame to the remote station. """ - self._log.debug("Sending DM") + self._log.info("Sending DM") self._transmit_frame( AX25DisconnectModeFrame( destination=self.address, @@ -797,11 +1015,24 @@ def _send_dm(self): ) ) + def _send_disc(self): + """ + Send a DISC frame to the remote station. + """ + self._log.info("Sending DISC") + self._transmit_frame( + AX25DisconnectFrame( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + ) + ) + def _send_ua(self): """ Send a UA frame to the remote station. """ - self._log.debug("Sending UA") + self._log.info("Sending UA") self._transmit_frame( AX25UnnumberedAcknowledgeFrame( destination=self.address, @@ -936,12 +1167,171 @@ def _transmit_iframe(self, ns): ) def _transmit_frame(self, frame, callback=None): - # Kick off the idle timer + # Update the last activity timestamp + self._last_act = self._loop.time() + + # Reset the idle timer self._reset_idle_timeout() return self._station()._interface().transmit(frame, callback=None) -class AX25PeerTestHandler(object): +class AX25PeerHelper(object): + """ + This is a base class for handling more complex acts like connecting, + negotiating parameters or sending test frames. + """ + + def __init__(self, peer, timeout): + self._peer = peer + self._loop = peer._loop + self._log = peer._log.getChild(self.__class__.__name__) + self._done = False + self._timeout = timeout + self._timeout_handle = None + + def _start_timer(self, callback): + self._timeout_handle = self._loop.call_later( + self._timeout, self._on_timeout + ) + + def _stop_timer(self): + if self._timeout_handle is None: + return + + if not self._timeout_handle.cancelled: + self._timeout_handle.cancel() + + self._timeout_handle = None + + def _finish(self, **kwargs): + self._done = True + self._stop_timer() + self.done_sig.emit(**kwargs) + + +class AX25PeerConnectionHandler(AX25PeerHelper): + """ + This class is used to manage the connection to the peer station. If the + station has not yet negotiated with the peer, this is done (unless we know + the peer won't tolerate it), then a SABM or SABME connection is made. + """ + + def __init__(self, peer): + super(AX25PeerConnectionHandler, self).__init__( + peer, peer._ack_timeout + ) + self._retries = peer._max_retries + + def _go(self): + if self._peer._negotiated: + # Already done, we can connect immediately + self._on_negotiated(response="already") + elif self._peer._protocol not in ( + AX25Version.AX25_22, + AX25Version.UNKNOWN, + ): + # Not compatible, just connect + self._on_negotiated(response="not_compatible") + else: + # Need to negotiate first. + self._peer._negotiate(self._on_negotiated) + + def _on_negotiated(self, response, **kwargs): + if response in ("xid", "frmr", "dm", "already", "retry"): + # We successfully negotiated with this station (or it was not + # required) + self._peer._uaframe_handler = self._on_receive_ua + self._peer._frmrframe_handler = self._on_receive_frmr + self._peer._dmframe_handler = self._on_receive_dm + self._peer._send_sabm() + self._start_timer() + else: + self._finish(response=response) + + def _on_receive_ua(self): + # Peer just acknowledged our connection + self._finish(response="ack") + + def _on_receive_frmr(self): + # Peer just rejected our connect frame, begin FRMR recovery. + self._peer._send_dm() + self._finish(response="frmr") + + def _on_receive_dm(self): + # Peer just rejected our connect frame. + self._finish(response="dm") + + def _on_timeout(self): + if self._retries: + self._retries -= 1 + self._on_negotiated(response="retry") + else: + self._finish(response="timeout") + + def _finish(self, **kwargs): + # Clean up hooks + self._peer._uaframe_handler = None + self._peer._frmrframe_handler = None + self._peer._dmframe_handler = None + super(AX25PeerConnectionHandler, self)._finish(**kwargs) + + +class AX25PeerNegotiationHandler(AX25PeerHelper): + """ + This class is used to manage the negotiation of link parameters with the + peer. Notably, if the peer is an AX.25 2.0, this loads defaults for that + revision of AX.25 and handles the FRMR/DM condition. + """ + + def __init__(self, peer): + super(AX25PeerNegotiationHandler, self).__init__( + peer, peer._ack_timeout + ) + self._retries = peer._max_retries + + def _go(self): + # Specs say AX.25 2.2 should respond with XID and 2.0 should respond + # with FRMR. It is also possible we could get a DM as some buggy AX.25 + # implementations respond with that in reply to unknown frames. + self._peer._xidframe_handler = self._on_receive_xid + self._peer._frmrframe_handler = self._on_receive_frmr + self._peer._dmframe_handler = self._on_receive_dm + self._peer._send_xid(cr=True) + self._start_timer() + + def _on_receive_xid(self, *args, **kwargs): + # XID frame received, we can consider ourselves done. + self._finish(response="xid") + + def _on_receive_frmr(self, *args, **kwargs): + # FRMR received. Evidently this station does not like XID. Caller + # will need to kiss and make up with the offended legacy station either + # with a SABM or DM. We can be certain this is not an AX.25 2.2 station. + self._finish(response="frmr") + + def _on_receive_dm(self, *args, **kwargs): + # DM received. This is not strictly in spec, but we'll treat it as a + # legacy AX.25 station telling us we're disconnected. No special + # handling needed. + self._finish(response="dm") + + def _on_timeout(self): + # No response received + if self._retries: + self._retries -= 1 + self._go() + else: + self._finish(response="timeout") + + def _finish(self, **kwargs): + # Clean up hooks + self._peer._xidframe_handler = None + self._peer._frmrframe_handler = None + self._peer._dmframe_handler = None + super(AX25PeerNegotiationHandler, self)._finish(**kwargs) + + +class AX25PeerTestHandler(AX25PeerHelper): """ This class is used to manage the sending of a TEST frame to the peer station and receiving the peer reply. Round-trip time is made available @@ -949,9 +1339,7 @@ class AX25PeerTestHandler(object): """ def __init__(self, peer, payload, timeout): - self._peer = peer - self._timeout = timeout - self._timeout_handle = None + super(AX25PeerTestHandler, self).__init__(peer, timeout) # Create the frame to send self._tx_frame = AX25TestFrame( @@ -1017,11 +1405,7 @@ def _go(self): Start the transmission. """ self.peer._transmit_frame(self.tx_frame, callback=self._transmit_done) - - # Start the time-out timer - self._timeout_handle = self._peer._loop.call_later( - self._timeout, self._on_timeout - ) + self._start_timer() def _transmit_done(self, *args, **kwargs): """ @@ -1037,20 +1421,15 @@ def _on_receive(self, frame, **kwargs): # We are done return - # Mark ourselves done - self._done = True - # Stop the clock! - self._timeout_handle.cancel() self._rx_time = self._peer._loop.time() # Stash the result and notify the caller self._rx_frame = frame - self.done_sig.emit(handler=self) + self._finish(handler=self) def _on_timeout(self): """ Process a time-out """ - self._done = True - self.done_sig.emit(handler=self) + self._finish(handler=self) From d0580099334e54851a071d6dc10c61a4cb61ec67 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 23 Nov 2019 23:22:43 +1000 Subject: [PATCH 055/207] peer: Implement S-frame handling logic --- aioax25/peer.py | 96 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 36a0f57..a88ba8c 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -487,8 +487,83 @@ def _on_receive_sframe(self, frame): """ Handle a S-frame from the peer. """ - # TODO - pass + if isinstance(frame, self._RRFrameClass): + self._on_receive_rr(frame) + elif isinstance(frame, self._RNRFrameClass): + self._on_receive_rnr(frame) + elif isinstance(frame, self._REJFrameClass): + self._on_receive_rej(frame) + elif isinstance(frame, self._SREJFrameClass): + self._on_receive_srej(frame) + + def _on_receive_rr(self, frame): + if frame.header.pf: + # Peer requesting our RR status + self._on_receive_rr_rnr_rej_query() + else: + # Received peer's RR status, peer no longer busy + self._log.debug("RR notification received from peer") + # AX.25 sect 4.3.2.1: "acknowledges properly received + # I frames up to and including N(R)-1" + self._ack_outstanding((frame.nr - 1) % self._modulo) + self._peer_busy = False + self._send_next_iframe() + + def _on_receive_rnr(self, frame): + if frame.header.pf: + # Peer requesting our RNR status + self._on_receive_rr_rnr_rej_query() + else: + # Received peer's RNR status, peer is busy + self._log.debug("RNR notification received from peer") + # AX.25 sect 4.3.2.2: "Frames up to N(R)-1 are acknowledged." + self._ack_outstanding((frame.nr - 1) % self._modulo) + self._peer_busy = True + + def _on_receive_rej(self, frame): + if frame.header.pf: + # Peer requesting rejected frame status. + self._on_receive_rr_rnr_rej_query() + else: + # Reject reject. + # AX.25 sect 4.3.2.3: "Any frames sent with a sequence number + # of N(R)-1 or less are acknowledged." + self._ack_outstanding((frame.nr - 1) % self._modulo) + # AX.25 2.2 section 6.4.7 says we set V(S) to this frame's + # N(R) and begin re-transmission. + self._send_state = frame.nr + self._send_next_iframe() + + def _on_receive_srej(self, frame): + if frame.header.pf: + # AX.25 2.2 sect 4.3.2.4: "If the P/F bit in the SREJ is set to + # '1', then I frames numbered up to N(R)-1 inclusive are considered + # as acknowledged." + self._ack_outstanding((frame.nr - 1) % self._modulo) + + # Re-send the outstanding frame + self._log.debug("Re-sending I-frame %d due to SREJ", frame.nr) + self._transmit_iframe(frame.nr) + + def _on_receive_rr_rnr_rej_query(self): + """ + Handle a RR or RNR query + """ + if self._local_busy: + self._log.debug("RR poll request from peer: we're busy") + self._send_rnr_notification() + else: + self._log.debug("RR poll request from peer: we're ready") + self._send_rr_notification() + + def _ack_outstanding(self, nr): + """ + Receive all frames up to N(R) + """ + self._log.debug("%d through to %d are received", self._send_state, nr) + while self._send_state != nr: + self._pending_iframes.pop(self._send_state) + self._send_state = (self._send_state + 1) % self._modulo def _on_receive_test(self, frame): self._log.debug("Received TEST response: %s", frame) @@ -1125,9 +1200,7 @@ def _send_next_iframe(self): """ Send the next I-frame, if there aren't too many frames pending. """ - if not len(self._pending_data) or ( - len(self._pending_iframes) >= self._max_outstanding - ): + if len(self._pending_iframes) >= self._max_outstanding: self._log.debug("Waiting for pending I-frames to be ACKed") return @@ -1135,13 +1208,18 @@ def _send_next_iframe(self): # it sends the I frame with the N(S) of the control field equal to # its current send state variable V(S)…" ns = self._send_state - assert ns not in self._pending_iframes, "Duplicate N(S) pending" + if ns not in self._pending_iframes: + if not self._pending_data: + # No data waiting + self._log.debug("No data pending transmission") + return - # Retrieve the next pending I-frame payload - (pid, payload) = self._pending_data.pop(0) - self._pending_iframes[ns] = (pid, payload) + # Retrieve the next pending I-frame payload and add it to the queue + self._pending_iframes[ns] = self._pending_data.pop(0) + self._log.debug("Creating new I-Frame %d", ns) # Send it + self._log.debug("Sending new I-Frame %d", ns) self._transmit_iframe(ns) # "After the I frame is sent, the send state variable is incremented From 529929869ff44abdfb4f9c147c77c7712400d420 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 10:58:40 +1000 Subject: [PATCH 056/207] peer: Move `peer` property, fix `_start_timer` --- aioax25/peer.py | 59 ++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index a88ba8c..5431c0a 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1267,7 +1267,14 @@ def __init__(self, peer, timeout): self._timeout = timeout self._timeout_handle = None - def _start_timer(self, callback): + @property + def peer(self): + """ + Return the peer being pinged + """ + return self._peer + + def _start_timer(self): self._timeout_handle = self._loop.call_later( self._timeout, self._on_timeout ) @@ -1282,6 +1289,9 @@ def _stop_timer(self): self._timeout_handle = None def _finish(self, **kwargs): + if self._done: + return + self._done = True self._stop_timer() self.done_sig.emit(**kwargs) @@ -1301,10 +1311,10 @@ def __init__(self, peer): self._retries = peer._max_retries def _go(self): - if self._peer._negotiated: + if self.peer._negotiated: # Already done, we can connect immediately self._on_negotiated(response="already") - elif self._peer._protocol not in ( + elif self.peer._protocol not in ( AX25Version.AX25_22, AX25Version.UNKNOWN, ): @@ -1312,16 +1322,16 @@ def _go(self): self._on_negotiated(response="not_compatible") else: # Need to negotiate first. - self._peer._negotiate(self._on_negotiated) + self.peer._negotiate(self._on_negotiated) def _on_negotiated(self, response, **kwargs): if response in ("xid", "frmr", "dm", "already", "retry"): # We successfully negotiated with this station (or it was not # required) - self._peer._uaframe_handler = self._on_receive_ua - self._peer._frmrframe_handler = self._on_receive_frmr - self._peer._dmframe_handler = self._on_receive_dm - self._peer._send_sabm() + self.peer._uaframe_handler = self._on_receive_ua + self.peer._frmrframe_handler = self._on_receive_frmr + self.peer._dmframe_handler = self._on_receive_dm + self.peer._send_sabm() self._start_timer() else: self._finish(response=response) @@ -1332,7 +1342,7 @@ def _on_receive_ua(self): def _on_receive_frmr(self): # Peer just rejected our connect frame, begin FRMR recovery. - self._peer._send_dm() + self.peer._send_dm() self._finish(response="frmr") def _on_receive_dm(self): @@ -1348,9 +1358,9 @@ def _on_timeout(self): def _finish(self, **kwargs): # Clean up hooks - self._peer._uaframe_handler = None - self._peer._frmrframe_handler = None - self._peer._dmframe_handler = None + self.peer._uaframe_handler = None + self.peer._frmrframe_handler = None + self.peer._dmframe_handler = None super(AX25PeerConnectionHandler, self)._finish(**kwargs) @@ -1371,10 +1381,10 @@ def _go(self): # Specs say AX.25 2.2 should respond with XID and 2.0 should respond # with FRMR. It is also possible we could get a DM as some buggy AX.25 # implementations respond with that in reply to unknown frames. - self._peer._xidframe_handler = self._on_receive_xid - self._peer._frmrframe_handler = self._on_receive_frmr - self._peer._dmframe_handler = self._on_receive_dm - self._peer._send_xid(cr=True) + self.peer._xidframe_handler = self._on_receive_xid + self.peer._frmrframe_handler = self._on_receive_frmr + self.peer._dmframe_handler = self._on_receive_dm + self.peer._send_xid(cr=True) self._start_timer() def _on_receive_xid(self, *args, **kwargs): @@ -1403,9 +1413,9 @@ def _on_timeout(self): def _finish(self, **kwargs): # Clean up hooks - self._peer._xidframe_handler = None - self._peer._frmrframe_handler = None - self._peer._dmframe_handler = None + self.peer._xidframe_handler = None + self.peer._frmrframe_handler = None + self.peer._dmframe_handler = None super(AX25PeerNegotiationHandler, self)._finish(**kwargs) @@ -1443,13 +1453,6 @@ def __init__(self, peer, payload, timeout): # Signal on "done" or time-out. self.done_sig = Signal() - @property - def peer(self): - """ - Return the peer being pinged - """ - return self._peer - @property def tx_time(self): """ @@ -1489,7 +1492,7 @@ def _transmit_done(self, *args, **kwargs): """ Note the time that the transmission took place. """ - self._tx_time = self._peer._loop.time() + self._tx_time = self.peer._loop.time() def _on_receive(self, frame, **kwargs): """ @@ -1500,7 +1503,7 @@ def _on_receive(self, frame, **kwargs): return # Stop the clock! - self._rx_time = self._peer._loop.time() + self._rx_time = self.peer._loop.time() # Stash the result and notify the caller self._rx_frame = frame From bfc7a6cfd06a81ac9864f0e7548b7fb5236d08c7 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 10:59:04 +1000 Subject: [PATCH 057/207] test mocks: Expand dummy peer class --- tests/mocks.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index ad0ba2e..926b85b 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +import logging +from aioax25.version import AX25Version + class DummyInterface(object): def __init__(self): @@ -17,13 +20,90 @@ def transmit(self, *args, **kwargs): self.transmit_calls.append((args, kwargs)) +class DummyLogger(object): + def __init__(self, name, parent=None): + self.parent = parent + self.name = name + self.logs = [] + + def log(self, level, msg, *args, **kwargs): + if parent is not None: + parent.logs.append((self.name, level, msg, args, kwargs)) + self.logs.append((self.name, level, msg, args, kwargs)) + + def critical(self, msg, *args, **kwargs): + self.log(logging.CRITICAL, msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + self.log(logging.DEBUG, msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + self.log(logging.ERROR, msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + self.log(logging.INFO, msg, *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + self.log(logging.WARNING, msg, *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + self.log(logging.WARNING, msg, *args, **kwargs) + + def getChild(self, name): + return DummyLogger(self.name + "." + name, parent=self) + + +class DummyTimeout(object): + def __init__(self, delay, callback, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.callback = callback + self.delay = delay + + self.cancelled = False + + def cancel(self): + assert not self.cancelled, "Cancel called twice!" + self.cancelled = True + + +class DummyIOLoop(object): + def __init__(self): + self.call_soon_list = [] + self.call_later_list = [] + + def call_soon(self, callback, *args, **kwargs): + self.call_soon_list.append((callback, args, kwargs)) + + def call_later(self, delay, callback, *args, **kwargs): + timeout = DummyTimeout(delay, callback, *args, **kwargs) + self.call_later_list.append(timeout) + return timeout + + class DummyPeer(object): def __init__(self, address): + self._log = DummyLogger("peer") + self._loop = DummyIOLoop() + + self._max_retries = 2 + self._ack_timeout = 0.1 + self.address_read = False self._address = address + self._negotiate_calls = [] + self.transmit_calls = [] self.on_receive_calls = [] + self._uaframe_handler = None + self._frmrframe_handler = None + self._dmframe_handler = None + self._xidframe_handler = None + + self._negotiated = False + self._protocol = AX25Version.UNKNOWN + @property def address(self): self.address_read = True @@ -31,3 +111,15 @@ def address(self): def _on_receive(self, *args, **kwargs): self.on_receive_calls.append((args, kwargs)) + + def _transmit_frame(self, frame, callback=None): + self.transmit_calls.append((frame, callback)) + + def _send_sabm(self): + self._transmit_frame("sabm") + + def _send_dm(self): + self._transmit_frame("dm") + + def _send_xid(self, cr=False): + self._transmit_frame("xid:cr=%s" % cr) From f3380fdb4add911fc2726cb95cb467adddbab545 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 11:00:21 +1000 Subject: [PATCH 058/207] peer tests: Add some helper tests --- tests/test_peer/__init__.py | 0 tests/test_peer/test_peerhelper.py | 83 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/test_peer/__init__.py create mode 100644 tests/test_peer/test_peerhelper.py diff --git a/tests/test_peer/__init__.py b/tests/test_peer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_peer/test_peerhelper.py b/tests/test_peer/test_peerhelper.py new file mode 100644 index 0000000..aaada0d --- /dev/null +++ b/tests/test_peer/test_peerhelper.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25PeerHelper +""" + +from nose.tools import eq_ + +from aioax25.peer import AX25PeerHelper +from aioax25.frame import AX25Address +from ..mocks import DummyPeer + + +def test_peerhelper_start_timer(): + """ + Test _start_timer sets up a timeout timer. + """ + peer = DummyPeer(AX25Address('VK4MSL')) + class TestHelper(AX25PeerHelper): + def _on_timeout(self): + pass + + helper = TestHelper(peer, timeout=0.1) + + assert helper._timeout_handle is None + + helper._start_timer() + eq_(len(peer._loop.call_later_list), 1) + timeout = peer._loop.call_later_list.pop(0) + + assert timeout is helper._timeout_handle + eq_(timeout.delay, 0.1) + eq_(timeout.callback, helper._on_timeout) + eq_(timeout.args, ()) + eq_(timeout.kwargs, {}) + + +def test_peerhelper_stop_timer(): + """ + Test _stop_timer clears an existing timeout timer. + """ + peer = DummyPeer(AX25Address('VK4MSL')) + helper = AX25PeerHelper(peer, timeout=0.1) + + # Inject a timeout timer + timeout = peer._loop.call_later(0, lambda : None) + helper._timeout_handle = timeout + assert not timeout.cancelled + + helper._stop_timer() + + assert timeout.cancelled + assert helper._timeout_handle is None + + +def test_peerhelper_stop_timer_cancelled(): + """ + Test _stop_timer does not call cancel on already cancelled timer. + """ + peer = DummyPeer(AX25Address('VK4MSL')) + helper = AX25PeerHelper(peer, timeout=0.1) + + # Inject a timeout timer + timeout = peer._loop.call_later(0, lambda : None) + helper._timeout_handle = timeout + + # Cancel it + timeout.cancel() + + # Now stop the timer, this should not call .cancel itself + helper._stop_timer() + assert helper._timeout_handle is None + + +def test_peerhelper_stop_timer_absent(): + """ + Test _stop_timer does nothing if time-out object absent. + """ + peer = DummyPeer(AX25Address('VK4MSL')) + helper = AX25PeerHelper(peer, timeout=0.1) + + # Cancel the non-existent timer, this should not trigger errors + helper._stop_timer() From 86bbe732ca653f1b28889e7b95e7291dd67bcb94 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 11:07:04 +1000 Subject: [PATCH 059/207] peer: Fix helper done_sig signal. --- aioax25/peer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 5431c0a..fabdc27 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1267,6 +1267,9 @@ def __init__(self, peer, timeout): self._timeout = timeout self._timeout_handle = None + # Signal on "done" or time-out. + self.done_sig = Signal() + @property def peer(self): """ @@ -1450,9 +1453,6 @@ def __init__(self, peer, payload, timeout): # Flag indicating we are done self._done = False - # Signal on "done" or time-out. - self.done_sig = Signal() - @property def tx_time(self): """ From 4978ad8fb1bd8630ea5ee4426afd778a17d988e8 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 11:07:23 +1000 Subject: [PATCH 060/207] peer tests: Add _finish tests --- tests/test_peer/test_peerhelper.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_peer/test_peerhelper.py b/tests/test_peer/test_peerhelper.py index aaada0d..d39ebea 100644 --- a/tests/test_peer/test_peerhelper.py +++ b/tests/test_peer/test_peerhelper.py @@ -81,3 +81,60 @@ def test_peerhelper_stop_timer_absent(): # Cancel the non-existent timer, this should not trigger errors helper._stop_timer() + + +def test_finish(): + """ + Test _finish stops the timer and emits the done signal. + """ + peer = DummyPeer(AX25Address('VK4MSL')) + helper = AX25PeerHelper(peer, timeout=0.1) + assert not helper._done + + # Hook the done signal + done_events = [] + helper.done_sig.connect(lambda **kw : done_events.append(kw)) + + # Inject a timeout timer + timeout = peer._loop.call_later(0, lambda : None) + helper._timeout_handle = timeout + + # Call _finish to end the helper + helper._finish(arg1='abc', arg2=123) + + # Task should be done + assert helper._done + + # Signal should have fired + eq_(done_events, [{'arg1': 'abc', 'arg2': 123}]) + + # Timeout should have been cancelled + assert timeout.cancelled + + +def test_finish_repeat(): + """ + Test _finish does nothing if already "done" + """ + peer = DummyPeer(AX25Address('VK4MSL')) + helper = AX25PeerHelper(peer, timeout=0.1) + + # Force the done flag. + helper._done = True + + # Hook the done signal + done_events = [] + helper.done_sig.connect(lambda **kw : done_events.append(kw)) + + # Inject a timeout timer + timeout = peer._loop.call_later(0, lambda : None) + helper._timeout_handle = timeout + + # Call _finish to end the helper + helper._finish(arg1='abc', arg2=123) + + # Signal should not have fired + eq_(done_events, []) + + # Timeout should not have been cancelled + assert not timeout.cancelled From 0097ad3e667a7400adfe81d9990ffc35155138c9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 11:09:45 +1000 Subject: [PATCH 061/207] mocks: Add time method to IO loop --- tests/mocks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index 926b85b..a8f6d1c 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import time import logging from aioax25.version import AX25Version @@ -66,6 +67,9 @@ def cancel(self): assert not self.cancelled, "Cancel called twice!" self.cancelled = True + def time(self): + return time.monotonic() + class DummyIOLoop(object): def __init__(self): From ad03bff32fd008fb5d9b941bc30fddb86f196f90 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:26:36 +1000 Subject: [PATCH 062/207] peer: Move pending test frame check logic. --- aioax25/peer.py | 6 +++--- tests/test_peer/test_peerhelper.py | 31 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index fabdc27..899ca27 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -264,9 +264,6 @@ def ping(self, payload=None, timeout=30.0, callback=None): """ Ping the peer and wait for a response. """ - if self._testframe_handler is not None: - raise RuntimeError("Existing ping request in progress") - handler = AX25PeerTestHandler(self, bytes(payload or b""), timeout) handler.done_sig.connect(self._on_test_done) @@ -1485,6 +1482,9 @@ def _go(self): """ Start the transmission. """ + if self.peer._testframe_handler is not None: + raise RuntimeError("Test frame already pending") + self.peer._testframe_handler = self self.peer._transmit_frame(self.tx_frame, callback=self._transmit_done) self._start_timer() diff --git a/tests/test_peer/test_peerhelper.py b/tests/test_peer/test_peerhelper.py index d39ebea..e50456c 100644 --- a/tests/test_peer/test_peerhelper.py +++ b/tests/test_peer/test_peerhelper.py @@ -15,7 +15,8 @@ def test_peerhelper_start_timer(): """ Test _start_timer sets up a timeout timer. """ - peer = DummyPeer(AX25Address('VK4MSL')) + peer = DummyPeer(AX25Address("VK4MSL")) + class TestHelper(AX25PeerHelper): def _on_timeout(self): pass @@ -39,11 +40,11 @@ def test_peerhelper_stop_timer(): """ Test _stop_timer clears an existing timeout timer. """ - peer = DummyPeer(AX25Address('VK4MSL')) + peer = DummyPeer(AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Inject a timeout timer - timeout = peer._loop.call_later(0, lambda : None) + timeout = peer._loop.call_later(0, lambda: None) helper._timeout_handle = timeout assert not timeout.cancelled @@ -57,11 +58,11 @@ def test_peerhelper_stop_timer_cancelled(): """ Test _stop_timer does not call cancel on already cancelled timer. """ - peer = DummyPeer(AX25Address('VK4MSL')) + peer = DummyPeer(AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Inject a timeout timer - timeout = peer._loop.call_later(0, lambda : None) + timeout = peer._loop.call_later(0, lambda: None) helper._timeout_handle = timeout # Cancel it @@ -76,7 +77,7 @@ def test_peerhelper_stop_timer_absent(): """ Test _stop_timer does nothing if time-out object absent. """ - peer = DummyPeer(AX25Address('VK4MSL')) + peer = DummyPeer(AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Cancel the non-existent timer, this should not trigger errors @@ -87,26 +88,26 @@ def test_finish(): """ Test _finish stops the timer and emits the done signal. """ - peer = DummyPeer(AX25Address('VK4MSL')) + peer = DummyPeer(AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) assert not helper._done # Hook the done signal done_events = [] - helper.done_sig.connect(lambda **kw : done_events.append(kw)) + helper.done_sig.connect(lambda **kw: done_events.append(kw)) # Inject a timeout timer - timeout = peer._loop.call_later(0, lambda : None) + timeout = peer._loop.call_later(0, lambda: None) helper._timeout_handle = timeout # Call _finish to end the helper - helper._finish(arg1='abc', arg2=123) + helper._finish(arg1="abc", arg2=123) # Task should be done assert helper._done # Signal should have fired - eq_(done_events, [{'arg1': 'abc', 'arg2': 123}]) + eq_(done_events, [{"arg1": "abc", "arg2": 123}]) # Timeout should have been cancelled assert timeout.cancelled @@ -116,7 +117,7 @@ def test_finish_repeat(): """ Test _finish does nothing if already "done" """ - peer = DummyPeer(AX25Address('VK4MSL')) + peer = DummyPeer(AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Force the done flag. @@ -124,14 +125,14 @@ def test_finish_repeat(): # Hook the done signal done_events = [] - helper.done_sig.connect(lambda **kw : done_events.append(kw)) + helper.done_sig.connect(lambda **kw: done_events.append(kw)) # Inject a timeout timer - timeout = peer._loop.call_later(0, lambda : None) + timeout = peer._loop.call_later(0, lambda: None) helper._timeout_handle = timeout # Call _finish to end the helper - helper._finish(arg1='abc', arg2=123) + helper._finish(arg1="abc", arg2=123) # Signal should not have fired eq_(done_events, []) From b92eb2e91b5b2969a66027c7b572173772547b1d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:26:57 +1000 Subject: [PATCH 063/207] peer: Fix test frame generation --- aioax25/peer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 899ca27..1f05ad8 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1433,7 +1433,7 @@ def __init__(self, peer, payload, timeout): self._tx_frame = AX25TestFrame( destination=peer.address, source=peer._station().address, - repeaters=self.reply_path, + repeaters=peer._station().reply_path, payload=payload, cr=True, ) From 0806e0f22adcd0108cd301211e8f2e5561b52c2e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:27:08 +1000 Subject: [PATCH 064/207] peer: Remove redundant _done init --- aioax25/peer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 1f05ad8..72a3fb2 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1447,9 +1447,6 @@ def __init__(self, peer, payload, timeout): # Time of reception self._rx_time = None - # Flag indicating we are done - self._done = False - @property def tx_time(self): """ From 3863050a3a120877eeb1d2267ff90182f5e16ee5 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:29:33 +1000 Subject: [PATCH 065/207] mocks: Add station to dummy peer object. --- tests/mocks.py | 13 ++++++++++++- tests/test_peer/test_peerhelper.py | 20 +++++++++++++------- tests/test_station/test_droppeer.py | 2 +- tests/test_station/test_getpeer.py | 2 +- tests/test_station/test_receive.py | 8 ++++---- 5 files changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/mocks.py b/tests/mocks.py index a8f6d1c..4921b5d 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -85,8 +85,15 @@ def call_later(self, delay, callback, *args, **kwargs): return timeout +class DummyStation(object): + def __init__(self, address, reply_path=None): + self.address = address + self.reply_path = reply_path or [] + + class DummyPeer(object): - def __init__(self, address): + def __init__(self, station, address): + self._station_ref = station self._log = DummyLogger("peer") self._loop = DummyIOLoop() @@ -108,6 +115,10 @@ def __init__(self, address): self._negotiated = False self._protocol = AX25Version.UNKNOWN + # Our fake weakref + def _station(self): + return self._station_ref + @property def address(self): self.address_read = True diff --git a/tests/test_peer/test_peerhelper.py b/tests/test_peer/test_peerhelper.py index e50456c..0f18abf 100644 --- a/tests/test_peer/test_peerhelper.py +++ b/tests/test_peer/test_peerhelper.py @@ -8,14 +8,15 @@ from aioax25.peer import AX25PeerHelper from aioax25.frame import AX25Address -from ..mocks import DummyPeer +from ..mocks import DummyPeer, DummyStation def test_peerhelper_start_timer(): """ Test _start_timer sets up a timeout timer. """ - peer = DummyPeer(AX25Address("VK4MSL")) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) class TestHelper(AX25PeerHelper): def _on_timeout(self): @@ -40,7 +41,8 @@ def test_peerhelper_stop_timer(): """ Test _stop_timer clears an existing timeout timer. """ - peer = DummyPeer(AX25Address("VK4MSL")) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Inject a timeout timer @@ -58,7 +60,8 @@ def test_peerhelper_stop_timer_cancelled(): """ Test _stop_timer does not call cancel on already cancelled timer. """ - peer = DummyPeer(AX25Address("VK4MSL")) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Inject a timeout timer @@ -77,7 +80,8 @@ def test_peerhelper_stop_timer_absent(): """ Test _stop_timer does nothing if time-out object absent. """ - peer = DummyPeer(AX25Address("VK4MSL")) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Cancel the non-existent timer, this should not trigger errors @@ -88,7 +92,8 @@ def test_finish(): """ Test _finish stops the timer and emits the done signal. """ - peer = DummyPeer(AX25Address("VK4MSL")) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) assert not helper._done @@ -117,7 +122,8 @@ def test_finish_repeat(): """ Test _finish does nothing if already "done" """ - peer = DummyPeer(AX25Address("VK4MSL")) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerHelper(peer, timeout=0.1) # Force the done flag. diff --git a/tests/test_station/test_droppeer.py b/tests/test_station/test_droppeer.py index 6e70d0e..1b01b1b 100644 --- a/tests/test_station/test_droppeer.py +++ b/tests/test_station/test_droppeer.py @@ -11,7 +11,7 @@ def test_known_peer_fetch_instance(): Test calling _drop_peer removes the peer """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - mypeer = DummyPeer(AX25Address("VK4BWI")) + mypeer = DummyPeer(station, AX25Address("VK4BWI")) # Inject the peer station._peers[mypeer._address] = mypeer diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py index 731a8db..cf05512 100644 --- a/tests/test_station/test_getpeer.py +++ b/tests/test_station/test_getpeer.py @@ -38,7 +38,7 @@ def test_known_peer_fetch_instance(): Test fetching an known peer returns that known peer """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - mypeer = DummyPeer(AX25Address("VK4BWI")) + mypeer = DummyPeer(station, AX25Address("VK4BWI")) # Inject the peer station._peers[mypeer._address] = mypeer diff --git a/tests/test_station/test_receive.py b/tests/test_station/test_receive.py index 34dbec6..2ddc326 100644 --- a/tests/test_station/test_receive.py +++ b/tests/test_station/test_receive.py @@ -64,8 +64,8 @@ def stub_on_test_frame(*args, **kwargs): station._on_test_frame = stub_on_test_frame # Inject a couple of peers - peer1 = DummyPeer(AX25Address("VK4MSL", ssid=7)) - peer2 = DummyPeer(AX25Address("VK4BWI", ssid=7)) + peer1 = DummyPeer(station, AX25Address("VK4MSL", ssid=7)) + peer2 = DummyPeer(station, AX25Address("VK4BWI", ssid=7)) station._peers[peer1._address] = peer1 station._peers[peer2._address] = peer2 @@ -106,8 +106,8 @@ def stub_on_test_frame(*args, **kwargs): station._on_test_frame = stub_on_test_frame # Inject a couple of peers - peer1 = DummyPeer(AX25Address("VK4MSL", ssid=7)) - peer2 = DummyPeer(AX25Address("VK4BWI", ssid=7)) + peer1 = DummyPeer(station, AX25Address("VK4MSL", ssid=7)) + peer2 = DummyPeer(station, AX25Address("VK4BWI", ssid=7)) station._peers[peer1._address] = peer1 station._peers[peer2._address] = peer2 From b727017d08d3121c9d500ccf79bbaadc5dfaa1f1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:35:08 +1000 Subject: [PATCH 066/207] mocks: Move time method to correct object --- tests/mocks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/mocks.py b/tests/mocks.py index 4921b5d..a1354e7 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -67,15 +67,15 @@ def cancel(self): assert not self.cancelled, "Cancel called twice!" self.cancelled = True - def time(self): - return time.monotonic() - class DummyIOLoop(object): def __init__(self): self.call_soon_list = [] self.call_later_list = [] + def time(self): + return time.monotonic() + def call_soon(self, callback, *args, **kwargs): self.call_soon_list.append((callback, args, kwargs)) From fdfc6fe8132e30436b0f17156ce25c60d0145f3f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:41:59 +1000 Subject: [PATCH 067/207] peer tests: Add tests for handling TEST frames. --- tests/mocks.py | 1 + tests/test_peer/test_peertest.py | 155 +++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 tests/test_peer/test_peertest.py diff --git a/tests/mocks.py b/tests/mocks.py index a1354e7..01c816e 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -107,6 +107,7 @@ def __init__(self, station, address): self.transmit_calls = [] self.on_receive_calls = [] + self._testframe_handler = None self._uaframe_handler = None self._frmrframe_handler = None self._dmframe_handler = None diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py new file mode 100644 index 0000000..f7e68d2 --- /dev/null +++ b/tests/test_peer/test_peertest.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25PeerTestHandler +""" + +from nose.tools import eq_, assert_almost_equal + +from aioax25.peer import AX25PeerTestHandler +from aioax25.frame import AX25Address, AX25TestFrame +from ..mocks import DummyPeer, DummyStation + + +def test_peertest_go(): + """ + Test _go transmits a test frame with CR=True and starts a timer. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + eq_(peer.transmit_calls, []) + + # Start it off + helper._go() + assert helper._timeout_handle is not None + eq_(helper._timeout_handle.delay, 0.1) + + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a test frame, with CR=True + assert frame is helper.tx_frame + assert isinstance(frame, AX25TestFrame) + assert frame.header.cr + + # Callback should be the _transmit_done method + eq_(callback, helper._transmit_done) + + # We should be registered to receive the reply + assert peer._testframe_handler is helper + + +def test_peertest_go_pending(): + """ + Test _go refuses to start if another test frame is pending. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + + # Inject a different helper + peer._testframe_handler = AX25PeerTestHandler(peer, + payload=b'test', timeout=0.2) + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + eq_(peer.transmit_calls, []) + + # Start it off + try: + helper._go() + assert False, 'Should not have worked' + except RuntimeError as e: + if str(e) != 'Test frame already pending': + raise + + +def test_peertest_transmit_done(): + """ + Test _transmit_done records time of transmission. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + + assert helper.tx_time is None + helper._transmit_done() + assert helper.tx_time is not None + + assert_almost_equal(peer._loop.time(), helper.tx_time, places=2) + + +def test_peertest_on_receive(): + """ + Test _on_receive records time of reception and finishes the helper. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + + # Hook the "done" event + done_events = [] + helper.done_sig.connect(lambda **kw : done_events.append(kw)) + + assert helper.rx_time is None + helper._on_receive(frame='Make believe TEST frame') + assert helper.rx_time is not None + + assert_almost_equal(peer._loop.time(), helper.rx_time, places=2) + eq_(helper.rx_frame, 'Make believe TEST frame') + + # We should be done now + eq_(len(done_events), 1) + done_evt = done_events.pop() + eq_(list(done_evt.keys()), ['handler']) + assert done_evt['handler'] is helper + + +def test_peertest_on_receive_done(): + """ + Test _on_receive ignores packets once done. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + + # Mark as done + helper._done = True + + # Hook the "done" event + done_events = [] + helper.done_sig.connect(lambda **kw : done_events.append(kw)) + + assert helper.rx_time is None + helper._on_receive(frame='Make believe TEST frame') + + assert helper.rx_time is None + assert helper.rx_frame is None + eq_(len(done_events), 0) + + +def test_peertest_on_timeout(): + """ + Test _on_timeout winds up the handler + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + + # Hook the "done" event + done_events = [] + helper.done_sig.connect(lambda **kw : done_events.append(kw)) + + helper._on_timeout() + + # We should be done now + eq_(len(done_events), 1) + done_evt = done_events.pop() + eq_(list(done_evt.keys()), ['handler']) + assert done_evt['handler'] is helper From f1cfc70c41840b23abebd4a72c928d5d5977bc16 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 12:43:06 +1000 Subject: [PATCH 068/207] .gitignore: Exclude coverage reports, egg info --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 266b308..d0108f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ __pycache__ *.egg-info *.pyc .coverage -coverage/ +aioax25.egg-info/ +cover/ testreports.xml dist/ build/ From 7a90fbc98497dbbe39db7233ee04a8229663a800 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 15:03:10 +1000 Subject: [PATCH 069/207] peer: Tweak hook management --- aioax25/peer.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 72a3fb2..5b0ea95 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1381,9 +1381,15 @@ def _go(self): # Specs say AX.25 2.2 should respond with XID and 2.0 should respond # with FRMR. It is also possible we could get a DM as some buggy AX.25 # implementations respond with that in reply to unknown frames. + if (self.peer._xidframe_handler is not None) \ + or (self.peer._frmrframe_handler is not None) \ + or (self.peer._dmframe_handler is not None): + raise RuntimeError('Another frame handler is busy') + self.peer._xidframe_handler = self._on_receive_xid self.peer._frmrframe_handler = self._on_receive_frmr self.peer._dmframe_handler = self._on_receive_dm + self.peer._send_xid(cr=True) self._start_timer() @@ -1413,9 +1419,12 @@ def _on_timeout(self): def _finish(self, **kwargs): # Clean up hooks - self.peer._xidframe_handler = None - self.peer._frmrframe_handler = None - self.peer._dmframe_handler = None + if self.peer._xidframe_handler == self._on_receive_xid: + self.peer._xidframe_handler = None + if self.peer._frmrframe_handler == self._on_receive_frmr: + self.peer._frmrframe_handler = None + if self.peer._dmframe_handler == self._on_receive_dm: + self.peer._dmframe_handler = None super(AX25PeerNegotiationHandler, self)._finish(**kwargs) From 4e454cdcc6250e3979b0576da43e89f6da358f78 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 15:03:27 +1000 Subject: [PATCH 070/207] peer: Add negotiation tests --- tests/test_peer/test_peernegotiation.py | 344 ++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 tests/test_peer/test_peernegotiation.py diff --git a/tests/test_peer/test_peernegotiation.py b/tests/test_peer/test_peernegotiation.py new file mode 100644 index 0000000..47be571 --- /dev/null +++ b/tests/test_peer/test_peernegotiation.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25PeerNegotiationHandler +""" + +from nose.tools import eq_, assert_almost_equal, assert_is, \ + assert_is_not_none, assert_is_none + +from aioax25.peer import AX25PeerNegotiationHandler +from aioax25.frame import AX25Address, AX25TestFrame +from ..mocks import DummyPeer, DummyStation + + +def test_peertest_go(): + """ + Test _go transmits a test frame with CR=True and starts a timer. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Start it off + helper._go() + assert_is_not_none(helper._timeout_handle) + eq_(helper._timeout_handle.delay, 0.1) + + # Helper should have hooked the handler events + eq_(peer._xidframe_handler, helper._on_receive_xid) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send an XID + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a test frame, with CR=True + eq_(frame, 'xid:cr=True') + assert_is_none(callback) + + +def test_peertest_go_xidframe_handler(): + """ + Test _go refuses to run if another XID frame handler is hooked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the XID handler + peer._xidframe_handler = lambda *a, **kwa : None + + # Try to start it off + try: + helper._go() + assert False, 'Should not have worked' + except RuntimeError as e: + if str(e) != 'Another frame handler is busy': + raise + + +def test_peertest_go_frmrframe_handler(): + """ + Test _go refuses to run if another FRMR frame handler is hooked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the FRMR handler + peer._frmrframe_handler = lambda *a, **kwa : None + + # Try to start it off + try: + helper._go() + assert False, 'Should not have worked' + except RuntimeError as e: + if str(e) != 'Another frame handler is busy': + raise + + +def test_peertest_go_dmframe_handler(): + """ + Test _go refuses to run if another DM frame handler is hooked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the DM handler + peer._dmframe_handler = lambda *a, **kwa : None + + # Try to start it off + try: + helper._go() + assert False, 'Should not have worked' + except RuntimeError as e: + if str(e) != 'Another frame handler is busy': + raise + + +def test_peertest_receive_xid(): + """ + Test _on_receive_xid ends the helper + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call _on_receive_xid + helper._on_receive_xid() + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'xid'}]) + + +def test_peertest_receive_frmr(): + """ + Test _on_receive_frmr ends the helper + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call _on_receive_frmr + helper._on_receive_frmr() + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'frmr'}]) + + +def test_peertest_receive_dm(): + """ + Test _on_receive_dm ends the helper + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call _on_receive_frmr + helper._on_receive_dm() + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'dm'}]) + + +def test_peertest_on_timeout_first(): + """ + Test _on_timeout retries if there are retries left + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # We should have retries left + eq_(helper._retries, 2) + + # Call the time-out handler + helper._on_timeout() + + # Check the time-out timer is re-started + assert_is_not_none(helper._timeout_handle) + eq_(helper._timeout_handle.delay, 0.1) + + # There should now be fewer retries left + eq_(helper._retries, 1) + + # Helper should have hooked the handler events + eq_(peer._xidframe_handler, helper._on_receive_xid) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send an XID + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a test frame, with CR=True + eq_(frame, 'xid:cr=True') + assert_is_none(callback) + + +def test_peertest_on_timeout_last(): + """ + Test _on_timeout finishes the helper if retries exhausted + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Pretend there are no more retries left + helper._retries = 0 + + # Pretend we're hooked up + peer._xidframe_handler = helper._on_receive_xid + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = helper._on_receive_dm + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call the time-out handler + helper._on_timeout() + + # Check the time-out timer is not re-started + assert_is_none(helper._timeout_handle) + + # Helper should have hooked the handler events + assert_is_none(peer._xidframe_handler) + assert_is_none(peer._frmrframe_handler) + assert_is_none(peer._dmframe_handler) + + # Station should not have been asked to send anything + eq_(len(peer.transmit_calls), 0) + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'timeout'}]) + + +def test_peertest_finish_disconnect_xid(): + """ + Test _finish leaves other XID hooks intact + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Pretend we're hooked up + dummy_xidframe_handler = lambda *a, **kw : None + peer._xidframe_handler = dummy_xidframe_handler + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = helper._on_receive_dm + + # Call the finish routine + helper._finish() + + # All except XID (which is not ours) should be disconnected + eq_(peer._xidframe_handler, dummy_xidframe_handler) + assert_is_none(peer._frmrframe_handler) + assert_is_none(peer._dmframe_handler) + + +def test_peertest_finish_disconnect_frmr(): + """ + Test _finish leaves other FRMR hooks intact + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Pretend we're hooked up + dummy_frmrframe_handler = lambda *a, **kw : None + peer._xidframe_handler = helper._on_receive_xid + peer._frmrframe_handler = dummy_frmrframe_handler + peer._dmframe_handler = helper._on_receive_dm + + # Call the finish routine + helper._finish() + + # All except XID (which is not ours) should be disconnected + assert_is_none(peer._xidframe_handler) + eq_(peer._frmrframe_handler, dummy_frmrframe_handler) + assert_is_none(peer._dmframe_handler) + + +def test_peertest_finish_disconnect_dm(): + """ + Test _finish leaves other DM hooks intact + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerNegotiationHandler(peer) + + # Pretend we're hooked up + dummy_dmframe_handler = lambda *a, **kw : None + peer._xidframe_handler = helper._on_receive_xid + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = dummy_dmframe_handler + + # Call the finish routine + helper._finish() + + # All except XID (which is not ours) should be disconnected + assert_is_none(peer._xidframe_handler) + assert_is_none(peer._frmrframe_handler) + eq_(peer._dmframe_handler, dummy_dmframe_handler) From a0b491aa234b66506e589a54b32143e9c3d101ef Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 15:07:14 +1000 Subject: [PATCH 071/207] peer tests: Rename negotiation test routines. --- tests/test_peer/test_peernegotiation.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_peer/test_peernegotiation.py b/tests/test_peer/test_peernegotiation.py index 47be571..a469a31 100644 --- a/tests/test_peer/test_peernegotiation.py +++ b/tests/test_peer/test_peernegotiation.py @@ -12,7 +12,7 @@ from ..mocks import DummyPeer, DummyStation -def test_peertest_go(): +def test_peerneg_go(): """ Test _go transmits a test frame with CR=True and starts a timer. """ @@ -44,7 +44,7 @@ def test_peertest_go(): assert_is_none(callback) -def test_peertest_go_xidframe_handler(): +def test_peerneg_go_xidframe_handler(): """ Test _go refuses to run if another XID frame handler is hooked. """ @@ -69,7 +69,7 @@ def test_peertest_go_xidframe_handler(): raise -def test_peertest_go_frmrframe_handler(): +def test_peerneg_go_frmrframe_handler(): """ Test _go refuses to run if another FRMR frame handler is hooked. """ @@ -94,7 +94,7 @@ def test_peertest_go_frmrframe_handler(): raise -def test_peertest_go_dmframe_handler(): +def test_peerneg_go_dmframe_handler(): """ Test _go refuses to run if another DM frame handler is hooked. """ @@ -119,7 +119,7 @@ def test_peertest_go_dmframe_handler(): raise -def test_peertest_receive_xid(): +def test_peerneg_receive_xid(): """ Test _on_receive_xid ends the helper """ @@ -143,7 +143,7 @@ def test_peertest_receive_xid(): eq_(done_evts, [{'response': 'xid'}]) -def test_peertest_receive_frmr(): +def test_peerneg_receive_frmr(): """ Test _on_receive_frmr ends the helper """ @@ -167,7 +167,7 @@ def test_peertest_receive_frmr(): eq_(done_evts, [{'response': 'frmr'}]) -def test_peertest_receive_dm(): +def test_peerneg_receive_dm(): """ Test _on_receive_dm ends the helper """ @@ -191,7 +191,7 @@ def test_peertest_receive_dm(): eq_(done_evts, [{'response': 'dm'}]) -def test_peertest_on_timeout_first(): +def test_peerneg_on_timeout_first(): """ Test _on_timeout retries if there are retries left """ @@ -231,7 +231,7 @@ def test_peertest_on_timeout_first(): assert_is_none(callback) -def test_peertest_on_timeout_last(): +def test_peerneg_on_timeout_last(): """ Test _on_timeout finishes the helper if retries exhausted """ @@ -275,7 +275,7 @@ def test_peertest_on_timeout_last(): eq_(done_evts, [{'response': 'timeout'}]) -def test_peertest_finish_disconnect_xid(): +def test_peerneg_finish_disconnect_xid(): """ Test _finish leaves other XID hooks intact """ @@ -298,7 +298,7 @@ def test_peertest_finish_disconnect_xid(): assert_is_none(peer._dmframe_handler) -def test_peertest_finish_disconnect_frmr(): +def test_peerneg_finish_disconnect_frmr(): """ Test _finish leaves other FRMR hooks intact """ @@ -321,7 +321,7 @@ def test_peertest_finish_disconnect_frmr(): assert_is_none(peer._dmframe_handler) -def test_peertest_finish_disconnect_dm(): +def test_peerneg_finish_disconnect_dm(): """ Test _finish leaves other DM hooks intact """ From dda0f05fbc298595a8407e036eaccbf73d0228c5 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 15:32:57 +1000 Subject: [PATCH 072/207] peer: Connection helper fixes --- aioax25/peer.py | 47 ++++++++--- tests/test_peer/test_peernegotiation.py | 101 +++++++++++++----------- tests/test_peer/test_peertest.py | 65 +++++++-------- 3 files changed, 121 insertions(+), 92 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 5b0ea95..29cfd19 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1314,10 +1314,10 @@ def _go(self): if self.peer._negotiated: # Already done, we can connect immediately self._on_negotiated(response="already") - elif self.peer._protocol not in ( - AX25Version.AX25_22, - AX25Version.UNKNOWN, - ): + elif ( + self.peer._protocol + not in (AX25Version.AX25_22, AX25Version.UNKNOWN) + ) or (self.peer._station()._protocol != AX25Version.AX25_22): # Not compatible, just connect self._on_negotiated(response="not_compatible") else: @@ -1325,9 +1325,24 @@ def _go(self): self.peer._negotiate(self._on_negotiated) def _on_negotiated(self, response, **kwargs): - if response in ("xid", "frmr", "dm", "already", "retry"): + if response in ( + "xid", + "frmr", + "dm", + "already", + "not_compatible", + "retry", + ): # We successfully negotiated with this station (or it was not # required) + if ( + (self.peer._uaframe_handler is not None) + or (self.peer._frmrframe_handler is not None) + or (self.peer._dmframe_handler is not None) + ): + # We're handling another frame now. + self._finish(response="station_busy") + return self.peer._uaframe_handler = self._on_receive_ua self.peer._frmrframe_handler = self._on_receive_frmr self.peer._dmframe_handler = self._on_receive_dm @@ -1358,9 +1373,15 @@ def _on_timeout(self): def _finish(self, **kwargs): # Clean up hooks - self.peer._uaframe_handler = None - self.peer._frmrframe_handler = None - self.peer._dmframe_handler = None + if self.peer._uaframe_handler == self._on_receive_ua: + self.peer._uaframe_handler = None + + if self.peer._frmrframe_handler == self._on_receive_frmr: + self.peer._frmrframe_handler = None + + if self.peer._dmframe_handler == self._on_receive_dm: + self.peer._dmframe_handler = None + super(AX25PeerConnectionHandler, self)._finish(**kwargs) @@ -1381,10 +1402,12 @@ def _go(self): # Specs say AX.25 2.2 should respond with XID and 2.0 should respond # with FRMR. It is also possible we could get a DM as some buggy AX.25 # implementations respond with that in reply to unknown frames. - if (self.peer._xidframe_handler is not None) \ - or (self.peer._frmrframe_handler is not None) \ - or (self.peer._dmframe_handler is not None): - raise RuntimeError('Another frame handler is busy') + if ( + (self.peer._xidframe_handler is not None) + or (self.peer._frmrframe_handler is not None) + or (self.peer._dmframe_handler is not None) + ): + raise RuntimeError("Another frame handler is busy") self.peer._xidframe_handler = self._on_receive_xid self.peer._frmrframe_handler = self._on_receive_frmr diff --git a/tests/test_peer/test_peernegotiation.py b/tests/test_peer/test_peernegotiation.py index a469a31..15ed6e3 100644 --- a/tests/test_peer/test_peernegotiation.py +++ b/tests/test_peer/test_peernegotiation.py @@ -4,8 +4,13 @@ Tests for AX25PeerNegotiationHandler """ -from nose.tools import eq_, assert_almost_equal, assert_is, \ - assert_is_not_none, assert_is_none +from nose.tools import ( + eq_, + assert_almost_equal, + assert_is, + assert_is_not_none, + assert_is_none, +) from aioax25.peer import AX25PeerNegotiationHandler from aioax25.frame import AX25Address, AX25TestFrame @@ -16,8 +21,8 @@ def test_peerneg_go(): """ Test _go transmits a test frame with CR=True and starts a timer. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -40,7 +45,7 @@ def test_peerneg_go(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a test frame, with CR=True - eq_(frame, 'xid:cr=True') + eq_(frame, "xid:cr=True") assert_is_none(callback) @@ -48,8 +53,8 @@ def test_peerneg_go_xidframe_handler(): """ Test _go refuses to run if another XID frame handler is hooked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -58,14 +63,14 @@ def test_peerneg_go_xidframe_handler(): eq_(peer.transmit_calls, []) # Hook the XID handler - peer._xidframe_handler = lambda *a, **kwa : None + peer._xidframe_handler = lambda *a, **kwa: None # Try to start it off try: helper._go() - assert False, 'Should not have worked' + assert False, "Should not have worked" except RuntimeError as e: - if str(e) != 'Another frame handler is busy': + if str(e) != "Another frame handler is busy": raise @@ -73,8 +78,8 @@ def test_peerneg_go_frmrframe_handler(): """ Test _go refuses to run if another FRMR frame handler is hooked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -83,14 +88,14 @@ def test_peerneg_go_frmrframe_handler(): eq_(peer.transmit_calls, []) # Hook the FRMR handler - peer._frmrframe_handler = lambda *a, **kwa : None + peer._frmrframe_handler = lambda *a, **kwa: None # Try to start it off try: helper._go() - assert False, 'Should not have worked' + assert False, "Should not have worked" except RuntimeError as e: - if str(e) != 'Another frame handler is busy': + if str(e) != "Another frame handler is busy": raise @@ -98,8 +103,8 @@ def test_peerneg_go_dmframe_handler(): """ Test _go refuses to run if another DM frame handler is hooked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -108,14 +113,14 @@ def test_peerneg_go_dmframe_handler(): eq_(peer.transmit_calls, []) # Hook the DM handler - peer._dmframe_handler = lambda *a, **kwa : None + peer._dmframe_handler = lambda *a, **kwa: None # Try to start it off try: helper._go() - assert False, 'Should not have worked' + assert False, "Should not have worked" except RuntimeError as e: - if str(e) != 'Another frame handler is busy': + if str(e) != "Another frame handler is busy": raise @@ -123,8 +128,8 @@ def test_peerneg_receive_xid(): """ Test _on_receive_xid ends the helper """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -133,22 +138,22 @@ def test_peerneg_receive_xid(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call _on_receive_xid helper._on_receive_xid() # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'xid'}]) + eq_(done_evts, [{"response": "xid"}]) def test_peerneg_receive_frmr(): """ Test _on_receive_frmr ends the helper """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -157,22 +162,22 @@ def test_peerneg_receive_frmr(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call _on_receive_frmr helper._on_receive_frmr() # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'frmr'}]) + eq_(done_evts, [{"response": "frmr"}]) def test_peerneg_receive_dm(): """ Test _on_receive_dm ends the helper """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -181,22 +186,22 @@ def test_peerneg_receive_dm(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call _on_receive_frmr helper._on_receive_dm() # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'dm'}]) + eq_(done_evts, [{"response": "dm"}]) def test_peerneg_on_timeout_first(): """ Test _on_timeout retries if there are retries left """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -227,7 +232,7 @@ def test_peerneg_on_timeout_first(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a test frame, with CR=True - eq_(frame, 'xid:cr=True') + eq_(frame, "xid:cr=True") assert_is_none(callback) @@ -235,8 +240,8 @@ def test_peerneg_on_timeout_last(): """ Test _on_timeout finishes the helper if retries exhausted """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up @@ -254,7 +259,7 @@ def test_peerneg_on_timeout_last(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call the time-out handler helper._on_timeout() @@ -272,19 +277,19 @@ def test_peerneg_on_timeout_last(): # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'timeout'}]) + eq_(done_evts, [{"response": "timeout"}]) def test_peerneg_finish_disconnect_xid(): """ Test _finish leaves other XID hooks intact """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Pretend we're hooked up - dummy_xidframe_handler = lambda *a, **kw : None + dummy_xidframe_handler = lambda *a, **kw: None peer._xidframe_handler = dummy_xidframe_handler peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = helper._on_receive_dm @@ -302,12 +307,12 @@ def test_peerneg_finish_disconnect_frmr(): """ Test _finish leaves other FRMR hooks intact """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Pretend we're hooked up - dummy_frmrframe_handler = lambda *a, **kw : None + dummy_frmrframe_handler = lambda *a, **kw: None peer._xidframe_handler = helper._on_receive_xid peer._frmrframe_handler = dummy_frmrframe_handler peer._dmframe_handler = helper._on_receive_dm @@ -325,12 +330,12 @@ def test_peerneg_finish_disconnect_dm(): """ Test _finish leaves other DM hooks intact """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerNegotiationHandler(peer) # Pretend we're hooked up - dummy_dmframe_handler = lambda *a, **kw : None + dummy_dmframe_handler = lambda *a, **kw: None peer._xidframe_handler = helper._on_receive_xid peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = dummy_dmframe_handler diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index f7e68d2..9cb6c62 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -15,9 +15,9 @@ def test_peertest_go(): """ Test _go transmits a test frame with CR=True and starts a timer. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) - helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerTestHandler(peer, payload=b"test", timeout=0.1) # Nothing should be set up assert helper._timeout_handle is None @@ -48,13 +48,14 @@ def test_peertest_go_pending(): """ Test _go refuses to start if another test frame is pending. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) - helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerTestHandler(peer, payload=b"test", timeout=0.1) # Inject a different helper - peer._testframe_handler = AX25PeerTestHandler(peer, - payload=b'test', timeout=0.2) + peer._testframe_handler = AX25PeerTestHandler( + peer, payload=b"test", timeout=0.2 + ) # Nothing should be set up assert helper._timeout_handle is None @@ -64,9 +65,9 @@ def test_peertest_go_pending(): # Start it off try: helper._go() - assert False, 'Should not have worked' + assert False, "Should not have worked" except RuntimeError as e: - if str(e) != 'Test frame already pending': + if str(e) != "Test frame already pending": raise @@ -74,9 +75,9 @@ def test_peertest_transmit_done(): """ Test _transmit_done records time of transmission. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) - helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerTestHandler(peer, payload=b"test", timeout=0.1) assert helper.tx_time is None helper._transmit_done() @@ -89,45 +90,45 @@ def test_peertest_on_receive(): """ Test _on_receive records time of reception and finishes the helper. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) - helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerTestHandler(peer, payload=b"test", timeout=0.1) # Hook the "done" event done_events = [] - helper.done_sig.connect(lambda **kw : done_events.append(kw)) + helper.done_sig.connect(lambda **kw: done_events.append(kw)) assert helper.rx_time is None - helper._on_receive(frame='Make believe TEST frame') + helper._on_receive(frame="Make believe TEST frame") assert helper.rx_time is not None assert_almost_equal(peer._loop.time(), helper.rx_time, places=2) - eq_(helper.rx_frame, 'Make believe TEST frame') + eq_(helper.rx_frame, "Make believe TEST frame") # We should be done now eq_(len(done_events), 1) done_evt = done_events.pop() - eq_(list(done_evt.keys()), ['handler']) - assert done_evt['handler'] is helper + eq_(list(done_evt.keys()), ["handler"]) + assert done_evt["handler"] is helper def test_peertest_on_receive_done(): """ Test _on_receive ignores packets once done. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) - helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerTestHandler(peer, payload=b"test", timeout=0.1) # Mark as done helper._done = True # Hook the "done" event done_events = [] - helper.done_sig.connect(lambda **kw : done_events.append(kw)) + helper.done_sig.connect(lambda **kw: done_events.append(kw)) assert helper.rx_time is None - helper._on_receive(frame='Make believe TEST frame') + helper._on_receive(frame="Make believe TEST frame") assert helper.rx_time is None assert helper.rx_frame is None @@ -138,18 +139,18 @@ def test_peertest_on_timeout(): """ Test _on_timeout winds up the handler """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) - helper = AX25PeerTestHandler(peer, payload=b'test', timeout=0.1) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerTestHandler(peer, payload=b"test", timeout=0.1) # Hook the "done" event done_events = [] - helper.done_sig.connect(lambda **kw : done_events.append(kw)) + helper.done_sig.connect(lambda **kw: done_events.append(kw)) helper._on_timeout() # We should be done now eq_(len(done_events), 1) done_evt = done_events.pop() - eq_(list(done_evt.keys()), ['handler']) - assert done_evt['handler'] is helper + eq_(list(done_evt.keys()), ["handler"]) + assert done_evt["handler"] is helper From ee681a9310752fdbf15cb3003f983adea1694e58 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 15:33:15 +1000 Subject: [PATCH 073/207] mocks: Add negotiation tweaks to dummy station --- tests/mocks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index 01c816e..0190fb5 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -89,6 +89,7 @@ class DummyStation(object): def __init__(self, address, reply_path=None): self.address = address self.reply_path = reply_path or [] + self._protocol = AX25Version.AX25_22 class DummyPeer(object): @@ -125,6 +126,9 @@ def address(self): self.address_read = True return self._address + def _negotiate(self, callback): + self._negotiate_calls.append(callback) + def _on_receive(self, *args, **kwargs): self.on_receive_calls.append((args, kwargs)) From 42e292bd8868b1f99ff806a742000f937a8a2b45 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 15:33:32 +1000 Subject: [PATCH 074/207] peer: Add connection test cases --- tests/test_peer/test_peerconnection.py | 497 +++++++++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 tests/test_peer/test_peerconnection.py diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py new file mode 100644 index 0000000..cef7bb7 --- /dev/null +++ b/tests/test_peer/test_peerconnection.py @@ -0,0 +1,497 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25PeerConnectionHandler +""" + +from nose.tools import eq_, assert_almost_equal, assert_is, \ + assert_is_not_none, assert_is_none + +from aioax25.version import AX25Version +from aioax25.peer import AX25PeerConnectionHandler +from aioax25.frame import AX25Address, AX25TestFrame +from ..mocks import DummyPeer, DummyStation + + +def test_peerconn_go(): + """ + Test _go triggers negotiation if the peer has not yet done so. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Start it off + helper._go() + + # We should hand off to the negotiation handler, so no timeout started yet: + assert_is_none(helper._timeout_handle) + + # There should be a call to negotiate, with a call-back pointing here. + eq_(peer._negotiate_calls, [helper._on_negotiated]) + + +def test_peerconn_go_peer_ax20(): + """ + Test _go skips negotiation for AX.25 2.0 stations. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station._protocol = AX25Version.AX25_20 + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Start it off + helper._go() + + # Check the time-out timer is started + assert_is_not_none(helper._timeout_handle) + eq_(helper._timeout_handle.delay, 0.1) + + # Helper should have hooked the handler events + eq_(peer._uaframe_handler, helper._on_receive_ua) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send a SABM + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a SABM frame + eq_(frame, 'sabm') + assert_is_none(callback) + + +def test_peerconn_go_peer_ax20(): + """ + Test _go skips negotiation for AX.25 2.0 peers. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + peer._protocol = AX25Version.AX25_20 + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Start it off + helper._go() + + # Check the time-out timer is started + assert_is_not_none(helper._timeout_handle) + eq_(helper._timeout_handle.delay, 0.1) + + # Helper should have hooked the handler events + eq_(peer._uaframe_handler, helper._on_receive_ua) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send a SABM + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a SABM frame + eq_(frame, 'sabm') + assert_is_none(callback) + + +def test_peerconn_go_prenegotiated(): + """ + Test _go skips negotiation if already completed. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Pretend we've done negotiation + peer._negotiated = True + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Start it off + helper._go() + + # Check the time-out timer is started + assert_is_not_none(helper._timeout_handle) + eq_(helper._timeout_handle.delay, 0.1) + + # Helper should have hooked the handler events + eq_(peer._uaframe_handler, helper._on_receive_ua) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send a SABM + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a SABM frame + eq_(frame, 'sabm') + assert_is_none(callback) + + +def test_peerconn_on_negotiated_failed(): + """ + Test _on_negotiated winds up the request if negotiation fails. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Try to connect + helper._on_negotiated('whoopsie') + eq_(done_evts, [{'response': 'whoopsie'}]) + + +def test_peerconn_on_negotiated_xidframe_handler(): + """ + Test _on_negotiated refuses to run if another UA frame handler is hooked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the UA handler + peer._uaframe_handler = lambda *a, **kwa : None + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Try to connect + helper._on_negotiated('xid') + eq_(done_evts, [{'response': 'station_busy'}]) + + +def test_peerconn_on_negotiated_frmrframe_handler(): + """ + Test _on_negotiated refuses to run if another FRMR frame handler is hooked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the FRMR handler + peer._frmrframe_handler = lambda *a, **kwa : None + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Try to connect + helper._on_negotiated('xid') + eq_(done_evts, [{'response': 'station_busy'}]) + + +def test_peerconn_on_negotiated_dmframe_handler(): + """ + Test _on_negotiated refuses to run if another DM frame handler is hooked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Hook the DM handler + peer._dmframe_handler = lambda *a, **kwa : None + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Try to connect + helper._on_negotiated('xid') + eq_(done_evts, [{'response': 'station_busy'}]) + + +def test_peerconn_on_negotiated_xid(): + """ + Test _on_negotiated triggers SABM transmission on receipt of XID + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Try to connect + helper._on_negotiated('xid') + + # Helper should not be done + assert not helper._done + + # Helper should be hooked + eq_(peer._uaframe_handler, helper._on_receive_ua) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send a SABM + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a SABM frame + eq_(frame, 'sabm') + assert_is_none(callback) + + +def test_peerconn_receive_ua(): + """ + Test _on_receive_ua ends the helper + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call _on_receive_ua + helper._on_receive_ua() + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'ack'}]) + + +def test_peerconn_receive_frmr(): + """ + Test _on_receive_frmr ends the helper + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call _on_receive_frmr + helper._on_receive_frmr() + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'frmr'}]) + + # Station should have been asked to send a DM + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a DM frame + eq_(frame, 'dm') + assert_is_none(callback) + + +def test_peerconn_receive_dm(): + """ + Test _on_receive_dm ends the helper + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call _on_receive_frmr + helper._on_receive_dm() + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'dm'}]) + + +def test_peerconn_on_timeout_first(): + """ + Test _on_timeout retries if there are retries left + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # We should have retries left + eq_(helper._retries, 2) + + # Call the time-out handler + helper._on_timeout() + + # Check the time-out timer is re-started + assert_is_not_none(helper._timeout_handle) + eq_(helper._timeout_handle.delay, 0.1) + + # There should now be fewer retries left + eq_(helper._retries, 1) + + # Helper should have hooked the handler events + eq_(peer._uaframe_handler, helper._on_receive_ua) + eq_(peer._frmrframe_handler, helper._on_receive_frmr) + eq_(peer._dmframe_handler, helper._on_receive_dm) + + # Station should have been asked to send an XID + eq_(len(peer.transmit_calls), 1) + (frame, callback) = peer.transmit_calls.pop(0) + + # Frame should be a SABM frame + eq_(frame, 'sabm') + assert_is_none(callback) + + +def test_peerconn_on_timeout_last(): + """ + Test _on_timeout finishes the helper if retries exhausted + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert_is_none(helper._timeout_handle) + assert not helper._done + eq_(peer.transmit_calls, []) + + # Pretend there are no more retries left + helper._retries = 0 + + # Pretend we're hooked up + peer._uaframe_handler = helper._on_receive_ua + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = helper._on_receive_dm + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + + # Call the time-out handler + helper._on_timeout() + + # Check the time-out timer is not re-started + assert_is_none(helper._timeout_handle) + + # Helper should have hooked the handler events + assert_is_none(peer._uaframe_handler) + assert_is_none(peer._frmrframe_handler) + assert_is_none(peer._dmframe_handler) + + # Station should not have been asked to send anything + eq_(len(peer.transmit_calls), 0) + + # See that the helper finished + assert_is(helper._done, True) + eq_(done_evts, [{'response': 'timeout'}]) + + +def test_peerconn_finish_disconnect_ua(): + """ + Test _finish leaves other UA hooks intact + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Pretend we're hooked up + dummy_uaframe_handler = lambda *a, **kw : None + peer._uaframe_handler = dummy_uaframe_handler + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = helper._on_receive_dm + + # Call the finish routine + helper._finish() + + # All except UA (which is not ours) should be disconnected + eq_(peer._uaframe_handler, dummy_uaframe_handler) + assert_is_none(peer._frmrframe_handler) + assert_is_none(peer._dmframe_handler) + + +def test_peerconn_finish_disconnect_frmr(): + """ + Test _finish leaves other FRMR hooks intact + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Pretend we're hooked up + dummy_frmrframe_handler = lambda *a, **kw : None + peer._uaframe_handler = helper._on_receive_ua + peer._frmrframe_handler = dummy_frmrframe_handler + peer._dmframe_handler = helper._on_receive_dm + + # Call the finish routine + helper._finish() + + # All except FRMR (which is not ours) should be disconnected + assert_is_none(peer._uaframe_handler) + eq_(peer._frmrframe_handler, dummy_frmrframe_handler) + assert_is_none(peer._dmframe_handler) + + +def test_peerconn_finish_disconnect_dm(): + """ + Test _finish leaves other DM hooks intact + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = DummyPeer(station, AX25Address('VK4MSL')) + helper = AX25PeerConnectionHandler(peer) + + # Pretend we're hooked up + dummy_dmframe_handler = lambda *a, **kw : None + peer._uaframe_handler = helper._on_receive_ua + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = dummy_dmframe_handler + + # Call the finish routine + helper._finish() + + # All except DM (which is not ours) should be disconnected + assert_is_none(peer._uaframe_handler) + assert_is_none(peer._frmrframe_handler) + eq_(peer._dmframe_handler, dummy_dmframe_handler) From 67d80b2521d071fb7b89b7dd18860b1dd930d42d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 16:19:51 +1000 Subject: [PATCH 075/207] mocks: Fix pass-through of logs to parent --- tests/mocks.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/mocks.py b/tests/mocks.py index 0190fb5..e8f92f5 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -27,11 +27,14 @@ def __init__(self, name, parent=None): self.name = name self.logs = [] - def log(self, level, msg, *args, **kwargs): - if parent is not None: - parent.logs.append((self.name, level, msg, args, kwargs)) + def _log(self, name, level, msg, *args, **kwargs): + if self.parent is not None: + self.parent._log(self.name, level, msg, *args, **kwargs) self.logs.append((self.name, level, msg, args, kwargs)) + def log(self, level, msg, *args, **kwargs): + self._log(self.name, level, msg, *args, **kwargs) + def critical(self, msg, *args, **kwargs): self.log(logging.CRITICAL, msg, *args, **kwargs) From 833bf1dc5089802d3d385bc9dc8be4468a0e59b9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 16:19:59 +1000 Subject: [PATCH 076/207] mocks: Set full duplex flag on station --- tests/mocks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mocks.py b/tests/mocks.py index e8f92f5..13534a7 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -92,6 +92,7 @@ class DummyStation(object): def __init__(self, address, reply_path=None): self.address = address self.reply_path = reply_path or [] + self._full_duplex = False self._protocol = AX25Version.AX25_22 From 9d9742de435e792b740075fcabffa3af624f7dcc Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 16:20:25 +1000 Subject: [PATCH 077/207] station, peer: Pass through full_duplex flag --- aioax25/peer.py | 2 + aioax25/station.py | 2 + tests/test_peer/test_peerconnection.py | 143 +++++++++++++------------ 3 files changed, 78 insertions(+), 69 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 29cfd19..dcaf6fc 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -124,6 +124,7 @@ def __init__( log, loop, reject_mode, + full_duplex, reply_path=None, locked_path=False, ): @@ -153,6 +154,7 @@ def __init__( # Internal state (see AX.25 2.2 spec 4.2.4) self._state = self.AX25PeerState.DISCONNECTED + self._full_duplex = full_duplex self._reject_mode = None self._max_outstanding = None # Decided when SABM(E) received self._modulo = None # Set when SABM(E) received diff --git a/aioax25/station.py b/aioax25/station.py index 2af2891..5040a34 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -94,6 +94,7 @@ def __init__( self._rr_delay = rr_delay self._rr_interval = rr_interval self._rnr_interval = rnr_interval + self._full_duplex = full_duplex self._log = log self._loop = loop @@ -158,6 +159,7 @@ def getpeer( pass # Not there, so set some defaults, then create + kwargs.setdefault("full_duplex", self._full_duplex) kwargs.setdefault("reject_mode", self._reject_mode) kwargs.setdefault("modulo128", self._modulo128) kwargs.setdefault("max_ifield", self._max_ifield) diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index cef7bb7..1da0547 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -4,8 +4,13 @@ Tests for AX25PeerConnectionHandler """ -from nose.tools import eq_, assert_almost_equal, assert_is, \ - assert_is_not_none, assert_is_none +from nose.tools import ( + eq_, + assert_almost_equal, + assert_is, + assert_is_not_none, + assert_is_none, +) from aioax25.version import AX25Version from aioax25.peer import AX25PeerConnectionHandler @@ -17,8 +22,8 @@ def test_peerconn_go(): """ Test _go triggers negotiation if the peer has not yet done so. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -39,9 +44,9 @@ def test_peerconn_go_peer_ax20(): """ Test _go skips negotiation for AX.25 2.0 stations. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) station._protocol = AX25Version.AX25_20 - peer = DummyPeer(station, AX25Address('VK4MSL')) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -65,7 +70,7 @@ def test_peerconn_go_peer_ax20(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, 'sabm') + eq_(frame, "sabm") assert_is_none(callback) @@ -73,8 +78,8 @@ def test_peerconn_go_peer_ax20(): """ Test _go skips negotiation for AX.25 2.0 peers. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) peer._protocol = AX25Version.AX25_20 helper = AX25PeerConnectionHandler(peer) @@ -99,7 +104,7 @@ def test_peerconn_go_peer_ax20(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, 'sabm') + eq_(frame, "sabm") assert_is_none(callback) @@ -107,8 +112,8 @@ def test_peerconn_go_prenegotiated(): """ Test _go skips negotiation if already completed. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Pretend we've done negotiation @@ -135,7 +140,7 @@ def test_peerconn_go_prenegotiated(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, 'sabm') + eq_(frame, "sabm") assert_is_none(callback) @@ -143,8 +148,8 @@ def test_peerconn_on_negotiated_failed(): """ Test _on_negotiated winds up the request if negotiation fails. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -154,19 +159,19 @@ def test_peerconn_on_negotiated_failed(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Try to connect - helper._on_negotiated('whoopsie') - eq_(done_evts, [{'response': 'whoopsie'}]) + helper._on_negotiated("whoopsie") + eq_(done_evts, [{"response": "whoopsie"}]) def test_peerconn_on_negotiated_xidframe_handler(): """ Test _on_negotiated refuses to run if another UA frame handler is hooked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -175,23 +180,23 @@ def test_peerconn_on_negotiated_xidframe_handler(): eq_(peer.transmit_calls, []) # Hook the UA handler - peer._uaframe_handler = lambda *a, **kwa : None + peer._uaframe_handler = lambda *a, **kwa: None # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Try to connect - helper._on_negotiated('xid') - eq_(done_evts, [{'response': 'station_busy'}]) + helper._on_negotiated("xid") + eq_(done_evts, [{"response": "station_busy"}]) def test_peerconn_on_negotiated_frmrframe_handler(): """ Test _on_negotiated refuses to run if another FRMR frame handler is hooked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -200,23 +205,23 @@ def test_peerconn_on_negotiated_frmrframe_handler(): eq_(peer.transmit_calls, []) # Hook the FRMR handler - peer._frmrframe_handler = lambda *a, **kwa : None + peer._frmrframe_handler = lambda *a, **kwa: None # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Try to connect - helper._on_negotiated('xid') - eq_(done_evts, [{'response': 'station_busy'}]) + helper._on_negotiated("xid") + eq_(done_evts, [{"response": "station_busy"}]) def test_peerconn_on_negotiated_dmframe_handler(): """ Test _on_negotiated refuses to run if another DM frame handler is hooked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -225,27 +230,27 @@ def test_peerconn_on_negotiated_dmframe_handler(): eq_(peer.transmit_calls, []) # Hook the DM handler - peer._dmframe_handler = lambda *a, **kwa : None + peer._dmframe_handler = lambda *a, **kwa: None # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Try to connect - helper._on_negotiated('xid') - eq_(done_evts, [{'response': 'station_busy'}]) + helper._on_negotiated("xid") + eq_(done_evts, [{"response": "station_busy"}]) def test_peerconn_on_negotiated_xid(): """ Test _on_negotiated triggers SABM transmission on receipt of XID """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Try to connect - helper._on_negotiated('xid') + helper._on_negotiated("xid") # Helper should not be done assert not helper._done @@ -260,7 +265,7 @@ def test_peerconn_on_negotiated_xid(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, 'sabm') + eq_(frame, "sabm") assert_is_none(callback) @@ -268,8 +273,8 @@ def test_peerconn_receive_ua(): """ Test _on_receive_ua ends the helper """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -278,22 +283,22 @@ def test_peerconn_receive_ua(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call _on_receive_ua helper._on_receive_ua() # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'ack'}]) + eq_(done_evts, [{"response": "ack"}]) def test_peerconn_receive_frmr(): """ Test _on_receive_frmr ends the helper """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -302,21 +307,21 @@ def test_peerconn_receive_frmr(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call _on_receive_frmr helper._on_receive_frmr() # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'frmr'}]) + eq_(done_evts, [{"response": "frmr"}]) # Station should have been asked to send a DM eq_(len(peer.transmit_calls), 1) (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a DM frame - eq_(frame, 'dm') + eq_(frame, "dm") assert_is_none(callback) @@ -324,8 +329,8 @@ def test_peerconn_receive_dm(): """ Test _on_receive_dm ends the helper """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -334,22 +339,22 @@ def test_peerconn_receive_dm(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call _on_receive_frmr helper._on_receive_dm() # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'dm'}]) + eq_(done_evts, [{"response": "dm"}]) def test_peerconn_on_timeout_first(): """ Test _on_timeout retries if there are retries left """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -380,7 +385,7 @@ def test_peerconn_on_timeout_first(): (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, 'sabm') + eq_(frame, "sabm") assert_is_none(callback) @@ -388,8 +393,8 @@ def test_peerconn_on_timeout_last(): """ Test _on_timeout finishes the helper if retries exhausted """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Nothing should be set up @@ -407,7 +412,7 @@ def test_peerconn_on_timeout_last(): # Hook the done signal done_evts = [] - helper.done_sig.connect(lambda **kw : done_evts.append(kw)) + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) # Call the time-out handler helper._on_timeout() @@ -425,19 +430,19 @@ def test_peerconn_on_timeout_last(): # See that the helper finished assert_is(helper._done, True) - eq_(done_evts, [{'response': 'timeout'}]) + eq_(done_evts, [{"response": "timeout"}]) def test_peerconn_finish_disconnect_ua(): """ Test _finish leaves other UA hooks intact """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Pretend we're hooked up - dummy_uaframe_handler = lambda *a, **kw : None + dummy_uaframe_handler = lambda *a, **kw: None peer._uaframe_handler = dummy_uaframe_handler peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = helper._on_receive_dm @@ -455,12 +460,12 @@ def test_peerconn_finish_disconnect_frmr(): """ Test _finish leaves other FRMR hooks intact """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Pretend we're hooked up - dummy_frmrframe_handler = lambda *a, **kw : None + dummy_frmrframe_handler = lambda *a, **kw: None peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = dummy_frmrframe_handler peer._dmframe_handler = helper._on_receive_dm @@ -478,12 +483,12 @@ def test_peerconn_finish_disconnect_dm(): """ Test _finish leaves other DM hooks intact """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = DummyPeer(station, AX25Address('VK4MSL')) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) # Pretend we're hooked up - dummy_dmframe_handler = lambda *a, **kw : None + dummy_dmframe_handler = lambda *a, **kw: None peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = dummy_dmframe_handler From c35a7b636c536874e4c34c809f3c05add927529b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 16:40:19 +1000 Subject: [PATCH 078/207] peer: Fix reject mode precedence --- aioax25/peer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index dcaf6fc..5c649b1 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -60,6 +60,10 @@ ) +# AX25RejectMode precedence: +_REJECT_MODE_PRECEDENCE = {"selective_rr": 2, "selective": 1, "implicit": 0} + + class AX25Peer(object): """ This class is a proxy representation of the remote AX.25 peer that may be @@ -72,15 +76,12 @@ class AX25RejectMode(enum.Enum): SELECTIVE = "selective" SELECTIVE_RR = "selective_rr" - # Value precedence: - _PRECEDENCE = {"selective_rr": 2, "selective": 1, "implicit": 0} - @property def precedence(self): """ Get the precedence of this mode. """ - return self._PRECEDENCE[self.value] + return _REJECT_MODE_PRECEDENCE[self.value] class AX25PeerState(enum.Enum): # DISCONNECTED: No connection has been established From 973b892c8775d2512c6cbc0e377d30743649971d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 16:40:35 +1000 Subject: [PATCH 079/207] peer: Avoid double-initialisation of reject mode, full duplex --- aioax25/peer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 5c649b1..7f895ab 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -139,6 +139,7 @@ def __init__( self._ack_timeout = ack_timeout self._idle_timeout = idle_timeout self._reject_mode = reject_mode + self._full_duplex = full_duplex self._max_ifield = max_ifield self._max_ifield_rx = max_ifield_rx self._max_retries = max_retries @@ -155,8 +156,6 @@ def __init__( # Internal state (see AX.25 2.2 spec 4.2.4) self._state = self.AX25PeerState.DISCONNECTED - self._full_duplex = full_duplex - self._reject_mode = None self._max_outstanding = None # Decided when SABM(E) received self._modulo = None # Set when SABM(E) received self._negotiated = False # Set to True after XID negotiation From 49767da4df1e9bbd8b30dfb6da8d49245b4b00fd Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 17:04:44 +1000 Subject: [PATCH 080/207] peer: Interpret I-field length in bits --- aioax25/peer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 7f895ab..b8b227e 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -956,7 +956,9 @@ def _process_xid_ifieldlenrx(self, param): self._log.debug("XID: Assuming default I-Field Receive Length") param = AX25_22_DEFAULT_XID_IFIELDRX - self._max_ifield = min([self._max_ifield, param.value]) + self._max_ifield = min( + [self._max_ifield, int(param.value / 8)] # Value is given in bits + ) self._log.debug( "XID: Setting I-Field Receive Length: %d", self._max_ifield ) From 8069cd4d0ca24c037c81020685ca95de1ae5bdb5 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 17:17:56 +1000 Subject: [PATCH 081/207] peer: Restrict RR and RNR to connected state. --- aioax25/peer.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index b8b227e..3e90eb1 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1170,32 +1170,34 @@ def _send_rr_notification(self): Send a RR notification frame """ self._cancel_rr_notification() - self._transmit_frame( - self._RRFrameClass( - destination=self.address, - source=self._station().address, - repeaters=self.reply_path, - pf=False, - nr=self._recv_state, - ) - ) - - def _send_rnr_notification(self): - """ - Send a RNR notification if the RNR interval has elapsed. - """ - now = self._loop.time() - if (now - self._last_rnr_sent) > self._rnr_interval: + if self._state == self.AX25PeerState.CONNECTED: self._transmit_frame( - self._RNRFrameClass( + self._RRFrameClass( destination=self.address, source=self._station().address, repeaters=self.reply_path, - nr=self._recv_seq, pf=False, + nr=self._recv_state, ) ) - self._last_rnr_sent = now + + def _send_rnr_notification(self): + """ + Send a RNR notification if the RNR interval has elapsed. + """ + if self._state == self.AX25PeerState.CONNECTED: + now = self._loop.time() + if (now - self._last_rnr_sent) > self._rnr_interval: + self._transmit_frame( + self._RNRFrameClass( + destination=self.address, + source=self._station().address, + repeaters=self.reply_path, + nr=self._recv_seq, + pf=False, + ) + ) + self._last_rnr_sent = now def _send_next_iframe(self): """ From 535ac84a717714cf36759fbf5a38229e6756a0a9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 18:02:25 +1000 Subject: [PATCH 082/207] peer: Handle the "no paths" case. Assume a direct connection is possible if we're not given a path. --- aioax25/peer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 3e90eb1..6db605c 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -242,11 +242,16 @@ def reply_path(self): all_paths = list(self._tx_path_score.items()) + [ (path, 0) for path in self._rx_path_count.keys() ] - all_paths.sort(key=lambda p: p[0]) - best_path = all_paths[-1][0] + + if all_paths: + all_paths.sort(key=lambda p: p[0]) + best_path = all_paths[-1][0] + else: + # If no paths exist, use whatver default path is set + best_path = self._repeaters # Use this until we have reason to change - self._reply_path = AX25Path(*best_path) + self._reply_path = AX25Path(*(best_path or [])) return self._reply_path From ecc06808d7c792045c4d975e027cc7d27ea45c6f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 18:03:02 +1000 Subject: [PATCH 083/207] peer: Fix XID frame generation --- aioax25/peer.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 6db605c..a0b8c7f 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1069,6 +1069,21 @@ def _send_xid(self, cr): self._max_ifield_rx * 8 ), AX25XIDWindowSizeTransmitParameter(self._max_outstanding), + AX25XIDWindowSizeReceiveParameter( + self._max_outstanding_mod128 + if self._modulo128 + else self._max_outstanding_mod8 + ), + AX25XIDAcknowledgeTimerParameter( + int(self._ack_timeout * 1000) + ), + AX25XIDIFieldLengthTransmitParameter( + self._max_ifield * 8 + ), + AX25XIDIFieldLengthReceiveParameter( + self._max_ifield_rx * 8 + ), + AX25XIDWindowSizeTransmitParameter(self._max_outstanding), AX25XIDWindowSizeReceiveParameter( self._max_outstanding_mod128 if self._modulus128 From 87c3d962a1b9e2f1bcd346de485e731d6076ac12 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 18:03:11 +1000 Subject: [PATCH 084/207] peer: Fix station protocol detection --- aioax25/peer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index a0b8c7f..280d09d 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -609,7 +609,7 @@ def _on_receive_sabm(self, frame): self._protocol = AX25Version.AX25_22 # Make sure both ends are enabled for AX.25 2.2 - if self._station().protocol != AX25Version.AX25_22: + if self._station()._protocol != AX25Version.AX25_22: # We are not in AX.25 2.2 mode. # # "A TNC that uses a version of AX.25 prior to v2.2 responds @@ -822,7 +822,7 @@ def _on_receive_xid(self, frame): Handle a request to negotiate parameters. """ self._log.info("Received XID from peer") - if self._station().protocol != AX25Version.AX25_22: + if self._station()._protocol != AX25Version.AX25_22: # Not supporting this in AX.25 2.0 mode self._log.warning( "Received XID from peer, we are not in AX.25 2.2 mode" From f7ee0bca9bd00592c23cb69f19029e13b62c8e18 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 18:03:37 +1000 Subject: [PATCH 085/207] mocks: Make interface accessible to mock station --- tests/mocks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index 13534a7..8a6b063 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -90,11 +90,15 @@ def call_later(self, delay, callback, *args, **kwargs): class DummyStation(object): def __init__(self, address, reply_path=None): + self._interface_ref = DummyInterface() self.address = address self.reply_path = reply_path or [] self._full_duplex = False self._protocol = AX25Version.AX25_22 + def _interface(self): + return self._interface_ref + class DummyPeer(object): def __init__(self, station, address): From 39fefa1c688f695b0dd69619db2bde6db59cc9b1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 24 Nov 2019 18:04:01 +1000 Subject: [PATCH 086/207] peer: Add tests for XID handling --- tests/test_peer/peer.py | 28 + tests/test_peer/test_xid.py | 1111 +++++++++++++++++++++++++++++++++++ 2 files changed, 1139 insertions(+) create mode 100644 tests/test_peer/peer.py create mode 100644 tests/test_peer/test_xid.py diff --git a/tests/test_peer/peer.py b/tests/test_peer/peer.py new file mode 100644 index 0000000..f0f7352 --- /dev/null +++ b/tests/test_peer/peer.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +""" +Fixture for initialising an AX25 Peer +""" + +from nose.tools import eq_, assert_almost_equal, assert_is, \ + assert_is_not_none, assert_is_none + +from aioax25.peer import AX25Peer +from aioax25.version import AX25Version +from ..mocks import DummyIOLoop, DummyLogger + + +class TestingAX25Peer(AX25Peer): + def __init__(self, station, address, repeaters, max_ifield=256, + max_ifield_rx=256, max_retries=10, max_outstanding_mod8=7, + max_outstanding_mod128=127, rr_delay=10.0, rr_interval=30.0, + rnr_interval=10.0, ack_timeout=3.0, idle_timeout=900.0, + protocol=AX25Version.UNKNOWN, modulo128=False, + reject_mode=AX25Peer.AX25RejectMode.SELECTIVE_RR, + full_duplex=False, reply_path=None, locked_path=False): + super(TestingAX25Peer, self).__init__( + station, address, repeaters, max_ifield, max_ifield_rx, + max_retries, max_outstanding_mod8, max_outstanding_mod128, + rr_delay, rr_interval, rnr_interval, ack_timeout, idle_timeout, + protocol, modulo128, DummyLogger('peer'), DummyIOLoop(), + reject_mode, full_duplex, reply_path, locked_path) diff --git a/tests/test_peer/test_xid.py b/tests/test_peer/test_xid.py new file mode 100644 index 0000000..9a275e5 --- /dev/null +++ b/tests/test_peer/test_xid.py @@ -0,0 +1,1111 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25Peer XID handling +""" + +from nose.tools import eq_, assert_almost_equal, assert_is, \ + assert_is_not_none, assert_is_none + +from aioax25.frame import AX25Address, AX25XIDClassOfProceduresParameter, \ + AX25XIDHDLCOptionalFunctionsParameter, \ + AX25XIDIFieldLengthTransmitParameter, \ + AX25XIDIFieldLengthReceiveParameter, \ + AX25XIDWindowSizeTransmitParameter, \ + AX25XIDWindowSizeReceiveParameter, \ + AX25XIDAcknowledgeTimerParameter, \ + AX25XIDRetriesParameter, \ + AX25XIDRawParameter, \ + AX25ExchangeIdentificationFrame, \ + AX25FrameRejectFrame +from aioax25.version import AX25Version +from .peer import TestingAX25Peer +from ..mocks import DummyStation + + +def test_peer_process_xid_cop_fds_fdp(): + """ + Test _process_xid_cop enables full-duplex if both stations negotiate it. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + full_duplex=True + ) + + # Pass in a CoP XID parameter + peer._process_xid_cop(AX25XIDClassOfProceduresParameter( + full_duplex=True, half_duplex=False + )) + + # Full duplex should be enabled + assert peer._full_duplex + + +def test_peer_process_xid_cop_fds_hdp(): + """ + Test _process_xid_cop disables full-duplex if the peer is half-duplex. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + full_duplex=True + ) + + # Pass in a CoP XID parameter + peer._process_xid_cop(AX25XIDClassOfProceduresParameter( + full_duplex=False, half_duplex=True + )) + + # Full duplex should be disabled + assert not peer._full_duplex + + +def test_peer_process_xid_cop_hds_fdp(): + """ + Test _process_xid_cop disables full-duplex if the station is half-duplex. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + full_duplex=False + ) + + # Pass in a CoP XID parameter + peer._process_xid_cop(AX25XIDClassOfProceduresParameter( + full_duplex=True, half_duplex=False + )) + + # Full duplex should be disabled + assert not peer._full_duplex + + +def test_peer_process_xid_cop_malformed_cop_fdx_hdx(): + """ + Test _process_xid_cop disables full-duplex the CoP frame sets both + half-duplex and full-duplex flags. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + full_duplex=True + ) + + # Pass in a CoP XID parameter + peer._process_xid_cop(AX25XIDClassOfProceduresParameter( + full_duplex=True, half_duplex=True + )) + + # Full duplex should be disabled + assert not peer._full_duplex + + +def test_peer_process_xid_cop_malformed_cop_nfdx_nhdx(): + """ + Test _process_xid_cop disables full-duplex the CoP frame clears both + half-duplex and full-duplex flags. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + full_duplex=True + ) + + # Pass in a CoP XID parameter + peer._process_xid_cop(AX25XIDClassOfProceduresParameter( + full_duplex=False, half_duplex=False + )) + + # Full duplex should be disabled + assert not peer._full_duplex + + +def test_peer_process_xid_cop_default(): + """ + Test _process_xid_cop assumes AX.25 2.2 defaults if given null CoP + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + full_duplex=True + ) + + # Pass in a CoP XID parameter + peer._process_xid_cop(AX25XIDRawParameter( + pi=AX25XIDClassOfProceduresParameter.PI, + pv=None + )) + + # Full duplex should be disabled + assert not peer._full_duplex + + +def test_peer_process_xid_hdlcoptfunc_stnssr_peerssr(): + """ + Test _process_xid_hdlcoptfunc sets SRR if both set SRR + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=True, srej=True + )) + + # Selective Reject-Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE_RR) + + +def test_peer_process_xid_hdlcoptfunc_stnsr_peerssr(): + """ + Test _process_xid_hdlcoptfunc sets SR if station sets SR + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=True, srej=True + )) + + # Selective Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + + +def test_peer_process_xid_hdlcoptfunc_stnssr_peersr(): + """ + Test _process_xid_hdlcoptfunc sets SR if peer sets SR + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=False, srej=True + )) + + # Selective Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + + +def test_peer_process_xid_hdlcoptfunc_stnsr_peersr(): + """ + Test _process_xid_hdlcoptfunc sets SR if both agree on SR + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=False, srej=True + )) + + # Selective Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + + +def test_peer_process_xid_hdlcoptfunc_stnir_peersr(): + """ + Test _process_xid_hdlcoptfunc sets IR if station sets IR (peer=SR) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=False, srej=True + )) + + # Implicit Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + + +def test_peer_process_xid_hdlcoptfunc_stnsr_peerir(): + """ + Test _process_xid_hdlcoptfunc sets IR if peer sets IR (station=SR) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=True, srej=False + )) + + # Implicit Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + + +def test_peer_process_xid_hdlcoptfunc_stnir_peerssr(): + """ + Test _process_xid_hdlcoptfunc sets IR if station sets IR (peer=SSR) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=True, srej=True + )) + + # Implicit Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + + +def test_peer_process_xid_hdlcoptfunc_stnssr_peerir(): + """ + Test _process_xid_hdlcoptfunc sets IR if peer sets IR (station=SSR) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=True, srej=False + )) + + # Implicit Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + + +def test_peer_process_xid_hdlcoptfunc_malformed_rej_srej(): + """ + Test _process_xid_hdlcoptfunc sets IR if peer clears REJ and SREJ + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + rej=False, srej=False + )) + + # Implicit Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + + +def test_peer_process_xid_hdlcoptfunc_default_rej_srej(): + """ + Test _process_xid_hdlcoptfunc sets SR if peer does not send value. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDRawParameter( + pi=AX25XIDHDLCOptionalFunctionsParameter.PI, + pv=None + )) + + # Selective Reject should be chosen. + eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + + +def test_peer_process_xid_hdlcoptfunc_s128_p128(): + """ + Test _process_xid_hdlcoptfunc sets mod128 if both agree + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + modulo8=False, modulo128=True + )) + + # Modulo128 should be chosen. + assert peer._modulo128 + + +def test_peer_process_xid_hdlcoptfunc_s128_p8(): + """ + Test _process_xid_hdlcoptfunc sets mod8 if peer sets mod8 + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + modulo8=True, modulo128=False + )) + + # Modulo8 should be chosen. + assert not peer._modulo128 + + +def test_peer_process_xid_hdlcoptfunc_s8_p128(): + """ + Test _process_xid_hdlcoptfunc sets mod8 if station sets mod8 + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=False + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + modulo8=False, modulo128=True + )) + + # Modulo8 should be chosen. + assert not peer._modulo128 + + +def test_peer_process_xid_hdlcoptfunc_s8_p8(): + """ + Test _process_xid_hdlcoptfunc sets mod8 if both agree on mod8 + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=False + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + modulo8=True, modulo128=False + )) + + # Modulo8 should be chosen. + assert not peer._modulo128 + + +def test_peer_process_xid_hdlcoptfunc_malformed_m8s_m128s(): + """ + Test _process_xid_hdlcoptfunc sets mod8 if peer sets mod8 and mod128 bits + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + modulo8=True, modulo128=True + )) + + # Modulo8 should be chosen. + assert not peer._modulo128 + + +def test_peer_process_xid_hdlcoptfunc_malformed_m8c_m128c(): + """ + Test _process_xid_hdlcoptfunc sets mod8 if peer clears mod8 and mod128 bits + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True + ) + + # Pass in a HDLC Optional Functions XID parameter + peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( + modulo8=False, modulo128=False + )) + + # Modulo8 should be chosen. + assert not peer._modulo128 + + +def test_peer_process_xid_ifieldlenrx_station_smaller(): + """ + Test _process_xid_ifieldlenrx chooses station's field length if it's smaller + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + max_ifield=128 + ) + + # Pass in a I-Field Length Receive XID parameter + peer._process_xid_ifieldlenrx(AX25XIDIFieldLengthReceiveParameter(2048)) + + # 128 bytes should be set + eq_(peer._max_ifield, 128) + + +def test_peer_process_xid_ifieldlenrx_peer_smaller(): + """ + Test _process_xid_ifieldlenrx chooses peer's field length if it's smaller + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + max_ifield=256 + ) + + # Pass in a I-Field Length Receive XID parameter + peer._process_xid_ifieldlenrx(AX25XIDIFieldLengthReceiveParameter(1024)) + + # 128 bytes should be set + eq_(peer._max_ifield, 128) + + +def test_peer_process_xid_ifieldlenrx_default(): + """ + Test _process_xid_ifieldlenrx assumes defaults if not given a value + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + max_ifield=256 + ) + + # Pass in a I-Field Length Receive XID parameter + peer._process_xid_ifieldlenrx(AX25XIDRawParameter( + pi=AX25XIDIFieldLengthReceiveParameter.PI, + pv=None + )) + + # 256 bytes should be set + eq_(peer._max_ifield, 256) + + +def test_peer_process_xid_winszrx_station_smaller(): + """ + Test _process_xid_winszrx chooses station's window size if it's smaller + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True, + max_outstanding_mod128=63 + ) + + # Pass in a Window Size Receive XID parameter + peer._process_xid_winszrx(AX25XIDWindowSizeReceiveParameter(127)) + + # 63 frames should be set + eq_(peer._max_outstanding, 63) + + +def test_peer_process_xid_winszrx_peer_smaller(): + """ + Test _process_xid_winszrx chooses peer's window size if it's smaller + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True, + max_outstanding_mod128=127 + ) + + # Pass in a Window Size Receive XID parameter + peer._process_xid_winszrx(AX25XIDWindowSizeReceiveParameter(63)) + + # 63 frames should be set + eq_(peer._max_outstanding, 63) + + +def test_peer_process_xid_winszrx_default(): + """ + Test _process_xid_winszrx assumes defaults if not given a value + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + modulo128=True, + max_outstanding_mod128=127 + ) + + # Pass in a Window Size Receive XID parameter + peer._process_xid_winszrx(AX25XIDRawParameter( + pi=AX25XIDWindowSizeReceiveParameter.PI, + pv=None + )) + + # 7 frames should be set + eq_(peer._max_outstanding, 7) + + +def test_peer_process_xid_acktimer_station_larger(): + """ + Test _process_xid_acktimer chooses station's acknowledge timer if it's larger + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + ack_timeout=10.0 + ) + + # Pass in a Acknowledge Timer XID parameter + peer._process_xid_acktimer(AX25XIDAcknowledgeTimerParameter(5000)) + + # 10 seconds should be set + eq_(peer._ack_timeout, 10.0) + + +def test_peer_process_xid_acktimer_peer_larger(): + """ + Test _process_xid_acktimer chooses peer's acknowledge timer if it's larger + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + ack_timeout=5.0 + ) + + # Pass in a Acknowledge Timer XID parameter + peer._process_xid_acktimer(AX25XIDAcknowledgeTimerParameter(10000)) + + # 10 seconds should be set + eq_(peer._ack_timeout, 10.0) + + +def test_peer_process_xid_acktimer_default(): + """ + Test _process_xid_acktimer assumes defaults if not given a value + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + ack_timeout=1.0 + ) + + # Pass in a Acknowledge Timer XID parameter + peer._process_xid_acktimer(AX25XIDRawParameter( + pi=AX25XIDAcknowledgeTimerParameter.PI, + pv=None + )) + + # 3 seconds should be set + eq_(peer._ack_timeout, 3.0) + + +def test_peer_process_xid_retrycounter_station_larger(): + """ + Test _process_xid_retrycounter chooses station's retry count if it's larger + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + max_retries=6 + ) + + # Pass in a Retries XID parameter + peer._process_xid_retrycounter(AX25XIDRetriesParameter(2)) + + # 6 retries should be set + eq_(peer._max_retries, 6) + + +def test_peer_process_xid_retrycounter_peer_larger(): + """ + Test _process_xid_retrycounter chooses peer's retry count if it's larger + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + max_retries=2 + ) + + # Pass in a Retries XID parameter + peer._process_xid_retrycounter(AX25XIDRetriesParameter(6)) + + # 6 retries should be set + eq_(peer._max_retries, 6) + + +def test_peer_process_xid_retrycounter_default(): + """ + Test _process_xid_retrycounter assumes defaults if not given a value + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None, + max_retries=0 + ) + + # Pass in a Retries XID parameter + peer._process_xid_retrycounter(AX25XIDRawParameter( + pi=AX25XIDRetriesParameter.PI, + pv=None + )) + + # 10 retries should be set + eq_(peer._max_retries, 10) + + +def test_peer_on_receive_xid_ax20_mode(): + """ + Test _on_receive_xid responds with FRMR when in AX.25 2.0 mode. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station._protocol = AX25Version.AX25_20 + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Nothing yet sent + eq_(interface.transmit_calls, []) + + # Pass in the XID frame to our AX.25 2.0 station. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[] + ) + ) + + # One frame sent + eq_(len(interface.transmit_calls), 1) + (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) + + # This should be a FRMR + eq_(tx_kwargs, {'callback': None}) + eq_(len(tx_args), 1) + (frame,) = tx_args + assert isinstance(frame, AX25FrameRejectFrame) + + # W bit should be set + assert frame.w + + # We should now be in the FRMR state + eq_(peer._state, peer.AX25PeerState.FRMR) + + +def test_peer_on_receive_xid_connecting(): + """ + Test _on_receive_xid ignores XID when connecting. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Nothing yet sent + eq_(interface.transmit_calls, []) + + # Set state + peer._state = TestingAX25Peer.AX25PeerState.CONNECTING + + # Pass in the XID frame to our AX.25 2.2 station. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=True + ) + ) + + # Still nothing yet sent + eq_(interface.transmit_calls, []) + + +def test_peer_on_receive_xid_disconnecting(): + """ + Test _on_receive_xid ignores XID when disconnecting. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Nothing yet sent + eq_(interface.transmit_calls, []) + + # Set state + peer._state = TestingAX25Peer.AX25PeerState.DISCONNECTING + + # Pass in the XID frame to our AX.25 2.2 station. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=True + ) + ) + + # Still nothing yet sent + eq_(interface.transmit_calls, []) + + +def test_peer_on_receive_xid_sets_proto_version(): + """ + Test _on_receive_xid sets protocol version if unknown. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Version should be unknown + eq_(peer._protocol, AX25Version.UNKNOWN) + + # Pass in the XID frame to our AX.25 2.2 station. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[] + ) + ) + + # We now should consider the other station as AX.25 2.2 or better + eq_(peer._protocol, AX25Version.AX25_22) + + +def test_peer_on_receive_xid_keeps_known_proto_version(): + """ + Test _on_receive_xid keeps existing protocol version if known. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + protocol=AX25Version.AX25_22, + repeaters=None + ) + + # Pass in the XID frame to our AX.25 2.2 station. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[] + ) + ) + + # We should still consider the other station as AX.25 2.2 or better + eq_(peer._protocol, AX25Version.AX25_22) + + +def test_peer_on_receive_xid_ignores_bad_fi(): + """ + Test _on_receive_xid ignores parameters if FI is unknown + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Stub out _process_xid_cop + def _stub_process_cop(param): + assert False, 'Should not be called' + peer._process_xid_cop = _stub_process_cop + + # Pass in the XID frame to our AX.25 2.2 station. + # There should be no assertion triggered. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[ + AX25XIDClassOfProceduresParameter( + half_duplex=True + ) + ], + fi=26 + ) + ) + + +def test_peer_on_receive_xid_ignores_bad_gi(): + """ + Test _on_receive_xid ignores parameters if GI is unknown + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Stub out _process_xid_cop + def _stub_process_cop(param): + assert False, 'Should not be called' + peer._process_xid_cop = _stub_process_cop + + # Pass in the XID frame to our AX.25 2.2 station. + # There should be no assertion triggered. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[ + AX25XIDClassOfProceduresParameter( + half_duplex=True + ) + ], + gi=26 + ) + ) + + +def test_peer_on_receive_xid_processes_parameters(): + """ + Test _on_receive_xid processes parameters on good XID frames + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Pass in the XID frame to our AX.25 2.2 station. + # There should be no assertion triggered. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[ + AX25XIDIFieldLengthReceiveParameter(512) + ] + ) + ) + + # Should be negotiated to 64 bytes + eq_(peer._max_ifield, 64) + + +def test_peer_on_receive_xid_reply(): + """ + Test _on_receive_xid sends reply if incoming frame has CR=True + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Nothing yet sent + eq_(interface.transmit_calls, []) + + # Pass in the XID frame to our AX.25 2.2 station. + peer._on_receive_xid( + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=True + ) + ) + + # This was a request, so there should be a reply waiting + eq_(len(interface.transmit_calls), 1) + (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) + + # This should be a XID + eq_(tx_kwargs, {'callback': None}) + eq_(len(tx_args), 1) + (frame,) = tx_args + assert isinstance(frame, AX25ExchangeIdentificationFrame) + + # CR bit should be clear + assert not frame.header.cr + + # Frame should reflect the settings of the station + eq_(len(frame.parameters), 8) + + param = frame.parameters[0] + assert isinstance(param, AX25XIDClassOfProceduresParameter) + assert param.half_duplex + assert not param.full_duplex + + param = frame.parameters[1] + assert isinstance(param, AX25XIDHDLCOptionalFunctionsParameter) + assert param.srej + assert not param.rej + assert not param.modulo128 + assert param.modulo8 + + param = frame.parameters[2] + assert isinstance(param, AX25XIDIFieldLengthTransmitParameter) + eq_(param.value, 2048) + + param = frame.parameters[3] + assert isinstance(param, AX25XIDIFieldLengthReceiveParameter) + eq_(param.value, 2048) + + param = frame.parameters[4] + assert isinstance(param, AX25XIDWindowSizeTransmitParameter) + eq_(param.value, 7) + + param = frame.parameters[5] + assert isinstance(param, AX25XIDWindowSizeReceiveParameter) + eq_(param.value, 7) + + param = frame.parameters[6] + assert isinstance(param, AX25XIDAcknowledgeTimerParameter) + eq_(param.value, 3000) + + param = frame.parameters[7] + assert isinstance(param, AX25XIDRetriesParameter) + eq_(param.value, 10) + + +def test_peer_on_receive_xid_relay(): + """ + Test _on_receive_xid sends relays to XID handler if CR=False + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=None + ) + + # Nothing yet sent + eq_(interface.transmit_calls, []) + + # Hook the XID handler + xid_events = [] + peer._xidframe_handler = lambda *a, **kw : xid_events.append((a, kw)) + + # Pass in the XID frame to our AX.25 2.2 station. + frame = AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=False + ) + peer._on_receive_xid(frame) + + # There should have been a XID event + eq_(len(xid_events), 1) + + # It should be passed our handler + (xid_args, xid_kwargs) = xid_events.pop(0) + (xid_frame,) = xid_args + assert frame is xid_frame From d2a87b23aa4c04d465e399df42f0d7edf06513bf Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 4 Sep 2021 16:58:30 +1000 Subject: [PATCH 087/207] unit tests: Port from nosetests to py.test --- tests/test_frame/test_iframe.py | 22 +- tests/test_frame/test_sframe.py | 22 +- tests/test_frame/test_uframe.py | 38 +- tests/test_frame/test_xid.py | 181 +++-- tests/test_peer/peer.py | 59 +- tests/test_peer/test_peerconnection.py | 194 +++-- tests/test_peer/test_peerhelper.py | 16 +- tests/test_peer/test_peernegotiation.py | 120 ++-- tests/test_peer/test_peertest.py | 32 +- tests/test_peer/test_xid.py | 855 +++++++++++------------ tests/test_station/test_attach_detach.py | 33 +- tests/test_station/test_constructor.py | 3 +- tests/test_station/test_getpeer.py | 6 +- tests/test_station/test_properties.py | 5 +- tests/test_station/test_receive.py | 49 +- tests/test_uint.py | 5 +- 16 files changed, 804 insertions(+), 836 deletions(-) diff --git a/tests/test_frame/test_iframe.py b/tests/test_frame/test_iframe.py index 5a0b3c0..869883f 100644 --- a/tests/test_frame/test_iframe.py +++ b/tests/test_frame/test_iframe.py @@ -6,7 +6,6 @@ AX2516BitInformationFrame, ) -from nose.tools import eq_ from ..hex import from_hex, hex_cmp @@ -28,10 +27,10 @@ def test_8bit_iframe_decode(): assert isinstance( frame, AX258BitInformationFrame ), "Did not decode to 8-bit I-Frame" - eq_(frame.nr, 6) - eq_(frame.ns, 2) - eq_(frame.pid, 0xFF) - eq_(frame.payload, b"This is a test") + assert frame.nr == 6 + assert frame.ns == 2 + assert frame.pid == 0xFF + assert frame.payload == b"This is a test" def test_16bit_iframe_decode(): @@ -52,10 +51,10 @@ def test_16bit_iframe_decode(): assert isinstance( frame, AX2516BitInformationFrame ), "Did not decode to 16-bit I-Frame" - eq_(frame.nr, 6) - eq_(frame.ns, 2) - eq_(frame.pid, 0xFF) - eq_(frame.payload, b"This is a test") + assert frame.nr == 6 + assert frame.ns == 2 + assert frame.pid == 0xFF + assert frame.payload == b"This is a test" def test_iframe_str(): @@ -72,10 +71,9 @@ def test_iframe_str(): payload=b"Testing 1 2 3", ) - eq_( - str(frame), + assert str(frame) == ( "VK4MSL>VK4BWI: N(R)=6 P/F=True N(S)=2 PID=0xff " - "Payload=b'Testing 1 2 3'", + "Payload=b'Testing 1 2 3'" ) diff --git a/tests/test_frame/test_sframe.py b/tests/test_frame/test_sframe.py index 68d6a78..947fcca 100644 --- a/tests/test_frame/test_sframe.py +++ b/tests/test_frame/test_sframe.py @@ -8,7 +8,6 @@ AX2516BitRejectFrame, ) -from nose.tools import eq_ from ..hex import from_hex, hex_cmp @@ -28,7 +27,7 @@ def test_sframe_payload_reject(): ) assert False, "Should not have worked" except ValueError as e: - eq_(str(e), "Supervisory frames do not support payloads.") + assert str(e) == "Supervisory frames do not support payloads." def test_16bs_truncated_reject(): @@ -46,7 +45,7 @@ def test_16bs_truncated_reject(): ) assert False, "Should not have worked" except ValueError as e: - eq_(str(e), "Insufficient packet data") + assert str(e) == "Insufficient packet data" def test_8bs_rr_frame(): @@ -62,7 +61,7 @@ def test_8bs_rr_frame(): modulo128=False, ) assert isinstance(frame, AX258BitReceiveReadyFrame) - eq_(frame.nr, 2) + assert frame.nr == 2 def test_16bs_rr_frame(): @@ -78,7 +77,7 @@ def test_16bs_rr_frame(): modulo128=True, ) assert isinstance(frame, AX2516BitReceiveReadyFrame) - eq_(frame.nr, 46) + assert frame.nr == 46 def test_16bs_rr_encode(): @@ -94,7 +93,7 @@ def test_16bs_rr_encode(): "ac 96 68 9a a6 98 e1" # Source "01 5d", # Control ) - eq_(frame.control, 0x5D01) + assert frame.control == 0x5D01 def test_8bs_rej_decode_frame(): @@ -112,8 +111,8 @@ def test_8bs_rej_decode_frame(): assert isinstance( frame, AX258BitRejectFrame ), "Did not decode to REJ frame" - eq_(frame.nr, 0) - eq_(frame.pf, False) + assert frame.nr == 0 + assert frame.pf == False def test_16bs_rej_decode_frame(): @@ -131,8 +130,8 @@ def test_16bs_rej_decode_frame(): assert isinstance( frame, AX2516BitRejectFrame ), "Did not decode to REJ frame" - eq_(frame.nr, 0) - eq_(frame.pf, False) + assert frame.nr == 0 + assert frame.pf == False def test_rr_frame_str(): @@ -143,8 +142,7 @@ def test_rr_frame_str(): destination="VK4BWI", source="VK4MSL", nr=6 ) - eq_( - str(frame), + assert str(frame) == ( "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame", ) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index 0b31911..6b2efd0 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -10,7 +10,6 @@ AX25TestFrame, ) -from nose.tools import eq_ from ..hex import from_hex, hex_cmp @@ -28,7 +27,7 @@ def test_decode_uframe(): assert isinstance( frame, AX25UnnumberedFrame ), "Did not decode to unnumbered frame" - eq_(frame.modifier, 0xC3) + assert frame.modifier == 0xC3 # We should see the control byte as our payload hex_cmp(frame.frame_payload, "c3") @@ -65,7 +64,7 @@ def test_decode_sabm_payload(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Frame does not support payload") + assert str(e) == "Frame does not support payload" def test_decode_uframe_payload(): @@ -82,10 +81,9 @@ def test_decode_uframe_payload(): ) assert False, "Should not have worked" except ValueError as e: - eq_( - str(e), + assert str(e) == ( "Unnumbered frames (other than UI and " - "FRMR) do not have payloads", + "FRMR) do not have payloads" ) @@ -104,14 +102,14 @@ def test_decode_frmr(): assert isinstance( frame, AX25FrameRejectFrame ), "Did not decode to FRMR frame" - eq_(frame.modifier, 0x87) - eq_(frame.w, True) - eq_(frame.x, False) - eq_(frame.y, False) - eq_(frame.z, False) - eq_(frame.vr, 1) - eq_(frame.frmr_cr, False) - eq_(frame.vs, 1) + assert frame.modifier == 0x87 + assert frame.w == True + assert frame.x == False + assert frame.y == False + assert frame.z == False + assert frame.vr == 1 + assert frame.frmr_cr == False + assert frame.vs == 1 def test_decode_frmr_len(): @@ -129,7 +127,7 @@ def test_decode_frmr_len(): ) assert False, "Should not have worked" except ValueError as e: - eq_(str(e), "Payload of FRMR must be 3 bytes") + assert str(e) == "Payload of FRMR must be 3 bytes" def test_decode_ui(): @@ -146,7 +144,7 @@ def test_decode_ui(): assert isinstance( frame, AX25UnnumberedInformationFrame ), "Did not decode to UI frame" - eq_(frame.pid, 0x11) + assert frame.pid == 0x11 hex_cmp(frame.payload, "22 33") @@ -164,7 +162,7 @@ def test_decode_ui_len(): ) assert False, "Should not have worked" except ValueError as e: - eq_(str(e), "Payload of UI must be at least one byte") + assert str(e) == "Payload of UI must be at least one byte" def test_encode_uframe(): @@ -263,7 +261,7 @@ def test_decode_test(): ) ) assert isinstance(frame, AX25TestFrame) - eq_(frame.payload, b"123456789...") + assert frame.payload == b"123456789..." def test_copy_test(): @@ -307,7 +305,7 @@ def test_encode_pf(): "f0" # PID "54 68 69 73 20 69 73 20 61 20 74 65 73 74", # Payload ) - eq_(frame.control, 0x13) + assert frame.control == 0x13 def test_encode_frmr_w(): @@ -626,4 +624,4 @@ def test_ui_str(): pid=0xF0, payload=b"This is a test", ) - eq_(str(frame), "VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'") + assert str(frame) == "VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'" diff --git a/tests/test_frame/test_xid.py b/tests/test_frame/test_xid.py index 3e59721..7983ac2 100644 --- a/tests/test_frame/test_xid.py +++ b/tests/test_frame/test_xid.py @@ -11,7 +11,6 @@ AX25XIDRetriesParameter, ) -from nose.tools import eq_ from ..hex import from_hex, hex_cmp @@ -85,24 +84,24 @@ def test_decode_xid(): ) ) assert isinstance(frame, AX25ExchangeIdentificationFrame) - eq_(frame.fi, 0x82) - eq_(frame.gi, 0x80) - eq_(len(frame.parameters), 4) + assert frame.fi == 0x82 + assert frame.gi == 0x80 + assert len(frame.parameters) == 4 param = frame.parameters[0] - eq_(param.pi, 0x11) - eq_(param.pv, b"\xaa") + assert param.pi == 0x11 + assert param.pv == b"\xaa" param = frame.parameters[1] - eq_(param.pi, 0x12) - eq_(param.pv, b"\xbb") + assert param.pi == 0x12 + assert param.pv == b"\xbb" param = frame.parameters[2] - eq_(param.pi, 0x13) - eq_(param.pv, b"\x11\x22") + assert param.pi == 0x13 + assert param.pv == b"\x11\x22" param = frame.parameters[3] - eq_(param.pi, 0x14) + assert param.pi == 0x14 assert param.pv is None @@ -126,31 +125,31 @@ def test_decode_xid_fig46(): "0a 01 03" ) ) - eq_(len(frame.parameters), 6) + assert len(frame.parameters) == 6 param = frame.parameters[0] - eq_(param.pi, AX25XIDParameterIdentifier.ClassesOfProcedure) - eq_(param.pv, b"\x00\x20") + assert param.pi == AX25XIDParameterIdentifier.ClassesOfProcedure + assert param.pv == b"\x00\x20" param = frame.parameters[1] - eq_(param.pi, AX25XIDParameterIdentifier.HDLCOptionalFunctions) - eq_(param.pv, b"\x86\xa8\x02") + assert param.pi == AX25XIDParameterIdentifier.HDLCOptionalFunctions + assert param.pv == b"\x86\xa8\x02" param = frame.parameters[2] - eq_(param.pi, AX25XIDParameterIdentifier.IFieldLengthReceive) - eq_(param.pv, b"\x04\x00") + assert param.pi == AX25XIDParameterIdentifier.IFieldLengthReceive + assert param.pv == b"\x04\x00" param = frame.parameters[3] - eq_(param.pi, AX25XIDParameterIdentifier.WindowSizeReceive) - eq_(param.pv, b"\x02") + assert param.pi == AX25XIDParameterIdentifier.WindowSizeReceive + assert param.pv == b"\x02" param = frame.parameters[4] - eq_(param.pi, AX25XIDParameterIdentifier.AcknowledgeTimer) - eq_(param.pv, b"\x10\x00") + assert param.pi == AX25XIDParameterIdentifier.AcknowledgeTimer + assert param.pv == b"\x10\x00" param = frame.parameters[5] - eq_(param.pi, AX25XIDParameterIdentifier.Retries) - eq_(param.pv, b"\x03") + assert param.pi == AX25XIDParameterIdentifier.Retries + assert param.pv == b"\x03" def test_decode_xid_truncated_header(): @@ -170,7 +169,7 @@ def test_decode_xid_truncated_header(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Truncated XID header") + assert str(e) == "Truncated XID header" def test_decode_xid_truncated_payload(): @@ -191,7 +190,7 @@ def test_decode_xid_truncated_payload(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Truncated XID data") + assert str(e) == "Truncated XID data" def test_decode_xid_truncated_param_header(): @@ -212,7 +211,7 @@ def test_decode_xid_truncated_param_header(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Insufficient data for parameter") + assert str(e) == "Insufficient data for parameter" def test_decode_xid_truncated_param_value(): @@ -233,7 +232,7 @@ def test_decode_xid_truncated_param_value(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Parameter is truncated") + assert str(e) == "Parameter is truncated" def test_copy_xid(): @@ -276,13 +275,13 @@ def test_decode_cop_param(): Test we can decode a Class Of Procedures parameter. """ param = AX25XIDClassOfProceduresParameter.decode(from_hex("80 20")) - eq_(param.half_duplex, True) - eq_(param.full_duplex, False) - eq_(param.unbalanced_nrm_pri, False) - eq_(param.unbalanced_nrm_sec, False) - eq_(param.unbalanced_arm_pri, False) - eq_(param.unbalanced_arm_sec, False) - eq_(param.reserved, 256) + assert param.half_duplex == True + assert param.full_duplex == False + assert param.unbalanced_nrm_pri == False + assert param.unbalanced_nrm_sec == False + assert param.unbalanced_arm_pri == False + assert param.unbalanced_arm_sec == False + assert param.reserved == 256 def test_copy_cop_param(): @@ -298,14 +297,14 @@ def test_copy_cop_param(): assert param is not copyparam # Ensure all parameters match - eq_(param.full_duplex, copyparam.full_duplex) - eq_(param.half_duplex, copyparam.half_duplex) - eq_(param.unbalanced_nrm_pri, copyparam.unbalanced_nrm_pri) - eq_(param.unbalanced_nrm_sec, copyparam.unbalanced_nrm_sec) - eq_(param.unbalanced_arm_pri, copyparam.unbalanced_arm_pri) - eq_(param.unbalanced_arm_sec, copyparam.unbalanced_arm_sec) - eq_(param.balanced_abm, copyparam.balanced_abm) - eq_(param.reserved, copyparam.reserved) + assert param.full_duplex == copyparam.full_duplex + assert param.half_duplex == copyparam.half_duplex + assert param.unbalanced_nrm_pri == copyparam.unbalanced_nrm_pri + assert param.unbalanced_nrm_sec == copyparam.unbalanced_nrm_sec + assert param.unbalanced_arm_pri == copyparam.unbalanced_arm_pri + assert param.unbalanced_arm_sec == copyparam.unbalanced_arm_sec + assert param.balanced_abm == copyparam.balanced_abm + assert param.reserved == copyparam.reserved def test_encode_cop_param(): @@ -331,32 +330,32 @@ def test_decode_hdlcfunc_param(): """ param = AX25XIDHDLCOptionalFunctionsParameter.decode(from_hex("86 a8 82")) # Specifically called out in the example (AX.25 2.2 spec Figure 4.6) - eq_(param.srej, True) - eq_(param.rej, True) - eq_(param.extd_addr, True) - eq_(param.fcs16, True) - eq_(param.modulo128, True) - eq_(param.sync_tx, True) + assert param.srej == True + assert param.rej == True + assert param.extd_addr == True + assert param.fcs16 == True + assert param.modulo128 == True + assert param.sync_tx == True # Changed by us to test round-tripping - eq_(param.reserved2, 2) + assert param.reserved2 == 2 # Modulo128 is on, so we expect this off - eq_(param.modulo8, False) + assert param.modulo8 == False # Expected defaults - eq_(param.srej_multiframe, False) - eq_(param.start_stop_transp, False) - eq_(param.start_stop_flow_ctl, False) - eq_(param.sync_tx, True) - eq_(param.fcs32, False) - eq_(param.rd, False) - eq_(param.test, True) - eq_(param.rset, False) - eq_(param.delete_i_cmd, False) - eq_(param.delete_i_resp, False) - eq_(param.basic_addr, False) - eq_(param.up, False) - eq_(param.sim_rim, False) - eq_(param.ui, False) - eq_(param.reserved1, False) + assert param.srej_multiframe == False + assert param.start_stop_transp == False + assert param.start_stop_flow_ctl == False + assert param.sync_tx == True + assert param.fcs32 == False + assert param.rd == False + assert param.test == True + assert param.rset == False + assert param.delete_i_cmd == False + assert param.delete_i_resp == False + assert param.basic_addr == False + assert param.up == False + assert param.sim_rim == False + assert param.ui == False + assert param.reserved1 == False def test_copy_hdlcfunc_param(): @@ -378,29 +377,29 @@ def test_copy_hdlcfunc_param(): assert param is not copyparam # Ensure all parameters match - eq_(param.modulo128, copyparam.modulo128) - eq_(param.modulo8, copyparam.modulo8) - eq_(param.srej, copyparam.srej) - eq_(param.rej, copyparam.rej) - eq_(param.srej_multiframe, copyparam.srej_multiframe) - eq_(param.start_stop_transp, copyparam.start_stop_transp) - eq_(param.start_stop_flow_ctl, copyparam.start_stop_flow_ctl) - eq_(param.start_stop_tx, copyparam.start_stop_tx) - eq_(param.sync_tx, copyparam.sync_tx) - eq_(param.fcs32, copyparam.fcs32) - eq_(param.fcs16, copyparam.fcs16) - eq_(param.rd, copyparam.rd) - eq_(param.test, copyparam.test) - eq_(param.rset, copyparam.rset) - eq_(param.delete_i_cmd, copyparam.delete_i_cmd) - eq_(param.delete_i_resp, copyparam.delete_i_resp) - eq_(param.extd_addr, copyparam.extd_addr) - eq_(param.basic_addr, copyparam.basic_addr) - eq_(param.up, copyparam.up) - eq_(param.sim_rim, copyparam.sim_rim) - eq_(param.ui, copyparam.ui) - eq_(param.reserved2, copyparam.reserved2) - eq_(param.reserved1, copyparam.reserved1) + assert param.modulo128 == copyparam.modulo128 + assert param.modulo8 == copyparam.modulo8 + assert param.srej == copyparam.srej + assert param.rej == copyparam.rej + assert param.srej_multiframe == copyparam.srej_multiframe + assert param.start_stop_transp == copyparam.start_stop_transp + assert param.start_stop_flow_ctl == copyparam.start_stop_flow_ctl + assert param.start_stop_tx == copyparam.start_stop_tx + assert param.sync_tx == copyparam.sync_tx + assert param.fcs32 == copyparam.fcs32 + assert param.fcs16 == copyparam.fcs16 + assert param.rd == copyparam.rd + assert param.test == copyparam.test + assert param.rset == copyparam.rset + assert param.delete_i_cmd == copyparam.delete_i_cmd + assert param.delete_i_resp == copyparam.delete_i_resp + assert param.extd_addr == copyparam.extd_addr + assert param.basic_addr == copyparam.basic_addr + assert param.up == copyparam.up + assert param.sim_rim == copyparam.sim_rim + assert param.ui == copyparam.ui + assert param.reserved2 == copyparam.reserved2 + assert param.reserved1 == copyparam.reserved1 def test_encode_hdlcfunc_param(): @@ -435,7 +434,7 @@ def test_decode_retries_param(): Test we can decode a Retries parameter. """ param = AX25XIDRetriesParameter.decode(from_hex("10")) - eq_(param.value, 16) + assert param.value == 16 def test_copy_retries_param(): @@ -447,4 +446,4 @@ def test_copy_retries_param(): assert param is not copyparam # Ensure all parameters match - eq_(param.value, copyparam.value) + assert param.value == copyparam.value diff --git a/tests/test_peer/peer.py b/tests/test_peer/peer.py index f0f7352..c0b4d66 100644 --- a/tests/test_peer/peer.py +++ b/tests/test_peer/peer.py @@ -4,25 +4,54 @@ Fixture for initialising an AX25 Peer """ -from nose.tools import eq_, assert_almost_equal, assert_is, \ - assert_is_not_none, assert_is_none - from aioax25.peer import AX25Peer from aioax25.version import AX25Version from ..mocks import DummyIOLoop, DummyLogger class TestingAX25Peer(AX25Peer): - def __init__(self, station, address, repeaters, max_ifield=256, - max_ifield_rx=256, max_retries=10, max_outstanding_mod8=7, - max_outstanding_mod128=127, rr_delay=10.0, rr_interval=30.0, - rnr_interval=10.0, ack_timeout=3.0, idle_timeout=900.0, - protocol=AX25Version.UNKNOWN, modulo128=False, - reject_mode=AX25Peer.AX25RejectMode.SELECTIVE_RR, - full_duplex=False, reply_path=None, locked_path=False): + def __init__( + self, + station, + address, + repeaters, + max_ifield=256, + max_ifield_rx=256, + max_retries=10, + max_outstanding_mod8=7, + max_outstanding_mod128=127, + rr_delay=10.0, + rr_interval=30.0, + rnr_interval=10.0, + ack_timeout=3.0, + idle_timeout=900.0, + protocol=AX25Version.UNKNOWN, + modulo128=False, + reject_mode=AX25Peer.AX25RejectMode.SELECTIVE_RR, + full_duplex=False, + reply_path=None, + locked_path=False, + ): super(TestingAX25Peer, self).__init__( - station, address, repeaters, max_ifield, max_ifield_rx, - max_retries, max_outstanding_mod8, max_outstanding_mod128, - rr_delay, rr_interval, rnr_interval, ack_timeout, idle_timeout, - protocol, modulo128, DummyLogger('peer'), DummyIOLoop(), - reject_mode, full_duplex, reply_path, locked_path) + station, + address, + repeaters, + max_ifield, + max_ifield_rx, + max_retries, + max_outstanding_mod8, + max_outstanding_mod128, + rr_delay, + rr_interval, + rnr_interval, + ack_timeout, + idle_timeout, + protocol, + modulo128, + DummyLogger("peer"), + DummyIOLoop(), + reject_mode, + full_duplex, + reply_path, + locked_path, + ) diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index 1da0547..5bc76b3 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -4,17 +4,9 @@ Tests for AX25PeerConnectionHandler """ -from nose.tools import ( - eq_, - assert_almost_equal, - assert_is, - assert_is_not_none, - assert_is_none, -) - from aioax25.version import AX25Version from aioax25.peer import AX25PeerConnectionHandler -from aioax25.frame import AX25Address, AX25TestFrame +from aioax25.frame import AX25Address from ..mocks import DummyPeer, DummyStation @@ -27,20 +19,20 @@ def test_peerconn_go(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Start it off helper._go() # We should hand off to the negotiation handler, so no timeout started yet: - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None # There should be a call to negotiate, with a call-back pointing here. - eq_(peer._negotiate_calls, [helper._on_negotiated]) + assert peer._negotiate_calls == [helper._on_negotiated] -def test_peerconn_go_peer_ax20(): +def test_peerconn_go_peer_ax20_stn(): """ Test _go skips negotiation for AX.25 2.0 stations. """ @@ -50,31 +42,31 @@ def test_peerconn_go_peer_ax20(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Start it off helper._go() # Check the time-out timer is started - assert_is_not_none(helper._timeout_handle) - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle is not None + assert helper._timeout_handle.delay == 0.1 # Helper should have hooked the handler events - eq_(peer._uaframe_handler, helper._on_receive_ua) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._uaframe_handler == helper._on_receive_ua + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send a SABM - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, "sabm") - assert_is_none(callback) + assert frame == "sabm" + assert callback is None -def test_peerconn_go_peer_ax20(): +def test_peerconn_go_peer_ax20_peer(): """ Test _go skips negotiation for AX.25 2.0 peers. """ @@ -84,28 +76,28 @@ def test_peerconn_go_peer_ax20(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Start it off helper._go() # Check the time-out timer is started - assert_is_not_none(helper._timeout_handle) - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle is not None + assert helper._timeout_handle.delay == 0.1 # Helper should have hooked the handler events - eq_(peer._uaframe_handler, helper._on_receive_ua) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._uaframe_handler == helper._on_receive_ua + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send a SABM - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, "sabm") - assert_is_none(callback) + assert frame == "sabm" + assert callback is None def test_peerconn_go_prenegotiated(): @@ -120,28 +112,28 @@ def test_peerconn_go_prenegotiated(): peer._negotiated = True # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Start it off helper._go() # Check the time-out timer is started - assert_is_not_none(helper._timeout_handle) - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle is not None + assert helper._timeout_handle.delay == 0.1 # Helper should have hooked the handler events - eq_(peer._uaframe_handler, helper._on_receive_ua) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._uaframe_handler == helper._on_receive_ua + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send a SABM - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, "sabm") - assert_is_none(callback) + assert frame == "sabm" + assert callback is None def test_peerconn_on_negotiated_failed(): @@ -153,9 +145,9 @@ def test_peerconn_on_negotiated_failed(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the done signal done_evts = [] @@ -163,7 +155,7 @@ def test_peerconn_on_negotiated_failed(): # Try to connect helper._on_negotiated("whoopsie") - eq_(done_evts, [{"response": "whoopsie"}]) + assert done_evts == [{"response": "whoopsie"}] def test_peerconn_on_negotiated_xidframe_handler(): @@ -175,9 +167,9 @@ def test_peerconn_on_negotiated_xidframe_handler(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the UA handler peer._uaframe_handler = lambda *a, **kwa: None @@ -188,7 +180,7 @@ def test_peerconn_on_negotiated_xidframe_handler(): # Try to connect helper._on_negotiated("xid") - eq_(done_evts, [{"response": "station_busy"}]) + assert done_evts == [{"response": "station_busy"}] def test_peerconn_on_negotiated_frmrframe_handler(): @@ -200,9 +192,9 @@ def test_peerconn_on_negotiated_frmrframe_handler(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the FRMR handler peer._frmrframe_handler = lambda *a, **kwa: None @@ -213,7 +205,7 @@ def test_peerconn_on_negotiated_frmrframe_handler(): # Try to connect helper._on_negotiated("xid") - eq_(done_evts, [{"response": "station_busy"}]) + assert done_evts == [{"response": "station_busy"}] def test_peerconn_on_negotiated_dmframe_handler(): @@ -225,9 +217,9 @@ def test_peerconn_on_negotiated_dmframe_handler(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the DM handler peer._dmframe_handler = lambda *a, **kwa: None @@ -238,7 +230,7 @@ def test_peerconn_on_negotiated_dmframe_handler(): # Try to connect helper._on_negotiated("xid") - eq_(done_evts, [{"response": "station_busy"}]) + assert done_evts == [{"response": "station_busy"}] def test_peerconn_on_negotiated_xid(): @@ -256,17 +248,17 @@ def test_peerconn_on_negotiated_xid(): assert not helper._done # Helper should be hooked - eq_(peer._uaframe_handler, helper._on_receive_ua) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._uaframe_handler == helper._on_receive_ua + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send a SABM - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, "sabm") - assert_is_none(callback) + assert frame == "sabm" + assert callback is None def test_peerconn_receive_ua(): @@ -278,7 +270,7 @@ def test_peerconn_receive_ua(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Hook the done signal @@ -289,8 +281,8 @@ def test_peerconn_receive_ua(): helper._on_receive_ua() # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "ack"}]) + assert helper._done is True + assert done_evts == [{"response": "ack"}] def test_peerconn_receive_frmr(): @@ -302,7 +294,7 @@ def test_peerconn_receive_frmr(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Hook the done signal @@ -313,16 +305,16 @@ def test_peerconn_receive_frmr(): helper._on_receive_frmr() # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "frmr"}]) + assert helper._done is True + assert done_evts == [{"response": "frmr"}] # Station should have been asked to send a DM - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a DM frame - eq_(frame, "dm") - assert_is_none(callback) + assert frame == "dm" + assert callback is None def test_peerconn_receive_dm(): @@ -334,7 +326,7 @@ def test_peerconn_receive_dm(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Hook the done signal @@ -345,8 +337,8 @@ def test_peerconn_receive_dm(): helper._on_receive_dm() # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "dm"}]) + assert helper._done is True + assert done_evts == [{"response": "dm"}] def test_peerconn_on_timeout_first(): @@ -358,35 +350,35 @@ def test_peerconn_on_timeout_first(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # We should have retries left - eq_(helper._retries, 2) + assert helper._retries == 2 # Call the time-out handler helper._on_timeout() # Check the time-out timer is re-started - assert_is_not_none(helper._timeout_handle) - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle is not None + assert helper._timeout_handle.delay == 0.1 # There should now be fewer retries left - eq_(helper._retries, 1) + assert helper._retries == 1 # Helper should have hooked the handler events - eq_(peer._uaframe_handler, helper._on_receive_ua) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._uaframe_handler == helper._on_receive_ua + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send an XID - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a SABM frame - eq_(frame, "sabm") - assert_is_none(callback) + assert frame == "sabm" + assert callback is None def test_peerconn_on_timeout_last(): @@ -398,9 +390,9 @@ def test_peerconn_on_timeout_last(): helper = AX25PeerConnectionHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Pretend there are no more retries left helper._retries = 0 @@ -418,19 +410,19 @@ def test_peerconn_on_timeout_last(): helper._on_timeout() # Check the time-out timer is not re-started - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None # Helper should have hooked the handler events - assert_is_none(peer._uaframe_handler) - assert_is_none(peer._frmrframe_handler) - assert_is_none(peer._dmframe_handler) + assert peer._uaframe_handler is None + assert peer._frmrframe_handler is None + assert peer._dmframe_handler is None # Station should not have been asked to send anything - eq_(len(peer.transmit_calls), 0) + assert len(peer.transmit_calls) == 0 # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "timeout"}]) + assert helper._done is True + assert done_evts == [{"response": "timeout"}] def test_peerconn_finish_disconnect_ua(): @@ -451,9 +443,9 @@ def test_peerconn_finish_disconnect_ua(): helper._finish() # All except UA (which is not ours) should be disconnected - eq_(peer._uaframe_handler, dummy_uaframe_handler) - assert_is_none(peer._frmrframe_handler) - assert_is_none(peer._dmframe_handler) + assert peer._uaframe_handler == dummy_uaframe_handler + assert peer._frmrframe_handler is None + assert peer._dmframe_handler is None def test_peerconn_finish_disconnect_frmr(): @@ -474,9 +466,9 @@ def test_peerconn_finish_disconnect_frmr(): helper._finish() # All except FRMR (which is not ours) should be disconnected - assert_is_none(peer._uaframe_handler) - eq_(peer._frmrframe_handler, dummy_frmrframe_handler) - assert_is_none(peer._dmframe_handler) + assert peer._uaframe_handler is None + assert peer._frmrframe_handler == dummy_frmrframe_handler + assert peer._dmframe_handler is None def test_peerconn_finish_disconnect_dm(): @@ -497,6 +489,6 @@ def test_peerconn_finish_disconnect_dm(): helper._finish() # All except DM (which is not ours) should be disconnected - assert_is_none(peer._uaframe_handler) - assert_is_none(peer._frmrframe_handler) - eq_(peer._dmframe_handler, dummy_dmframe_handler) + assert peer._uaframe_handler is None + assert peer._frmrframe_handler is None + assert peer._dmframe_handler == dummy_dmframe_handler diff --git a/tests/test_peer/test_peerhelper.py b/tests/test_peer/test_peerhelper.py index 0f18abf..7063056 100644 --- a/tests/test_peer/test_peerhelper.py +++ b/tests/test_peer/test_peerhelper.py @@ -4,8 +4,6 @@ Tests for AX25PeerHelper """ -from nose.tools import eq_ - from aioax25.peer import AX25PeerHelper from aioax25.frame import AX25Address from ..mocks import DummyPeer, DummyStation @@ -27,14 +25,14 @@ def _on_timeout(self): assert helper._timeout_handle is None helper._start_timer() - eq_(len(peer._loop.call_later_list), 1) + assert len(peer._loop.call_later_list) == 1 timeout = peer._loop.call_later_list.pop(0) assert timeout is helper._timeout_handle - eq_(timeout.delay, 0.1) - eq_(timeout.callback, helper._on_timeout) - eq_(timeout.args, ()) - eq_(timeout.kwargs, {}) + assert timeout.delay == 0.1 + assert timeout.callback == helper._on_timeout + assert timeout.args == () + assert timeout.kwargs == {} def test_peerhelper_stop_timer(): @@ -112,7 +110,7 @@ def test_finish(): assert helper._done # Signal should have fired - eq_(done_events, [{"arg1": "abc", "arg2": 123}]) + assert done_events == [{"arg1": "abc", "arg2": 123}] # Timeout should have been cancelled assert timeout.cancelled @@ -141,7 +139,7 @@ def test_finish_repeat(): helper._finish(arg1="abc", arg2=123) # Signal should not have fired - eq_(done_events, []) + assert done_events == [] # Timeout should not have been cancelled assert not timeout.cancelled diff --git a/tests/test_peer/test_peernegotiation.py b/tests/test_peer/test_peernegotiation.py index 15ed6e3..69b62c4 100644 --- a/tests/test_peer/test_peernegotiation.py +++ b/tests/test_peer/test_peernegotiation.py @@ -4,16 +4,8 @@ Tests for AX25PeerNegotiationHandler """ -from nose.tools import ( - eq_, - assert_almost_equal, - assert_is, - assert_is_not_none, - assert_is_none, -) - from aioax25.peer import AX25PeerNegotiationHandler -from aioax25.frame import AX25Address, AX25TestFrame +from aioax25.frame import AX25Address from ..mocks import DummyPeer, DummyStation @@ -26,27 +18,27 @@ def test_peerneg_go(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Start it off helper._go() - assert_is_not_none(helper._timeout_handle) - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle is not None + assert helper._timeout_handle.delay == 0.1 # Helper should have hooked the handler events - eq_(peer._xidframe_handler, helper._on_receive_xid) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._xidframe_handler == helper._on_receive_xid + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send an XID - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a test frame, with CR=True - eq_(frame, "xid:cr=True") - assert_is_none(callback) + assert frame == "xid:cr=True" + assert callback is None def test_peerneg_go_xidframe_handler(): @@ -58,9 +50,9 @@ def test_peerneg_go_xidframe_handler(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the XID handler peer._xidframe_handler = lambda *a, **kwa: None @@ -83,9 +75,9 @@ def test_peerneg_go_frmrframe_handler(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the FRMR handler peer._frmrframe_handler = lambda *a, **kwa: None @@ -108,9 +100,9 @@ def test_peerneg_go_dmframe_handler(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Hook the DM handler peer._dmframe_handler = lambda *a, **kwa: None @@ -133,7 +125,7 @@ def test_peerneg_receive_xid(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Hook the done signal @@ -144,8 +136,8 @@ def test_peerneg_receive_xid(): helper._on_receive_xid() # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "xid"}]) + assert helper._done is True + assert done_evts == [{"response": "xid"}] def test_peerneg_receive_frmr(): @@ -157,7 +149,7 @@ def test_peerneg_receive_frmr(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Hook the done signal @@ -168,8 +160,8 @@ def test_peerneg_receive_frmr(): helper._on_receive_frmr() # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "frmr"}]) + assert helper._done is True + assert done_evts == [{"response": "frmr"}] def test_peerneg_receive_dm(): @@ -181,7 +173,7 @@ def test_peerneg_receive_dm(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done # Hook the done signal @@ -192,8 +184,8 @@ def test_peerneg_receive_dm(): helper._on_receive_dm() # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "dm"}]) + assert helper._done is True + assert done_evts == [{"response": "dm"}] def test_peerneg_on_timeout_first(): @@ -205,35 +197,35 @@ def test_peerneg_on_timeout_first(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # We should have retries left - eq_(helper._retries, 2) + assert helper._retries == 2 # Call the time-out handler helper._on_timeout() # Check the time-out timer is re-started - assert_is_not_none(helper._timeout_handle) - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle is not None + assert helper._timeout_handle.delay == 0.1 # There should now be fewer retries left - eq_(helper._retries, 1) + assert helper._retries == 1 # Helper should have hooked the handler events - eq_(peer._xidframe_handler, helper._on_receive_xid) - eq_(peer._frmrframe_handler, helper._on_receive_frmr) - eq_(peer._dmframe_handler, helper._on_receive_dm) + assert peer._xidframe_handler == helper._on_receive_xid + assert peer._frmrframe_handler == helper._on_receive_frmr + assert peer._dmframe_handler == helper._on_receive_dm # Station should have been asked to send an XID - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a test frame, with CR=True - eq_(frame, "xid:cr=True") - assert_is_none(callback) + assert frame == "xid:cr=True" + assert callback is None def test_peerneg_on_timeout_last(): @@ -245,9 +237,9 @@ def test_peerneg_on_timeout_last(): helper = AX25PeerNegotiationHandler(peer) # Nothing should be set up - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Pretend there are no more retries left helper._retries = 0 @@ -265,19 +257,19 @@ def test_peerneg_on_timeout_last(): helper._on_timeout() # Check the time-out timer is not re-started - assert_is_none(helper._timeout_handle) + assert helper._timeout_handle is None # Helper should have hooked the handler events - assert_is_none(peer._xidframe_handler) - assert_is_none(peer._frmrframe_handler) - assert_is_none(peer._dmframe_handler) + assert peer._xidframe_handler is None + assert peer._frmrframe_handler is None + assert peer._dmframe_handler is None # Station should not have been asked to send anything - eq_(len(peer.transmit_calls), 0) + assert len(peer.transmit_calls) == 0 # See that the helper finished - assert_is(helper._done, True) - eq_(done_evts, [{"response": "timeout"}]) + assert helper._done is True + assert done_evts == [{"response": "timeout"}] def test_peerneg_finish_disconnect_xid(): @@ -298,9 +290,9 @@ def test_peerneg_finish_disconnect_xid(): helper._finish() # All except XID (which is not ours) should be disconnected - eq_(peer._xidframe_handler, dummy_xidframe_handler) - assert_is_none(peer._frmrframe_handler) - assert_is_none(peer._dmframe_handler) + assert peer._xidframe_handler == dummy_xidframe_handler + assert peer._frmrframe_handler is None + assert peer._dmframe_handler is None def test_peerneg_finish_disconnect_frmr(): @@ -321,9 +313,9 @@ def test_peerneg_finish_disconnect_frmr(): helper._finish() # All except XID (which is not ours) should be disconnected - assert_is_none(peer._xidframe_handler) - eq_(peer._frmrframe_handler, dummy_frmrframe_handler) - assert_is_none(peer._dmframe_handler) + assert peer._xidframe_handler is None + assert peer._frmrframe_handler == dummy_frmrframe_handler + assert peer._dmframe_handler is None def test_peerneg_finish_disconnect_dm(): @@ -344,6 +336,6 @@ def test_peerneg_finish_disconnect_dm(): helper._finish() # All except XID (which is not ours) should be disconnected - assert_is_none(peer._xidframe_handler) - assert_is_none(peer._frmrframe_handler) - eq_(peer._dmframe_handler, dummy_dmframe_handler) + assert peer._xidframe_handler is None + assert peer._frmrframe_handler is None + assert peer._dmframe_handler == dummy_dmframe_handler diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index 9cb6c62..c44dc21 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -4,8 +4,6 @@ Tests for AX25PeerTestHandler """ -from nose.tools import eq_, assert_almost_equal - from aioax25.peer import AX25PeerTestHandler from aioax25.frame import AX25Address, AX25TestFrame from ..mocks import DummyPeer, DummyStation @@ -22,14 +20,14 @@ def test_peertest_go(): # Nothing should be set up assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Start it off helper._go() assert helper._timeout_handle is not None - eq_(helper._timeout_handle.delay, 0.1) + assert helper._timeout_handle.delay == 0.1 - eq_(len(peer.transmit_calls), 1) + assert len(peer.transmit_calls) == 1 (frame, callback) = peer.transmit_calls.pop(0) # Frame should be a test frame, with CR=True @@ -38,7 +36,7 @@ def test_peertest_go(): assert frame.header.cr # Callback should be the _transmit_done method - eq_(callback, helper._transmit_done) + assert callback == helper._transmit_done # We should be registered to receive the reply assert peer._testframe_handler is helper @@ -60,7 +58,7 @@ def test_peertest_go_pending(): # Nothing should be set up assert helper._timeout_handle is None assert not helper._done - eq_(peer.transmit_calls, []) + assert peer.transmit_calls == [] # Start it off try: @@ -83,7 +81,9 @@ def test_peertest_transmit_done(): helper._transmit_done() assert helper.tx_time is not None - assert_almost_equal(peer._loop.time(), helper.tx_time, places=2) + assert int((peer._loop.time()) * (10**2)) == int( + (helper.tx_time) * (10**2) + ) def test_peertest_on_receive(): @@ -102,13 +102,15 @@ def test_peertest_on_receive(): helper._on_receive(frame="Make believe TEST frame") assert helper.rx_time is not None - assert_almost_equal(peer._loop.time(), helper.rx_time, places=2) - eq_(helper.rx_frame, "Make believe TEST frame") + assert int((peer._loop.time()) * (10**2)) == int( + (helper.rx_time) * (10**2) + ) + assert helper.rx_frame == "Make believe TEST frame" # We should be done now - eq_(len(done_events), 1) + assert len(done_events) == 1 done_evt = done_events.pop() - eq_(list(done_evt.keys()), ["handler"]) + assert list(done_evt.keys()) == ["handler"] assert done_evt["handler"] is helper @@ -132,7 +134,7 @@ def test_peertest_on_receive_done(): assert helper.rx_time is None assert helper.rx_frame is None - eq_(len(done_events), 0) + assert len(done_events) == 0 def test_peertest_on_timeout(): @@ -150,7 +152,7 @@ def test_peertest_on_timeout(): helper._on_timeout() # We should be done now - eq_(len(done_events), 1) + assert len(done_events) == 1 done_evt = done_events.pop() - eq_(list(done_evt.keys()), ["handler"]) + assert list(done_evt.keys()) == ["handler"] assert done_evt["handler"] is helper diff --git a/tests/test_peer/test_xid.py b/tests/test_peer/test_xid.py index 9a275e5..05bf76d 100644 --- a/tests/test_peer/test_xid.py +++ b/tests/test_peer/test_xid.py @@ -4,20 +4,20 @@ Tests for AX25Peer XID handling """ -from nose.tools import eq_, assert_almost_equal, assert_is, \ - assert_is_not_none, assert_is_none - -from aioax25.frame import AX25Address, AX25XIDClassOfProceduresParameter, \ - AX25XIDHDLCOptionalFunctionsParameter, \ - AX25XIDIFieldLengthTransmitParameter, \ - AX25XIDIFieldLengthReceiveParameter, \ - AX25XIDWindowSizeTransmitParameter, \ - AX25XIDWindowSizeReceiveParameter, \ - AX25XIDAcknowledgeTimerParameter, \ - AX25XIDRetriesParameter, \ - AX25XIDRawParameter, \ - AX25ExchangeIdentificationFrame, \ - AX25FrameRejectFrame +from aioax25.frame import ( + AX25Address, + AX25XIDClassOfProceduresParameter, + AX25XIDHDLCOptionalFunctionsParameter, + AX25XIDIFieldLengthTransmitParameter, + AX25XIDIFieldLengthReceiveParameter, + AX25XIDWindowSizeTransmitParameter, + AX25XIDWindowSizeReceiveParameter, + AX25XIDAcknowledgeTimerParameter, + AX25XIDRetriesParameter, + AX25XIDRawParameter, + AX25ExchangeIdentificationFrame, + AX25FrameRejectFrame, +) from aioax25.version import AX25Version from .peer import TestingAX25Peer from ..mocks import DummyStation @@ -27,18 +27,18 @@ def test_peer_process_xid_cop_fds_fdp(): """ Test _process_xid_cop enables full-duplex if both stations negotiate it. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + full_duplex=True, ) # Pass in a CoP XID parameter - peer._process_xid_cop(AX25XIDClassOfProceduresParameter( - full_duplex=True, half_duplex=False - )) + peer._process_xid_cop( + AX25XIDClassOfProceduresParameter(full_duplex=True, half_duplex=False) + ) # Full duplex should be enabled assert peer._full_duplex @@ -48,18 +48,18 @@ def test_peer_process_xid_cop_fds_hdp(): """ Test _process_xid_cop disables full-duplex if the peer is half-duplex. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + full_duplex=True, ) # Pass in a CoP XID parameter - peer._process_xid_cop(AX25XIDClassOfProceduresParameter( - full_duplex=False, half_duplex=True - )) + peer._process_xid_cop( + AX25XIDClassOfProceduresParameter(full_duplex=False, half_duplex=True) + ) # Full duplex should be disabled assert not peer._full_duplex @@ -69,18 +69,18 @@ def test_peer_process_xid_cop_hds_fdp(): """ Test _process_xid_cop disables full-duplex if the station is half-duplex. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - full_duplex=False + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + full_duplex=False, ) # Pass in a CoP XID parameter - peer._process_xid_cop(AX25XIDClassOfProceduresParameter( - full_duplex=True, half_duplex=False - )) + peer._process_xid_cop( + AX25XIDClassOfProceduresParameter(full_duplex=True, half_duplex=False) + ) # Full duplex should be disabled assert not peer._full_duplex @@ -91,18 +91,18 @@ def test_peer_process_xid_cop_malformed_cop_fdx_hdx(): Test _process_xid_cop disables full-duplex the CoP frame sets both half-duplex and full-duplex flags. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + full_duplex=True, ) # Pass in a CoP XID parameter - peer._process_xid_cop(AX25XIDClassOfProceduresParameter( - full_duplex=True, half_duplex=True - )) + peer._process_xid_cop( + AX25XIDClassOfProceduresParameter(full_duplex=True, half_duplex=True) + ) # Full duplex should be disabled assert not peer._full_duplex @@ -113,18 +113,20 @@ def test_peer_process_xid_cop_malformed_cop_nfdx_nhdx(): Test _process_xid_cop disables full-duplex the CoP frame clears both half-duplex and full-duplex flags. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + full_duplex=True, ) # Pass in a CoP XID parameter - peer._process_xid_cop(AX25XIDClassOfProceduresParameter( - full_duplex=False, half_duplex=False - )) + peer._process_xid_cop( + AX25XIDClassOfProceduresParameter( + full_duplex=False, half_duplex=False + ) + ) # Full duplex should be disabled assert not peer._full_duplex @@ -134,19 +136,18 @@ def test_peer_process_xid_cop_default(): """ Test _process_xid_cop assumes AX.25 2.2 defaults if given null CoP """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + full_duplex=True, ) # Pass in a CoP XID parameter - peer._process_xid_cop(AX25XIDRawParameter( - pi=AX25XIDClassOfProceduresParameter.PI, - pv=None - )) + peer._process_xid_cop( + AX25XIDRawParameter(pi=AX25XIDClassOfProceduresParameter.PI, pv=None) + ) # Full duplex should be disabled assert not peer._full_duplex @@ -156,229 +157,230 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peerssr(): """ Test _process_xid_hdlcoptfunc sets SRR if both set SRR """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=True, srej=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=True, srej=True) + ) # Selective Reject-Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE_RR) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE_RR def test_peer_process_xid_hdlcoptfunc_stnsr_peerssr(): """ Test _process_xid_hdlcoptfunc sets SR if station sets SR """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=True, srej=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=True, srej=True) + ) # Selective Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_stnssr_peersr(): """ Test _process_xid_hdlcoptfunc sets SR if peer sets SR """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=False, srej=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=False, srej=True) + ) # Selective Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_stnsr_peersr(): """ Test _process_xid_hdlcoptfunc sets SR if both agree on SR """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=False, srej=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=False, srej=True) + ) # Selective Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_stnir_peersr(): """ Test _process_xid_hdlcoptfunc sets IR if station sets IR (peer=SR) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=False, srej=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=False, srej=True) + ) # Implicit Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_stnsr_peerir(): """ Test _process_xid_hdlcoptfunc sets IR if peer sets IR (station=SR) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=True, srej=False - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=True, srej=False) + ) # Implicit Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_stnir_peerssr(): """ Test _process_xid_hdlcoptfunc sets IR if station sets IR (peer=SSR) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=True, srej=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=True, srej=True) + ) # Implicit Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_stnssr_peerir(): """ Test _process_xid_hdlcoptfunc sets IR if peer sets IR (station=SSR) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=True, srej=False - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=True, srej=False) + ) # Implicit Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_malformed_rej_srej(): """ Test _process_xid_hdlcoptfunc sets IR if peer clears REJ and SREJ """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - rej=False, srej=False - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(rej=False, srej=False) + ) # Implicit Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.IMPLICIT) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_default_rej_srej(): """ Test _process_xid_hdlcoptfunc sets SR if peer does not send value. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDRawParameter( - pi=AX25XIDHDLCOptionalFunctionsParameter.PI, - pv=None - )) + peer._process_xid_hdlcoptfunc( + AX25XIDRawParameter( + pi=AX25XIDHDLCOptionalFunctionsParameter.PI, pv=None + ) + ) # Selective Reject should be chosen. - eq_(peer._reject_mode, TestingAX25Peer.AX25RejectMode.SELECTIVE) + assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_s128_p128(): """ Test _process_xid_hdlcoptfunc sets mod128 if both agree """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - modulo8=False, modulo128=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(modulo8=False, modulo128=True) + ) # Modulo128 should be chosen. assert peer._modulo128 @@ -388,18 +390,18 @@ def test_peer_process_xid_hdlcoptfunc_s128_p8(): """ Test _process_xid_hdlcoptfunc sets mod8 if peer sets mod8 """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - modulo8=True, modulo128=False - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(modulo8=True, modulo128=False) + ) # Modulo8 should be chosen. assert not peer._modulo128 @@ -409,18 +411,18 @@ def test_peer_process_xid_hdlcoptfunc_s8_p128(): """ Test _process_xid_hdlcoptfunc sets mod8 if station sets mod8 """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=False + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=False, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - modulo8=False, modulo128=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(modulo8=False, modulo128=True) + ) # Modulo8 should be chosen. assert not peer._modulo128 @@ -430,18 +432,18 @@ def test_peer_process_xid_hdlcoptfunc_s8_p8(): """ Test _process_xid_hdlcoptfunc sets mod8 if both agree on mod8 """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=False + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=False, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - modulo8=True, modulo128=False - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(modulo8=True, modulo128=False) + ) # Modulo8 should be chosen. assert not peer._modulo128 @@ -451,18 +453,18 @@ def test_peer_process_xid_hdlcoptfunc_malformed_m8s_m128s(): """ Test _process_xid_hdlcoptfunc sets mod8 if peer sets mod8 and mod128 bits """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - modulo8=True, modulo128=True - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(modulo8=True, modulo128=True) + ) # Modulo8 should be chosen. assert not peer._modulo128 @@ -472,18 +474,18 @@ def test_peer_process_xid_hdlcoptfunc_malformed_m8c_m128c(): """ Test _process_xid_hdlcoptfunc sets mod8 if peer clears mod8 and mod128 bits """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, ) # Pass in a HDLC Optional Functions XID parameter - peer._process_xid_hdlcoptfunc(AX25XIDHDLCOptionalFunctionsParameter( - modulo8=False, modulo128=False - )) + peer._process_xid_hdlcoptfunc( + AX25XIDHDLCOptionalFunctionsParameter(modulo8=False, modulo128=False) + ) # Modulo8 should be chosen. assert not peer._modulo128 @@ -493,278 +495,274 @@ def test_peer_process_xid_ifieldlenrx_station_smaller(): """ Test _process_xid_ifieldlenrx chooses station's field length if it's smaller """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - max_ifield=128 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + max_ifield=128, ) # Pass in a I-Field Length Receive XID parameter peer._process_xid_ifieldlenrx(AX25XIDIFieldLengthReceiveParameter(2048)) # 128 bytes should be set - eq_(peer._max_ifield, 128) + assert peer._max_ifield == 128 def test_peer_process_xid_ifieldlenrx_peer_smaller(): """ Test _process_xid_ifieldlenrx chooses peer's field length if it's smaller """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - max_ifield=256 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + max_ifield=256, ) # Pass in a I-Field Length Receive XID parameter peer._process_xid_ifieldlenrx(AX25XIDIFieldLengthReceiveParameter(1024)) # 128 bytes should be set - eq_(peer._max_ifield, 128) + assert peer._max_ifield == 128 def test_peer_process_xid_ifieldlenrx_default(): """ Test _process_xid_ifieldlenrx assumes defaults if not given a value """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - max_ifield=256 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + max_ifield=256, ) # Pass in a I-Field Length Receive XID parameter - peer._process_xid_ifieldlenrx(AX25XIDRawParameter( - pi=AX25XIDIFieldLengthReceiveParameter.PI, - pv=None - )) + peer._process_xid_ifieldlenrx( + AX25XIDRawParameter( + pi=AX25XIDIFieldLengthReceiveParameter.PI, pv=None + ) + ) # 256 bytes should be set - eq_(peer._max_ifield, 256) + assert peer._max_ifield == 256 def test_peer_process_xid_winszrx_station_smaller(): """ Test _process_xid_winszrx chooses station's window size if it's smaller """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True, - max_outstanding_mod128=63 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, + max_outstanding_mod128=63, ) # Pass in a Window Size Receive XID parameter peer._process_xid_winszrx(AX25XIDWindowSizeReceiveParameter(127)) # 63 frames should be set - eq_(peer._max_outstanding, 63) + assert peer._max_outstanding == 63 def test_peer_process_xid_winszrx_peer_smaller(): """ Test _process_xid_winszrx chooses peer's window size if it's smaller """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True, - max_outstanding_mod128=127 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, + max_outstanding_mod128=127, ) # Pass in a Window Size Receive XID parameter peer._process_xid_winszrx(AX25XIDWindowSizeReceiveParameter(63)) # 63 frames should be set - eq_(peer._max_outstanding, 63) + assert peer._max_outstanding == 63 def test_peer_process_xid_winszrx_default(): """ Test _process_xid_winszrx assumes defaults if not given a value """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - modulo128=True, - max_outstanding_mod128=127 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + modulo128=True, + max_outstanding_mod128=127, ) # Pass in a Window Size Receive XID parameter - peer._process_xid_winszrx(AX25XIDRawParameter( - pi=AX25XIDWindowSizeReceiveParameter.PI, - pv=None - )) + peer._process_xid_winszrx( + AX25XIDRawParameter(pi=AX25XIDWindowSizeReceiveParameter.PI, pv=None) + ) # 7 frames should be set - eq_(peer._max_outstanding, 7) + assert peer._max_outstanding == 7 def test_peer_process_xid_acktimer_station_larger(): """ Test _process_xid_acktimer chooses station's acknowledge timer if it's larger """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - ack_timeout=10.0 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + ack_timeout=10.0, ) # Pass in a Acknowledge Timer XID parameter peer._process_xid_acktimer(AX25XIDAcknowledgeTimerParameter(5000)) # 10 seconds should be set - eq_(peer._ack_timeout, 10.0) + assert peer._ack_timeout == 10.0 def test_peer_process_xid_acktimer_peer_larger(): """ Test _process_xid_acktimer chooses peer's acknowledge timer if it's larger """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - ack_timeout=5.0 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + ack_timeout=5.0, ) # Pass in a Acknowledge Timer XID parameter peer._process_xid_acktimer(AX25XIDAcknowledgeTimerParameter(10000)) # 10 seconds should be set - eq_(peer._ack_timeout, 10.0) + assert peer._ack_timeout == 10.0 def test_peer_process_xid_acktimer_default(): """ Test _process_xid_acktimer assumes defaults if not given a value """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - ack_timeout=1.0 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + ack_timeout=1.0, ) # Pass in a Acknowledge Timer XID parameter - peer._process_xid_acktimer(AX25XIDRawParameter( - pi=AX25XIDAcknowledgeTimerParameter.PI, - pv=None - )) + peer._process_xid_acktimer( + AX25XIDRawParameter(pi=AX25XIDAcknowledgeTimerParameter.PI, pv=None) + ) # 3 seconds should be set - eq_(peer._ack_timeout, 3.0) + assert peer._ack_timeout == 3.0 def test_peer_process_xid_retrycounter_station_larger(): """ Test _process_xid_retrycounter chooses station's retry count if it's larger """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - max_retries=6 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + max_retries=6, ) # Pass in a Retries XID parameter peer._process_xid_retrycounter(AX25XIDRetriesParameter(2)) # 6 retries should be set - eq_(peer._max_retries, 6) + assert peer._max_retries == 6 def test_peer_process_xid_retrycounter_peer_larger(): """ Test _process_xid_retrycounter chooses peer's retry count if it's larger """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - max_retries=2 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + max_retries=2, ) # Pass in a Retries XID parameter peer._process_xid_retrycounter(AX25XIDRetriesParameter(6)) # 6 retries should be set - eq_(peer._max_retries, 6) + assert peer._max_retries == 6 def test_peer_process_xid_retrycounter_default(): """ Test _process_xid_retrycounter assumes defaults if not given a value """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None, - max_retries=0 + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + max_retries=0, ) # Pass in a Retries XID parameter - peer._process_xid_retrycounter(AX25XIDRawParameter( - pi=AX25XIDRetriesParameter.PI, - pv=None - )) + peer._process_xid_retrycounter( + AX25XIDRawParameter(pi=AX25XIDRetriesParameter.PI, pv=None) + ) # 10 retries should be set - eq_(peer._max_retries, 10) + assert peer._max_retries == 10 def test_peer_on_receive_xid_ax20_mode(): """ Test _on_receive_xid responds with FRMR when in AX.25 2.0 mode. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) station._protocol = AX25Version.AX25_20 interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] # Pass in the XID frame to our AX.25 2.0 station. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[] - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + ) ) # One frame sent - eq_(len(interface.transmit_calls), 1) + assert len(interface.transmit_calls) == 1 (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) # This should be a FRMR - eq_(tx_kwargs, {'callback': None}) - eq_(len(tx_args), 1) + assert tx_kwargs == {"callback": None} + assert len(tx_args) == 1 (frame,) = tx_args assert isinstance(frame, AX25FrameRejectFrame) @@ -772,162 +770,148 @@ def test_peer_on_receive_xid_ax20_mode(): assert frame.w # We should now be in the FRMR state - eq_(peer._state, peer.AX25PeerState.FRMR) + assert peer._state == peer.AX25PeerState.FRMR def test_peer_on_receive_xid_connecting(): """ Test _on_receive_xid ignores XID when connecting. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] # Set state peer._state = TestingAX25Peer.AX25PeerState.CONNECTING # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[], - cr=True - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=True, + ) ) # Still nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] def test_peer_on_receive_xid_disconnecting(): """ Test _on_receive_xid ignores XID when disconnecting. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] # Set state peer._state = TestingAX25Peer.AX25PeerState.DISCONNECTING # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[], - cr=True - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=True, + ) ) # Still nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] def test_peer_on_receive_xid_sets_proto_version(): """ Test _on_receive_xid sets protocol version if unknown. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - interface = station._interface() + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Version should be unknown - eq_(peer._protocol, AX25Version.UNKNOWN) + assert peer._protocol == AX25Version.UNKNOWN # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[] - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + ) ) # We now should consider the other station as AX.25 2.2 or better - eq_(peer._protocol, AX25Version.AX25_22) + assert peer._protocol == AX25Version.AX25_22 def test_peer_on_receive_xid_keeps_known_proto_version(): """ Test _on_receive_xid keeps existing protocol version if known. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - interface = station._interface() + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - protocol=AX25Version.AX25_22, - repeaters=None + station=station, + address=AX25Address("VK4MSL"), + protocol=AX25Version.AX25_22, + repeaters=None, ) # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[] - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + ) ) # We should still consider the other station as AX.25 2.2 or better - eq_(peer._protocol, AX25Version.AX25_22) + assert peer._protocol == AX25Version.AX25_22 def test_peer_on_receive_xid_ignores_bad_fi(): """ Test _on_receive_xid ignores parameters if FI is unknown """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - interface = station._interface() + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Stub out _process_xid_cop def _stub_process_cop(param): - assert False, 'Should not be called' + assert False, "Should not be called" + peer._process_xid_cop = _stub_process_cop # Pass in the XID frame to our AX.25 2.2 station. # There should be no assertion triggered. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[ - AX25XIDClassOfProceduresParameter( - half_duplex=True - ) - ], - fi=26 - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[AX25XIDClassOfProceduresParameter(half_duplex=True)], + fi=26, + ) ) @@ -935,33 +919,27 @@ def test_peer_on_receive_xid_ignores_bad_gi(): """ Test _on_receive_xid ignores parameters if GI is unknown """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - interface = station._interface() + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Stub out _process_xid_cop def _stub_process_cop(param): - assert False, 'Should not be called' + assert False, "Should not be called" + peer._process_xid_cop = _stub_process_cop # Pass in the XID frame to our AX.25 2.2 station. # There should be no assertion triggered. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[ - AX25XIDClassOfProceduresParameter( - half_duplex=True - ) - ], - gi=26 - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[AX25XIDClassOfProceduresParameter(half_duplex=True)], + gi=26, + ) ) @@ -969,64 +947,57 @@ def test_peer_on_receive_xid_processes_parameters(): """ Test _on_receive_xid processes parameters on good XID frames """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - interface = station._interface() + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Pass in the XID frame to our AX.25 2.2 station. # There should be no assertion triggered. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[ - AX25XIDIFieldLengthReceiveParameter(512) - ] - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[AX25XIDIFieldLengthReceiveParameter(512)], + ) ) # Should be negotiated to 64 bytes - eq_(peer._max_ifield, 64) + assert peer._max_ifield == 64 def test_peer_on_receive_xid_reply(): """ Test _on_receive_xid sends reply if incoming frame has CR=True """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive_xid( - AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[], - cr=True - ) + AX25ExchangeIdentificationFrame( + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=True, + ) ) # This was a request, so there should be a reply waiting - eq_(len(interface.transmit_calls), 1) + assert len(interface.transmit_calls) == 1 (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) # This should be a XID - eq_(tx_kwargs, {'callback': None}) - eq_(len(tx_args), 1) + assert tx_kwargs == {"callback": None} + assert len(tx_args) == 1 (frame,) = tx_args assert isinstance(frame, AX25ExchangeIdentificationFrame) @@ -1034,7 +1005,7 @@ def test_peer_on_receive_xid_reply(): assert not frame.header.cr # Frame should reflect the settings of the station - eq_(len(frame.parameters), 8) + assert len(frame.parameters) == 8 param = frame.parameters[0] assert isinstance(param, AX25XIDClassOfProceduresParameter) @@ -1050,60 +1021,58 @@ def test_peer_on_receive_xid_reply(): param = frame.parameters[2] assert isinstance(param, AX25XIDIFieldLengthTransmitParameter) - eq_(param.value, 2048) + assert param.value == 2048 param = frame.parameters[3] assert isinstance(param, AX25XIDIFieldLengthReceiveParameter) - eq_(param.value, 2048) + assert param.value == 2048 param = frame.parameters[4] assert isinstance(param, AX25XIDWindowSizeTransmitParameter) - eq_(param.value, 7) + assert param.value == 7 param = frame.parameters[5] assert isinstance(param, AX25XIDWindowSizeReceiveParameter) - eq_(param.value, 7) + assert param.value == 7 param = frame.parameters[6] assert isinstance(param, AX25XIDAcknowledgeTimerParameter) - eq_(param.value, 3000) + assert param.value == 3000 param = frame.parameters[7] assert isinstance(param, AX25XIDRetriesParameter) - eq_(param.value, 10) + assert param.value == 10 def test_peer_on_receive_xid_relay(): """ Test _on_receive_xid sends relays to XID handler if CR=False """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=None + station=station, address=AX25Address("VK4MSL"), repeaters=None ) # Nothing yet sent - eq_(interface.transmit_calls, []) + assert interface.transmit_calls == [] # Hook the XID handler xid_events = [] - peer._xidframe_handler = lambda *a, **kw : xid_events.append((a, kw)) + peer._xidframe_handler = lambda *a, **kw: xid_events.append((a, kw)) # Pass in the XID frame to our AX.25 2.2 station. frame = AX25ExchangeIdentificationFrame( - destination=station.address, - source=peer.address, - repeaters=None, - parameters=[], - cr=False - ) + destination=station.address, + source=peer.address, + repeaters=None, + parameters=[], + cr=False, + ) peer._on_receive_xid(frame) # There should have been a XID event - eq_(len(xid_events), 1) + assert len(xid_events) == 1 # It should be passed our handler (xid_args, xid_kwargs) = xid_events.pop(0) diff --git a/tests/test_station/test_attach_detach.py b/tests/test_station/test_attach_detach.py index 8713f76..671286e 100644 --- a/tests/test_station/test_attach_detach.py +++ b/tests/test_station/test_attach_detach.py @@ -2,7 +2,6 @@ from aioax25.station import AX25Station -from nose.tools import eq_ from ..mocks import DummyInterface @@ -14,16 +13,16 @@ def test_attach(): station = AX25Station(interface=interface, callsign="VK4MSL-5") station.attach() - eq_(len(interface.bind_calls), 1) - eq_(len(interface.unbind_calls), 0) - eq_(len(interface.transmit_calls), 0) + assert len(interface.bind_calls) == 1 + assert len(interface.unbind_calls) == 0 + assert len(interface.transmit_calls) == 0 (args, kwargs) = interface.bind_calls.pop() - eq_(args, (station._on_receive,)) - eq_(set(kwargs.keys()), set(["callsign", "ssid", "regex"])) - eq_(kwargs["callsign"], "VK4MSL") - eq_(kwargs["ssid"], 5) - eq_(kwargs["regex"], False) + assert args == (station._on_receive,) + assert set(kwargs.keys()) == set(["callsign", "ssid", "regex"]) + assert kwargs["callsign"] == "VK4MSL" + assert kwargs["ssid"] == 5 + assert kwargs["regex"] == False def test_detach(): @@ -34,13 +33,13 @@ def test_detach(): station = AX25Station(interface=interface, callsign="VK4MSL-5") station.detach() - eq_(len(interface.bind_calls), 0) - eq_(len(interface.unbind_calls), 1) - eq_(len(interface.transmit_calls), 0) + assert len(interface.bind_calls) == 0 + assert len(interface.unbind_calls) == 1 + assert len(interface.transmit_calls) == 0 (args, kwargs) = interface.unbind_calls.pop() - eq_(args, (station._on_receive,)) - eq_(set(kwargs.keys()), set(["callsign", "ssid", "regex"])) - eq_(kwargs["callsign"], "VK4MSL") - eq_(kwargs["ssid"], 5) - eq_(kwargs["regex"], False) + assert args == (station._on_receive,) + assert set(kwargs.keys()) == set(["callsign", "ssid", "regex"]) + assert kwargs["callsign"] == "VK4MSL" + assert kwargs["ssid"] == 5 + assert kwargs["regex"] == False diff --git a/tests/test_station/test_constructor.py b/tests/test_station/test_constructor.py index 6ea9c0b..e9ad813 100644 --- a/tests/test_station/test_constructor.py +++ b/tests/test_station/test_constructor.py @@ -3,7 +3,6 @@ from aioax25.station import AX25Station from aioax25.version import AX25Version -from nose.tools import eq_ from ..mocks import DummyInterface @@ -52,4 +51,4 @@ def test_constructor_protocol(): ) assert False, "Should not have worked" except ValueError as e: - eq_(str(e), "'1.x' not a supported AX.25 protocol version") + assert str(e) == "'1.x' not a supported AX.25 protocol version" diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py index cf05512..db572e5 100644 --- a/tests/test_station/test_getpeer.py +++ b/tests/test_station/test_getpeer.py @@ -4,7 +4,6 @@ from aioax25.peer import AX25Peer from aioax25.frame import AX25Address -from nose.tools import eq_ from ..mocks import DummyInterface, DummyPeer @@ -17,10 +16,9 @@ def test_unknown_peer_nocreate_keyerror(): station.getpeer("VK4BWI", create=False) assert False, "Should not have worked" except KeyError as e: - eq_( - str(e), + assert str(e) == ( "AX25Address(callsign=VK4BWI, ssid=0, " - "ch=False, res0=True, res1=True, extension=False)", + "ch=False, res0=True, res1=True, extension=False)" ) diff --git a/tests/test_station/test_properties.py b/tests/test_station/test_properties.py index 21de4ac..4796910 100644 --- a/tests/test_station/test_properties.py +++ b/tests/test_station/test_properties.py @@ -4,7 +4,6 @@ from aioax25.version import AX25Version from aioax25.frame import AX25Address -from nose.tools import eq_ from ..mocks import DummyInterface @@ -13,7 +12,7 @@ def test_address(): Test the address of the station is set from the constructor. """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - eq_(station.address, AX25Address(callsign="VK4MSL", ssid=5)) + assert station.address == AX25Address(callsign="VK4MSL", ssid=5) def test_protocol(): @@ -25,4 +24,4 @@ def test_protocol(): callsign="VK4MSL-5", protocol=AX25Version.AX25_20, ) - eq_(station.protocol, AX25Version.AX25_20) + assert station.protocol == AX25Version.AX25_20 diff --git a/tests/test_station/test_receive.py b/tests/test_station/test_receive.py index 2ddc326..4bcaac6 100644 --- a/tests/test_station/test_receive.py +++ b/tests/test_station/test_receive.py @@ -7,7 +7,6 @@ AX25UnnumberedInformationFrame, ) -from nose.tools import eq_ from ..mocks import DummyInterface, DummyPeer @@ -29,25 +28,25 @@ def test_testframe_cmd_echo(): ) # There should be no peers - eq_(station._peers, {}) + assert station._peers == {} # There should be a reply queued up - eq_(interface.bind_calls, []) - eq_(interface.unbind_calls, []) - eq_(len(interface.transmit_calls), 1) + assert interface.bind_calls == [] + assert interface.unbind_calls == [] + assert len(interface.transmit_calls) == 1 (tx_call_args, tx_call_kwargs) = interface.transmit_calls.pop() - eq_(tx_call_kwargs, {}) - eq_(len(tx_call_args), 1) + assert tx_call_kwargs == {} + assert len(tx_call_args) == 1 frame = tx_call_args[0] # The reply should have the source/destination swapped and the # CR bit cleared. assert isinstance(frame, AX25TestFrame), "Not a test frame" - eq_(frame.header.cr, False) - eq_(frame.header.destination, AX25Address("VK4MSL", ssid=7)) - eq_(frame.header.source, AX25Address("VK4MSL", ssid=5)) - eq_(frame.payload, b"This is a test frame") + assert frame.header.cr == False + assert frame.header.destination == AX25Address("VK4MSL", ssid=7) + assert frame.header.source == AX25Address("VK4MSL", ssid=5) + assert frame.payload == b"This is a test frame" def test_route_testframe_reply(): @@ -79,16 +78,16 @@ def stub_on_test_frame(*args, **kwargs): station._on_receive(frame=txframe) # There should be no replies queued - eq_(interface.bind_calls, []) - eq_(interface.unbind_calls, []) - eq_(interface.transmit_calls, []) + assert interface.bind_calls == [] + assert interface.unbind_calls == [] + assert interface.transmit_calls == [] # This should have gone to peer1, not peer2 - eq_(peer2.on_receive_calls, []) - eq_(len(peer1.on_receive_calls), 1) + assert peer2.on_receive_calls == [] + assert len(peer1.on_receive_calls) == 1 (rx_call_args, rx_call_kwargs) = peer1.on_receive_calls.pop() - eq_(rx_call_kwargs, {}) - eq_(len(rx_call_args), 1) + assert rx_call_kwargs == {} + assert len(rx_call_args) == 1 assert rx_call_args[0] is txframe @@ -122,14 +121,14 @@ def stub_on_test_frame(*args, **kwargs): station._on_receive(frame=txframe) # There should be no replies queued - eq_(interface.bind_calls, []) - eq_(interface.unbind_calls, []) - eq_(interface.transmit_calls, []) + assert interface.bind_calls == [] + assert interface.unbind_calls == [] + assert interface.transmit_calls == [] # This should have gone to peer2, not peer1 - eq_(peer1.on_receive_calls, []) - eq_(len(peer2.on_receive_calls), 1) + assert peer1.on_receive_calls == [] + assert len(peer2.on_receive_calls) == 1 (rx_call_args, rx_call_kwargs) = peer2.on_receive_calls.pop() - eq_(rx_call_kwargs, {}) - eq_(len(rx_call_args), 1) + assert rx_call_kwargs == {} + assert len(rx_call_args) == 1 assert rx_call_args[0] is txframe diff --git a/tests/test_uint.py b/tests/test_uint.py index 46252d0..c3f173a 100644 --- a/tests/test_uint.py +++ b/tests/test_uint.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 from aioax25.uint import encode, decode -from nose.tools import eq_ from .hex import from_hex, hex_cmp @@ -49,11 +48,11 @@ def test_decode_be(): """ Test we can decode big-endian integers. """ - eq_(decode(from_hex("11 22 33"), big_endian=True), 0x112233) + assert decode(from_hex("11 22 33"), big_endian=True) == 0x112233 def test_decode_le(): """ Test we can decode little-endian integers. """ - eq_(decode(from_hex("11 22 33"), big_endian=False), 0x332211) + assert decode(from_hex("11 22 33"), big_endian=False) == 0x332211 From 9b10e45336d45d692c3da5d1df51ffd5d641771f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 10:03:02 +1000 Subject: [PATCH 088/207] peer unit tests: Drop nosecompat test library usage. --- tests/test_peer/test_peertest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index c44dc21..aa464bc 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -4,6 +4,8 @@ Tests for AX25PeerTestHandler """ +from pytest import approx + from aioax25.peer import AX25PeerTestHandler from aioax25.frame import AX25Address, AX25TestFrame from ..mocks import DummyPeer, DummyStation @@ -81,9 +83,7 @@ def test_peertest_transmit_done(): helper._transmit_done() assert helper.tx_time is not None - assert int((peer._loop.time()) * (10**2)) == int( - (helper.tx_time) * (10**2) - ) + assert approx(peer._loop.time()) == helper.tx_time def test_peertest_on_receive(): @@ -102,9 +102,7 @@ def test_peertest_on_receive(): helper._on_receive(frame="Make believe TEST frame") assert helper.rx_time is not None - assert int((peer._loop.time()) * (10**2)) == int( - (helper.rx_time) * (10**2) - ) + assert approx(peer._loop.time()) == helper.rx_time assert helper.rx_frame == "Make believe TEST frame" # We should be done now From 1d2b15c35d364162c371353d8267938274428c9a Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 10:23:46 +1000 Subject: [PATCH 089/207] peer: Fix path selection, add tests --- aioax25/peer.py | 4 +- tests/test_peer/test_replypath.py | 72 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/test_peer/test_replypath.py diff --git a/aioax25/peer.py b/aioax25/peer.py index 280d09d..6ecf0ad 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -244,10 +244,10 @@ def reply_path(self): ] if all_paths: - all_paths.sort(key=lambda p: p[0]) + all_paths.sort(key=lambda p: p[1]) best_path = all_paths[-1][0] else: - # If no paths exist, use whatver default path is set + # If no paths exist, use whatever default path is set best_path = self._repeaters # Use this until we have reason to change diff --git a/tests/test_peer/test_replypath.py b/tests/test_peer/test_replypath.py new file mode 100644 index 0000000..b7aecb1 --- /dev/null +++ b/tests/test_peer/test_replypath.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25Peer reply path handling +""" + +from aioax25.frame import AX25Address, AX25Path +from .peer import TestingAX25Peer +from ..mocks import DummyStation + + +def test_peer_reply_path_locked(): + """ + Test reply_path with a locked path returns the repeaters given + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(AX25Address("VK4RZB")), + locked_path=True, + ) + + # Ensure not pre-determined path is set + peer._reply_path = None + + assert list(peer.reply_path) == [AX25Address("VK4RZB")] + + +def test_peer_reply_path_predetermined(): + """ + Test reply_path with pre-determined path returns the chosen path + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + locked_path=False, + ) + + # Inject pre-determined path + peer._reply_path = AX25Path(AX25Address("VK4RZB")) + + assert list(peer.reply_path) == [AX25Address("VK4RZB")] + + +def test_peer_reply_path_weight_score(): + """ + Test reply_path tries to select the "best" scoring path. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + locked_path=False, + ) + + # Ensure not pre-determined path is set + peer._reply_path = None + + # Inject path scores + peer._tx_path_score = { + AX25Path(AX25Address("VK4RZB")): 2, + AX25Path(AX25Address("VK4RZA")): 1, + } + + assert list(peer.reply_path) == [AX25Address("VK4RZB")] + + # We should also use this from now on: + assert list(peer._reply_path) == [AX25Address("VK4RZB")] From aa478fdfaae9bd293f916c3604c0a9690677b500 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 10:31:59 +1000 Subject: [PATCH 090/207] peer: Pick most frequently seen RX path if no TX paths rated. --- aioax25/peer.py | 9 +++++---- tests/test_peer/test_replypath.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 6ecf0ad..d9f6f18 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -239,12 +239,13 @@ def reply_path(self): return self._repeaters # Enumerate all possible paths and select the "best" path - all_paths = list(self._tx_path_score.items()) + [ - (path, 0) for path in self._rx_path_count.keys() - ] + # - most highly rated TX path + # - most frequently seen RX path + all_paths = list( + sorted(self._tx_path_score.items(), key=lambda i: i[1]) + ) + list(sorted(self._rx_path_count.items(), key=lambda i: i[1])) if all_paths: - all_paths.sort(key=lambda p: p[1]) best_path = all_paths[-1][0] else: # If no paths exist, use whatever default path is set diff --git a/tests/test_peer/test_replypath.py b/tests/test_peer/test_replypath.py index b7aecb1..059e762 100644 --- a/tests/test_peer/test_replypath.py +++ b/tests/test_peer/test_replypath.py @@ -70,3 +70,33 @@ def test_peer_reply_path_weight_score(): # We should also use this from now on: assert list(peer._reply_path) == [AX25Address("VK4RZB")] + + +def test_peer_reply_path_rx_count(): + """ + Test reply_path considers received paths if no rated TX path. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + locked_path=False, + ) + + # Ensure not pre-determined path is set + peer._reply_path = None + + # Ensure empty TX path scores + peer._tx_path_score = {} + + # Inject path counts + peer._rx_path_count = { + AX25Path(AX25Address("VK4RZB")): 2, + AX25Path(AX25Address("VK4RZA")): 1, + } + + assert list(peer.reply_path) == [AX25Address("VK4RZB")] + + # We should also use this from now on: + assert list(peer._reply_path) == [AX25Address("VK4RZB")] From 681033391e192c9dc5f2e7e340f5643890e23f3f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 10:40:00 +1000 Subject: [PATCH 091/207] peer unit tests: Lean on AX25Path `__str__` for test readability. We test this separately in other unit tests, so should be safe. --- tests/test_peer/test_replypath.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/tests/test_peer/test_replypath.py b/tests/test_peer/test_replypath.py index 059e762..ba134ee 100644 --- a/tests/test_peer/test_replypath.py +++ b/tests/test_peer/test_replypath.py @@ -17,14 +17,14 @@ def test_peer_reply_path_locked(): peer = TestingAX25Peer( station=station, address=AX25Address("VK4MSL"), - repeaters=AX25Path(AX25Address("VK4RZB")), + repeaters=AX25Path("VK4RZB"), locked_path=True, ) # Ensure not pre-determined path is set peer._reply_path = None - assert list(peer.reply_path) == [AX25Address("VK4RZB")] + assert str(peer.reply_path) == "VK4RZB" def test_peer_reply_path_predetermined(): @@ -40,9 +40,9 @@ def test_peer_reply_path_predetermined(): ) # Inject pre-determined path - peer._reply_path = AX25Path(AX25Address("VK4RZB")) + peer._reply_path = AX25Path("VK4RZB") - assert list(peer.reply_path) == [AX25Address("VK4RZB")] + assert str(peer.reply_path) == "VK4RZB" def test_peer_reply_path_weight_score(): @@ -61,15 +61,12 @@ def test_peer_reply_path_weight_score(): peer._reply_path = None # Inject path scores - peer._tx_path_score = { - AX25Path(AX25Address("VK4RZB")): 2, - AX25Path(AX25Address("VK4RZA")): 1, - } + peer._tx_path_score = {AX25Path("VK4RZB"): 2, AX25Path("VK4RZA"): 1} - assert list(peer.reply_path) == [AX25Address("VK4RZB")] + assert str(peer.reply_path) == "VK4RZB" # We should also use this from now on: - assert list(peer._reply_path) == [AX25Address("VK4RZB")] + assert str(peer._reply_path) == "VK4RZB" def test_peer_reply_path_rx_count(): @@ -91,12 +88,9 @@ def test_peer_reply_path_rx_count(): peer._tx_path_score = {} # Inject path counts - peer._rx_path_count = { - AX25Path(AX25Address("VK4RZB")): 2, - AX25Path(AX25Address("VK4RZA")): 1, - } + peer._rx_path_count = {AX25Path("VK4RZB"): 2, AX25Path("VK4RZA"): 1} - assert list(peer.reply_path) == [AX25Address("VK4RZB")] + assert str(peer.reply_path) == "VK4RZB" # We should also use this from now on: - assert list(peer._reply_path) == [AX25Address("VK4RZB")] + assert str(peer._reply_path) == "VK4RZB" From ef32b07acfbdff13f5dc86fe72162517e99821e2 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 10:42:18 +1000 Subject: [PATCH 092/207] peer: Test path weight rating --- tests/test_peer/test_replypath.py | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_peer/test_replypath.py b/tests/test_peer/test_replypath.py index ba134ee..65fd4d2 100644 --- a/tests/test_peer/test_replypath.py +++ b/tests/test_peer/test_replypath.py @@ -94,3 +94,62 @@ def test_peer_reply_path_rx_count(): # We should also use this from now on: assert str(peer._reply_path) == "VK4RZB" + + +# Path weighting + + +def test_weight_path_absolute(): + """ + Test we can set the score for a given path. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + locked_path=False, + ) + + # Ensure known weights + peer._tx_path_score = { + tuple(AX25Path("VK4RZB", "VK4RZA")): 1, + tuple(AX25Path("VK4RZA")): 2, + } + + # Rate a few paths + peer.weight_path(AX25Path("VK4RZB*", "VK4RZA*"), 5, relative=False) + peer.weight_path(AX25Path("VK4RZA*"), 3, relative=False) + + assert peer._tx_path_score == { + tuple(AX25Path("VK4RZB", "VK4RZA")): 5, + tuple(AX25Path("VK4RZA")): 3, + } + + +def test_weight_path_relative(): + """ + Test we can increment the score for a given path. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=None, + locked_path=False, + ) + + # Ensure known weights + peer._tx_path_score = { + tuple(AX25Path("VK4RZB", "VK4RZA")): 5, + tuple(AX25Path("VK4RZA")): 3, + } + + # Rate a few paths + peer.weight_path(AX25Path("VK4RZB*", "VK4RZA*"), 2, relative=True) + peer.weight_path(AX25Path("VK4RZA*"), 1, relative=True) + + assert peer._tx_path_score == { + tuple(AX25Path("VK4RZB", "VK4RZA")): 7, + tuple(AX25Path("VK4RZA")): 4, + } From b331bdc0954b229511a895ecb09144a03c9739c4 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 11:14:28 +1000 Subject: [PATCH 093/207] peer unit tests: Test ping() method --- tests/test_peer/test_peertest.py | 112 ++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index aa464bc..653736a 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -7,8 +7,9 @@ from pytest import approx from aioax25.peer import AX25PeerTestHandler -from aioax25.frame import AX25Address, AX25TestFrame +from aioax25.frame import AX25Address, AX25TestFrame, AX25Path from ..mocks import DummyPeer, DummyStation +from .peer import TestingAX25Peer def test_peertest_go(): @@ -154,3 +155,112 @@ def test_peertest_on_timeout(): done_evt = done_events.pop() assert list(done_evt.keys()) == ["handler"] assert done_evt["handler"] is helper + + +# Integration into AX25Peer + + +def test_peer_ping(): + """ + Test that calling peer.ping() sets up a AX25PeerTestHandler + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the peer's _transmit_frame method + tx_frames = [] + + def _transmit_frame(frame, callback): + tx_frames.append(frame) + callback() + + peer._transmit_frame = _transmit_frame + + # Send a ping request + handler = peer.ping() + + # We should have a reference to the handler + assert isinstance(handler, AX25PeerTestHandler) + + # Handler should have sent a frame with an empty payload + assert len(tx_frames) == 1 + assert isinstance(tx_frames[0], AX25TestFrame) + assert tx_frames[0].payload == b"" + + +def test_peer_ping_payload(): + """ + Test that we can supply a payload to the ping request + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the peer's _transmit_frame method + tx_frames = [] + + def _transmit_frame(frame, callback): + tx_frames.append(frame) + callback() + + peer._transmit_frame = _transmit_frame + + # Send a ping request + handler = peer.ping(payload=b"testing") + + # We should have a reference to the handler + assert isinstance(handler, AX25PeerTestHandler) + + # Handler should have sent a frame with an empty payload + assert len(tx_frames) == 1 + assert isinstance(tx_frames[0], AX25TestFrame) + assert tx_frames[0].payload == b"testing" + + +def test_peer_ping_cb(): + """ + Test that peer.ping() attaches callback if given + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the peer's _transmit_frame method + tx_frames = [] + + def _transmit_frame(frame, callback): + tx_frames.append(frame) + callback() + + peer._transmit_frame = _transmit_frame + + # Create a callback routine + cb_args = [] + + def _callback(*args, **kwargs): + cb_args.append((args, kwargs)) + + # Send a ping request + handler = peer.ping(callback=_callback) + + # We should have a reference to the handler + assert isinstance(handler, AX25PeerTestHandler) + + # Pass a reply to the handler to trigger completion + handler._on_receive(frame=b"test") + + # Our callback should have been called on completion + assert cb_args == [((), {"handler": handler})] From 19f30059c9f11affad5a9cbe9405407a2a86757e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 11:25:21 +1000 Subject: [PATCH 094/207] peer: Fix comparison of states --- aioax25/peer.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index d9f6f18..e38f41a 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -285,7 +285,7 @@ def connect(self): """ Connect to the remote node. """ - if self._state in self.AX25PeerState.DISCONNECTED: + if self._state is self.AX25PeerState.DISCONNECTED: handler = AX25PeerConnectionHandler(self) handler.done_sig.connect(self._on_connect_response) handler._go() @@ -294,7 +294,7 @@ def accept(self): """ Accept an incoming connection from the peer. """ - if self._state in self.AX25PeerState.INCOMING_CONNECTION: + if self._state is self.AX25PeerState.INCOMING_CONNECTION: self._log.info("Accepting incoming connection") # Send a UA and set ourselves as connected self._stop_incoming_connect_timer() @@ -305,7 +305,7 @@ def reject(self): """ Reject an incoming connection from the peer. """ - if self._state in self.AX25PeerState.INCOMING_CONNECTION: + if self._state is self.AX25PeerState.INCOMING_CONNECTION: self._log.info("Rejecting incoming connection") # Send a DM and set ourselves as disconnected self._stop_incoming_connect_timer() @@ -316,7 +316,7 @@ def disconnect(self): """ Disconnect from the remote node. """ - if self._state == self.AX25PeerState.CONNECTED: + if self._state is self.AX25PeerState.CONNECTED: self._uaframe_handler = self._on_disconnect self._send_disc() @@ -341,7 +341,7 @@ def _cleanup(self): """ Clean up the instance of this peer as the activity has expired. """ - if self._state != self.AX25PeerState.DISCONNECTED: + if self._state is not self.AX25PeerState.DISCONNECTED: self._log.warning("Disconnecting peer due to inactivity") self._send_dm() @@ -368,7 +368,7 @@ def _on_receive(self, frame): # AX.25 2.2 sect 6.3.1: "The originating TNC sending a SABM(E) command # ignores and discards any frames except SABM, DISC, UA and DM frames # from the distant TNC." - if (self._state == self.AX25PeerState.CONNECTING) and not isinstance( + if (self._state is self.AX25PeerState.CONNECTING) and not isinstance( frame, ( AX25SetAsyncBalancedModeFrame, # SABM @@ -389,7 +389,7 @@ def _on_receive(self, frame): # a DM response frame. Any other command received while the DXE is in # the frame reject state will cause another FRMR to be sent out with # the same information field as originally sent." - if (self._state == self.AX25PeerState.FRMR) and not isinstance( + if (self._state is self.AX25PeerState.FRMR) and not isinstance( frame, (AX25SetAsyncBalancedModeFrame, AX25DisconnectFrame), # SABM ): # DISC @@ -426,7 +426,7 @@ def _on_receive(self, frame): elif isinstance(frame, AX25RawFrame): # This is either an I or S frame. We should know enough now to # decode it properly. - if self._state == self.AX25PeerState.CONNECTED: + if self._state is self.AX25PeerState.CONNECTED: # A connection is in progress, we can decode this frame = AX25Frame.decode( frame, modulo128=(self._modulo == 128) @@ -652,7 +652,7 @@ def _stop_incoming_connect_timer(self): self._incoming_connect_timeout_handle = None def _on_incoming_connect_timeout(self): - if self._state == self.AX25PeerState.INCOMING_CONNECTION: + if self._state is self.AX25PeerState.INCOMING_CONNECTION: self._incoming_connect_timeout_handle = None self.reject() @@ -759,7 +759,7 @@ def _reset_connection_state(self): self._pending_data = [] def _set_conn_state(self, state): - if self._state == state: + if self._state is state: # Nothing to do return @@ -1191,7 +1191,7 @@ def _send_rr_notification(self): Send a RR notification frame """ self._cancel_rr_notification() - if self._state == self.AX25PeerState.CONNECTED: + if self._state is self.AX25PeerState.CONNECTED: self._transmit_frame( self._RRFrameClass( destination=self.address, @@ -1206,7 +1206,7 @@ def _send_rnr_notification(self): """ Send a RNR notification if the RNR interval has elapsed. """ - if self._state == self.AX25PeerState.CONNECTED: + if self._state is self.AX25PeerState.CONNECTED: now = self._loop.time() if (now - self._last_rnr_sent) > self._rnr_interval: self._transmit_frame( From f53185922afc10778f4ca4677531a94785ad3a89 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 14 Nov 2021 11:33:59 +1000 Subject: [PATCH 095/207] peer unit tests: Test connect() method --- tests/test_peer/test_peerconnection.py | 65 +++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index 5bc76b3..cc5b3bf 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -6,7 +6,8 @@ from aioax25.version import AX25Version from aioax25.peer import AX25PeerConnectionHandler -from aioax25.frame import AX25Address +from aioax25.frame import AX25Address, AX25Path +from .peer import TestingAX25Peer from ..mocks import DummyPeer, DummyStation @@ -492,3 +493,65 @@ def test_peerconn_finish_disconnect_dm(): assert peer._uaframe_handler is None assert peer._frmrframe_handler is None assert peer._dmframe_handler == dummy_dmframe_handler + + +# AX25Peer integration + +def test_connect_not_disconnected(): + """ + Test that calling peer.connect() when not disconnected does nothing. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub negotiation, this should not get called + def _negotiate(*args, **kwargs): + assert False, 'Should not have been called' + peer._negotiate = _negotiate + + # Ensure _negotiate() gets called if we try to connect + peer._negotiated = False + + # Override the state to ensure connection attempt never happens + peer._state = peer.AX25PeerState.CONNECTED + + # Now try connecting + peer.connect() + + +def test_connect_when_disconnected(): + """ + Test that calling peer.connect() when disconnected initiates connection + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub negotiation, we'll just throw an error to see if it gets called + class ConnectionStarted(Exception): + pass + def _negotiate(*args, **kwargs): + raise ConnectionStarted() + peer._negotiate = _negotiate + + # Ensure _negotiate() gets called if we try to connect + peer._negotiated = False + + # Ensure disconnected state + peer._state = peer.AX25PeerState.DISCONNECTED + + # Now try connecting + try: + peer.connect() + assert False, 'Did not call _negotiate' + except ConnectionStarted: + pass From e2ef5373f97b6751841bddaebab7cf28398eee68 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 14:31:27 +1000 Subject: [PATCH 096/207] peer unit tests: Move connect() test cases --- tests/test_peer/test_connection.py | 70 ++++++++++++++++++++++++++ tests/test_peer/test_peerconnection.py | 62 ----------------------- 2 files changed, 70 insertions(+), 62 deletions(-) create mode 100644 tests/test_peer/test_connection.py diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py new file mode 100644 index 0000000..996cf99 --- /dev/null +++ b/tests/test_peer/test_connection.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +""" +Test handling of outgoing connection logic +""" + +from aioax25.frame import AX25Address, AX25Path +from .peer import TestingAX25Peer +from ..mocks import DummyStation + +# Connection establishment + +def test_connect_not_disconnected(): + """ + Test that calling peer.connect() when not disconnected does nothing. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub negotiation, this should not get called + def _negotiate(*args, **kwargs): + assert False, 'Should not have been called' + peer._negotiate = _negotiate + + # Ensure _negotiate() gets called if we try to connect + peer._negotiated = False + + # Override the state to ensure connection attempt never happens + peer._state = peer.AX25PeerState.CONNECTED + + # Now try connecting + peer.connect() + + +def test_connect_when_disconnected(): + """ + Test that calling peer.connect() when disconnected initiates connection + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub negotiation, we'll just throw an error to see if it gets called + class ConnectionStarted(Exception): + pass + def _negotiate(*args, **kwargs): + raise ConnectionStarted() + peer._negotiate = _negotiate + + # Ensure _negotiate() gets called if we try to connect + peer._negotiated = False + + # Ensure disconnected state + peer._state = peer.AX25PeerState.DISCONNECTED + + # Now try connecting + try: + peer.connect() + assert False, 'Did not call _negotiate' + except ConnectionStarted: + pass diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index cc5b3bf..2853e24 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -493,65 +493,3 @@ def test_peerconn_finish_disconnect_dm(): assert peer._uaframe_handler is None assert peer._frmrframe_handler is None assert peer._dmframe_handler == dummy_dmframe_handler - - -# AX25Peer integration - -def test_connect_not_disconnected(): - """ - Test that calling peer.connect() when not disconnected does nothing. - """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True - ) - - # Stub negotiation, this should not get called - def _negotiate(*args, **kwargs): - assert False, 'Should not have been called' - peer._negotiate = _negotiate - - # Ensure _negotiate() gets called if we try to connect - peer._negotiated = False - - # Override the state to ensure connection attempt never happens - peer._state = peer.AX25PeerState.CONNECTED - - # Now try connecting - peer.connect() - - -def test_connect_when_disconnected(): - """ - Test that calling peer.connect() when disconnected initiates connection - """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) - peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True - ) - - # Stub negotiation, we'll just throw an error to see if it gets called - class ConnectionStarted(Exception): - pass - def _negotiate(*args, **kwargs): - raise ConnectionStarted() - peer._negotiate = _negotiate - - # Ensure _negotiate() gets called if we try to connect - peer._negotiated = False - - # Ensure disconnected state - peer._state = peer.AX25PeerState.DISCONNECTED - - # Now try connecting - try: - peer.connect() - assert False, 'Did not call _negotiate' - except ConnectionStarted: - pass From 2c0fd5a5e8fa25cce755dd2355bfebedddec4e25 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 15:36:46 +1000 Subject: [PATCH 097/207] unit test mocks: Add `connection_request` signal to mock station. --- tests/mocks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index 8a6b063..e7595fc 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -2,6 +2,7 @@ import time import logging +from signalslot import Signal from aioax25.version import AX25Version @@ -95,6 +96,7 @@ def __init__(self, address, reply_path=None): self.reply_path = reply_path or [] self._full_duplex = False self._protocol = AX25Version.AX25_22 + self.connection_request = Signal() def _interface(self): return self._interface_ref From 7d1e9667105fab84d270103d926248d4cc3b099a Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 15:37:12 +1000 Subject: [PATCH 098/207] peer unit tests: Test connection establishment logic. --- tests/test_peer/test_connection.py | 568 ++++++++++++++++++++++++++++- 1 file changed, 567 insertions(+), 1 deletion(-) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 996cf99..72ef337 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -4,7 +4,15 @@ Test handling of outgoing connection logic """ -from aioax25.frame import AX25Address, AX25Path +from aioax25.version import AX25Version +from aioax25.frame import AX25Address, AX25Path, \ + AX25DisconnectFrame, \ + AX25DisconnectModeFrame, \ + AX25FrameRejectFrame, \ + AX25UnnumberedAcknowledgeFrame, \ + AX25TestFrame, \ + AX25SetAsyncBalancedModeFrame, \ + AX25SetAsyncBalancedModeExtendedFrame from .peer import TestingAX25Peer from ..mocks import DummyStation @@ -68,3 +76,561 @@ def _negotiate(*args, **kwargs): assert False, 'Did not call _negotiate' except ConnectionStarted: pass + + +# SABM(E) transmission + +def test_send_sabm(): + """ + Test we can send a SABM (modulo-8) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub _transmit_frame + sent = [] + def _transmit_frame(frame): + sent.append(frame) + peer._transmit_frame = _transmit_frame + + peer._send_sabm() + + try: + frame = sent.pop(0) + except IndexError: + assert False, 'No frames were sent' + + assert isinstance(frame, AX25SetAsyncBalancedModeFrame) + assert str(frame.header.destination) == 'VK4MSL' + assert str(frame.header.source) == 'VK4MSL-1' + assert str(frame.header.repeaters) == 'VK4RZB' + assert len(sent) == 0 + + assert peer._state == peer.AX25PeerState.CONNECTING + + +def test_send_sabme(): + """ + Test we can send a SABM (modulo-128) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + peer._modulo128 = True + + # Stub _transmit_frame + sent = [] + def _transmit_frame(frame): + sent.append(frame) + peer._transmit_frame = _transmit_frame + + peer._send_sabm() + + try: + frame = sent.pop(0) + except IndexError: + assert False, 'No frames were sent' + + assert isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) + assert str(frame.header.destination) == 'VK4MSL' + assert str(frame.header.source) == 'VK4MSL-1' + assert str(frame.header.repeaters) == 'VK4RZB' + assert len(sent) == 0 + + assert peer._state == peer.AX25PeerState.CONNECTING + + +# SABM response handling + + +def test_recv_ignore_frmr(): + """ + Test that we ignore FRMR from peer when connecting. + + (AX.25 2.2 sect 6.3.1) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Stub FRMR handling + def _on_receive_frmr(): + assert False, 'Should not have been called' + peer._on_receive_frmr = _on_receive_frmr + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + peer._on_receive( + AX25FrameRejectFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + w=False, x=False, y=False, z=False, + frmr_cr=False, vs=0, vr=0, frmr_control=0 + ) + ) + + +def test_recv_ignore_test(): + """ + Test that we ignore TEST from peer when connecting. + + (AX.25 2.2 sect 6.3.1) + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Stub TEST handling + def _on_receive_test(): + assert False, 'Should not have been called' + peer._on_receive_test = _on_receive_test + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + peer._on_receive( + AX25TestFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + payload=b'Frame to be ignored' + ) + ) + + +def test_recv_ua(): + """ + Test that UA is handled. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Create a handler for receiving the UA + count = dict(ua=0) + def _on_receive_ua(): + count['ua'] += 1 + peer._uaframe_handler = _on_receive_ua + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + peer._on_receive( + AX25UnnumberedAcknowledgeFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + # Our handler should have been called + assert count == dict(ua=1) + + +def test_recv_disc(): + """ + Test that DISC is handled. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Stub _send_ua and _on_disconnect + count = dict(send_ua=0, on_disc=0) + def _send_ua(): + count['send_ua'] += 1 + peer._send_ua = _send_ua + def _on_disconnect(): + count['on_disc'] += 1 + peer._on_disconnect = _on_disconnect + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + peer._on_receive( + AX25DisconnectFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + # Our handlers should have been called + assert count == dict(send_ua=1, on_disc=1) + + +def test_recv_dm(): + """ + Test that DM is handled. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Stub _dmframe_handler and _on_disconnect + count = dict(on_disc=0) + def _dmframe_handler(): + assert False, '_dmframe_handler should not have been called' + peer._dmframe_handler = _dmframe_handler + def _on_disconnect(): + count['on_disc'] += 1 + peer._on_disconnect = _on_disconnect + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + peer._on_receive( + AX25DisconnectModeFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + # Our handlers should have been called + assert count == dict(on_disc=1) + + # We should not have removed the DM frame handler + assert peer._dmframe_handler is not None + + +def test_recv_sabm(): + """ + Test that SABM is handled. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Stub _on_receive_sabm, we'll test it fully later + frames = [] + def _on_receive_sabm(frame): + frames.append(frame) + peer._on_receive_sabm = _on_receive_sabm + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + frame = AX25SetAsyncBalancedModeFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + peer._on_receive(frame) + + assert frames == [frame] + + +def test_recv_sabme(): + """ + Test that SABME is handled. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda : None + + # Stub _on_receive_sabm, we'll test it fully later + frames = [] + def _on_receive_sabm(frame): + frames.append(frame) + peer._on_receive_sabm = _on_receive_sabm + + # Set the state + peer._state = peer.AX25PeerState.CONNECTING + + # Inject a frame + frame = AX25SetAsyncBalancedModeExtendedFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + peer._on_receive(frame) + + assert frames == [frame] + + +# SABM(E) handling + + +def test_on_receive_sabm_init(): + """ + Test the incoming connection is initialised on receipt of SABM. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub _init_connection + count = dict(init=0, start_timer=0, conn_rq=0) + def _init_connection(extended): + assert extended is False + count['init'] += 1 + peer._init_connection = _init_connection + + # Stub _start_incoming_connect_timer + def _start_incoming_connect_timer(): + count['start_timer'] += 1 + peer._start_incoming_connect_timer = _start_incoming_connect_timer + + # Hook connection request event + def _on_conn_rq(**kwargs): + assert kwargs == dict(peer=peer) + count['conn_rq'] += 1 + station.connection_request.connect(_on_conn_rq) + + peer._on_receive_sabm( + AX25SetAsyncBalancedModeFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + +def test_on_receive_sabme_init(): + """ + Test the incoming connection is initialised on receipt of SABME. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Assume we know it's an AX.25 2.2 peer + peer._protocol = AX25Version.AX25_22 + + # Stub _init_connection + count = dict(init=0, start_timer=0, conn_rq=0) + def _init_connection(extended): + assert extended is True + count['init'] += 1 + peer._init_connection = _init_connection + + # Stub _start_incoming_connect_timer + def _start_incoming_connect_timer(): + count['start_timer'] += 1 + peer._start_incoming_connect_timer = _start_incoming_connect_timer + + # Hook connection request event + def _on_conn_rq(**kwargs): + assert kwargs == dict(peer=peer) + count['conn_rq'] += 1 + station.connection_request.connect(_on_conn_rq) + + peer._on_receive_sabm( + AX25SetAsyncBalancedModeExtendedFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + +def test_on_receive_sabme_init_unknown_peer_ver(): + """ + Test we switch the peer to AX.25 2.2 mode on receipt of SABME + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Assume we do not know the peer's AX.25 version + peer._protocol = AX25Version.UNKNOWN + + # Stub _init_connection + count = dict(init=0, start_timer=0, conn_rq=0) + def _init_connection(extended): + assert extended is True + count['init'] += 1 + peer._init_connection = _init_connection + + # Stub _start_incoming_connect_timer + def _start_incoming_connect_timer(): + count['start_timer'] += 1 + peer._start_incoming_connect_timer = _start_incoming_connect_timer + + # Hook connection request event + def _on_conn_rq(**kwargs): + assert kwargs == dict(peer=peer) + count['conn_rq'] += 1 + station.connection_request.connect(_on_conn_rq) + + peer._on_receive_sabm( + AX25SetAsyncBalancedModeExtendedFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + assert peer._protocol == AX25Version.AX25_22 + + +def test_on_receive_sabme_ax25_20_station(): + """ + Test we reject SABME if station is in AX.25 2.0 mode + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set AX.25 2.0 mode on the station + station._protocol = AX25Version.AX25_20 + + # Stub _send_frmr + frmr = [] + def _send_frmr(frame, **kwargs): + frmr.append((frame, kwargs)) + peer._send_frmr = _send_frmr + + # Stub _init_connection + def _init_connection(extended): + assert False, 'Should not have been called' + peer._init_connection = _init_connection + + # Stub _start_incoming_connect_timer + def _start_incoming_connect_timer(): + assert False, 'Should not have been called' + peer._start_incoming_connect_timer = _start_incoming_connect_timer + + # Hook connection request event + def _on_conn_rq(**kwargs): + assert False, 'Should not have been called' + station.connection_request.connect(_on_conn_rq) + + frame = AX25SetAsyncBalancedModeExtendedFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + peer._on_receive_sabm(frame) + + assert frmr == [(frame, dict(w=True))] + + +def test_on_receive_sabme_ax25_20_peer(): + """ + Test we reject SABME if peer not in AX.25 2.2 mode + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Assume the peer runs AX.25 2.0 + peer._protocol = AX25Version.AX25_20 + + # Stub _send_dm + count = dict(send_dm=0) + def _send_dm(): + count['send_dm'] += 1 + peer._send_dm = _send_dm + + # Stub _init_connection + def _init_connection(extended): + assert False, 'Should not have been called' + peer._init_connection = _init_connection + + # Stub _start_incoming_connect_timer + def _start_incoming_connect_timer(): + assert False, 'Should not have been called' + peer._start_incoming_connect_timer = _start_incoming_connect_timer + + # Hook connection request event + def _on_conn_rq(**kwargs): + assert False, 'Should not have been called' + station.connection_request.connect(_on_conn_rq) + + peer._on_receive_sabm( + AX25SetAsyncBalancedModeExtendedFrame( + destination=AX25Address('VK4MSL-1'), + source=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB') + ) + ) + + assert count == dict(send_dm=1) From 354b3c79ccfdf142a3064a231a960208083dc3f1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 15:44:14 +1000 Subject: [PATCH 099/207] peer: Fix logic for DM frame reception If we're connecting, the `AX25PeerConnectionHandler` through the `_dmframe_handler` hook, should be handling the incoming `DM` frame. I think this is a logic error in my part so let's fix it. --- aioax25/peer.py | 5 +- tests/test_peer/test_connection.py | 420 ++++++++++++++++------------- 2 files changed, 239 insertions(+), 186 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index e38f41a..9e727d5 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -807,10 +807,7 @@ def _on_receive_dm(self): """ Handle a disconnect request from this peer. """ - if self._state in ( - self.AX25PeerState.CONNECTED, - self.AX25PeerState.CONNECTING, - ): + if self._state is self.AX25PeerState.CONNECTED: # Set ourselves as disconnected self._log.info("Received DM from peer") self._on_disconnect() diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 72ef337..5f8c26d 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -5,34 +5,39 @@ """ from aioax25.version import AX25Version -from aioax25.frame import AX25Address, AX25Path, \ - AX25DisconnectFrame, \ - AX25DisconnectModeFrame, \ - AX25FrameRejectFrame, \ - AX25UnnumberedAcknowledgeFrame, \ - AX25TestFrame, \ - AX25SetAsyncBalancedModeFrame, \ - AX25SetAsyncBalancedModeExtendedFrame +from aioax25.frame import ( + AX25Address, + AX25Path, + AX25DisconnectFrame, + AX25DisconnectModeFrame, + AX25FrameRejectFrame, + AX25UnnumberedAcknowledgeFrame, + AX25TestFrame, + AX25SetAsyncBalancedModeFrame, + AX25SetAsyncBalancedModeExtendedFrame, +) from .peer import TestingAX25Peer from ..mocks import DummyStation # Connection establishment + def test_connect_not_disconnected(): """ Test that calling peer.connect() when not disconnected does nothing. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub negotiation, this should not get called def _negotiate(*args, **kwargs): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._negotiate = _negotiate # Ensure _negotiate() gets called if we try to connect @@ -49,19 +54,21 @@ def test_connect_when_disconnected(): """ Test that calling peer.connect() when disconnected initiates connection """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub negotiation, we'll just throw an error to see if it gets called class ConnectionStarted(Exception): pass + def _negotiate(*args, **kwargs): raise ConnectionStarted() + peer._negotiate = _negotiate # Ensure _negotiate() gets called if we try to connect @@ -73,29 +80,32 @@ def _negotiate(*args, **kwargs): # Now try connecting try: peer.connect() - assert False, 'Did not call _negotiate' + assert False, "Did not call _negotiate" except ConnectionStarted: pass # SABM(E) transmission + def test_send_sabm(): """ Test we can send a SABM (modulo-8) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub _transmit_frame sent = [] + def _transmit_frame(frame): sent.append(frame) + peer._transmit_frame = _transmit_frame peer._send_sabm() @@ -103,12 +113,12 @@ def _transmit_frame(frame): try: frame = sent.pop(0) except IndexError: - assert False, 'No frames were sent' + assert False, "No frames were sent" assert isinstance(frame, AX25SetAsyncBalancedModeFrame) - assert str(frame.header.destination) == 'VK4MSL' - assert str(frame.header.source) == 'VK4MSL-1' - assert str(frame.header.repeaters) == 'VK4RZB' + assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.repeaters) == "VK4RZB" assert len(sent) == 0 assert peer._state == peer.AX25PeerState.CONNECTING @@ -118,19 +128,21 @@ def test_send_sabme(): """ Test we can send a SABM (modulo-128) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._modulo128 = True # Stub _transmit_frame sent = [] + def _transmit_frame(frame): sent.append(frame) + peer._transmit_frame = _transmit_frame peer._send_sabm() @@ -138,12 +150,12 @@ def _transmit_frame(frame): try: frame = sent.pop(0) except IndexError: - assert False, 'No frames were sent' + assert False, "No frames were sent" assert isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) - assert str(frame.header.destination) == 'VK4MSL' - assert str(frame.header.source) == 'VK4MSL-1' - assert str(frame.header.repeaters) == 'VK4RZB' + assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.repeaters) == "VK4RZB" assert len(sent) == 0 assert peer._state == peer.AX25PeerState.CONNECTING @@ -158,20 +170,21 @@ def test_recv_ignore_frmr(): (AX.25 2.2 sect 6.3.1) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Stub FRMR handling def _on_receive_frmr(): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._on_receive_frmr = _on_receive_frmr # Set the state @@ -179,13 +192,19 @@ def _on_receive_frmr(): # Inject a frame peer._on_receive( - AX25FrameRejectFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - w=False, x=False, y=False, z=False, - frmr_cr=False, vs=0, vr=0, frmr_control=0 - ) + AX25FrameRejectFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + w=False, + x=False, + y=False, + z=False, + frmr_cr=False, + vs=0, + vr=0, + frmr_control=0, + ) ) @@ -195,20 +214,21 @@ def test_recv_ignore_test(): (AX.25 2.2 sect 6.3.1) """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Stub TEST handling def _on_receive_test(): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._on_receive_test = _on_receive_test # Set the state @@ -216,12 +236,12 @@ def _on_receive_test(): # Inject a frame peer._on_receive( - AX25TestFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - payload=b'Frame to be ignored' - ) + AX25TestFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"Frame to be ignored", + ) ) @@ -229,21 +249,23 @@ def test_recv_ua(): """ Test that UA is handled. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Create a handler for receiving the UA count = dict(ua=0) + def _on_receive_ua(): - count['ua'] += 1 + count["ua"] += 1 + peer._uaframe_handler = _on_receive_ua # Set the state @@ -251,11 +273,11 @@ def _on_receive_ua(): # Inject a frame peer._on_receive( - AX25UnnumberedAcknowledgeFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') - ) + AX25UnnumberedAcknowledgeFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) ) # Our handler should have been called @@ -266,24 +288,28 @@ def test_recv_disc(): """ Test that DISC is handled. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Stub _send_ua and _on_disconnect count = dict(send_ua=0, on_disc=0) + def _send_ua(): - count['send_ua'] += 1 + count["send_ua"] += 1 + peer._send_ua = _send_ua + def _on_disconnect(): - count['on_disc'] += 1 + count["on_disc"] += 1 + peer._on_disconnect = _on_disconnect # Set the state @@ -291,11 +317,11 @@ def _on_disconnect(): # Inject a frame peer._on_receive( - AX25DisconnectFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') - ) + AX25DisconnectFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) ) # Our handlers should have been called @@ -306,24 +332,28 @@ def test_recv_dm(): """ Test that DM is handled. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Stub _dmframe_handler and _on_disconnect - count = dict(on_disc=0) + count = dict(dmframe_handler=0) + def _dmframe_handler(): - assert False, '_dmframe_handler should not have been called' + count["dmframe_handler"] += 1 + peer._dmframe_handler = _dmframe_handler + def _on_disconnect(): - count['on_disc'] += 1 + assert False, "_dmframe_handler should not have been called" + peer._on_disconnect = _on_disconnect # Set the state @@ -331,39 +361,41 @@ def _on_disconnect(): # Inject a frame peer._on_receive( - AX25DisconnectModeFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') - ) + AX25DisconnectModeFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) ) - # Our handlers should have been called - assert count == dict(on_disc=1) + # Our handler should have been called + assert count == dict(dmframe_handler=1) - # We should not have removed the DM frame handler - assert peer._dmframe_handler is not None + # We should have removed the DM frame handler + assert peer._dmframe_handler is None def test_recv_sabm(): """ Test that SABM is handled. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Stub _on_receive_sabm, we'll test it fully later frames = [] + def _on_receive_sabm(frame): frames.append(frame) + peer._on_receive_sabm = _on_receive_sabm # Set the state @@ -371,9 +403,9 @@ def _on_receive_sabm(frame): # Inject a frame frame = AX25SetAsyncBalancedModeFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) peer._on_receive(frame) @@ -384,21 +416,23 @@ def test_recv_sabme(): """ Test that SABME is handled. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub idle time-out handling - peer._reset_idle_timeout = lambda : None + peer._reset_idle_timeout = lambda: None # Stub _on_receive_sabm, we'll test it fully later frames = [] + def _on_receive_sabm(frame): frames.append(frame) + peer._on_receive_sabm = _on_receive_sabm # Set the state @@ -406,9 +440,9 @@ def _on_receive_sabm(frame): # Inject a frame frame = AX25SetAsyncBalancedModeExtendedFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) peer._on_receive(frame) @@ -422,37 +456,41 @@ def test_on_receive_sabm_init(): """ Test the incoming connection is initialised on receipt of SABM. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub _init_connection count = dict(init=0, start_timer=0, conn_rq=0) + def _init_connection(extended): assert extended is False - count['init'] += 1 + count["init"] += 1 + peer._init_connection = _init_connection # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): - count['start_timer'] += 1 + count["start_timer"] += 1 + peer._start_incoming_connect_timer = _start_incoming_connect_timer # Hook connection request event def _on_conn_rq(**kwargs): assert kwargs == dict(peer=peer) - count['conn_rq'] += 1 + count["conn_rq"] += 1 + station.connection_request.connect(_on_conn_rq) peer._on_receive_sabm( AX25SetAsyncBalancedModeFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) ) @@ -461,12 +499,12 @@ def test_on_receive_sabme_init(): """ Test the incoming connection is initialised on receipt of SABME. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Assume we know it's an AX.25 2.2 peer @@ -474,27 +512,31 @@ def test_on_receive_sabme_init(): # Stub _init_connection count = dict(init=0, start_timer=0, conn_rq=0) + def _init_connection(extended): assert extended is True - count['init'] += 1 + count["init"] += 1 + peer._init_connection = _init_connection # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): - count['start_timer'] += 1 + count["start_timer"] += 1 + peer._start_incoming_connect_timer = _start_incoming_connect_timer # Hook connection request event def _on_conn_rq(**kwargs): assert kwargs == dict(peer=peer) - count['conn_rq'] += 1 + count["conn_rq"] += 1 + station.connection_request.connect(_on_conn_rq) peer._on_receive_sabm( AX25SetAsyncBalancedModeExtendedFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) ) @@ -503,12 +545,12 @@ def test_on_receive_sabme_init_unknown_peer_ver(): """ Test we switch the peer to AX.25 2.2 mode on receipt of SABME """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Assume we do not know the peer's AX.25 version @@ -516,27 +558,31 @@ def test_on_receive_sabme_init_unknown_peer_ver(): # Stub _init_connection count = dict(init=0, start_timer=0, conn_rq=0) + def _init_connection(extended): assert extended is True - count['init'] += 1 + count["init"] += 1 + peer._init_connection = _init_connection # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): - count['start_timer'] += 1 + count["start_timer"] += 1 + peer._start_incoming_connect_timer = _start_incoming_connect_timer # Hook connection request event def _on_conn_rq(**kwargs): assert kwargs == dict(peer=peer) - count['conn_rq'] += 1 + count["conn_rq"] += 1 + station.connection_request.connect(_on_conn_rq) peer._on_receive_sabm( AX25SetAsyncBalancedModeExtendedFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) ) @@ -547,12 +593,12 @@ def test_on_receive_sabme_ax25_20_station(): """ Test we reject SABME if station is in AX.25 2.0 mode """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set AX.25 2.0 mode on the station @@ -560,29 +606,34 @@ def test_on_receive_sabme_ax25_20_station(): # Stub _send_frmr frmr = [] + def _send_frmr(frame, **kwargs): frmr.append((frame, kwargs)) + peer._send_frmr = _send_frmr # Stub _init_connection def _init_connection(extended): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._init_connection = _init_connection # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._start_incoming_connect_timer = _start_incoming_connect_timer # Hook connection request event def _on_conn_rq(**kwargs): - assert False, 'Should not have been called' + assert False, "Should not have been called" + station.connection_request.connect(_on_conn_rq) frame = AX25SetAsyncBalancedModeExtendedFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) peer._on_receive_sabm(frame) @@ -593,12 +644,12 @@ def test_on_receive_sabme_ax25_20_peer(): """ Test we reject SABME if peer not in AX.25 2.2 mode """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Assume the peer runs AX.25 2.0 @@ -606,30 +657,35 @@ def test_on_receive_sabme_ax25_20_peer(): # Stub _send_dm count = dict(send_dm=0) + def _send_dm(): - count['send_dm'] += 1 + count["send_dm"] += 1 + peer._send_dm = _send_dm # Stub _init_connection def _init_connection(extended): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._init_connection = _init_connection # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): - assert False, 'Should not have been called' + assert False, "Should not have been called" + peer._start_incoming_connect_timer = _start_incoming_connect_timer # Hook connection request event def _on_conn_rq(**kwargs): - assert False, 'Should not have been called' + assert False, "Should not have been called" + station.connection_request.connect(_on_conn_rq) peer._on_receive_sabm( AX25SetAsyncBalancedModeExtendedFrame( - destination=AX25Address('VK4MSL-1'), - source=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB') + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), ) ) From 9e8f3ee1ff7873b5ea118c3dbd4a7b5c1cddfdac Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 16:52:11 +1000 Subject: [PATCH 100/207] peer: Initialise connection on receipt of UA. If we receive a `UA` after sending `SABM(E)`, assume the connection is established and initialise the counters. --- aioax25/peer.py | 1 + tests/test_peer/test_peerconnection.py | 54 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 9e727d5..8efc9f4 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1377,6 +1377,7 @@ def _on_negotiated(self, response, **kwargs): def _on_receive_ua(self): # Peer just acknowledged our connection + self.peer._init_connection(self.peer._modulo128) self._finish(response="ack") def _on_receive_frmr(self): diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index 2853e24..1f9d7ab 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -270,10 +270,61 @@ def test_peerconn_receive_ua(): peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) + # Assume we're using modulo-8 mode + peer._modulo128 = False + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + + # Stub peer _init_connection + count = dict(init=0) + + def _init_connection(extended): + assert extended is False, "Should be in Modulo-8 mode" + count["init"] += 1 + + peer._init_connection = _init_connection + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) + + # Call _on_receive_ua + helper._on_receive_ua() + + # We should have initialised the connection + assert count == dict(init=1) + + # See that the helper finished + assert helper._done is True + assert done_evts == [{"response": "ack"}] + + +def test_peerconn_receive_ua_mod128(): + """ + Test _on_receive_ua handles Mod128 mode + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Assume we're using modulo-128 mode + peer._modulo128 = True + # Nothing should be set up assert helper._timeout_handle is None assert not helper._done + # Stub peer _init_connection + count = dict(init=0) + + def _init_connection(extended): + assert extended is True, "Should be in Modulo-128 mode" + count["init"] += 1 + + peer._init_connection = _init_connection + # Hook the done signal done_evts = [] helper.done_sig.connect(lambda **kw: done_evts.append(kw)) @@ -281,6 +332,9 @@ def test_peerconn_receive_ua(): # Call _on_receive_ua helper._on_receive_ua() + # We should have initialised the connection + assert count == dict(init=1) + # See that the helper finished assert helper._done is True assert done_evts == [{"response": "ack"}] From 0171af127c4f5d45294aa56ae12afc71a52ac405 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 17:19:39 +1000 Subject: [PATCH 101/207] peer: Treat incoming SABM(E) in CONNECTING state as UA. If we receive a `SABM(E)` while we are connecting to the target peer station, treat this as a `UA` and send a `UA` in response. This is not strictly in the AX.25 specifications, but thinking about the situation, seems the safest course of action to avoid getting into a "confused" state. --- aioax25/peer.py | 10 +++ tests/mocks.py | 1 + tests/test_peer/test_peerconnection.py | 98 +++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 8efc9f4..7cabaae 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1363,10 +1363,12 @@ def _on_negotiated(self, response, **kwargs): (self.peer._uaframe_handler is not None) or (self.peer._frmrframe_handler is not None) or (self.peer._dmframe_handler is not None) + or (self.peer._sabmframe_handler is not None) ): # We're handling another frame now. self._finish(response="station_busy") return + self.peer._sabmframe_handler = self._on_receive_sabm self.peer._uaframe_handler = self._on_receive_ua self.peer._frmrframe_handler = self._on_receive_frmr self.peer._dmframe_handler = self._on_receive_dm @@ -1380,6 +1382,11 @@ def _on_receive_ua(self): self.peer._init_connection(self.peer._modulo128) self._finish(response="ack") + def _on_receive_sabm(self): + # Peer was connecting to us, we'll treat this as a UA. + self.peer._send_ua() + self._finish(response="ack") + def _on_receive_frmr(self): # Peer just rejected our connect frame, begin FRMR recovery. self.peer._send_dm() @@ -1398,6 +1405,9 @@ def _on_timeout(self): def _finish(self, **kwargs): # Clean up hooks + if self.peer._sabmframe_handler == self._on_receive_sabm: + self.peer._sabmframe_handler = None + if self.peer._uaframe_handler == self._on_receive_ua: self.peer._uaframe_handler = None diff --git a/tests/mocks.py b/tests/mocks.py index e7595fc..3b893f9 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -122,6 +122,7 @@ def __init__(self, station, address): self._uaframe_handler = None self._frmrframe_handler = None self._dmframe_handler = None + self._sabmframe_handler = None self._xidframe_handler = None self._negotiated = False diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index 1f9d7ab..b11923e 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -159,7 +159,32 @@ def test_peerconn_on_negotiated_failed(): assert done_evts == [{"response": "whoopsie"}] -def test_peerconn_on_negotiated_xidframe_handler(): +def test_peerconn_on_negotiated_sabmframe_handler(): + """ + Test _on_negotiated refuses to run if another SABM frame handler is hooked. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + assert peer.transmit_calls == [] + + # Hook the SABM handler + peer._sabmframe_handler = lambda *a, **kwa: None + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) + + # Try to connect + helper._on_negotiated("xid") + assert done_evts == [{"response": "station_busy"}] + + +def test_peerconn_on_negotiated_uaframe_handler(): """ Test _on_negotiated refuses to run if another UA frame handler is hooked. """ @@ -340,6 +365,41 @@ def _init_connection(extended): assert done_evts == [{"response": "ack"}] +def test_peerconn_receive_sabm(): + """ + Test _on_receive_sabm ends the helper + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + + # Stub peer _send_ua + count = dict(send_ua=0) + + def _send_ua(): + count["send_ua"] += 1 + + peer._send_ua = _send_ua + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) + + # Call _on_receive_sabm + helper._on_receive_sabm() + + # We should have ACKed the SABM + assert count == dict(send_ua=1) + + # See that the helper finished + assert helper._done is True + assert done_evts == [{"response": "ack"}] + + def test_peerconn_receive_frmr(): """ Test _on_receive_frmr ends the helper @@ -423,6 +483,7 @@ def test_peerconn_on_timeout_first(): assert helper._retries == 1 # Helper should have hooked the handler events + assert peer._sabmframe_handler == helper._on_receive_sabm assert peer._uaframe_handler == helper._on_receive_ua assert peer._frmrframe_handler == helper._on_receive_frmr assert peer._dmframe_handler == helper._on_receive_dm @@ -453,6 +514,7 @@ def test_peerconn_on_timeout_last(): helper._retries = 0 # Pretend we're hooked up + peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = helper._on_receive_dm @@ -467,7 +529,8 @@ def test_peerconn_on_timeout_last(): # Check the time-out timer is not re-started assert helper._timeout_handle is None - # Helper should have hooked the handler events + # Helper should have unhooked the handler events + assert peer._sabmframe_handler is None assert peer._uaframe_handler is None assert peer._frmrframe_handler is None assert peer._dmframe_handler is None @@ -490,6 +553,7 @@ def test_peerconn_finish_disconnect_ua(): # Pretend we're hooked up dummy_uaframe_handler = lambda *a, **kw: None + peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = dummy_uaframe_handler peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = helper._on_receive_dm @@ -498,11 +562,37 @@ def test_peerconn_finish_disconnect_ua(): helper._finish() # All except UA (which is not ours) should be disconnected + assert peer._sabmframe_handler is None assert peer._uaframe_handler == dummy_uaframe_handler assert peer._frmrframe_handler is None assert peer._dmframe_handler is None +def test_peerconn_finish_disconnect_sabm(): + """ + Test _finish leaves other SABM hooks intact + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Pretend we're hooked up + dummy_sabmframe_handler = lambda *a, **kw: None + peer._sabmframe_handler = dummy_sabmframe_handler + peer._uaframe_handler = helper._on_receive_ua + peer._frmrframe_handler = helper._on_receive_frmr + peer._dmframe_handler = helper._on_receive_dm + + # Call the finish routine + helper._finish() + + # All except SABM (which is not ours) should be disconnected + assert peer._sabmframe_handler == dummy_sabmframe_handler + assert peer._uaframe_handler is None + assert peer._frmrframe_handler is None + assert peer._dmframe_handler is None + + def test_peerconn_finish_disconnect_frmr(): """ Test _finish leaves other FRMR hooks intact @@ -513,6 +603,7 @@ def test_peerconn_finish_disconnect_frmr(): # Pretend we're hooked up dummy_frmrframe_handler = lambda *a, **kw: None + peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = dummy_frmrframe_handler peer._dmframe_handler = helper._on_receive_dm @@ -521,6 +612,7 @@ def test_peerconn_finish_disconnect_frmr(): helper._finish() # All except FRMR (which is not ours) should be disconnected + assert peer._sabmframe_handler is None assert peer._uaframe_handler is None assert peer._frmrframe_handler == dummy_frmrframe_handler assert peer._dmframe_handler is None @@ -536,6 +628,7 @@ def test_peerconn_finish_disconnect_dm(): # Pretend we're hooked up dummy_dmframe_handler = lambda *a, **kw: None + peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = dummy_dmframe_handler @@ -544,6 +637,7 @@ def test_peerconn_finish_disconnect_dm(): helper._finish() # All except DM (which is not ours) should be disconnected + assert peer._sabmframe_handler is None assert peer._uaframe_handler is None assert peer._frmrframe_handler is None assert peer._dmframe_handler == dummy_dmframe_handler From 49fecb2acb769535ffc657602d8ef10648d020c2 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 18 Nov 2021 17:24:59 +1000 Subject: [PATCH 102/207] peer: Pass SABM(E) to handler if outgoing connection attempt in progress. --- aioax25/peer.py | 21 ++++++++++++++++----- tests/test_peer/test_connection.py | 30 ++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 7cabaae..265098a 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -205,6 +205,7 @@ def __init__( # Handling of various incoming frames self._testframe_handler = None self._xidframe_handler = None + self._sabmframe_handler = None self._uaframe_handler = None self._dmframe_handler = None self._frmrframe_handler = None @@ -634,11 +635,21 @@ def _on_receive_sabm(self, frame): # Set up the connection state self._init_connection(extended) - # Set the incoming connection state, and emit a signal via the - # station's 'connection_request' signal. - self._set_conn_state(self.AX25PeerState.INCOMING_CONNECTION) - self._start_incoming_connect_timer() - self._station().connection_request.emit(peer=self) + # Are we already connecting ourselves to this station? If yes, + # we should just treat their SABM(E) as a UA, since _clearly_ both + # parties wish to connect. + if self._state == self.AX25PeerState.CONNECTING: + self._log.info( + "Auto-accepting incoming connection as we are waiting for " + "UA from our connection attempt." + ) + self._sabmframe_handler() + else: + # Set the incoming connection state, and emit a signal via the + # station's 'connection_request' signal. + self._set_conn_state(self.AX25PeerState.INCOMING_CONNECTION) + self._start_incoming_connect_timer() + self._station().connection_request.emit(peer=self) def _start_incoming_connect_timer(self): self._incoming_connect_timeout_handle = self._loop.call_later( diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 5f8c26d..9fbafcf 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -452,9 +452,9 @@ def _on_receive_sabm(frame): # SABM(E) handling -def test_on_receive_sabm_init(): +def test_on_receive_sabm_while_connecting(): """ - Test the incoming connection is initialised on receipt of SABM. + Test that SABM is handled safely while UA from SABM pending """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -464,8 +464,11 @@ def test_on_receive_sabm_init(): locked_path=True, ) + # Assume we're already connecting to the station + peer._state = peer.AX25PeerState.CONNECTING + # Stub _init_connection - count = dict(init=0, start_timer=0, conn_rq=0) + count = dict(init=0, sabmframe_handler=0) def _init_connection(extended): assert extended is False @@ -473,16 +476,21 @@ def _init_connection(extended): peer._init_connection = _init_connection + # Stub _sabmframe_handler + def _sabmframe_handler(): + count["sabmframe_handler"] += 1 + + peer._sabmframe_handler = _sabmframe_handler + # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): - count["start_timer"] += 1 + assert False, "Should not be starting connect timer" peer._start_incoming_connect_timer = _start_incoming_connect_timer # Hook connection request event def _on_conn_rq(**kwargs): - assert kwargs == dict(peer=peer) - count["conn_rq"] += 1 + assert False, "Should not be reporting connection attempt" station.connection_request.connect(_on_conn_rq) @@ -494,6 +502,8 @@ def _on_conn_rq(**kwargs): ) ) + assert count == dict(init=1, sabmframe_handler=1) + def test_on_receive_sabme_init(): """ @@ -519,6 +529,12 @@ def _init_connection(extended): peer._init_connection = _init_connection + # Stub _sabmframe_handler + def _sabmframe_handler(): + assert False, "We should be handling the SABM(E) ourselves" + + peer._sabmframe_handler = _sabmframe_handler + # Stub _start_incoming_connect_timer def _start_incoming_connect_timer(): count["start_timer"] += 1 @@ -540,6 +556,8 @@ def _on_conn_rq(**kwargs): ) ) + assert count == dict(init=1, start_timer=1, conn_rq=1) + def test_on_receive_sabme_init_unknown_peer_ver(): """ From 11e9f1951991959bc3ae9a03f59b6291062c65cb Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 12:39:46 +1000 Subject: [PATCH 103/207] pyproject.toml: Enable coverage by default --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 547b0a1..9e1bdb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ line-length = 78 [tool.pytest.ini_options] log_cli=true +addopts=--cov=aioax25 --cov-report=html --cov-report=term [tool.setuptools.dynamic] version = {attr = "aioax25.__version__"} From dc3e89018472abee81f3518f55b91a1492472e21 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 13:04:57 +1000 Subject: [PATCH 104/207] peer unit tests: Test incoming connection acceptance and rejection --- tests/test_peer/test_connection.py | 133 +++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 9fbafcf..76c50a9 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -708,3 +708,136 @@ def _on_conn_rq(**kwargs): ) assert count == dict(send_dm=1) + + +# Connection acceptance and rejection handling + + +def test_accept_connected_noop(): + """ + Test calling .accept() while not receiving a connection is a no-op. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set the state to known value + peer._state = peer.AX25PeerState.CONNECTED + + # Stub functions that should not be called + def _stop_incoming_connect_timer(): + assert False, 'Should not have stopped connect timer' + peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + + def _send_ua(): + assert False, 'Should not have sent UA' + peer._send_ua = _send_ua + + # Try accepting a ficticious connection + peer.accept() + + assert peer._state == peer.AX25PeerState.CONNECTED + + +def test_accept_incoming_ua(): + """ + Test calling .accept() with incoming connection sends UA. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set the state to known value + peer._state = peer.AX25PeerState.INCOMING_CONNECTION + + # Stub functions that should be called + actions = [] + def _stop_incoming_connect_timer(): + actions.append('stop-connect-timer') + peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + + def _send_ua(): + actions.append('sent-ua') + peer._send_ua = _send_ua + + # Try accepting a ficticious connection + peer.accept() + + assert peer._state == peer.AX25PeerState.CONNECTED + assert actions == [ + 'stop-connect-timer', + 'sent-ua' + ] + + +def test_reject_connected_noop(): + """ + Test calling .reject() while not receiving a connection is a no-op. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set the state to known value + peer._state = peer.AX25PeerState.CONNECTED + + # Stub functions that should not be called + def _stop_incoming_connect_timer(): + assert False, 'Should not have stopped connect timer' + peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + + def _send_dm(): + assert False, 'Should not have sent DM' + peer._send_dm = _send_dm + + # Try rejecting a ficticious connection + peer.reject() + + assert peer._state == peer.AX25PeerState.CONNECTED + + +def test_reject_incoming_dm(): + """ + Test calling .reject() with no incoming connection is a no-op. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set the state to known value + peer._state = peer.AX25PeerState.INCOMING_CONNECTION + + # Stub functions that should be called + actions = [] + def _stop_incoming_connect_timer(): + actions.append('stop-connect-timer') + peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + + def _send_dm(): + actions.append('sent-dm') + peer._send_dm = _send_dm + + # Try rejecting a ficticious connection + peer.reject() + + assert peer._state == peer.AX25PeerState.DISCONNECTED + assert actions == [ + 'stop-connect-timer', + 'sent-dm' + ] From 50356e93802ebe13f766a55848ddf0c3df09f862 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 13:16:36 +1000 Subject: [PATCH 105/207] peer: Set DISCONNECTING state when sending DISC --- aioax25/peer.py | 1 + tests/test_peer/test_connection.py | 69 ++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 265098a..da69c49 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -319,6 +319,7 @@ def disconnect(self): """ if self._state is self.AX25PeerState.CONNECTED: self._uaframe_handler = self._on_disconnect + self._set_conn_state(self.AX25PeerState.DISCONNECTING) self._send_disc() def _cancel_idle_timeout(self): diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 76c50a9..21729b3 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -841,3 +841,72 @@ def _send_dm(): 'stop-connect-timer', 'sent-dm' ] + + +# Connection closure + + +def test_disconnect_disconnected_noop(): + """ + Test calling .disconnect() while not connected is a no-op. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set the state to known value + peer._state = peer.AX25PeerState.CONNECTING + + # A dummy UA handler + def _dummy_ua_handler(): + assert False, 'Should not get called' + peer._uaframe_handler = _dummy_ua_handler + + # Stub functions that should not be called + def _send_disc(): + assert False, 'Should not have sent DISC frame' + peer._send_disc = _send_disc + + # Try disconnecting a ficticious connection + peer.disconnect() + + assert peer._state == peer.AX25PeerState.CONNECTING + assert peer._uaframe_handler == _dummy_ua_handler + + +def test_disconnect_connected_disc(): + """ + Test calling .disconnect() while connected sends a DISC. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Set the state to known value + peer._state = peer.AX25PeerState.CONNECTED + + # A dummy UA handler + def _dummy_ua_handler(): + assert False, 'Should not get called' + peer._uaframe_handler = _dummy_ua_handler + + # Stub functions that should be called + actions = [] + def _send_disc(): + actions.append('sent-disc') + peer._send_disc = _send_disc + + # Try disconnecting a ficticious connection + peer.disconnect() + + assert peer._state == peer.AX25PeerState.DISCONNECTING + assert actions == ['sent-disc'] + assert peer._uaframe_handler == peer._on_disconnect From 8b19fa2edb94ac87740f9d8267f51d9f6688d627 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 13:23:34 +1000 Subject: [PATCH 106/207] peer: Rename incoming connect timeout to ACK timeout. We'll use this for handling `DISC` responses too. --- aioax25/peer.py | 27 ++--- pyproject.toml | 4 +- tests/test_peer/test_connection.py | 157 +++++++++++++++-------------- 3 files changed, 100 insertions(+), 88 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index da69c49..9afcde1 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -181,7 +181,7 @@ def __init__( self._SREJFrameClass = None # Timeouts - self._incoming_connect_timeout_handle = None + self._ack_timeout_handle = None self._idle_timeout_handle = None self._rr_notification_timeout_handle = None @@ -298,7 +298,7 @@ def accept(self): if self._state is self.AX25PeerState.INCOMING_CONNECTION: self._log.info("Accepting incoming connection") # Send a UA and set ourselves as connected - self._stop_incoming_connect_timer() + self._stop_ack_timer() self._set_conn_state(self.AX25PeerState.CONNECTED) self._send_ua() @@ -309,7 +309,7 @@ def reject(self): if self._state is self.AX25PeerState.INCOMING_CONNECTION: self._log.info("Rejecting incoming connection") # Send a DM and set ourselves as disconnected - self._stop_incoming_connect_timer() + self._stop_ack_timer() self._set_conn_state(self.AX25PeerState.DISCONNECTED) self._send_dm() @@ -649,23 +649,26 @@ def _on_receive_sabm(self, frame): # Set the incoming connection state, and emit a signal via the # station's 'connection_request' signal. self._set_conn_state(self.AX25PeerState.INCOMING_CONNECTION) - self._start_incoming_connect_timer() + self._start_connect_ack_timer() self._station().connection_request.emit(peer=self) - def _start_incoming_connect_timer(self): - self._incoming_connect_timeout_handle = self._loop.call_later( - self._ack_timeout, self._on_incoming_connect_timeout + def _start_connect_ack_timer(self): + self._start_ack_timer(self._on_incoming_connect_timeout) + + def _start_ack_timer(self, handler): + self._ack_timeout_handle = self._loop.call_later( + self._ack_timeout, handler ) - def _stop_incoming_connect_timer(self): - if self._incoming_connect_timeout_handle is not None: - self._incoming_connect_timeout_handle.cancel() + def _stop_ack_timer(self): + if self._ack_timeout_handle is not None: + self._ack_timeout_handle.cancel() - self._incoming_connect_timeout_handle = None + self._ack_timeout_handle = None def _on_incoming_connect_timeout(self): if self._state is self.AX25PeerState.INCOMING_CONNECTION: - self._incoming_connect_timeout_handle = None + self._ack_timeout_handle = None self.reject() def _on_connect_response(self, response, **kwargs): diff --git a/pyproject.toml b/pyproject.toml index 9e1bdb0..6256cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,8 @@ email = "me@vk4msl.id.au" line-length = 78 [tool.pytest.ini_options] -log_cli=true -addopts=--cov=aioax25 --cov-report=html --cov-report=term +log_cli = true +addopts = "--cov=aioax25 --cov-report=html --cov-report=term" [tool.setuptools.dynamic] version = {attr = "aioax25.__version__"} diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 21729b3..71e9bb9 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -482,11 +482,11 @@ def _sabmframe_handler(): peer._sabmframe_handler = _sabmframe_handler - # Stub _start_incoming_connect_timer - def _start_incoming_connect_timer(): + # Stub _start_connect_ack_timer + def _start_connect_ack_timer(): assert False, "Should not be starting connect timer" - peer._start_incoming_connect_timer = _start_incoming_connect_timer + peer._start_connect_ack_timer = _start_connect_ack_timer # Hook connection request event def _on_conn_rq(**kwargs): @@ -535,11 +535,11 @@ def _sabmframe_handler(): peer._sabmframe_handler = _sabmframe_handler - # Stub _start_incoming_connect_timer - def _start_incoming_connect_timer(): + # Stub _start_connect_ack_timer + def _start_connect_ack_timer(): count["start_timer"] += 1 - peer._start_incoming_connect_timer = _start_incoming_connect_timer + peer._start_connect_ack_timer = _start_connect_ack_timer # Hook connection request event def _on_conn_rq(**kwargs): @@ -583,11 +583,11 @@ def _init_connection(extended): peer._init_connection = _init_connection - # Stub _start_incoming_connect_timer - def _start_incoming_connect_timer(): + # Stub _start_connect_ack_timer + def _start_connect_ack_timer(): count["start_timer"] += 1 - peer._start_incoming_connect_timer = _start_incoming_connect_timer + peer._start_connect_ack_timer = _start_connect_ack_timer # Hook connection request event def _on_conn_rq(**kwargs): @@ -636,11 +636,11 @@ def _init_connection(extended): peer._init_connection = _init_connection - # Stub _start_incoming_connect_timer - def _start_incoming_connect_timer(): + # Stub _start_connect_ack_timer + def _start_connect_ack_timer(): assert False, "Should not have been called" - peer._start_incoming_connect_timer = _start_incoming_connect_timer + peer._start_connect_ack_timer = _start_connect_ack_timer # Hook connection request event def _on_conn_rq(**kwargs): @@ -687,11 +687,11 @@ def _init_connection(extended): peer._init_connection = _init_connection - # Stub _start_incoming_connect_timer - def _start_incoming_connect_timer(): + # Stub _start_connect_ack_timer + def _start_connect_ack_timer(): assert False, "Should not have been called" - peer._start_incoming_connect_timer = _start_incoming_connect_timer + peer._start_connect_ack_timer = _start_connect_ack_timer # Hook connection request event def _on_conn_rq(**kwargs): @@ -717,24 +717,26 @@ def test_accept_connected_noop(): """ Test calling .accept() while not receiving a connection is a no-op. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set the state to known value peer._state = peer.AX25PeerState.CONNECTED # Stub functions that should not be called - def _stop_incoming_connect_timer(): - assert False, 'Should not have stopped connect timer' - peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + def _stop_ack_timer(): + assert False, "Should not have stopped connect timer" + + peer._stop_ack_timer = _stop_ack_timer def _send_ua(): - assert False, 'Should not have sent UA' + assert False, "Should not have sent UA" + peer._send_ua = _send_ua # Try accepting a ficticious connection @@ -747,12 +749,12 @@ def test_accept_incoming_ua(): """ Test calling .accept() with incoming connection sends UA. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set the state to known value @@ -760,46 +762,48 @@ def test_accept_incoming_ua(): # Stub functions that should be called actions = [] - def _stop_incoming_connect_timer(): - actions.append('stop-connect-timer') - peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + + def _stop_ack_timer(): + actions.append("stop-connect-timer") + + peer._stop_ack_timer = _stop_ack_timer def _send_ua(): - actions.append('sent-ua') + actions.append("sent-ua") + peer._send_ua = _send_ua # Try accepting a ficticious connection peer.accept() assert peer._state == peer.AX25PeerState.CONNECTED - assert actions == [ - 'stop-connect-timer', - 'sent-ua' - ] + assert actions == ["stop-connect-timer", "sent-ua"] def test_reject_connected_noop(): """ Test calling .reject() while not receiving a connection is a no-op. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set the state to known value peer._state = peer.AX25PeerState.CONNECTED # Stub functions that should not be called - def _stop_incoming_connect_timer(): - assert False, 'Should not have stopped connect timer' - peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + def _stop_ack_timer(): + assert False, "Should not have stopped connect timer" + + peer._stop_ack_timer = _stop_ack_timer def _send_dm(): - assert False, 'Should not have sent DM' + assert False, "Should not have sent DM" + peer._send_dm = _send_dm # Try rejecting a ficticious connection @@ -812,12 +816,12 @@ def test_reject_incoming_dm(): """ Test calling .reject() with no incoming connection is a no-op. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set the state to known value @@ -825,22 +829,22 @@ def test_reject_incoming_dm(): # Stub functions that should be called actions = [] - def _stop_incoming_connect_timer(): - actions.append('stop-connect-timer') - peer._stop_incoming_connect_timer = _stop_incoming_connect_timer + + def _stop_ack_timer(): + actions.append("stop-connect-timer") + + peer._stop_ack_timer = _stop_ack_timer def _send_dm(): - actions.append('sent-dm') + actions.append("sent-dm") + peer._send_dm = _send_dm # Try rejecting a ficticious connection peer.reject() assert peer._state == peer.AX25PeerState.DISCONNECTED - assert actions == [ - 'stop-connect-timer', - 'sent-dm' - ] + assert actions == ["stop-connect-timer", "sent-dm"] # Connection closure @@ -850,12 +854,12 @@ def test_disconnect_disconnected_noop(): """ Test calling .disconnect() while not connected is a no-op. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set the state to known value @@ -863,12 +867,14 @@ def test_disconnect_disconnected_noop(): # A dummy UA handler def _dummy_ua_handler(): - assert False, 'Should not get called' + assert False, "Should not get called" + peer._uaframe_handler = _dummy_ua_handler # Stub functions that should not be called def _send_disc(): - assert False, 'Should not have sent DISC frame' + assert False, "Should not have sent DISC frame" + peer._send_disc = _send_disc # Try disconnecting a ficticious connection @@ -882,12 +888,12 @@ def test_disconnect_connected_disc(): """ Test calling .disconnect() while connected sends a DISC. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Set the state to known value @@ -895,18 +901,21 @@ def test_disconnect_connected_disc(): # A dummy UA handler def _dummy_ua_handler(): - assert False, 'Should not get called' + assert False, "Should not get called" + peer._uaframe_handler = _dummy_ua_handler # Stub functions that should be called actions = [] + def _send_disc(): - actions.append('sent-disc') + actions.append("sent-disc") + peer._send_disc = _send_disc # Try disconnecting a ficticious connection peer.disconnect() assert peer._state == peer.AX25PeerState.DISCONNECTING - assert actions == ['sent-disc'] + assert actions == ["sent-disc"] assert peer._uaframe_handler == peer._on_disconnect From 5efebd3f5503f9e900486dd081bb6cb98dc77e1b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 13:30:25 +1000 Subject: [PATCH 107/207] peer: Start timer after sending DISC. In case the station does not hear us, or we don't hear the `UA` in reply, if nothing is heard, clean up the connection anyway. --- aioax25/peer.py | 18 ++++++++++++++++-- tests/test_peer/test_connection.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 9afcde1..39f9651 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -321,6 +321,7 @@ def disconnect(self): self._uaframe_handler = self._on_disconnect self._set_conn_state(self.AX25PeerState.DISCONNECTING) self._send_disc() + self._start_disconnect_ack_timer() def _cancel_idle_timeout(self): """ @@ -655,6 +656,9 @@ def _on_receive_sabm(self, frame): def _start_connect_ack_timer(self): self._start_ack_timer(self._on_incoming_connect_timeout) + def _start_disconnect_ack_timer(self): + self._start_ack_timer(self._on_disc_ua_timeout) + def _start_ack_timer(self, handler): self._ack_timeout_handle = self._loop.call_later( self._ack_timeout, handler @@ -787,18 +791,28 @@ def _set_conn_state(self, state): station=self._station(), peer=self, state=self._state ) + def _on_disc_ua_timeout(self): + if self._state is self.AX25PeerState.DISCONNECTING: + # Assume we are disconnected. + self._ack_timeout_handle = None + self._on_disconnect() + def _on_disconnect(self): """ Clean up the connection. """ self._log.info("Disconnected from peer") - # Send a UA and set ourselves as disconnected + + # Cancel disconnect timers + self._stop_ack_timer() + + # Set ourselves as disconnected self._set_conn_state(self.AX25PeerState.DISCONNECTED) # Reset our state self._reset_connection_state() - # Data pending to be sent. + # Clear data pending to be sent. self._pending_data = [] def _on_receive_disc(self): diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 71e9bb9..49709f6 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -877,6 +877,11 @@ def _send_disc(): peer._send_disc = _send_disc + def _start_disconnect_ack_timer(): + assert False, "Should not have started disconnect timer" + + peer._start_disconnect_ack_timer = _start_disconnect_ack_timer + # Try disconnecting a ficticious connection peer.disconnect() @@ -913,9 +918,14 @@ def _send_disc(): peer._send_disc = _send_disc + def _start_disconnect_ack_timer(): + actions.append("start-ack-timer") + + peer._start_disconnect_ack_timer = _start_disconnect_ack_timer + # Try disconnecting a ficticious connection peer.disconnect() assert peer._state == peer.AX25PeerState.DISCONNECTING - assert actions == ["sent-disc"] + assert actions == ["sent-disc", "start-ack-timer"] assert peer._uaframe_handler == peer._on_disconnect From 7b5ed0569249d352a36743116bb5a5f44ce1f005 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 13:41:50 +1000 Subject: [PATCH 108/207] station: Drop _drop_peer, use weak dictionary instead Dropping a peer might have unexpected results if the application is still holding a reference to the peer that got dropped. While we could put guards against this, it'd be simpler to just use a weak reference. --- aioax25/peer.py | 2 -- aioax25/station.py | 8 +------- tests/test_station/test_droppeer.py | 22 ---------------------- 3 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 tests/test_station/test_droppeer.py diff --git a/aioax25/peer.py b/aioax25/peer.py index 39f9651..61add8d 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -348,8 +348,6 @@ def _cleanup(self): self._log.warning("Disconnecting peer due to inactivity") self._send_dm() - self._station()._drop_peer(self) - # Cancel other timers self._cancel_rr_notification() diff --git a/aioax25/station.py b/aioax25/station.py index 5040a34..3e77ad1 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -99,7 +99,7 @@ def __init__( self._loop = loop # Remote station handlers - self._peers = {} + self._peers = weakref.WeakValueDictionary() # Signal emitted when a SABM(E) is received self.connection_request = Signal() @@ -186,12 +186,6 @@ def getpeer( self._peers[address] = peer return peer - def _drop_peer(self, peer): - """ - Drop a peer. This is called by the peer when its idle timeout expires. - """ - self._peers.pop(peer.address, None) - def _on_receive(self, frame, **kwargs): """ Handling of incoming messages. diff --git a/tests/test_station/test_droppeer.py b/tests/test_station/test_droppeer.py deleted file mode 100644 index 1b01b1b..0000000 --- a/tests/test_station/test_droppeer.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -from aioax25.station import AX25Station -from aioax25.frame import AX25Address - -from ..mocks import DummyInterface, DummyPeer - - -def test_known_peer_fetch_instance(): - """ - Test calling _drop_peer removes the peer - """ - station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - mypeer = DummyPeer(station, AX25Address("VK4BWI")) - - # Inject the peer - station._peers[mypeer._address] = mypeer - - # Drop the peer - station._drop_peer(mypeer) - assert mypeer._address not in station._peers - assert mypeer.address_read From fee885ea1e3bdb5ccf10e4751a59d6d6331ca8ae Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 14:31:25 +1000 Subject: [PATCH 109/207] peer: Refine clean-up logic, add tests --- aioax25/peer.py | 10 +- tests/test_peer/test_cleanup.py | 250 ++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 tests/test_peer/test_cleanup.py diff --git a/aioax25/peer.py b/aioax25/peer.py index 61add8d..93b4b2e 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -344,9 +344,15 @@ def _cleanup(self): """ Clean up the instance of this peer as the activity has expired. """ - if self._state is not self.AX25PeerState.DISCONNECTED: + if self._state not in ( + self.AX25PeerState.DISCONNECTED, + self.AX25PeerState.DISCONNECTING, + ): self._log.warning("Disconnecting peer due to inactivity") - self._send_dm() + if self._state is self.AX25PeerState.CONNECTED: + self.disconnect() + else: + self._send_dm() # Cancel other timers self._cancel_rr_notification() diff --git a/tests/test_peer/test_cleanup.py b/tests/test_peer/test_cleanup.py new file mode 100644 index 0000000..258d805 --- /dev/null +++ b/tests/test_peer/test_cleanup.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 + +""" +Test handling of clean-up logic +""" + +from aioax25.frame import AX25Address, AX25Path +from .peer import TestingAX25Peer +from ..mocks import DummyStation, DummyTimeout + +# Idle time-out cancellation + + +def test_cancel_idle_timeout_inactive(): + """ + Test that calling _cancel_idle_timeout with no time-out is a no-op. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Constructor resets the timer, so discard that time-out handle + # This is safe because TestingAX25Peer does not use a real IOLoop + peer._idle_timeout_handle = None + + peer._cancel_idle_timeout() + + +def test_cancel_idle_timeout_active(): + """ + Test that calling _cancel_idle_timeout active time-out cancels it. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + timeout = DummyTimeout(0, lambda: None) + peer._idle_timeout_handle = timeout + + peer._cancel_idle_timeout() + + assert peer._idle_timeout_handle is None + assert timeout.cancelled is True + + +# Idle time-out reset + + +def test_reset_idle_timeout(): + """ + Test that calling _reset_idle_timeout re-creates a time-out object + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Grab the original time-out created by the constructor + orig_timeout = peer._idle_timeout_handle + assert orig_timeout is not None + + # Reset the time-out + peer._reset_idle_timeout() + + assert peer._idle_timeout_handle is not orig_timeout + assert orig_timeout.cancelled is True + + # New time-out should call the appropriate routine at the right time + assert peer._idle_timeout_handle.delay == peer._idle_timeout + assert peer._idle_timeout_handle.callback == peer._cleanup + + +# Clean-up steps + + +def test_cleanup_disconnected(): + """ + Test that clean-up whilst disconnect just cancels RR notifications + """ + # Most of the time, there will be no pending RR notifications, so + # _cancel_rr_notification will be a no-op in this case. + + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub methods + + actions = [] + + def _cancel_rr_notification(): + actions.append("cancel-rr") + + peer._cancel_rr_notification = _cancel_rr_notification + + def disconnect(): + assert False, "Should not call disconnect" + + peer.disconnect = disconnect + + def _send_dm(): + assert False, "Should not send DM" + + peer._send_dm = _send_dm + + # Set state + peer._state = peer.AX25PeerState.DISCONNECTED + + # Do clean-up + peer._cleanup() + + assert actions == ["cancel-rr"] + + +def test_cleanup_disconnecting(): + """ + Test that clean-up whilst disconnecting cancels RR notification + """ + # Most of the time, there will be no pending RR notifications, so + # _cancel_rr_notification will be a no-op in this case. + + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub methods + + actions = [] + + def _cancel_rr_notification(): + actions.append("cancel-rr") + + peer._cancel_rr_notification = _cancel_rr_notification + + def disconnect(): + assert False, "Should not call disconnect" + + peer.disconnect = disconnect + + def _send_dm(): + assert False, "Should not send DM" + + peer._send_dm = _send_dm + + # Set state + peer._state = peer.AX25PeerState.DISCONNECTING + + # Do clean-up + peer._cleanup() + + assert actions == ["cancel-rr"] + + +def test_cleanup_connecting(): + """ + Test that clean-up whilst connecting sends DM then cancels RR notifications + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub methods + + actions = [] + + def _cancel_rr_notification(): + actions.append("cancel-rr") + + peer._cancel_rr_notification = _cancel_rr_notification + + def disconnect(): + assert False, "Should not call disconnect" + + peer.disconnect = disconnect + + def _send_dm(): + actions.append("sent-dm") + + peer._send_dm = _send_dm + + # Set state + peer._state = peer.AX25PeerState.CONNECTING + + # Do clean-up + peer._cleanup() + + assert actions == ["sent-dm", "cancel-rr"] + + +def test_cleanup_connected(): + """ + Test that clean-up whilst connected sends DISC then cancels RR notifications + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub methods + + actions = [] + + def _cancel_rr_notification(): + actions.append("cancel-rr") + + peer._cancel_rr_notification = _cancel_rr_notification + + def disconnect(): + actions.append("disconnect") + + peer.disconnect = disconnect + + def _send_dm(): + assert False, "Should not send DM" + + peer._send_dm = _send_dm + + # Set state + peer._state = peer.AX25PeerState.CONNECTED + + # Do clean-up + peer._cleanup() + + assert actions == ["disconnect", "cancel-rr"] From 39a9b56d5e56a7c8b7d6c22beb4ecc86ac470244 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 14:47:42 +1000 Subject: [PATCH 110/207] peer unit tests: Test RX path stats gathering --- tests/test_peer/test_rx_path.py | 131 ++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/test_peer/test_rx_path.py diff --git a/tests/test_peer/test_rx_path.py b/tests/test_peer/test_rx_path.py new file mode 100644 index 0000000..0c2a35c --- /dev/null +++ b/tests/test_peer/test_rx_path.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +""" +Tests for receive path handling +""" + +from aioax25.frame import AX25Address, AX25TestFrame, AX25Path +from ..mocks import DummyStation +from .peer import TestingAX25Peer + + +def test_rx_path_stats_unlocked(): + """ + Test that incoming message paths are counted when path NOT locked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=False + ) + + # Stub the peer's _on_receive_test method + rx_frames = [] + def _on_receive_test(frame): + rx_frames.append(frame) + peer._on_receive_test = _on_receive_test + + # Send a few test frames via different paths + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 1', + cr=True + )) + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZA*', 'VK4RZB*'), + payload=b'test 2', + cr=True + )) + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZD*', 'VK4RZB*'), + payload=b'test 3', + cr=True + )) + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 4', + cr=True + )) + + # For test readability, convert the tuple keys to strings + # AX25Path et all has its own tests for str. + rx_path_count = dict([ + (str(AX25Path(*path)), count) + for path, count + in peer._rx_path_count.items() + ]) + + assert rx_path_count == { + 'VK4RZB': 2, + 'VK4RZA,VK4RZB': 1, + 'VK4RZD,VK4RZB': 1 + } + + +def test_rx_path_stats_locked(): + """ + Test that incoming message paths are NOT counted when path locked. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + # Stub the peer's _on_receive_test method + rx_frames = [] + def _on_receive_test(frame): + rx_frames.append(frame) + peer._on_receive_test = _on_receive_test + + # Send a few test frames via different paths + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 1', + cr=True + )) + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZA*', 'VK4RZB*'), + payload=b'test 2', + cr=True + )) + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZD*', 'VK4RZB*'), + payload=b'test 3', + cr=True + )) + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 4', + cr=True + )) + + # For test readability, convert the tuple keys to strings + # AX25Path et all has its own tests for str. + rx_path_count = dict([ + (str(AX25Path(*path)), count) + for path, count + in peer._rx_path_count.items() + ]) + + assert rx_path_count == {} From eb6e0ee9729323c66fe3559696db1cdd70308a36 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 14:58:43 +1000 Subject: [PATCH 111/207] peer unit tests: Tweak test suite name --- tests/test_peer/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 49709f6..35d3080 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Test handling of outgoing connection logic +Test handling of incoming and outgoing connection logic """ from aioax25.version import AX25Version From 0c6e8f85f3f48e4d6284b2fd4ffeaac2ce7a2fe7 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 15:08:58 +1000 Subject: [PATCH 112/207] peer: Pass weak reference to test frame handler. --- aioax25/peer.py | 2 +- tests/test_peer/test_peertest.py | 5 +- tests/test_peer/test_rx_path.py | 180 +++++++++++++++++-------------- 3 files changed, 106 insertions(+), 81 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 93b4b2e..4537a77 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1581,7 +1581,7 @@ def _go(self): """ if self.peer._testframe_handler is not None: raise RuntimeError("Test frame already pending") - self.peer._testframe_handler = self + self.peer._testframe_handler = weakref.ref(self) self.peer._transmit_frame(self.tx_frame, callback=self._transmit_done) self._start_timer() diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index 653736a..d82f5fe 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -5,6 +5,7 @@ """ from pytest import approx +import weakref from aioax25.peer import AX25PeerTestHandler from aioax25.frame import AX25Address, AX25TestFrame, AX25Path @@ -42,7 +43,9 @@ def test_peertest_go(): assert callback == helper._transmit_done # We should be registered to receive the reply - assert peer._testframe_handler is helper + assert peer._testframe_handler is not None + assert isinstance(peer._testframe_handler, weakref.ref) + assert peer._testframe_handler() is helper def test_peertest_go_pending(): diff --git a/tests/test_peer/test_rx_path.py b/tests/test_peer/test_rx_path.py index 0c2a35c..f3d9b0f 100644 --- a/tests/test_peer/test_rx_path.py +++ b/tests/test_peer/test_rx_path.py @@ -13,62 +13,73 @@ def test_rx_path_stats_unlocked(): """ Test that incoming message paths are counted when path NOT locked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=False + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=False, ) # Stub the peer's _on_receive_test method rx_frames = [] + def _on_receive_test(frame): rx_frames.append(frame) + peer._on_receive_test = _on_receive_test # Send a few test frames via different paths - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 1', - cr=True - )) - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZA*', 'VK4RZB*'), - payload=b'test 2', - cr=True - )) - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZD*', 'VK4RZB*'), - payload=b'test 3', - cr=True - )) - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 4', - cr=True - )) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 1", + cr=True, + ) + ) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZA*", "VK4RZB*"), + payload=b"test 2", + cr=True, + ) + ) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZD*", "VK4RZB*"), + payload=b"test 3", + cr=True, + ) + ) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 4", + cr=True, + ) + ) # For test readability, convert the tuple keys to strings # AX25Path et all has its own tests for str. - rx_path_count = dict([ - (str(AX25Path(*path)), count) - for path, count - in peer._rx_path_count.items() - ]) + rx_path_count = dict( + [ + (str(AX25Path(*path)), count) + for path, count in peer._rx_path_count.items() + ] + ) assert rx_path_count == { - 'VK4RZB': 2, - 'VK4RZA,VK4RZB': 1, - 'VK4RZD,VK4RZB': 1 + "VK4RZB": 2, + "VK4RZA,VK4RZB": 1, + "VK4RZD,VK4RZB": 1, } @@ -76,56 +87,67 @@ def test_rx_path_stats_locked(): """ Test that incoming message paths are NOT counted when path locked. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) # Stub the peer's _on_receive_test method rx_frames = [] + def _on_receive_test(frame): rx_frames.append(frame) + peer._on_receive_test = _on_receive_test # Send a few test frames via different paths - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 1', - cr=True - )) - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZA*', 'VK4RZB*'), - payload=b'test 2', - cr=True - )) - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZD*', 'VK4RZB*'), - payload=b'test 3', - cr=True - )) - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 4', - cr=True - )) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 1", + cr=True, + ) + ) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZA*", "VK4RZB*"), + payload=b"test 2", + cr=True, + ) + ) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZD*", "VK4RZB*"), + payload=b"test 3", + cr=True, + ) + ) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 4", + cr=True, + ) + ) # For test readability, convert the tuple keys to strings # AX25Path et all has its own tests for str. - rx_path_count = dict([ - (str(AX25Path(*path)), count) - for path, count - in peer._rx_path_count.items() - ]) + rx_path_count = dict( + [ + (str(AX25Path(*path)), count) + for path, count in peer._rx_path_count.items() + ] + ) assert rx_path_count == {} From bc3469054c6a02f5d776a7ed84a93ebdc7eb903f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 15:24:33 +1000 Subject: [PATCH 113/207] peer: Fix handling of TEST frame responses, add unit tests. --- aioax25/peer.py | 7 +- tests/test_peer/test_peertest.py | 199 +++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 4537a77..8c98093 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -584,15 +584,18 @@ def _on_receive_test(self, frame): handler = self._testframe_handler() if not handler: + # It's dead now. + self._testframe_handler = None return - self.handler._on_receive(frame) + handler._on_receive(frame) def _on_test_done(self, handler, **kwargs): if not self._testframe_handler: return - if handler is not self._testframe_handler(): + real_handler = self._testframe_handler() + if (real_handler is not None) and (handler is not real_handler): return self._testframe_handler = None diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index d82f5fe..4aa3f60 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -267,3 +267,202 @@ def _callback(*args, **kwargs): # Our callback should have been called on completion assert cb_args == [((), {"handler": handler})] + + +# Handling of incoming TEST frame +# +# The AX25Station class itself will respond to TEST frames +# where the CR bit is set to True. AX25Peer only handles +# the case where CR is set to False. + +def test_on_receive_test_no_handler(): + """ + Test that a TEST frame with no handler does nothing. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._testframe_handler = None + + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 1', + cr=False + )) + + +def test_on_receive_test_stale_handler(): + """ + Test that a TEST frame with stale handler cleans up reference. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + class DummyHandler(): + pass + + handler = DummyHandler() + peer._testframe_handler = weakref.ref(handler) + + # Now destroy the new handler + del handler + assert peer._testframe_handler is not None + assert peer._testframe_handler() is None + + # See what it does + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 1', + cr=False + )) + + assert peer._testframe_handler is None + + +def test_on_receive_test_valid_handler(): + """ + Test that a TEST frame with valid handler pass on frame. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + class DummyHandler(): + def __init__(self): + self.frames = [] + + def _on_receive(self, frame): + self.frames.append(frame) + + + handler = DummyHandler() + peer._testframe_handler = weakref.ref(handler) + + # See what it does + frame = AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 1', + cr=False + ) + peer._on_receive(frame) + + assert handler.frames == [frame] + + +def test_on_test_done_no_handler(): + """ + Test that a TEST frame with no handler does nothing. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._testframe_handler = None + + class DummyHandler(): + pass + + peer._on_test_done( + handler=DummyHandler() + ) + + +def test_on_test_done_stale_handler(): + """ + Test that a TEST frame with stale handler cleans up reference. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + class DummyHandler(): + pass + + handler = DummyHandler() + peer._testframe_handler = weakref.ref(handler) + + # Now destroy the new handler + del handler + assert peer._testframe_handler is not None + assert peer._testframe_handler() is None + + # See what it does + peer._on_test_done(handler=DummyHandler()) + + assert peer._testframe_handler is None + + +def test_on_test_done_wrong_handler(): + """ + Test that a TEST frame with wrong handler ignores signal. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + class DummyHandler(): + pass + + handler = DummyHandler() + peer._testframe_handler = weakref.ref(handler) + + # See what it does with the wrong handler reference + peer._on_test_done(handler=DummyHandler()) + + assert peer._testframe_handler() is handler + + +def test_on_test_done_valid_handler(): + """ + Test that a TEST frame with valid handler pass on frame. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + class DummyHandler(): + pass + + handler = DummyHandler() + peer._testframe_handler = weakref.ref(handler) + + # See what it does + peer._on_test_done(handler=handler) + + assert peer._testframe_handler is None From e3c144bbb0811ffdcf1e04dfdbfe132037e532da Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 15:52:35 +1000 Subject: [PATCH 114/207] peer: Tweak `FRMR` handling Sending `SABM` is probably a better option than sending `DM`. (I suspect I was supposed to send `DISC` not `SABM`.) --- aioax25/peer.py | 7 +- tests/test_peer/test_frmr.py | 194 +++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 tests/test_peer/test_frmr.py diff --git a/aioax25/peer.py b/aioax25/peer.py index 8c98093..3d50642 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -408,7 +408,7 @@ def _on_receive(self, frame): return self._on_receive_test(frame) elif isinstance(frame, AX25FrameRejectFrame): # FRMR frame - return self._on_receive_frmr() + return self._on_receive_frmr(frame) elif isinstance(frame, AX25UnnumberedAcknowledgeFrame): # UA frame return self._on_receive_ua() @@ -607,8 +607,9 @@ def _on_receive_frmr(self, frame): # Pass to handler self._frmrframe_handler(frame) else: - # No handler, send DM to recover - self._send_dm() + # No handler, send SABM to recover (this is clearly an AX.25 2.0 + # station) + self._send_sabm() def _on_receive_sabm(self, frame): extended = isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) diff --git a/tests/test_peer/test_frmr.py b/tests/test_peer/test_frmr.py new file mode 100644 index 0000000..b33f522 --- /dev/null +++ b/tests/test_peer/test_frmr.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +""" +Tests for FRMR handling +""" + +from pytest import approx +import weakref + +from aioax25.frame import AX25Address, AX25Path, \ + AX25FrameRejectFrame, AX25SetAsyncBalancedModeFrame, \ + AX25DisconnectFrame, AX25UnnumberedAcknowledgeFrame, \ + AX25TestFrame +from ..mocks import DummyPeer, DummyStation +from .peer import TestingAX25Peer + + +def test_on_receive_frmr_no_handler(): + """ + Test that a FRMR frame with no handler sends SABM. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._frmrframe_handler = None + + actions = [] + def _send_sabm(): + actions.append('sent-sabm') + peer._send_sabm = _send_sabm + + peer._on_receive(AX25FrameRejectFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + w=False, x=False, y=False, z=False, + vr=0, frmr_cr=False, vs=0, + frmr_control=0 + )) + + assert actions == ['sent-sabm'] + + +def test_on_receive_frmr_with_handler(): + """ + Test that a FRMR frame passes to given FRMR handler. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + frames = [] + def _frmr_handler(frame): + frames.append(frame) + peer._frmrframe_handler = _frmr_handler + + def _send_dm(): + assert False, 'Should not send DM' + peer._send_dm = _send_dm + + frame = AX25FrameRejectFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + w=False, x=False, y=False, z=False, + vr=0, frmr_cr=False, vs=0, + frmr_control=0 + ) + peer._on_receive(frame) + + assert frames == [frame] + + +# Test handling whilst in FRMR handling mode + +def test_on_receive_in_frmr_drop_test(): + """ + Test _on_receive drops TEST frames when in FRMR state. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._state = peer.AX25PeerState.FRMR + + def _on_receive_test(*a, **kwa): + assert False, 'Should have ignored frame' + peer._on_receive_test = _on_receive_test + + peer._on_receive(AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + payload=b'test 1', + cr=False + )) + + +def test_on_receive_in_frmr_drop_ua(): + """ + Test _on_receive drops UA frames when in FRMR state. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._state = peer.AX25PeerState.FRMR + + def _on_receive_ua(*a, **kwa): + assert False, 'Should have ignored frame' + peer._on_receive_ua = _on_receive_ua + + peer._on_receive(AX25UnnumberedAcknowledgeFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + cr=False + )) + + +def test_on_receive_in_frmr_pass_sabm(): + """ + Test _on_receive passes SABM frames when in FRMR state. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._state = peer.AX25PeerState.FRMR + + frames = [] + def _on_receive_sabm(frame): + frames.append(frame) + peer._on_receive_sabm = _on_receive_sabm + + frame = AX25SetAsyncBalancedModeFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + cr=False + ) + peer._on_receive(frame) + + assert frames == [frame] + + +def test_on_receive_in_frmr_pass_disc(): + """ + Test _on_receive passes DISC frames when in FRMR state. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path('VK4RZB'), + locked_path=True + ) + + peer._state = peer.AX25PeerState.FRMR + + events = [] + def _on_receive_disc(): + events.append('disc') + peer._on_receive_disc = _on_receive_disc + + peer._on_receive(AX25DisconnectFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path('VK4RZB*'), + cr=False + )) + + assert events == ['disc'] From a792e79dbdaf7c2ff50d2d408342f91d570ee9a1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 15:55:30 +1000 Subject: [PATCH 115/207] peer unit tests: Pass XID tests through _on_receive --- tests/test_peer/test_frmr.py | 191 ++++++++++++++++++------------- tests/test_peer/test_peertest.py | 130 ++++++++++----------- tests/test_peer/test_xid.py | 40 +++---- 3 files changed, 199 insertions(+), 162 deletions(-) diff --git a/tests/test_peer/test_frmr.py b/tests/test_peer/test_frmr.py index b33f522..fe0caa7 100644 --- a/tests/test_peer/test_frmr.py +++ b/tests/test_peer/test_frmr.py @@ -7,10 +7,15 @@ from pytest import approx import weakref -from aioax25.frame import AX25Address, AX25Path, \ - AX25FrameRejectFrame, AX25SetAsyncBalancedModeFrame, \ - AX25DisconnectFrame, AX25UnnumberedAcknowledgeFrame, \ - AX25TestFrame +from aioax25.frame import ( + AX25Address, + AX25Path, + AX25FrameRejectFrame, + AX25SetAsyncBalancedModeFrame, + AX25DisconnectFrame, + AX25UnnumberedAcknowledgeFrame, + AX25TestFrame, +) from ..mocks import DummyPeer, DummyStation from .peer import TestingAX25Peer @@ -19,61 +24,78 @@ def test_on_receive_frmr_no_handler(): """ Test that a FRMR frame with no handler sends SABM. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._frmrframe_handler = None actions = [] + def _send_sabm(): - actions.append('sent-sabm') + actions.append("sent-sabm") + peer._send_sabm = _send_sabm - peer._on_receive(AX25FrameRejectFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - w=False, x=False, y=False, z=False, - vr=0, frmr_cr=False, vs=0, - frmr_control=0 - )) + peer._on_receive( + AX25FrameRejectFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + w=False, + x=False, + y=False, + z=False, + vr=0, + frmr_cr=False, + vs=0, + frmr_control=0, + ) + ) - assert actions == ['sent-sabm'] + assert actions == ["sent-sabm"] def test_on_receive_frmr_with_handler(): """ Test that a FRMR frame passes to given FRMR handler. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) frames = [] + def _frmr_handler(frame): frames.append(frame) + peer._frmrframe_handler = _frmr_handler def _send_dm(): - assert False, 'Should not send DM' + assert False, "Should not send DM" + peer._send_dm = _send_dm frame = AX25FrameRejectFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - w=False, x=False, y=False, z=False, - vr=0, frmr_cr=False, vs=0, - frmr_control=0 + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + w=False, + x=False, + y=False, + z=False, + vr=0, + frmr_cr=False, + vs=0, + frmr_control=0, ) peer._on_receive(frame) @@ -82,83 +104,92 @@ def _send_dm(): # Test handling whilst in FRMR handling mode + def test_on_receive_in_frmr_drop_test(): """ Test _on_receive drops TEST frames when in FRMR state. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._state = peer.AX25PeerState.FRMR def _on_receive_test(*a, **kwa): - assert False, 'Should have ignored frame' + assert False, "Should have ignored frame" + peer._on_receive_test = _on_receive_test - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 1', - cr=False - )) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 1", + cr=False, + ) + ) def test_on_receive_in_frmr_drop_ua(): """ Test _on_receive drops UA frames when in FRMR state. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._state = peer.AX25PeerState.FRMR def _on_receive_ua(*a, **kwa): - assert False, 'Should have ignored frame' + assert False, "Should have ignored frame" + peer._on_receive_ua = _on_receive_ua - peer._on_receive(AX25UnnumberedAcknowledgeFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - cr=False - )) + peer._on_receive( + AX25UnnumberedAcknowledgeFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + cr=False, + ) + ) def test_on_receive_in_frmr_pass_sabm(): """ Test _on_receive passes SABM frames when in FRMR state. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._state = peer.AX25PeerState.FRMR frames = [] + def _on_receive_sabm(frame): frames.append(frame) + peer._on_receive_sabm = _on_receive_sabm frame = AX25SetAsyncBalancedModeFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - cr=False + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + cr=False, ) peer._on_receive(frame) @@ -169,26 +200,30 @@ def test_on_receive_in_frmr_pass_disc(): """ Test _on_receive passes DISC frames when in FRMR state. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._state = peer.AX25PeerState.FRMR events = [] + def _on_receive_disc(): - events.append('disc') + events.append("disc") + peer._on_receive_disc = _on_receive_disc - peer._on_receive(AX25DisconnectFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - cr=False - )) + peer._on_receive( + AX25DisconnectFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + cr=False, + ) + ) - assert events == ['disc'] + assert events == ["disc"] diff --git a/tests/test_peer/test_peertest.py b/tests/test_peer/test_peertest.py index 4aa3f60..b603954 100644 --- a/tests/test_peer/test_peertest.py +++ b/tests/test_peer/test_peertest.py @@ -275,42 +275,45 @@ def _callback(*args, **kwargs): # where the CR bit is set to True. AX25Peer only handles # the case where CR is set to False. + def test_on_receive_test_no_handler(): """ Test that a TEST frame with no handler does nothing. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._testframe_handler = None - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 1', - cr=False - )) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 1", + cr=False, + ) + ) def test_on_receive_test_stale_handler(): """ Test that a TEST frame with stale handler cleans up reference. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) - class DummyHandler(): + class DummyHandler: pass handler = DummyHandler() @@ -322,13 +325,15 @@ class DummyHandler(): assert peer._testframe_handler() is None # See what it does - peer._on_receive(AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 1', - cr=False - )) + peer._on_receive( + AX25TestFrame( + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 1", + cr=False, + ) + ) assert peer._testframe_handler is None @@ -337,32 +342,31 @@ def test_on_receive_test_valid_handler(): """ Test that a TEST frame with valid handler pass on frame. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) - class DummyHandler(): + class DummyHandler: def __init__(self): self.frames = [] def _on_receive(self, frame): self.frames.append(frame) - handler = DummyHandler() peer._testframe_handler = weakref.ref(handler) # See what it does frame = AX25TestFrame( - destination=peer.address, - source=station.address, - repeaters=AX25Path('VK4RZB*'), - payload=b'test 1', - cr=False + destination=peer.address, + source=station.address, + repeaters=AX25Path("VK4RZB*"), + payload=b"test 1", + cr=False, ) peer._on_receive(frame) @@ -373,37 +377,35 @@ def test_on_test_done_no_handler(): """ Test that a TEST frame with no handler does nothing. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) peer._testframe_handler = None - class DummyHandler(): + class DummyHandler: pass - peer._on_test_done( - handler=DummyHandler() - ) + peer._on_test_done(handler=DummyHandler()) def test_on_test_done_stale_handler(): """ Test that a TEST frame with stale handler cleans up reference. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) - class DummyHandler(): + class DummyHandler: pass handler = DummyHandler() @@ -424,15 +426,15 @@ def test_on_test_done_wrong_handler(): """ Test that a TEST frame with wrong handler ignores signal. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) - class DummyHandler(): + class DummyHandler: pass handler = DummyHandler() @@ -448,15 +450,15 @@ def test_on_test_done_valid_handler(): """ Test that a TEST frame with valid handler pass on frame. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path('VK4RZB'), - locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, ) - class DummyHandler(): + class DummyHandler: pass handler = DummyHandler() diff --git a/tests/test_peer/test_xid.py b/tests/test_peer/test_xid.py index 05bf76d..b550b0f 100644 --- a/tests/test_peer/test_xid.py +++ b/tests/test_peer/test_xid.py @@ -734,7 +734,7 @@ def test_peer_process_xid_retrycounter_default(): def test_peer_on_receive_xid_ax20_mode(): """ - Test _on_receive_xid responds with FRMR when in AX.25 2.0 mode. + Test _on_receive responds with FRMR when in AX.25 2.0 mode. """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) station._protocol = AX25Version.AX25_20 @@ -747,7 +747,7 @@ def test_peer_on_receive_xid_ax20_mode(): assert interface.transmit_calls == [] # Pass in the XID frame to our AX.25 2.0 station. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -775,7 +775,7 @@ def test_peer_on_receive_xid_ax20_mode(): def test_peer_on_receive_xid_connecting(): """ - Test _on_receive_xid ignores XID when connecting. + Test _on_receive ignores XID when connecting. """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() @@ -790,7 +790,7 @@ def test_peer_on_receive_xid_connecting(): peer._state = TestingAX25Peer.AX25PeerState.CONNECTING # Pass in the XID frame to our AX.25 2.2 station. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -806,7 +806,7 @@ def test_peer_on_receive_xid_connecting(): def test_peer_on_receive_xid_disconnecting(): """ - Test _on_receive_xid ignores XID when disconnecting. + Test _on_receive ignores XID when disconnecting. """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() @@ -821,7 +821,7 @@ def test_peer_on_receive_xid_disconnecting(): peer._state = TestingAX25Peer.AX25PeerState.DISCONNECTING # Pass in the XID frame to our AX.25 2.2 station. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -837,7 +837,7 @@ def test_peer_on_receive_xid_disconnecting(): def test_peer_on_receive_xid_sets_proto_version(): """ - Test _on_receive_xid sets protocol version if unknown. + Test _on_receive sets protocol version if unknown. """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -848,7 +848,7 @@ def test_peer_on_receive_xid_sets_proto_version(): assert peer._protocol == AX25Version.UNKNOWN # Pass in the XID frame to our AX.25 2.2 station. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -863,7 +863,7 @@ def test_peer_on_receive_xid_sets_proto_version(): def test_peer_on_receive_xid_keeps_known_proto_version(): """ - Test _on_receive_xid keeps existing protocol version if known. + Test _on_receive keeps existing protocol version if known. """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -874,7 +874,7 @@ def test_peer_on_receive_xid_keeps_known_proto_version(): ) # Pass in the XID frame to our AX.25 2.2 station. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -889,7 +889,7 @@ def test_peer_on_receive_xid_keeps_known_proto_version(): def test_peer_on_receive_xid_ignores_bad_fi(): """ - Test _on_receive_xid ignores parameters if FI is unknown + Test _on_receive ignores parameters if FI is unknown """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -904,7 +904,7 @@ def _stub_process_cop(param): # Pass in the XID frame to our AX.25 2.2 station. # There should be no assertion triggered. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -917,7 +917,7 @@ def _stub_process_cop(param): def test_peer_on_receive_xid_ignores_bad_gi(): """ - Test _on_receive_xid ignores parameters if GI is unknown + Test _on_receive ignores parameters if GI is unknown """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -932,7 +932,7 @@ def _stub_process_cop(param): # Pass in the XID frame to our AX.25 2.2 station. # There should be no assertion triggered. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -945,7 +945,7 @@ def _stub_process_cop(param): def test_peer_on_receive_xid_processes_parameters(): """ - Test _on_receive_xid processes parameters on good XID frames + Test _on_receive processes parameters on good XID frames """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -954,7 +954,7 @@ def test_peer_on_receive_xid_processes_parameters(): # Pass in the XID frame to our AX.25 2.2 station. # There should be no assertion triggered. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -969,7 +969,7 @@ def test_peer_on_receive_xid_processes_parameters(): def test_peer_on_receive_xid_reply(): """ - Test _on_receive_xid sends reply if incoming frame has CR=True + Test _on_receive sends reply if incoming frame has CR=True """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() @@ -981,7 +981,7 @@ def test_peer_on_receive_xid_reply(): assert interface.transmit_calls == [] # Pass in the XID frame to our AX.25 2.2 station. - peer._on_receive_xid( + peer._on_receive( AX25ExchangeIdentificationFrame( destination=station.address, source=peer.address, @@ -1046,7 +1046,7 @@ def test_peer_on_receive_xid_reply(): def test_peer_on_receive_xid_relay(): """ - Test _on_receive_xid sends relays to XID handler if CR=False + Test _on_receive sends relays to XID handler if CR=False """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() @@ -1069,7 +1069,7 @@ def test_peer_on_receive_xid_relay(): parameters=[], cr=False, ) - peer._on_receive_xid(frame) + peer._on_receive(frame) # There should have been a XID event assert len(xid_events) == 1 From ccf8d16041ca6a084edd2b9b517fb021d5e15afa Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 27 Nov 2021 15:59:23 +1000 Subject: [PATCH 116/207] Revert "pyproject.toml: Enable coverage by default" This reverts commit eeab73c82683c8479057e133ae07385f2eb48395. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6256cca..7cf391e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ line-length = 78 [tool.pytest.ini_options] log_cli = true -addopts = "--cov=aioax25 --cov-report=html --cov-report=term" [tool.setuptools.dynamic] version = {attr = "aioax25.__version__"} From 511e10ac747e881d8e0739638ebcca27363d4467 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 19:32:00 +1000 Subject: [PATCH 117/207] issue #17: frame: Fix SABM and SABME bit flip. --- aioax25/frame.py | 4 ++-- tests/test_frame/test_uframe.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index ad0711c..25ed3cc 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1209,7 +1209,7 @@ class AX25SetAsyncBalancedModeFrame(AX25BaseUnnumberedFrame): AX.25 node. """ - MODIFIER = 0b01101111 + MODIFIER = 0b00101111 CR = True @@ -1224,7 +1224,7 @@ class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): AX.25 node, using modulo 128 acknowledgements. """ - MODIFIER = 0b00101111 + MODIFIER = 0b01101111 CR = True diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index 6b2efd0..bc01921 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -41,7 +41,7 @@ def test_decode_sabm(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "6f" # Control byte + "2f" # Control byte ) ) assert isinstance( @@ -58,7 +58,7 @@ def test_decode_sabm_payload(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "6f" # Control byte + "2f" # Control byte "11 22 33 44 55" # Payload ) ) From 651628589f376529aa075e95090f8366aa45e68e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 19:34:51 +1000 Subject: [PATCH 118/207] issue #17: frame: Add unit test for SABME decode --- tests/test_frame/test_uframe.py | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index bc01921..cd89516 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -7,6 +7,7 @@ AX25UnnumberedFrame, AX25DisconnectModeFrame, AX25SetAsyncBalancedModeFrame, + AX25SetAsyncBalancedModeExtendedFrame, AX25TestFrame, ) @@ -67,6 +68,40 @@ def test_decode_sabm_payload(): assert str(e) == "Frame does not support payload" +def test_decode_sabme(): + """ + Test that a SABME frame is recognised and decoded. + """ + frame = AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "6f" # Control byte + ) + ) + assert isinstance( + frame, AX25SetAsyncBalancedModeExtendedFrame + ), "Did not decode to SABME frame" + + +def test_decode_sabme_payload(): + """ + Test that a SABME frame forbids payload. + """ + try: + AX25Frame.decode( + from_hex( + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "6f" # Control byte + "11 22 33 44 55" # Payload + ) + ) + assert False, "This should not have worked" + except ValueError as e: + eq_(str(e), "Frame does not support payload") + + def test_decode_uframe_payload(): """ Test that U-frames other than FRMR and UI are forbidden to have payloads. From 377f00320f1326dafd03f4cca7fe7075ddf35152 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 19:46:18 +1000 Subject: [PATCH 119/207] issue #17: frame: Fix pass-through of P/F bit --- aioax25/frame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aioax25/frame.py b/aioax25/frame.py index 25ed3cc..02458dd 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1187,6 +1187,7 @@ def __init__( modifier=self.MODIFIER, repeaters=repeaters, cr=cr, + pf=pf, timestamp=timestamp, deadline=deadline, ) From 82e7401f35c844269850b70785a6ce973f1f2a9c Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 19:47:27 +1000 Subject: [PATCH 120/207] issue #17: frame: Update test data with P/F bit set. As observed in actual TNC firmware behaviour. --- aioax25/frame.py | 2 ++ tests/test_frame/test_uframe.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 02458dd..3c69586 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1212,6 +1212,7 @@ class AX25SetAsyncBalancedModeFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b00101111 CR = True + PF = True AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeFrame) @@ -1227,6 +1228,7 @@ class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b01101111 CR = True + PF = True AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeExtendedFrame) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index cd89516..2ea657f 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -42,7 +42,7 @@ def test_decode_sabm(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "2f" # Control byte + "3f" # Control byte ) ) assert isinstance( @@ -59,7 +59,7 @@ def test_decode_sabm_payload(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "2f" # Control byte + "3f" # Control byte "11 22 33 44 55" # Payload ) ) @@ -76,7 +76,7 @@ def test_decode_sabme(): from_hex( "ac 96 68 84 ae 92 e0" # Destination "ac 96 68 9a a6 98 61" # Source - "6f" # Control byte + "7f" # Control byte ) ) assert isinstance( From 005f5eb1f9fd8eb802cc965d04a6c34af94a63bd Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 19:48:06 +1000 Subject: [PATCH 121/207] issue #17: frame: Add unit tests for emitting SABM(E) --- tests/test_frame/test_uframe.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index 2ea657f..ba5e19d 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -343,6 +343,36 @@ def test_encode_pf(): assert frame.control == 0x13 +def test_encode_sabm(): + """ + Test we can encode a SABM frame. + """ + frame = AX25SetAsyncBalancedModeFrame( + destination="VK4BWI", source="VK4MSL" + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "3f", # Control byte + ) + + +def test_encode_sabme(): + """ + Test we can encode a SABME frame. + """ + frame = AX25SetAsyncBalancedModeExtendedFrame( + destination="VK4BWI", source="VK4MSL" + ) + hex_cmp( + bytes(frame), + "ac 96 68 84 ae 92 e0" # Destination + "ac 96 68 9a a6 98 61" # Source + "7f", # Control byte + ) + + def test_encode_frmr_w(): """ Test we can set the W bit on a FRMR frame. From 6ab497c1ddc6d76b08892a3a39409fb566cc72e4 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 19:58:50 +1000 Subject: [PATCH 122/207] .coveragerc: Exclude tests from coverage report --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 1ae85db..ce87a9d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,4 @@ [run] branch=true source=aioax25 +omit=tests From 641eb243cac065bc1ef61ae03d6a6cdc53c8afbb Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 20:16:33 +1000 Subject: [PATCH 123/207] peer: On receipt of an I-frame, push it to the received signal handler. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now, provide both the frame and the payload. This is a naïve approach that assumes there are no missed or out-of-order frames at this point. It's possible the `frame.ns != self._recv_seq` check earlier may in fact filter this out for us, but this needs research. --- aioax25/peer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 3d50642..6e27f1d 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -481,6 +481,12 @@ def _on_receive_iframe(self, frame): # manners:…" self._recv_state = (self._recv_state + 1) % self._modulo + # TODO: the payload here may be a repeat of data already seen, or + # for _future_ data (i.e. there's an I-frame that got missed in between + # the last one we saw, and this one). How do we handle this possibly + # out-of-order data? + self.received_information.emit(frame=frame, payload=frame.payload) + # "a) If it has an I frame to send, that I frame may be sent with the # transmitted N(R) equal to its receive state V(R) (thus acknowledging # the received frame)." From 2b2f331bf111895168d98472b23a923c840ff035 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 20:35:00 +1000 Subject: [PATCH 124/207] peer: Add some unit tests for sending certain frame types --- tests/test_peer/test_disc.py | 41 ++++++++++++++++++++++++++++++++++++ tests/test_peer/test_dm.py | 41 ++++++++++++++++++++++++++++++++++++ tests/test_peer/test_ua.py | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 tests/test_peer/test_disc.py create mode 100644 tests/test_peer/test_dm.py create mode 100644 tests/test_peer/test_ua.py diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py new file mode 100644 index 0000000..975fb00 --- /dev/null +++ b/tests/test_peer/test_disc.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25Peer DISC handling +""" + +from aioax25.frame import AX25Address, AX25Path, AX25DisconnectFrame +from aioax25.version import AX25Version +from .peer import TestingAX25Peer +from ..mocks import DummyStation + + +def test_peer_send_disc(): + """ + Test _send_disc correctly addresses and sends a DISC frame. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True + ) + + # Request a DISC frame be sent + peer._send_disc() + + # This was a request, so there should be a reply waiting + assert len(interface.transmit_calls) == 1 + (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) + + # This should be a DISC + assert tx_kwargs == {'callback': None} + assert len(tx_args) == 1 + (frame,) = tx_args + assert isinstance(frame, AX25DisconnectFrame) + + assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py new file mode 100644 index 0000000..18acfa6 --- /dev/null +++ b/tests/test_peer/test_dm.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25Peer DM handling +""" + +from aioax25.frame import AX25Address, AX25Path, AX25DisconnectModeFrame +from aioax25.version import AX25Version +from .peer import TestingAX25Peer +from ..mocks import DummyStation + + +def test_peer_send_dm(): + """ + Test _send_dm correctly addresses and sends a DM frame. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True + ) + + # Request a DM frame be sent + peer._send_dm() + + # This was a request, so there should be a reply waiting + assert len(interface.transmit_calls) == 1 + (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) + + # This should be a DM + assert tx_kwargs == {'callback': None} + assert len(tx_args) == 1 + (frame,) = tx_args + assert isinstance(frame, AX25DisconnectModeFrame) + + assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" diff --git a/tests/test_peer/test_ua.py b/tests/test_peer/test_ua.py new file mode 100644 index 0000000..bb363a5 --- /dev/null +++ b/tests/test_peer/test_ua.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25Peer UA handling +""" + +from aioax25.frame import AX25Address, AX25Path, AX25UnnumberedAcknowledgeFrame +from aioax25.version import AX25Version +from .peer import TestingAX25Peer +from ..mocks import DummyStation + + +def test_peer_send_ua(): + """ + Test _send_ua correctly addresses and sends a UA frame. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True + ) + + # Request a UA frame be sent + peer._send_ua() + + # This was a request, so there should be a reply waiting + assert len(interface.transmit_calls) == 1 + (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) + + # This should be a UA + assert tx_kwargs == {'callback': None} + assert len(tx_args) == 1 + (frame,) = tx_args + assert isinstance(frame, AX25UnnumberedAcknowledgeFrame) + + assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" From fe4f8783099640f0a9ba00f5dc7419eabc535912 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 20:45:35 +1000 Subject: [PATCH 125/207] peer: Test reception of DM frame. --- tests/test_peer/test_dm.py | 48 +++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index 18acfa6..f641131 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -7,7 +7,53 @@ from aioax25.frame import AX25Address, AX25Path, AX25DisconnectModeFrame from aioax25.version import AX25Version from .peer import TestingAX25Peer -from ..mocks import DummyStation +from ..mocks import DummyStation, DummyTimeout + + +def test_peer_recv_dm(): + """ + Test when receiving a DM whilst connected, the peer disconnects. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True + ) + + # Set some dummy data in fields -- this should be cleared out. + ack_timer = DummyTimeout(None, None) + peer._ack_timeout_handle = ack_timer + peer._state = peer.AX25PeerState.CONNECTED + peer._send_state = 1 + peer._send_seq = 2 + peer._recv_state = 3 + peer._recv_seq = 4 + peer._ack_state = 5 + peer._pending_iframes = dict(comment="pending data") + peer._pending_data = ["pending data"] + + # Pass the peer a DM frame + peer._on_receive( + AX25DisconnectModeFrame( + destination=station.address, + source=peer.address, + repeaters=None + ) + ) + + # We should now be "disconnected" + assert peer._ack_timeout_handle is None + assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._send_state == 0 + assert peer._send_seq == 0 + assert peer._recv_state == 0 + assert peer._recv_seq == 0 + assert peer._ack_state == 0 + assert peer._pending_iframes == {} + assert peer._pending_data == [] def test_peer_send_dm(): From a6c55802b310d6b5eed8b9047483c4434f54a01b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 21:00:01 +1000 Subject: [PATCH 126/207] peer: Test reception of DISC frame Also fix comment while we're here. --- tests/test_peer/test_disc.py | 67 ++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index 975fb00..da39102 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -4,10 +4,71 @@ Tests for AX25Peer DISC handling """ -from aioax25.frame import AX25Address, AX25Path, AX25DisconnectFrame +from aioax25.frame import AX25Address, AX25Path, AX25DisconnectFrame, \ + AX25UnnumberedAcknowledgeFrame from aioax25.version import AX25Version from .peer import TestingAX25Peer -from ..mocks import DummyStation +from ..mocks import DummyStation, DummyTimeout + + +def test_peer_recv_disc(): + """ + Test when receiving a DISC whilst connected, the peer disconnects. + """ + station = DummyStation(AX25Address('VK4MSL', ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address('VK4MSL'), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, locked_path=True + ) + + # Set some dummy data in fields -- this should be cleared out. + ack_timer = DummyTimeout(None, None) + peer._ack_timeout_handle = ack_timer + peer._state = peer.AX25PeerState.CONNECTED + peer._send_state = 1 + peer._send_seq = 2 + peer._recv_state = 3 + peer._recv_seq = 4 + peer._ack_state = 5 + peer._pending_iframes = dict(comment="pending data") + peer._pending_data = ["pending data"] + + # Pass the peer a DISC frame + peer._on_receive( + AX25DisconnectFrame( + destination=station.address, + source=peer.address, + repeaters=None + ) + ) + + # This was a request, so there should be a reply waiting + assert len(interface.transmit_calls) == 1 + (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) + + # This should be a UA in reply to the DISC + assert tx_kwargs == {'callback': None} + assert len(tx_args) == 1 + (frame,) = tx_args + assert isinstance(frame, AX25UnnumberedAcknowledgeFrame) + + assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" + + # We should now be "disconnected" + assert peer._ack_timeout_handle is None + assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._send_state == 0 + assert peer._send_seq == 0 + assert peer._recv_state == 0 + assert peer._recv_seq == 0 + assert peer._ack_state == 0 + assert peer._pending_iframes == {} + assert peer._pending_data == [] def test_peer_send_disc(): @@ -26,7 +87,7 @@ def test_peer_send_disc(): # Request a DISC frame be sent peer._send_disc() - # This was a request, so there should be a reply waiting + # There should be our outgoing request here assert len(interface.transmit_calls) == 1 (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) From 782614b5817b84da02ce21f7d58b2012df1777d9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 21:01:07 +1000 Subject: [PATCH 127/207] peer: Fix comments in unit tests --- tests/test_peer/test_dm.py | 2 +- tests/test_peer/test_ua.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index f641131..6e0b826 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -72,7 +72,7 @@ def test_peer_send_dm(): # Request a DM frame be sent peer._send_dm() - # This was a request, so there should be a reply waiting + # There should be a frame sent assert len(interface.transmit_calls) == 1 (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) diff --git a/tests/test_peer/test_ua.py b/tests/test_peer/test_ua.py index bb363a5..f401a04 100644 --- a/tests/test_peer/test_ua.py +++ b/tests/test_peer/test_ua.py @@ -26,7 +26,7 @@ def test_peer_send_ua(): # Request a UA frame be sent peer._send_ua() - # This was a request, so there should be a reply waiting + # There should be a frame sent assert len(interface.transmit_calls) == 1 (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) From 0049acc90bb949df4bf6505528e240e03b1c058e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 1 Apr 2023 21:42:18 +1000 Subject: [PATCH 128/207] peer: Add some more log statements --- aioax25/peer.py | 123 ++++++++++++++++++++++++++++++++++- tests/test_peer/test_disc.py | 41 ++++++------ tests/test_peer/test_dm.py | 30 ++++----- tests/test_peer/test_ua.py | 18 +++-- 4 files changed, 168 insertions(+), 44 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 6e27f1d..0247a4f 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -248,9 +248,16 @@ def reply_path(self): if all_paths: best_path = all_paths[-1][0] + self._log.info( + "Choosing highest rated TX/most common RX path: %s", + best_path, + ) else: # If no paths exist, use whatever default path is set best_path = self._repeaters + self._log.info( + "Choosing given path for replies: %s", best_path + ) # Use this until we have reason to change self._reply_path = AX25Path(*(best_path or [])) @@ -268,11 +275,17 @@ def weight_path(self, path, weight, relative=True): if relative: weight += self._tx_path_score.get(path, 0) self._tx_path_score[path] = weight + self._log.debug("Weighted score for %s: %d", path, weight) def ping(self, payload=None, timeout=30.0, callback=None): """ Ping the peer and wait for a response. """ + self._log.debug( + "Beginning ping of station (payload=%r timeout=%r)", + payload, + timeout, + ) handler = AX25PeerTestHandler(self, bytes(payload or b""), timeout) handler.done_sig.connect(self._on_test_done) @@ -287,9 +300,15 @@ def connect(self): Connect to the remote node. """ if self._state is self.AX25PeerState.DISCONNECTED: + self._log.info("Initiating connection to remote peer") handler = AX25PeerConnectionHandler(self) handler.done_sig.connect(self._on_connect_response) handler._go() + else: + self._log.info( + "Will not connect to peer now, currently in state %s.", + self._state.name, + ) def accept(self): """ @@ -301,6 +320,12 @@ def accept(self): self._stop_ack_timer() self._set_conn_state(self.AX25PeerState.CONNECTED) self._send_ua() + else: + self._log.info( + "Will not accept connection from peer now, " + "currently in state %s.", + self._state.name, + ) def reject(self): """ @@ -312,6 +337,12 @@ def reject(self): self._stop_ack_timer() self._set_conn_state(self.AX25PeerState.DISCONNECTED) self._send_dm() + else: + self._log.info( + "Will not reject connection from peer now, " + "currently in state %s.", + self._state.name, + ) def disconnect(self): """ @@ -322,6 +353,12 @@ def disconnect(self): self._set_conn_state(self.AX25PeerState.DISCONNECTING) self._send_disc() self._start_disconnect_ack_timer() + else: + self._log.info( + "Will not disconnect from peer now, " + "currently in state %s.", + self._state.name, + ) def _cancel_idle_timeout(self): """ @@ -353,6 +390,10 @@ def _cleanup(self): self.disconnect() else: self._send_dm() + else: + self._log.debug( + "Clean-up initiated in state %s", self._state.name + ) # Cancel other timers self._cancel_rr_notification() @@ -370,7 +411,11 @@ def _on_receive(self, frame): if not self._locked_path: # Increment the received frame count path = tuple(reversed(frame.header.repeaters.reply)) - self._rx_path_count[path] = self._rx_path_count.get(path, 0) + 1 + pathcount = self._rx_path_count.get(path, 0) + 1 + self._log.debug( + "Observed %d frame(s) via path %s", pathcount, path + ) + self._rx_path_count[path] = pathcount # AX.25 2.2 sect 6.3.1: "The originating TNC sending a SABM(E) command # ignores and discards any frames except SABM, DISC, UA and DM frames @@ -450,6 +495,9 @@ def _on_receive(self, frame): ) else: # No connection in progress, send a DM. + self._log.debug( + "Received I or S frame in state %s", self._state.name + ) return self._send_dm() def _on_receive_iframe(self, frame): @@ -474,6 +522,11 @@ def _on_receive_iframe(self, frame): # is not in the busy condition,…" if frame.ns != self._recv_seq: # TODO: should we send a REJ/SREJ after a time-out? + self._log.debug( + "I-frame sequence is %s, expecting %s, ignoring", + frame.ns, + self._recv_seq, + ) return # "…it accepts the received I frame, @@ -517,6 +570,7 @@ def _on_receive_sframe(self, frame): def _on_receive_rr(self, frame): if frame.header.pf: # Peer requesting our RR status + self._log.debug("RR status requested from peer") self._on_receive_rr_rnr_rej_query() else: # Received peer's RR status, peer no longer busy @@ -530,6 +584,7 @@ def _on_receive_rr(self, frame): def _on_receive_rnr(self, frame): if frame.header.pf: # Peer requesting our RNR status + self._log.debug("RNR status requested from peer") self._on_receive_rr_rnr_rej_query() else: # Received peer's RNR status, peer is busy @@ -541,9 +596,11 @@ def _on_receive_rnr(self, frame): def _on_receive_rej(self, frame): if frame.header.pf: # Peer requesting rejected frame status. + self._log.debug("REJ status requested from peer") self._on_receive_rr_rnr_rej_query() else: # Reject reject. + self._log.debug("REJ notification received from peer") # AX.25 sect 4.3.2.3: "Any frames sent with a sequence number # of N(R)-1 or less are acknowledged." self._ack_outstanding((frame.nr - 1) % self._modulo) @@ -557,6 +614,7 @@ def _on_receive_srej(self, frame): # AX.25 2.2 sect 4.3.2.4: "If the P/F bit in the SREJ is set to # '1', then I frames numbered up to N(R)-1 inclusive are considered # as acknowledged." + self._log.debug("SREJ received with P/F=1") self._ack_outstanding((frame.nr - 1) % self._modulo) # Re-send the outstanding frame @@ -598,12 +656,15 @@ def _on_receive_test(self, frame): def _on_test_done(self, handler, **kwargs): if not self._testframe_handler: + self._log.debug("TEST completed without frame handler") return real_handler = self._testframe_handler() if (real_handler is not None) and (handler is not real_handler): + self._log.debug("TEST completed with stale handler") return + self._log.debug("TEST completed, removing handler") self._testframe_handler = None def _on_receive_frmr(self, frame): @@ -624,6 +685,7 @@ def _on_receive_sabm(self, frame): # If we don't know the protocol of the peer, we can safely assume # AX.25 2.2 now. if self._protocol == AX25Version.UNKNOWN: + self._log.info("Assuming sender is AX.25 2.2") self._protocol = AX25Version.AX25_22 # Make sure both ends are enabled for AX.25 2.2 @@ -663,8 +725,10 @@ def _on_receive_sabm(self, frame): else: # Set the incoming connection state, and emit a signal via the # station's 'connection_request' signal. + self._log.debug("Preparing incoming connection") self._set_conn_state(self.AX25PeerState.INCOMING_CONNECTION) self._start_connect_ack_timer() + self._log.debug("Announcing incoming connection") self._station().connection_request.emit(peer=self) def _start_connect_ack_timer(self): @@ -686,11 +750,17 @@ def _stop_ack_timer(self): def _on_incoming_connect_timeout(self): if self._state is self.AX25PeerState.INCOMING_CONNECTION: + self._log.info("Incoming connection timed out") self._ack_timeout_handle = None self.reject() + else: + self._log.debug( + "Incoming connection time-out in state %s", self._state.name + ) def _on_connect_response(self, response, **kwargs): # Handle the connection result. + self._log.debug("Connection response: %r", response) if response == "ack": # We're in. self._set_conn_state(self.AX25PeerState.CONNECTED) @@ -708,6 +778,7 @@ def _negotiate(self, callback): "%s does not support negotiation" % self._protocol.value ) + self._log.debug("Attempting protocol negotiation") handler = AX25PeerNegotiationHandler(self) handler.done_sig.connect(self._on_negotiate_result) handler.done_sig.connect(callback) @@ -720,6 +791,7 @@ def _on_negotiate_result(self, response, **kwargs): """ Handle the negotiation response. """ + self._log.debug("Negotiation response: %r", response) if response in ("frmr", "dm"): # Other station did not like this. self._log.info( @@ -741,6 +813,7 @@ def _on_negotiate_result(self, response, **kwargs): self._protocol = AX25Version.AX25_22 self._negotiated = True + self._log.debug("XID negotiation complete") self._set_conn_state(self.AX25PeerState.DISCONNECTED) def _init_connection(self, extended): @@ -749,6 +822,7 @@ def _init_connection(self, extended): """ if extended: # Set the maximum outstanding frames variable + self._log.debug("Initialising AX.25 2.2 mod128 connection") self._max_outstanding = self._max_outstanding_mod128 # Initialise the modulo value @@ -761,6 +835,7 @@ def _init_connection(self, extended): self._REJFrameClass = AX2516BitRejectFrame self._SREJFrameClass = AX2516BitSelectiveRejectFrame else: + self._log.debug("Initialising AX.25 2.0 mod8 connection") # Set the maximum outstanding frames variable self._max_outstanding = self._max_outstanding_mod8 @@ -778,6 +853,8 @@ def _init_connection(self, extended): self._reset_connection_state() def _reset_connection_state(self): + self._log.debug("Resetting the peer state") + # Reset our state self._send_state = 0 # AKA V(S) self._send_seq = 0 # AKA N(S) @@ -807,9 +884,12 @@ def _set_conn_state(self, state): def _on_disc_ua_timeout(self): if self._state is self.AX25PeerState.DISCONNECTING: + self._log.info("DISC timed out, assuming we're disconnected") # Assume we are disconnected. self._ack_timeout_handle = None self._on_disconnect() + else: + self._log.debug("DISC time-out in state %s", self._state.name) def _on_disconnect(self): """ @@ -845,6 +925,8 @@ def _on_receive_ua(self): self._log.info("Received UA from peer") if self._uaframe_handler: self._uaframe_handler() + else: + self._log.debug("No one cares about the UA") def _on_receive_dm(self): """ @@ -855,8 +937,15 @@ def _on_receive_dm(self): self._log.info("Received DM from peer") self._on_disconnect() elif self._dmframe_handler: + self._log.debug( + "Received DM from peer whilst in state %s", self._state.name + ) self._dmframe_handler() self._dmframe_handler = None + else: + self._log.debug( + "No one cares about the DM in state %s", self._state.name + ) def _on_receive_xid(self, frame): """ @@ -877,12 +966,16 @@ def _on_receive_xid(self, frame): # AX.25 2.2 sect 4.3.3.7: "A station receiving an XID command # returns an XID response unless a UA response to a mode setting # command is awaiting transmission, or a FRMR condition exists". - self._log.warning("UA is pending, dropping received XID") + self._log.warning( + "UA is pending, dropping received XID (state %s)", + self._state.name, + ) return # We have received an XID, AX.25 2.0 and earlier stations do not know # this frame, so clearly this is at least AX.25 2.2. if self._protocol == AX25Version.UNKNOWN: + self._log.info("Assuming AX.25 2.2 peer") self._protocol = AX25Version.AX25_22 # Don't process the contents of the frame unless FI and GI match. @@ -936,15 +1029,21 @@ def _on_receive_xid(self, frame): if frame.header.cr: # Other station is requesting negotiation, send response. + self._log.debug("Sending XID response") self._send_xid(cr=False) elif self._xidframe_handler is not None: # This is a reply to our XID + self._log.debug("Forwarding XID response") self._xidframe_handler(frame) self._xidframe_handler = None + else: + # No one cared? + self._log.debug("Received XID response but no one cares") # Having received the XID, we consider ourselves having negotiated # parameters. Future connections will skip this step. self._negotiated = True + self._log.debug("XID negotiation complete") def _process_xid_cop(self, param): if param.pv is None: @@ -1361,6 +1460,7 @@ def _finish(self, **kwargs): self._done = True self._stop_timer() + self._log.debug("finished: %r", kwargs) self.done_sig.emit(**kwargs) @@ -1380,15 +1480,18 @@ def __init__(self, peer): def _go(self): if self.peer._negotiated: # Already done, we can connect immediately + self._log.debug("XID negotiation already done") self._on_negotiated(response="already") elif ( self.peer._protocol not in (AX25Version.AX25_22, AX25Version.UNKNOWN) ) or (self.peer._station()._protocol != AX25Version.AX25_22): # Not compatible, just connect + self._log.debug("XID negotiation not supported") self._on_negotiated(response="not_compatible") else: # Need to negotiate first. + self._log.debug("XID negotiation to be attempted") self.peer._negotiate(self._on_negotiated) def _on_negotiated(self, response, **kwargs): @@ -1409,8 +1512,13 @@ def _on_negotiated(self, response, **kwargs): or (self.peer._sabmframe_handler is not None) ): # We're handling another frame now. + self._log.debug("Received XID, but we're busy") self._finish(response="station_busy") return + + self._log.debug( + "XID done (state %s), beginning connection", response + ) self.peer._sabmframe_handler = self._on_receive_sabm self.peer._uaframe_handler = self._on_receive_ua self.peer._frmrframe_handler = self._on_receive_frmr @@ -1418,46 +1526,57 @@ def _on_negotiated(self, response, **kwargs): self.peer._send_sabm() self._start_timer() else: + self._log.debug("Bailing out due to XID response %s", response) self._finish(response=response) def _on_receive_ua(self): # Peer just acknowledged our connection + self._log.debug("UA received, connection established") self.peer._init_connection(self.peer._modulo128) self._finish(response="ack") def _on_receive_sabm(self): # Peer was connecting to us, we'll treat this as a UA. + self._log.debug("SABM received, connection established") self.peer._send_ua() self._finish(response="ack") def _on_receive_frmr(self): # Peer just rejected our connect frame, begin FRMR recovery. + self._log.debug("FRMR received, recovering") self.peer._send_dm() self._finish(response="frmr") def _on_receive_dm(self): # Peer just rejected our connect frame. + self._log.debug("DM received, bailing here") self._finish(response="dm") def _on_timeout(self): if self._retries: self._retries -= 1 + self._log.debug("Retrying, remaining=%d", self._retries) self._on_negotiated(response="retry") else: + self._log.debug("Giving up") self._finish(response="timeout") def _finish(self, **kwargs): # Clean up hooks if self.peer._sabmframe_handler == self._on_receive_sabm: + self._log.debug("Unhooking SABM handler") self.peer._sabmframe_handler = None if self.peer._uaframe_handler == self._on_receive_ua: + self._log.debug("Unhooking UA handler") self.peer._uaframe_handler = None if self.peer._frmrframe_handler == self._on_receive_frmr: + self._log.debug("Unhooking FRMR handler") self.peer._frmrframe_handler = None if self.peer._dmframe_handler == self._on_receive_dm: + self._log.debug("Unhooking DM handler") self.peer._dmframe_handler = None super(AX25PeerConnectionHandler, self)._finish(**kwargs) diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index da39102..e4702eb 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -4,8 +4,12 @@ Tests for AX25Peer DISC handling """ -from aioax25.frame import AX25Address, AX25Path, AX25DisconnectFrame, \ - AX25UnnumberedAcknowledgeFrame +from aioax25.frame import ( + AX25Address, + AX25Path, + AX25DisconnectFrame, + AX25UnnumberedAcknowledgeFrame, +) from aioax25.version import AX25Version from .peer import TestingAX25Peer from ..mocks import DummyStation, DummyTimeout @@ -15,13 +19,14 @@ def test_peer_recv_disc(): """ Test when receiving a DISC whilst connected, the peer disconnects. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), - full_duplex=True, locked_path=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, + locked_path=True, ) # Set some dummy data in fields -- this should be cleared out. @@ -38,11 +43,9 @@ def test_peer_recv_disc(): # Pass the peer a DISC frame peer._on_receive( - AX25DisconnectFrame( - destination=station.address, - source=peer.address, - repeaters=None - ) + AX25DisconnectFrame( + destination=station.address, source=peer.address, repeaters=None + ) ) # This was a request, so there should be a reply waiting @@ -50,7 +53,7 @@ def test_peer_recv_disc(): (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) # This should be a UA in reply to the DISC - assert tx_kwargs == {'callback': None} + assert tx_kwargs == {"callback": None} assert len(tx_args) == 1 (frame,) = tx_args assert isinstance(frame, AX25UnnumberedAcknowledgeFrame) @@ -75,13 +78,13 @@ def test_peer_send_disc(): """ Test _send_disc correctly addresses and sends a DISC frame. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, ) # Request a DISC frame be sent @@ -92,7 +95,7 @@ def test_peer_send_disc(): (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) # This should be a DISC - assert tx_kwargs == {'callback': None} + assert tx_kwargs == {"callback": None} assert len(tx_args) == 1 (frame,) = tx_args assert isinstance(frame, AX25DisconnectFrame) diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index 6e0b826..6986ff8 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -14,13 +14,13 @@ def test_peer_recv_dm(): """ Test when receiving a DM whilst connected, the peer disconnects. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, ) # Set some dummy data in fields -- this should be cleared out. @@ -37,11 +37,9 @@ def test_peer_recv_dm(): # Pass the peer a DM frame peer._on_receive( - AX25DisconnectModeFrame( - destination=station.address, - source=peer.address, - repeaters=None - ) + AX25DisconnectModeFrame( + destination=station.address, source=peer.address, repeaters=None + ) ) # We should now be "disconnected" @@ -60,13 +58,13 @@ def test_peer_send_dm(): """ Test _send_dm correctly addresses and sends a DM frame. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, ) # Request a DM frame be sent @@ -77,7 +75,7 @@ def test_peer_send_dm(): (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) # This should be a DM - assert tx_kwargs == {'callback': None} + assert tx_kwargs == {"callback": None} assert len(tx_args) == 1 (frame,) = tx_args assert isinstance(frame, AX25DisconnectModeFrame) diff --git a/tests/test_peer/test_ua.py b/tests/test_peer/test_ua.py index f401a04..bcbfa1b 100644 --- a/tests/test_peer/test_ua.py +++ b/tests/test_peer/test_ua.py @@ -4,7 +4,11 @@ Tests for AX25Peer UA handling """ -from aioax25.frame import AX25Address, AX25Path, AX25UnnumberedAcknowledgeFrame +from aioax25.frame import ( + AX25Address, + AX25Path, + AX25UnnumberedAcknowledgeFrame, +) from aioax25.version import AX25Version from .peer import TestingAX25Peer from ..mocks import DummyStation @@ -14,13 +18,13 @@ def test_peer_send_ua(): """ Test _send_ua correctly addresses and sends a UA frame. """ - station = DummyStation(AX25Address('VK4MSL', ssid=1)) + station = DummyStation(AX25Address("VK4MSL", ssid=1)) interface = station._interface() peer = TestingAX25Peer( - station=station, - address=AX25Address('VK4MSL'), - repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), - full_duplex=True + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, ) # Request a UA frame be sent @@ -31,7 +35,7 @@ def test_peer_send_ua(): (tx_args, tx_kwargs) = interface.transmit_calls.pop(0) # This should be a UA - assert tx_kwargs == {'callback': None} + assert tx_kwargs == {"callback": None} assert len(tx_args) == 1 (frame,) = tx_args assert isinstance(frame, AX25UnnumberedAcknowledgeFrame) From 19934c9afb2ba455afe10efd703a76785735653b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 10:23:06 +1000 Subject: [PATCH 129/207] coding style: Apply `black` Sadly, this means some of the aligned `=` operators has been lost, but the code is more consistent overall, so net benefit is still positive. --- aioax25/peer.py | 15 --------------- tests/test_frame/test_xid.py | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 0247a4f..d0fa59e 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1217,21 +1217,6 @@ def _send_xid(self, cr): AX25XIDAcknowledgeTimerParameter( int(self._ack_timeout * 1000) ), - AX25XIDIFieldLengthTransmitParameter( - self._max_ifield * 8 - ), - AX25XIDIFieldLengthReceiveParameter( - self._max_ifield_rx * 8 - ), - AX25XIDWindowSizeTransmitParameter(self._max_outstanding), - AX25XIDWindowSizeReceiveParameter( - self._max_outstanding_mod128 - if self._modulus128 - else self._max_outstanding_mod8 - ), - AX25XIDAcknowledgeTimerParameter( - int(self._ack_timeout * 1000) - ), AX25XIDRetriesParameter(self._max_retries), ], cr=cr, diff --git a/tests/test_frame/test_xid.py b/tests/test_frame/test_xid.py index 7983ac2..10efa5f 100644 --- a/tests/test_frame/test_xid.py +++ b/tests/test_frame/test_xid.py @@ -46,13 +46,13 @@ def test_encode_xid(): "80" # Group Ident "00 16" # Group length # First parameter: CoP - "02" # Parameter ID - "02" # Length - "00 21" # Value + "02" + "02" + "00 21" # Parameter ID # Length # Value # Second parameter: HDLC Optional Functions - "03" # Parameter ID - "03" # Length - "86 a8 02" # Value + "03" + "03" + "86 a8 02" # Parameter ID # Length # Value # Third parameter: I field receive size "06" "02" "04 00" # Fourth parameter: retries @@ -261,12 +261,11 @@ def test_copy_xid(): "80" # Group Ident "00 06" # Group length # First parameter - "12" # Parameter ID - "02" # Length - "34 56" # Value + "12" + "02" + "34 56" # Parameter ID # Length # Value # Second parameter - "34" # Parameter ID - "00", # Length (no value) + "34" "00", # Parameter ID # Length (no value) ) From 972f85e973df78701a746b2aa3383e7b1e6578e5 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 10:34:02 +1000 Subject: [PATCH 130/207] frame: Re-instate some manual formatting --- aioax25/frame.py | 147 +++++++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 69 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 3c69586..3ea0f56 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -13,9 +13,9 @@ decoder to fully decode an arbitrary AX.25 frame. Thankfully the control field is sent little-endian format, so the first byte we -encounter is the least significant bits -- which is sufficient to identify whether -the frame is an I, S or U frame: the least significant two bits carry this -information. +encounter is the least significant bits -- which is sufficient to identify +whether the frame is an I, S or U frame: the least significant two bits carry +this information. For this reason there are 3 sub-classes of this top-level class: @@ -33,12 +33,12 @@ Decoding is done by calling the AX25Frame.decode class method. This takes two parameters: - - data: either raw bytes or an AX25Frame class. The latter form is useful when - you've previously decoded a frame as a AX25RawFrame and need to further - dissect it as either a AX258BitFrame or AX2516BitFrame sub-class. + - data: either raw bytes or an AX25Frame class. The latter form is useful + when you've previously decoded a frame as a AX25RawFrame and need to + further dissect it as either a AX258BitFrame or AX2516BitFrame sub-class. - - modulo128: by default is None, but if set to a boolean, will decode I or S - frames accordingly instead of just returning AX25RawFrame. + - modulo128: by default is None, but if set to a boolean, will decode I or + S frames accordingly instead of just returning AX25RawFrame. """ import re @@ -56,28 +56,30 @@ class AX25Frame(object): Base class for AX.25 frames. """ + # fmt: off # The following are the same for 8 and 16-bit control fields. - CONTROL_I_MASK = 0b00000001 - CONTROL_I_VAL = 0b00000000 + CONTROL_I_MASK = 0b00000001 + CONTROL_I_VAL = 0b00000000 CONTROL_US_MASK = 0b00000011 - CONTROL_S_VAL = 0b00000001 - CONTROL_U_VAL = 0b00000011 + CONTROL_S_VAL = 0b00000001 + CONTROL_U_VAL = 0b00000011 # PID codes - PID_ISO8208_CCITT = 0x01 + PID_ISO8208_CCITT = 0x01 PID_VJ_IP4_COMPRESS = 0x06 - PID_VJ_IP4 = 0x07 - PID_SEGMENTATION = 0x08 - PID_TEXNET = 0xC3 - PID_LINKQUALITY = 0xC4 - PID_APPLETALK = 0xCA - PID_APPLETALK_ARP = 0xCB - PID_ARPA_IP4 = 0xCC - PID_APRA_ARP = 0xCD - PID_FLEXNET = 0xCE - PID_NETROM = 0xCF - PID_NO_L3 = 0xF0 - PID_ESCAPE = 0xFF + PID_VJ_IP4 = 0x07 + PID_SEGMENTATION = 0x08 + PID_TEXNET = 0xC3 + PID_LINKQUALITY = 0xC4 + PID_APPLETALK = 0xCA + PID_APPLETALK_ARP = 0xCB + PID_ARPA_IP4 = 0xCC + PID_APRA_ARP = 0xCD + PID_FLEXNET = 0xCE + PID_NETROM = 0xCF + PID_NO_L3 = 0xF0 + PID_ESCAPE = 0xFF + # fmt: on @classmethod def decode(cls, data, modulo128=None): @@ -348,13 +350,13 @@ def frame_payload(self): class AX25RawFrame(AX25Frame): """ A representation of a raw AX.25 frame. This class is intended to capture - partially decoded frame data in the case where we don't know whether a control - field is 8 or 16-bits wide. + partially decoded frame data in the case where we don't know whether a + control field is 8 or 16-bits wide. - It may be fed to the AX25Frame.decode function again with modulo128=False for - known 8-bit frames, or modulo128=True for known 16-bit frames. For digipeating - applications, often no further dissection is necessary and so the frame can be - used as-is. + It may be fed to the AX25Frame.decode function again with modulo128=False + for known 8-bit frames, or modulo128=True for known 16-bit frames. For + digipeating applications, often no further dissection is necessary and so + the frame can be used as-is. """ def __init__( @@ -978,16 +980,18 @@ class AX25FrameRejectFrame(AX25UnnumberedFrame): Not much effort has been made to decode the meaning of these bits. """ + # fmt: off MODIFIER = 0b10000111 - W_MASK = 0b00000001 - X_MASK = 0b00000010 - Y_MASK = 0b00000100 - Z_MASK = 0b00001000 - VR_MASK = 0b11100000 - VR_POS = 5 - CR_MASK = 0b00010000 - VS_MASK = 0b00001110 - VS_POS = 1 + W_MASK = 0b00000001 + X_MASK = 0b00000010 + Y_MASK = 0b00000100 + Z_MASK = 0b00001000 + VR_MASK = 0b11100000 + VR_POS = 5 + CR_MASK = 0b00010000 + VS_MASK = 0b00001110 + VS_POS = 1 + # fmt: on @classmethod def decode(cls, header, control, data): @@ -1252,7 +1256,8 @@ class AX25DisconnectModeFrame(AX25BaseUnnumberedFrame): """ Disconnect mode frame. - This frame is used to indicate to the other station that it is disconnected. + This frame is used to indicate to the other station that it is + disconnected. """ MODIFIER = 0b00001111 @@ -1430,16 +1435,18 @@ class AX25XIDClassOfProceduresParameter(AX25XIDParameter): PI = AX25XIDParameterIdentifier.ClassesOfProcedure + # fmt: off # Bit fields for this parameter: - BALANCED_ABM = 0b0000000000000001 # Should be 1 + BALANCED_ABM = 0b0000000000000001 # Should be 1 UNBALANCED_NRM_PRI = 0b0000000000000010 # Should be 0 UNBALANCED_NRM_SEC = 0b0000000000000100 # Should be 0 UNBALANCED_ARM_PRI = 0b0000000000001000 # Should be 0 UNBALANCED_ARM_SEC = 0b0000000000010000 # Should be 0 - HALF_DUPLEX = 0b0000000000100000 # Should oppose FULL_DUPLEX - FULL_DUPLEX = 0b0000000001000000 # Should oppose HALF_DUPLEX - RESERVED_MASK = 0b1111111110000000 # Should be all zeros - RESERVED_POS = 7 + HALF_DUPLEX = 0b0000000000100000 # Should oppose FULL_DUPLEX + FULL_DUPLEX = 0b0000000001000000 # Should oppose HALF_DUPLEX + RESERVED_MASK = 0b1111111110000000 # Should be all zeros + RESERVED_POS = 7 + # fmt: on @classmethod def decode(cls, pv): @@ -1564,31 +1571,33 @@ class AX25XIDHDLCOptionalFunctionsParameter(AX25XIDParameter): PI = AX25XIDParameterIdentifier.HDLCOptionalFunctions + # fmt: off # Bit fields for this parameter: - RESERVED1 = 0b000000000000000000000001 # Should be 0 - REJ = 0b000000000000000000000010 # Negotiable - SREJ = 0b000000000000000000000100 # Negotiable - UI = 0b000000000000000000001000 # Should be 0 - SIM_RIM = 0b000000000000000000010000 # Should be 0 - UP = 0b000000000000000000100000 # Should be 0 - BASIC_ADDR = 0b000000000000000001000000 # Should be 1 - EXTD_ADDR = 0b000000000000000010000000 # Should be 0 - DELETE_I_RESP = 0b000000000000000100000000 # Should be 0 - DELETE_I_CMD = 0b000000000000001000000000 # Should be 0 - MODULO8 = 0b000000000000010000000000 # Negotiable - MODULO128 = 0b000000000000100000000000 # Negotiable - RSET = 0b000000000001000000000000 # Should be 0 - TEST = 0b000000000010000000000000 # Should be 1 - RD = 0b000000000100000000000000 # Should be 0 - FCS16 = 0b000000001000000000000000 # Should be 1 - FCS32 = 0b000000010000000000000000 # Should be 0 - SYNC_TX = 0b000000100000000000000000 # Should be 1 - START_STOP_TX = 0b000001000000000000000000 # Should be 0 + RESERVED1 = 0b000000000000000000000001 # Should be 0 + REJ = 0b000000000000000000000010 # Negotiable + SREJ = 0b000000000000000000000100 # Negotiable + UI = 0b000000000000000000001000 # Should be 0 + SIM_RIM = 0b000000000000000000010000 # Should be 0 + UP = 0b000000000000000000100000 # Should be 0 + BASIC_ADDR = 0b000000000000000001000000 # Should be 1 + EXTD_ADDR = 0b000000000000000010000000 # Should be 0 + DELETE_I_RESP = 0b000000000000000100000000 # Should be 0 + DELETE_I_CMD = 0b000000000000001000000000 # Should be 0 + MODULO8 = 0b000000000000010000000000 # Negotiable + MODULO128 = 0b000000000000100000000000 # Negotiable + RSET = 0b000000000001000000000000 # Should be 0 + TEST = 0b000000000010000000000000 # Should be 1 + RD = 0b000000000100000000000000 # Should be 0 + FCS16 = 0b000000001000000000000000 # Should be 1 + FCS32 = 0b000000010000000000000000 # Should be 0 + SYNC_TX = 0b000000100000000000000000 # Should be 1 + START_STOP_TX = 0b000001000000000000000000 # Should be 0 START_STOP_FLOW_CTL = 0b000010000000000000000000 # Should be 0 - START_STOP_TRANSP = 0b000100000000000000000000 # Should be 0 - SREJ_MULTIFRAME = 0b001000000000000000000000 # Should be 0 - RESERVED2_MASK = 0b110000000000000000000000 # Should be 00 - RESERVED2_POS = 22 + START_STOP_TRANSP = 0b000100000000000000000000 # Should be 0 + SREJ_MULTIFRAME = 0b001000000000000000000000 # Should be 0 + RESERVED2_MASK = 0b110000000000000000000000 # Should be 00 + RESERVED2_POS = 22 + # fmt: on @classmethod def decode(cls, pv): From eb9e967c3ee7d38115970a91c614395722f71bd1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 10:43:25 +1000 Subject: [PATCH 131/207] aprs.datatype: Re-instate manual formatting of block --- aioax25/aprs/datatype.py | 50 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/aioax25/aprs/datatype.py b/aioax25/aprs/datatype.py index 0c80d19..82a8eab 100644 --- a/aioax25/aprs/datatype.py +++ b/aioax25/aprs/datatype.py @@ -13,28 +13,30 @@ class APRSDataType(Enum): not including unused or reserved types. Page 17 of APRS 1.0.1 spec. """ - MIC_E_BETA0 = 0x1C - MIC_E_OLD_BETA0 = 0x1D - POSITION = ord("!") - PEET_BROS_WX1 = ord("#") - RAW_GPRS_ULT2K = ord("$") - AGRELO_DFJR = ord("%") - RESERVED_MAP = ord("&") - MIC_E_OLD = ord("'") - ITEM = ord(")") - PEET_BROS_WX2 = ord("*") - TEST_DATA = ord(",") - POSITION_TS = ord("/") - MESSAGE = ord(":") - OBJECT = ord(";") - STATIONCAP = ord("<") - POSITION_MSGCAP = ord("=") - STATUS = ord(">") - QUERY = ord("?") + # fmt: off + MIC_E_BETA0 = 0x1C + MIC_E_OLD_BETA0 = 0x1D + POSITION = ord("!") + PEET_BROS_WX1 = ord("#") + RAW_GPRS_ULT2K = ord("$") + AGRELO_DFJR = ord("%") + RESERVED_MAP = ord("&") + MIC_E_OLD = ord("'") + ITEM = ord(")") + PEET_BROS_WX2 = ord("*") + TEST_DATA = ord(",") + POSITION_TS = ord("/") + MESSAGE = ord(":") + OBJECT = ord(";") + STATIONCAP = ord("<") + POSITION_MSGCAP = ord("=") + STATUS = ord(">") + QUERY = ord("?") POSITION_TS_MSGCAP = ord("@") - TELEMETRY = ord("T") - MAIDENHEAD = ord("[") - WX = ord("_") - MIC_E = ord("`") - USER_DEFINED = ord("{") - THIRD_PARTY = ord("}") + TELEMETRY = ord("T") + MAIDENHEAD = ord("[") + WX = ord("_") + MIC_E = ord("`") + USER_DEFINED = ord("{") + THIRD_PARTY = ord("}") + # fmt: on From 5a7ec8ca06cc0b93d2d9f5c9d39b162d9bf30b1d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 11:38:35 +1000 Subject: [PATCH 132/207] peer: Test connection initialisation --- aioax25/peer.py | 2 +- tests/test_peer/test_connection.py | 117 ++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index d0fa59e..a326e59 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -835,7 +835,7 @@ def _init_connection(self, extended): self._REJFrameClass = AX2516BitRejectFrame self._SREJFrameClass = AX2516BitSelectiveRejectFrame else: - self._log.debug("Initialising AX.25 2.0 mod8 connection") + self._log.debug("Initialising AX.25 2.0/2.2 mod8 connection") # Set the maximum outstanding frames variable self._max_outstanding = self._max_outstanding_mod8 diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 35d3080..73831ce 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -15,9 +15,19 @@ AX25TestFrame, AX25SetAsyncBalancedModeFrame, AX25SetAsyncBalancedModeExtendedFrame, + AX258BitInformationFrame, + AX258BitReceiveReadyFrame, + AX258BitReceiveNotReadyFrame, + AX258BitRejectFrame, + AX258BitSelectiveRejectFrame, + AX2516BitInformationFrame, + AX2516BitReceiveReadyFrame, + AX2516BitReceiveNotReadyFrame, + AX2516BitRejectFrame, + AX2516BitSelectiveRejectFrame, ) from .peer import TestingAX25Peer -from ..mocks import DummyStation +from ..mocks import DummyStation, DummyTimeout # Connection establishment @@ -710,6 +720,111 @@ def _on_conn_rq(**kwargs): assert count == dict(send_dm=1) +# Connection initialisation + + +def test_init_connection_mod8(): + """ + Test _init_connection can initialise a standard mod-8 connection. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Set some dummy data in fields -- this should be cleared out or set + # to sane values. + ack_timer = DummyTimeout(None, None) + peer._send_state = 1 + peer._send_seq = 2 + peer._recv_state = 3 + peer._recv_seq = 4 + peer._ack_state = 5 + peer._modulo = 6 + peer._max_outstanding = 7 + peer._IFrameClass = None + peer._RRFrameClass = None + peer._RNRFrameClass = None + peer._REJFrameClass = None + peer._SREJFrameClass = None + peer._pending_iframes = dict(comment="pending data") + peer._pending_data = ["pending data"] + + peer._init_connection(extended=False) + + # These should be set for a Mod-8 connection + assert peer._max_outstanding == 7 + assert peer._modulo == 8 + assert peer._IFrameClass is AX258BitInformationFrame + assert peer._RRFrameClass is AX258BitReceiveReadyFrame + assert peer._RNRFrameClass is AX258BitReceiveNotReadyFrame + assert peer._REJFrameClass is AX258BitRejectFrame + assert peer._SREJFrameClass is AX258BitSelectiveRejectFrame + + # These should be initialised to initial state + assert peer._send_state == 0 + assert peer._send_seq == 0 + assert peer._recv_state == 0 + assert peer._recv_seq == 0 + assert peer._ack_state == 0 + assert peer._pending_iframes == {} + assert peer._pending_data == [] + + +def test_init_connection_mod128(): + """ + Test _init_connection can initialise a mod-128 connection. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Set some dummy data in fields -- this should be cleared out or set + # to sane values. + ack_timer = DummyTimeout(None, None) + peer._send_state = 1 + peer._send_seq = 2 + peer._recv_state = 3 + peer._recv_seq = 4 + peer._ack_state = 5 + peer._modulo = 6 + peer._max_outstanding = 7 + peer._IFrameClass = None + peer._RRFrameClass = None + peer._RNRFrameClass = None + peer._REJFrameClass = None + peer._SREJFrameClass = None + peer._pending_iframes = dict(comment="pending data") + peer._pending_data = ["pending data"] + + peer._init_connection(extended=True) + + # These should be set for a Mod-128 connection + assert peer._max_outstanding == 127 + assert peer._modulo == 128 + assert peer._IFrameClass is AX2516BitInformationFrame + assert peer._RRFrameClass is AX2516BitReceiveReadyFrame + assert peer._RNRFrameClass is AX2516BitReceiveNotReadyFrame + assert peer._REJFrameClass is AX2516BitRejectFrame + assert peer._SREJFrameClass is AX2516BitSelectiveRejectFrame + + # These should be initialised to initial state + assert peer._send_state == 0 + assert peer._send_seq == 0 + assert peer._recv_state == 0 + assert peer._recv_seq == 0 + assert peer._ack_state == 0 + assert peer._pending_iframes == {} + assert peer._pending_data == [] + + # Connection acceptance and rejection handling From 6e6fedbb89dbd0e74bdc91a002a91917e3db4371 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 11:44:24 +1000 Subject: [PATCH 133/207] peer: Test state change logic --- tests/test_peer/test_state.py | 68 +++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_peer/test_state.py diff --git a/tests/test_peer/test_state.py b/tests/test_peer/test_state.py new file mode 100644 index 0000000..efb8485 --- /dev/null +++ b/tests/test_peer/test_state.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +""" +Test state transition logic +""" + +from aioax25.frame import AX25Address, AX25Path +from .peer import TestingAX25Peer +from ..mocks import DummyStation + +# Idle time-out cancellation + + +def test_state_unchanged(): + """ + Test that _set_conn_state is a no-op if the state is not different. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + state_changes = [] + def _on_state_change(**kwargs): + state_changes.append(kwargs) + + peer.connect_state_changed.connect(_on_state_change) + + assert peer._state is peer.AX25PeerState.DISCONNECTED + + peer._set_conn_state(peer.AX25PeerState.DISCONNECTED) + + assert state_changes == [] + + +def test_state_changed(): + """ + Test that _set_conn_state reports and stores state changes. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + state_changes = [] + def _on_state_change(**kwargs): + state_changes.append(kwargs) + + peer.connect_state_changed.connect(_on_state_change) + + assert peer._state is peer.AX25PeerState.DISCONNECTED + + peer._set_conn_state(peer.AX25PeerState.CONNECTED) + + assert peer._state is peer.AX25PeerState.CONNECTED + assert state_changes[1:] == [] + + change = state_changes.pop(0) + assert change.pop("station") is station + assert change.pop("peer") is peer + assert change.pop("state") is peer.AX25PeerState.CONNECTED + assert change == {} From ade5d9bd1e768ed099e42d2de5a5fad05a0fefec Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 11:49:05 +1000 Subject: [PATCH 134/207] peer: Test DISC UA time-out handling --- tests/test_peer/test_disc.py | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index e4702eb..27951fa 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -15,6 +15,9 @@ from ..mocks import DummyStation, DummyTimeout +# DISC reception handling + + def test_peer_recv_disc(): """ Test when receiving a DISC whilst connected, the peer disconnects. @@ -74,6 +77,9 @@ def test_peer_recv_disc(): assert peer._pending_data == [] +# DISC transmission + + def test_peer_send_disc(): """ Test _send_disc correctly addresses and sends a DISC frame. @@ -103,3 +109,49 @@ def test_peer_send_disc(): assert str(frame.header.destination) == "VK4MSL" assert str(frame.header.source) == "VK4MSL-1" assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" + + +# DISC UA time-out handling + + +def test_peer_ua_timeout_disconnecting(): + """ + Test _on_disc_ua_timeout cleans up the connection if no UA heard + from peer after DISC frame. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, + ) + + peer._state = peer.AX25PeerState.DISCONNECTING + peer._ack_timeout_handle = "time-out handle" + + peer._on_disc_ua_timeout() + + assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._ack_timeout_handle is None + + +def test_peer_ua_timeout_notdisconnecting(): + """ + Test _on_disc_ua_timeout does nothing if not disconnecting. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, + ) + + peer._state = peer.AX25PeerState.CONNECTED + peer._ack_timeout_handle = "time-out handle" + + peer._on_disc_ua_timeout() + + assert peer._state is peer.AX25PeerState.CONNECTED + assert peer._ack_timeout_handle == "time-out handle" From e66a959d0552a062ad16a19e7fea4706900c57b7 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 11:51:28 +1000 Subject: [PATCH 135/207] peer: Check handling of unsolicited UA --- tests/test_peer/test_ua.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_peer/test_ua.py b/tests/test_peer/test_ua.py index bcbfa1b..3fe8ef5 100644 --- a/tests/test_peer/test_ua.py +++ b/tests/test_peer/test_ua.py @@ -14,6 +14,29 @@ from ..mocks import DummyStation +# UA reception + + +def test_peer_recv_ua(): + """ + Test _on_receive_ua does nothing if no UA expected. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, + ) + + peer._on_receive_ua() + + # does nothing + + +# UA transmission + + def test_peer_send_ua(): """ Test _send_ua correctly addresses and sends a UA frame. From 9f3483d88f74b89ab3b5d895d18293245129224e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 11:55:23 +1000 Subject: [PATCH 136/207] peer: Test DM handling --- tests/test_peer/test_dm.py | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index 6986ff8..be262d6 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -10,6 +10,9 @@ from ..mocks import DummyStation, DummyTimeout +# DM reception + + def test_peer_recv_dm(): """ Test when receiving a DM whilst connected, the peer disconnects. @@ -54,6 +57,53 @@ def test_peer_recv_dm(): assert peer._pending_data == [] +def test_peer_recv_dm_disconnected(): + """ + Test when receiving a DM whilst not connected, the peer does nothing. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + interface = station._interface() + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), + full_duplex=True, + ) + + # Set some dummy data in fields -- this should be cleared out. + ack_timer = DummyTimeout(None, None) + peer._ack_timeout_handle = ack_timer + peer._state = peer.AX25PeerState.NEGOTIATING + peer._send_state = 1 + peer._send_seq = 2 + peer._recv_state = 3 + peer._recv_seq = 4 + peer._ack_state = 5 + peer._pending_iframes = dict(comment="pending data") + peer._pending_data = ["pending data"] + + # Pass the peer a DM frame + peer._on_receive( + AX25DisconnectModeFrame( + destination=station.address, source=peer.address, repeaters=None + ) + ) + + # State should be unchanged from before + assert peer._ack_timeout_handle is ack_timer + assert peer._state is peer.AX25PeerState.NEGOTIATING + assert peer._send_state == 1 + assert peer._send_seq == 2 + assert peer._recv_state == 3 + assert peer._recv_seq == 4 + assert peer._ack_state == 5 + assert peer._pending_iframes == dict(comment="pending data") + assert peer._pending_data == ["pending data"] + + +# DM transmission + + def test_peer_send_dm(): """ Test _send_dm correctly addresses and sends a DM frame. From 60015fd6c77dcfdac7bd41670819e97eb2115ded Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 16:43:14 +1000 Subject: [PATCH 137/207] frame: Make U frames set P/F by default Most seem to require this bit to be set, so make that the default, and in the exceptions, we'll set `PF = False` where needed. --- aioax25/frame.py | 4 +--- tests/test_frame/test_uframe.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 3ea0f56..08932e8 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1154,7 +1154,7 @@ class AX25BaseUnnumberedFrame(AX25UnnumberedFrame): """ # Defaults for PF, CR fields - PF = False + PF = True CR = False @classmethod @@ -1216,7 +1216,6 @@ class AX25SetAsyncBalancedModeFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b00101111 CR = True - PF = True AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeFrame) @@ -1232,7 +1231,6 @@ class AX25SetAsyncBalancedModeExtendedFrame(AX25BaseUnnumberedFrame): MODIFIER = 0b01101111 CR = True - PF = True AX25UnnumberedFrame.register(AX25SetAsyncBalancedModeExtendedFrame) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index ba5e19d..fb6814a 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -585,7 +585,7 @@ def test_encode_dm_frame(): bytes(frame), "ac 96 68 84 ae 92 60" # Destination "ac 96 68 9a a6 98 e1" # Source - "0f", # Control + "1f", # Control ) @@ -622,7 +622,7 @@ def test_dm_copy(): bytes(framecopy), "ac 96 68 84 ae 92 60" # Destination "ac 96 68 9a a6 98 e1" # Source - "0f", # Control + "1f", # Control ) From c4009a49c8f213acfbea5693667b628d42a839da Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 16:51:39 +1000 Subject: [PATCH 138/207] kiss: Add some more debug statements --- aioax25/kiss.py | 5 ++++ tests/test_kiss/test_protocol.py | 39 ++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/aioax25/kiss.py b/aioax25/kiss.py index d125ee1..41657c8 100644 --- a/aioax25/kiss.py +++ b/aioax25/kiss.py @@ -417,6 +417,7 @@ def _make_protocol(self): Return a Protocol instance that will handle the KISS traffic for the asyncio transport. """ + self._log.debug("Constructing protocol object") return KISSProtocol( self._on_connect, self._receive, @@ -431,6 +432,7 @@ async def _open_connection(self): # pragma: no cover raise NotImplementedError("Abstract function") def _open(self): + self._log.debug("Awaiting KISS transport") ensure_future(self._open_connection()) def _on_connect(self, transport): @@ -483,6 +485,7 @@ def __init__(self, device, baudrate, *args, **kwargs): self._baudrate = baudrate async def _open_connection(self): + self._log.debug("Delegating to KISS serial device %r", self._device) await create_serial_connection( self._loop, self._make_protocol, @@ -683,6 +686,7 @@ def __init__(self, on_connect, on_receive, on_close, log): def connection_made(self, transport): try: + self._log.debug("Announcing connection: %r", transport) self._on_connect(transport) except: self._log.exception("Failed to handle connection establishment") @@ -717,6 +721,7 @@ def __init__(self, on_connect, on_receive, on_close, log): def connection_made(self, transport): try: + self._log.debug("Announcing connection: %r", transport) self._on_connect(transport) except: self._log.exception("Failed to handle connection establishment") diff --git a/tests/test_kiss/test_protocol.py b/tests/test_kiss/test_protocol.py index 9ab2816..b6146cf 100644 --- a/tests/test_kiss/test_protocol.py +++ b/tests/test_kiss/test_protocol.py @@ -12,6 +12,9 @@ class DummyTransport(object): def __init__(self): self.closed = False + def __repr__(self): + return "" + def close(self): assert self.closed is False, "Already closed" self.closed = True @@ -48,9 +51,16 @@ def on_close(*args, **kwargs): assert on_connect_calls == [((transport,), {})] assert transport.closed is False - assert logger.logrecords == [] + assert logger.logrecords[1:] == [] assert logger.children == {} + log = logger.logrecords[0] + assert log["ex_tb"] is None + assert log["ex_type"] is None + assert log["ex_val"] is None + assert log["args"][0] == "Announcing connection: %r" + assert log["args"][1] is transport + def test_protocol_connection_made_err(logger): """ @@ -80,9 +90,16 @@ def on_close(*args, **kwargs): assert logger.children == {} assert len(logger.logrecords) > 0 - assert logger.logrecords[1:] == [] + assert logger.logrecords[2:] == [] log = logger.logrecords[0] + assert log["ex_tb"] is None + assert log["ex_type"] is None + assert log["ex_val"] is None + assert log["args"][0] == "Announcing connection: %r" + assert log["args"][1] is transport + + log = logger.logrecords[1] assert log.pop("ex_type", None) == TestError log.pop("ex_val", None) log.pop("ex_tb", None) @@ -279,9 +296,16 @@ def on_close(*args, **kwargs): assert on_connect_calls == [((transport,), {})] assert transport.closed is False - assert logger.logrecords == [] + assert logger.logrecords[1:] == [] assert logger.children == {} + log = logger.logrecords[0] + assert log["ex_tb"] is None + assert log["ex_type"] is None + assert log["ex_val"] is None + assert log["args"][0] == "Announcing connection: %r" + assert log["args"][1] is transport + def test_subproc_protocol_connection_made_err(logger): """ @@ -313,9 +337,16 @@ def on_close(*args, **kwargs): assert logger.children == {} assert len(logger.logrecords) > 0 - assert logger.logrecords[1:] == [] + assert logger.logrecords[2:] == [] log = logger.logrecords[0] + assert log["ex_tb"] is None + assert log["ex_type"] is None + assert log["ex_val"] is None + assert log["args"][0] == "Announcing connection: %r" + assert log["args"][1] is transport + + log = logger.logrecords[1] assert log.pop("ex_type", None) == TestError log.pop("ex_val", None) log.pop("ex_tb", None) From dccf9e314f9205f79118a48849bfcb804ec9623c Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 16:52:10 +1000 Subject: [PATCH 139/207] router: Add some debug statements for bind/unbind. --- aioax25/router.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aioax25/router.py b/aioax25/router.py index 642273c..a8ed60e 100644 --- a/aioax25/router.py +++ b/aioax25/router.py @@ -53,6 +53,10 @@ def bind(self, callback, callsign, ssid=0, regex=False): else: call_receivers = self._receiver_str.setdefault(callsign, {}) + self._log.debug( + "Binding callsign %r (regex %r) SSID %r to %r", + callsign, regex, ssid, callback + ) call_receivers.setdefault(ssid, []).append(callback) def unbind(self, callback, callsign, ssid=0, regex=False): @@ -73,6 +77,10 @@ def unbind(self, callback, callsign, ssid=0, regex=False): try: ssid_receivers.remove(callback) + self._log.debug( + "Unbound callsign %r (regex %r) SSID %r to %r", + callsign, regex, ssid, callback + ) except ValueError: return From af1e05ecd82d66f4a302e5bee265d206c3d117c0 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 16:53:43 +1000 Subject: [PATCH 140/207] peer: Implement `send()` method Not fully certain this is correct, but it seems to be working somewhat. --- aioax25/peer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index a326e59..8c99195 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -128,6 +128,7 @@ def __init__( full_duplex, reply_path=None, locked_path=False, + max_segment_sz=64, # TODO: figure out max segment size ): """ Create a peer context for the station named by 'address'. @@ -149,6 +150,7 @@ def __init__( self._rr_delay = rr_delay self._rr_interval = rr_interval self._rnr_interval = rnr_interval + self._max_segment_sz = max_segment_sz self._locked_path = bool(locked_path) self._protocol = protocol self._log = log @@ -360,6 +362,18 @@ def disconnect(self): self._state.name, ) + def send(self, payload, pid=AX25Frame.PID_NO_L3): + """ + Send the given payload data to the remote station. + """ + while payload: + block = payload[:self._max_segment_sz] + payload = payload[self._max_segment_sz:] + + self._pending_data.append((pid, block)) + + self._send_next_iframe() + def _cancel_idle_timeout(self): """ Cancel the idle timeout handle From d3d0efc2a4a9f6e75ce465f564dfc1a6b4e8f021 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 16:56:44 +1000 Subject: [PATCH 141/207] peer: `pf` bit is in the frame not its header. --- aioax25/peer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 8c99195..fc7fea8 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -582,7 +582,7 @@ def _on_receive_sframe(self, frame): self._on_receive_srej(frame) def _on_receive_rr(self, frame): - if frame.header.pf: + if frame.pf: # Peer requesting our RR status self._log.debug("RR status requested from peer") self._on_receive_rr_rnr_rej_query() @@ -596,7 +596,7 @@ def _on_receive_rr(self, frame): self._send_next_iframe() def _on_receive_rnr(self, frame): - if frame.header.pf: + if frame.pf: # Peer requesting our RNR status self._log.debug("RNR status requested from peer") self._on_receive_rr_rnr_rej_query() @@ -608,7 +608,7 @@ def _on_receive_rnr(self, frame): self._peer_busy = True def _on_receive_rej(self, frame): - if frame.header.pf: + if frame.pf: # Peer requesting rejected frame status. self._log.debug("REJ status requested from peer") self._on_receive_rr_rnr_rej_query() @@ -624,7 +624,7 @@ def _on_receive_rej(self, frame): self._send_next_iframe() def _on_receive_srej(self, frame): - if frame.header.pf: + if frame.pf: # AX.25 2.2 sect 4.3.2.4: "If the P/F bit in the SREJ is set to # '1', then I frames numbered up to N(R)-1 inclusive are considered # as acknowledged." From ef82c49c0e47724d56321914f35d78c255d98616 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 2 Apr 2023 17:04:40 +1000 Subject: [PATCH 142/207] peer: Send `SABM` to connecting station after ACKing its `SABM` Seems we need to send a `SABM` back the other way and wait for the `UA` from the remote station, _then_ we are officially connected. There's still some bugs, but this code does result in a connection at least. --- aioax25/peer.py | 10 ++++++++-- tests/test_peer/test_connection.py | 12 +++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index fc7fea8..c5e921e 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -320,8 +320,9 @@ def accept(self): self._log.info("Accepting incoming connection") # Send a UA and set ourselves as connected self._stop_ack_timer() - self._set_conn_state(self.AX25PeerState.CONNECTED) self._send_ua() + self._uaframe_handler = self._on_connect_sabm_ua + self._send_sabm() else: self._log.info( "Will not accept connection from peer now, " @@ -772,6 +773,10 @@ def _on_incoming_connect_timeout(self): "Incoming connection time-out in state %s", self._state.name ) + def _on_connect_sabm_ua(self): + self._log.info("Connection accepted by peer") + self._set_conn_state(self.AX25PeerState.CONNECTED) + def _on_connect_response(self, response, **kwargs): # Handle the connection result. self._log.debug("Connection response: %r", response) @@ -1185,7 +1190,8 @@ def _send_sabm(self): repeaters=self.reply_path, ) ) - self._set_conn_state(self.AX25PeerState.CONNECTING) + if self._state is not self.AX25PeerState.INCOMING_CONNECTION: + self._set_conn_state(self.AX25PeerState.CONNECTING) def _send_xid(self, cr): self._transmit_frame( diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 73831ce..8cb1b2f 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -862,7 +862,7 @@ def _send_ua(): def test_accept_incoming_ua(): """ - Test calling .accept() with incoming connection sends UA. + Test calling .accept() with incoming connection sends UA then SABM. """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = TestingAX25Peer( @@ -888,11 +888,17 @@ def _send_ua(): peer._send_ua = _send_ua + def _send_sabm(): + actions.append("sent-sabm") + + peer._send_sabm = _send_sabm + # Try accepting a ficticious connection peer.accept() - assert peer._state == peer.AX25PeerState.CONNECTED - assert actions == ["stop-connect-timer", "sent-ua"] + assert peer._state is peer.AX25PeerState.INCOMING_CONNECTION + assert actions == ["stop-connect-timer", "sent-ua", "sent-sabm"] + assert peer._uaframe_handler == peer._on_connect_sabm_ua def test_reject_connected_noop(): From 498625497eac4804d8dcd696ba770b51af497e14 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:12:07 +1000 Subject: [PATCH 143/207] peer: Expect SABMs both ways when connecting. We UA their SABM, then send them a SABM for them to UA. --- aioax25/peer.py | 34 ++++-- tests/test_peer/test_peerconnection.py | 160 ++++++++++++++++++++++--- 2 files changed, 168 insertions(+), 26 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index c5e921e..91fe569 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -368,8 +368,8 @@ def send(self, payload, pid=AX25Frame.PID_NO_L3): Send the given payload data to the remote station. """ while payload: - block = payload[:self._max_segment_sz] - payload = payload[self._max_segment_sz:] + block = payload[: self._max_segment_sz] + payload = payload[self._max_segment_sz :] self._pending_data.append((pid, block)) @@ -1481,6 +1481,8 @@ def __init__(self, peer): peer, peer._ack_timeout ) self._retries = peer._max_retries + self._our_sabm_acked = False + self._their_sabm_acked = False def _go(self): if self.peer._negotiated: @@ -1536,15 +1538,31 @@ def _on_negotiated(self, response, **kwargs): def _on_receive_ua(self): # Peer just acknowledged our connection - self._log.debug("UA received, connection established") - self.peer._init_connection(self.peer._modulo128) - self._finish(response="ack") + self._log.debug("UA received") + self._our_sabm_acked = True + self._check_connection_init() def _on_receive_sabm(self): - # Peer was connecting to us, we'll treat this as a UA. - self._log.debug("SABM received, connection established") + # Peer sent us a SABM. + self._log.debug("SABM received, sending UA") self.peer._send_ua() - self._finish(response="ack") + self._their_sabm_acked = True + self._check_connection_init() + + def _check_connection_init(self): + self._log.debug( + "UA status: ours=%s theirs=%s", + self._our_sabm_acked, + self._their_sabm_acked, + ) + if not self._our_sabm_acked: + self._log.debug("Waiting for peer to send UA for our SABM") + elif not self._their_sabm_acked: + self._log.debug("Waiting for peer's SABM to us") + else: + self._log.info("Connection is established") + self.peer._init_connection(self.peer._modulo128) + self._finish(response="ack") def _on_receive_frmr(self): # Peer just rejected our connect frame, begin FRMR recovery. diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index b11923e..01a621e 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -287,9 +287,9 @@ def test_peerconn_on_negotiated_xid(): assert callback is None -def test_peerconn_receive_ua(): +def test_peerconn_check_connection_init(): """ - Test _on_receive_ua ends the helper + Test _check_connection_init finalises connection if both SABMs ACKed """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = DummyPeer(station, AX25Address("VK4MSL")) @@ -298,6 +298,10 @@ def test_peerconn_receive_ua(): # Assume we're using modulo-8 mode peer._modulo128 = False + # Mark SABMs ACKed + helper._our_sabm_acked = True + helper._their_sabm_acked = True + # Nothing should be set up assert helper._timeout_handle is None assert not helper._done @@ -315,8 +319,8 @@ def _init_connection(extended): done_evts = [] helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - # Call _on_receive_ua - helper._on_receive_ua() + # Call _check_connection_init + helper._check_connection_init() # We should have initialised the connection assert count == dict(init=1) @@ -326,9 +330,9 @@ def _init_connection(extended): assert done_evts == [{"response": "ack"}] -def test_peerconn_receive_ua_mod128(): +def test_peerconn_check_connection_init_mod128(): """ - Test _on_receive_ua handles Mod128 mode + Test _check_connection_init finalises mod128 connections too """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = DummyPeer(station, AX25Address("VK4MSL")) @@ -337,6 +341,10 @@ def test_peerconn_receive_ua_mod128(): # Assume we're using modulo-128 mode peer._modulo128 = True + # Mark SABMs ACKed + helper._our_sabm_acked = True + helper._their_sabm_acked = True + # Nothing should be set up assert helper._timeout_handle is None assert not helper._done @@ -354,8 +362,8 @@ def _init_connection(extended): done_evts = [] helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - # Call _on_receive_ua - helper._on_receive_ua() + # Call _check_connection_init + helper._check_connection_init() # We should have initialised the connection assert count == dict(init=1) @@ -365,6 +373,124 @@ def _init_connection(extended): assert done_evts == [{"response": "ack"}] +def test_peerconn_check_connection_init_notoursabm(): + """ + Test _check_connection_init does nothing if our SABM not ACKed + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Assume we're using modulo-8 mode + peer._modulo128 = False + + # Mark their SABM ACKed, but not ours + helper._our_sabm_acked = False + helper._their_sabm_acked = True + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + + # Stub peer _init_connection + count = dict(init=0) + + def _init_connection(extended): + assert extended is False, "Should be in Modulo-8 mode" + count["init"] += 1 + + peer._init_connection = _init_connection + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) + + # Call _check_connection_init + helper._check_connection_init() + + # We should NOT have initialised the connection + assert count == dict(init=0) + + # See that the helper is NOT finished + assert helper._done is False + assert done_evts == [] + + +def test_peerconn_check_connection_init_nottheirsabm(): + """ + Test _check_connection_init does nothing if their SABM not ACKed + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Assume we're using modulo-8 mode + peer._modulo128 = False + + # Mark our SABM ACKed, but not theirs + helper._our_sabm_acked = True + helper._their_sabm_acked = False + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + + # Stub peer _init_connection + count = dict(init=0) + + def _init_connection(extended): + assert extended is False, "Should be in Modulo-8 mode" + count["init"] += 1 + + peer._init_connection = _init_connection + + # Hook the done signal + done_evts = [] + helper.done_sig.connect(lambda **kw: done_evts.append(kw)) + + # Call _check_connection_init + helper._check_connection_init() + + # We should NOT have initialised the connection + assert count == dict(init=0) + + # See that the helper is NOT finished + assert helper._done is False + assert done_evts == [] + + +def test_peerconn_receive_ua(): + """ + Test _on_receive_ua marks the SABM as ACKed + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = DummyPeer(station, AX25Address("VK4MSL")) + helper = AX25PeerConnectionHandler(peer) + + # Nothing should be set up + assert helper._timeout_handle is None + assert not helper._done + + # Stub helper _check_connection_init + count = dict(check=0) + + def _check_connection_init(): + count["check"] += 1 + + helper._check_connection_init = _check_connection_init + + assert helper._our_sabm_acked is False + + # Call _on_receive_ua + helper._on_receive_ua() + + # Our SABM should be marked as ACKed + assert helper._our_sabm_acked is True + + # We should have checked the ACK status + assert count == dict(check=1) + + def test_peerconn_receive_sabm(): """ Test _on_receive_sabm ends the helper @@ -378,26 +504,24 @@ def test_peerconn_receive_sabm(): assert not helper._done # Stub peer _send_ua - count = dict(send_ua=0) + count = dict(send_ua=0, check=0) def _send_ua(): count["send_ua"] += 1 peer._send_ua = _send_ua - # Hook the done signal - done_evts = [] - helper.done_sig.connect(lambda **kw: done_evts.append(kw)) + # Stub helper _check_connection_init + def _check_connection_init(): + count["check"] += 1 + + helper._check_connection_init = _check_connection_init # Call _on_receive_sabm helper._on_receive_sabm() - # We should have ACKed the SABM - assert count == dict(send_ua=1) - - # See that the helper finished - assert helper._done is True - assert done_evts == [{"response": "ack"}] + # We should have ACKed the SABM and checked the connection status + assert count == dict(send_ua=1, check=1) def test_peerconn_receive_frmr(): From 62457a8c2899a6aa282798975e509bdddbe7fee5 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:20:36 +1000 Subject: [PATCH 144/207] peer: Rename parameter to `paclen`, set to 128. As consistent with existing TNCs on the market. --- aioax25/peer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 91fe569..5b01d2f 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -128,7 +128,7 @@ def __init__( full_duplex, reply_path=None, locked_path=False, - max_segment_sz=64, # TODO: figure out max segment size + paclen=128, ): """ Create a peer context for the station named by 'address'. @@ -150,7 +150,7 @@ def __init__( self._rr_delay = rr_delay self._rr_interval = rr_interval self._rnr_interval = rnr_interval - self._max_segment_sz = max_segment_sz + self._paclen = paclen self._locked_path = bool(locked_path) self._protocol = protocol self._log = log @@ -368,8 +368,8 @@ def send(self, payload, pid=AX25Frame.PID_NO_L3): Send the given payload data to the remote station. """ while payload: - block = payload[: self._max_segment_sz] - payload = payload[self._max_segment_sz :] + block = payload[: self._paclen] + payload = payload[self._paclen :] self._pending_data.append((pid, block)) From dc591ef40168416f78a5e57b5f968f1950d987a0 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:21:22 +1000 Subject: [PATCH 145/207] router: `python3 -m black` clean-up --- aioax25/router.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aioax25/router.py b/aioax25/router.py index a8ed60e..67380ae 100644 --- a/aioax25/router.py +++ b/aioax25/router.py @@ -55,7 +55,10 @@ def bind(self, callback, callsign, ssid=0, regex=False): self._log.debug( "Binding callsign %r (regex %r) SSID %r to %r", - callsign, regex, ssid, callback + callsign, + regex, + ssid, + callback, ) call_receivers.setdefault(ssid, []).append(callback) @@ -79,7 +82,10 @@ def unbind(self, callback, callsign, ssid=0, regex=False): ssid_receivers.remove(callback) self._log.debug( "Unbound callsign %r (regex %r) SSID %r to %r", - callsign, regex, ssid, callback + callsign, + regex, + ssid, + callback, ) except ValueError: return From cd5236153297ca160b6bbf4f9699c6a8e9080967 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:21:36 +1000 Subject: [PATCH 146/207] peer unit tests: `python3 -m black` clean-up --- tests/test_peer/test_disc.py | 4 ++-- tests/test_peer/test_state.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index 27951fa..d08ee64 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -126,7 +126,7 @@ def test_peer_ua_timeout_disconnecting(): repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), full_duplex=True, ) - + peer._state = peer.AX25PeerState.DISCONNECTING peer._ack_timeout_handle = "time-out handle" @@ -147,7 +147,7 @@ def test_peer_ua_timeout_notdisconnecting(): repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), full_duplex=True, ) - + peer._state = peer.AX25PeerState.CONNECTED peer._ack_timeout_handle = "time-out handle" diff --git a/tests/test_peer/test_state.py b/tests/test_peer/test_state.py index efb8485..73c265d 100644 --- a/tests/test_peer/test_state.py +++ b/tests/test_peer/test_state.py @@ -24,6 +24,7 @@ def test_state_unchanged(): ) state_changes = [] + def _on_state_change(**kwargs): state_changes.append(kwargs) @@ -49,6 +50,7 @@ def test_state_changed(): ) state_changes = [] + def _on_state_change(**kwargs): state_changes.append(kwargs) From 468c1ec40a32dab4d70ad50cfa95c35aa6b1577d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:23:44 +1000 Subject: [PATCH 147/207] peer: Add `received_frame` and `sent_frame` signals For packet logging and debugging purposes. --- aioax25/peer.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 5b01d2f..10375ce 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -18,6 +18,7 @@ AX25SetAsyncBalancedModeFrame, AX25SetAsyncBalancedModeExtendedFrame, AX25ExchangeIdentificationFrame, + AX25UnnumberedFrame, AX25UnnumberedAcknowledgeFrame, AX25TestFrame, AX25DisconnectFrame, @@ -214,6 +215,12 @@ def __init__( # Signals: + # Fired when any frame is received from the peer + self.received_frame = Signal() + + # Fired when any frame is sent to the peer + self.sent_frame = Signal() + # Fired when an I-frame is received self.received_information = Signal() @@ -463,6 +470,10 @@ def _on_receive(self, frame): self._log.debug("Dropping frame due to FRMR: %s", frame) return + # Is this a U frame? I frames and S frames must be decoded elsewhere. + if isinstance(frame, AX25UnnumberedFrame): + self.received_frame.emit(frame=frame, peer=self) + if isinstance(frame, AX25TestFrame): # TEST frame return self._on_receive_test(frame) @@ -498,6 +509,7 @@ def _on_receive(self, frame): frame = AX25Frame.decode( frame, modulo128=(self._modulo == 128) ) + self.received_frame.emit(frame=frame, peer=self) if isinstance(frame, AX25InformationFrameMixin): # This is an I-frame return self._on_receive_iframe(frame) @@ -513,6 +525,7 @@ def _on_receive(self, frame): self._log.debug( "Received I or S frame in state %s", self._state.name ) + self.received_frame.emit(frame=frame, peer=self) return self._send_dm() def _on_receive_iframe(self, frame): @@ -1413,6 +1426,8 @@ def _transmit_iframe(self, ns): ) def _transmit_frame(self, frame, callback=None): + self.sent_frame.emit(frame=frame, peer=self) + # Update the last activity timestamp self._last_act = self._loop.time() From 5040b86c4f3306fecc36fc3ff7e0004fcf99de6b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:28:37 +1000 Subject: [PATCH 148/207] pytest.ini: Set some pytest options. --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..6c2449b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --log-level=DEBUG --cov=aioax25 --cov-report=term --cov-report=html --cov-branch From cb1d09a8b88840cc97bed3cfbe989c39499ba1a9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sun, 16 Apr 2023 14:49:15 +1000 Subject: [PATCH 149/207] peer: Add some logging statements to track state variables. --- aioax25/peer.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 10375ce..bc175bc 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -10,6 +10,7 @@ import weakref import enum +import logging from .version import AX25Version from .frame import ( @@ -164,11 +165,16 @@ def __init__( self._negotiated = False # Set to True after XID negotiation self._connected = False # Set to true on SABM UA self._last_act = 0 # Time of last activity - self._send_state = 0 # AKA V(S) + self._send_state=0 # AKA V(S) + self._send_state_name = "V(S)" self._send_seq = 0 # AKA N(S) - self._recv_state = 0 # AKA V(R) + self._send_seq_name = "N(S)" + self._recv_state=0 # AKA V(R) + self._recv_state_name = "V(R)" self._recv_seq = 0 # AKA N(R) + self._recv_seq_name = "N(R)" self._ack_state = 0 # AKA V(A) + self._ack_state_name = "V(A)" self._local_busy = False # Local end busy, respond to # RR and I-frames with RNR. self._peer_busy = False # Peer busy, await RR. @@ -560,7 +566,7 @@ def _on_receive_iframe(self, frame): # "…it accepts the received I frame, # increments its receive state variable, and acts in one of the following # manners:…" - self._recv_state = (self._recv_state + 1) % self._modulo + self._update_state("_recv_state", delta=1) # TODO: the payload here may be a repeat of data already seen, or # for _future_ data (i.e. there's an I-frame that got missed in between @@ -634,7 +640,7 @@ def _on_receive_rej(self, frame): self._ack_outstanding((frame.nr - 1) % self._modulo) # AX.25 2.2 section 6.4.7 says we set V(S) to this frame's # N(R) and begin re-transmission. - self._send_state = frame.nr + self._update_state("_send_state", value=frame.nr) self._send_next_iframe() def _on_receive_srej(self, frame): @@ -666,8 +672,13 @@ def _ack_outstanding(self, nr): """ self._log.debug("%d through to %d are received", self._send_state, nr) while self._send_state != nr: - self._pending_iframes.pop(self._send_state) - self._send_state = (self._send_state + 1) % self._modulo + frame = self._pending_iframes.pop(self._send_state) + if self._log.isEnabledFor(logging.DEBUG): + self._log.debug( + "Popped %s off pending queue, N(R)s pending: %s", + frame, ", ".join(sorted(self._pending_iframes.keys())) + ) + self._update_state("_send_state", delta=1) def _on_receive_test(self, frame): self._log.debug("Received TEST response: %s", frame) @@ -888,9 +899,9 @@ def _reset_connection_state(self): self._log.debug("Resetting the peer state") # Reset our state - self._send_state = 0 # AKA V(S) + self._update_state("_send_state", value=0) # AKA V(S) self._send_seq = 0 # AKA N(S) - self._recv_state = 0 # AKA V(R) + self._update_state("_recv_state", value=0) # AKA V(R) self._recv_seq = 0 # AKA N(R) self._ack_state = 0 # AKA V(A) @@ -1405,7 +1416,7 @@ def _send_next_iframe(self): # "After the I frame is sent, the send state variable is incremented # by one." - self._send_state = (self._send_state + 1) % self._modulo + self._update_state("_send_state", delta=1) def _transmit_iframe(self, ns): """ @@ -1435,6 +1446,17 @@ def _transmit_frame(self, frame, callback=None): self._reset_idle_timeout() return self._station()._interface().transmit(frame, callback=None) + def _update_state(self, prop, delta=None, value=None): + if value is None: + value = getattr(self, prop) + + if delta is not None: + value += delta + value %= self._modulo + + self._log.debug("%s = %s", getattr(self, "%s_name" % prop), value) + setattr(self, prop, value) + class AX25PeerHelper(object): """ From edc4a871d890329d3739bd9127f2773eb785f228 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 17 Apr 2023 06:53:59 +1000 Subject: [PATCH 150/207] `python3 -m black` code clean-up --- aioax25/peer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index bc175bc..89bb439 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -165,11 +165,11 @@ def __init__( self._negotiated = False # Set to True after XID negotiation self._connected = False # Set to true on SABM UA self._last_act = 0 # Time of last activity - self._send_state=0 # AKA V(S) + self._send_state = 0 # AKA V(S) self._send_state_name = "V(S)" self._send_seq = 0 # AKA N(S) self._send_seq_name = "N(S)" - self._recv_state=0 # AKA V(R) + self._recv_state = 0 # AKA V(R) self._recv_state_name = "V(R)" self._recv_seq = 0 # AKA N(R) self._recv_seq_name = "N(R)" @@ -676,7 +676,8 @@ def _ack_outstanding(self, nr): if self._log.isEnabledFor(logging.DEBUG): self._log.debug( "Popped %s off pending queue, N(R)s pending: %s", - frame, ", ".join(sorted(self._pending_iframes.keys())) + frame, + ", ".join(sorted(self._pending_iframes.keys())), ) self._update_state("_send_state", delta=1) From 611ee2c8e830fe44eabde16457e193a0a2523d63 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 19 Oct 2023 14:54:03 +1000 Subject: [PATCH 151/207] frame unit tests: Translate `nosecompat` tests to bare `assert` --- tests/test_frame/test_ax25frame.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 40e6396..38bab00 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -11,6 +11,7 @@ AX258BitInformationFrame, AX2516BitInformationFrame, ) + from ..hex import from_hex, hex_cmp # Basic frame operations @@ -343,8 +344,9 @@ def test_rr_frame_str(): destination="VK4BWI", source="VK4MSL", nr=6 ) - assert str(frame) == ( - "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame" + assert ( + str(frame) + == "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame" ) From 12dcc1bff16f8726e73e076a8c262ac7ade352d1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 19 Oct 2023 15:15:07 +1000 Subject: [PATCH 152/207] frame unit tests: Retire `eq_` from nosetests --- tests/test_frame/test_ax25frame.py | 10 +++++----- tests/test_frame/test_sframe.py | 5 +++-- tests/test_frame/test_uframe.py | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 38bab00..b8c66ff 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -193,7 +193,7 @@ def test_ui_str(): pid=0xF0, payload=b"This is a test", ) - assert str(frame) == "VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'" + assert str(frame) == ("VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'") def test_ui_tnc2(): @@ -344,9 +344,8 @@ def test_rr_frame_str(): destination="VK4BWI", source="VK4MSL", nr=6 ) - assert ( - str(frame) - == "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame" + assert str(frame) == ( + "VK4MSL>VK4BWI: N(R)=6 P/F=False " "AX258BitReceiveReadyFrame" ) @@ -434,7 +433,8 @@ def test_iframe_str(): ) assert str(frame) == ( - "VK4MSL>VK4BWI: N(R)=6 P/F=True N(S)=2 PID=0xff " + "VK4MSL>VK4BWI: " + "N(R)=6 P/F=True N(S)=2 PID=0xff " "Payload=b'Testing 1 2 3'" ) diff --git a/tests/test_frame/test_sframe.py b/tests/test_frame/test_sframe.py index 947fcca..9c6b78b 100644 --- a/tests/test_frame/test_sframe.py +++ b/tests/test_frame/test_sframe.py @@ -142,8 +142,9 @@ def test_rr_frame_str(): destination="VK4BWI", source="VK4MSL", nr=6 ) - assert str(frame) == ( - "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame", + assert ( + str(frame) + == "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame" ) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index fb6814a..761679e 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -99,7 +99,7 @@ def test_decode_sabme_payload(): ) assert False, "This should not have worked" except ValueError as e: - eq_(str(e), "Frame does not support payload") + assert str(e) == "Frame does not support payload" def test_decode_uframe_payload(): From 12c5552b609836b7389a79f917a7839e34f391ce Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 19 Oct 2023 15:23:41 +1000 Subject: [PATCH 153/207] frame: Expand `str()` operator for further debugging data. We no longer rely on this being TNC-2 compatible, there's a `tnc2` property for that. --- aioax25/frame.py | 54 ++++++++++++++++++++++++++---- tests/test_frame/test_ax25frame.py | 17 +++++++--- tests/test_frame/test_iframe.py | 3 +- tests/test_frame/test_sframe.py | 5 ++- tests/test_frame/test_uframe.py | 10 ++++-- 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index 08932e8..2c9a765 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -192,7 +192,11 @@ def __bytes__(self): return bytes(self._encode()) def __str__(self): - return str(self._header) + return "%s %s:\nPayload=%r" % ( + self.__class__.__name__, + self.header, + self.frame_payload + ) @property def timestamp(self): @@ -281,6 +285,14 @@ def __init__( deadline=deadline, ) + def __str__(self): + return "%s %s: Control=0x%02x\nPayload=%r" % ( + self.__class__.__name__, + self.header, + self.control, + self.payload + ) + @property def control(self): return self._control @@ -333,6 +345,14 @@ def __init__( deadline=deadline, ) + def __str__(self): + return "%s %s: Control=0x%04x\nPayload=%r" % ( + self.__class__.__name__, + self.header, + self.control, + self.payload + ) + @property def control(self): return self._control @@ -475,6 +495,15 @@ def __init__( self._pf = bool(pf) self._modifier = int(modifier) & self.MODIFIER_MASK + def __str__(self): + return "%s %s: Control=0x%02x P/F=%s Modifier=0x%02x" % ( + self.__class__.__name__, + self.header, + self.control, + self.pf, + self.modifier, + ) + @property def _control(self): """ @@ -608,7 +637,8 @@ def _control(self): ) def __str__(self): - return "%s: N(R)=%d P/F=%s N(S)=%d PID=0x%02x Payload=%r" % ( + return "%s %s: N(R)=%d P/F=%s N(S)=%d PID=0x%02x\nPayload=%r" % ( + self.__class__.__name__, self.header, self.nr, self.pf, @@ -724,11 +754,12 @@ def _control(self): ) def __str__(self): - return "%s: N(R)=%d P/F=%s %s" % ( + return "%s %s: N(R)=%d P/F=%s Code=0x%02x" % ( + self.__class__.__name__, self.header, self.nr, self.pf, - self.__class__.__name__, + self.code, ) def _copy(self): @@ -935,8 +966,15 @@ def frame_payload(self): ) def __str__(self): - return "%s: PID=0x%02x Payload=%r" % ( + return ( + "%s %s: Control=0x%02x P/F=%s Modifier=0x%02x " + "PID=0x%02x\nPayload=%r" + ) % ( + self.__class__.__name__, self.header, + self.control, + self.pf, + self.modifier, self.pid, self.payload, ) @@ -1160,7 +1198,11 @@ class AX25BaseUnnumberedFrame(AX25UnnumberedFrame): @classmethod def decode(cls, header, control, data): if len(data): - raise ValueError("Frame does not support payload") + raise ValueError( + "Frame does not support payload" + " (header=%s control=0x%02x data=%s)" + % (header, control, data.hex()) + ) return cls( destination=header.destination, diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index b8c66ff..4b309d9 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -179,7 +179,9 @@ def test_raw_str(): frame = AX25RawFrame( destination="VK4BWI", source="VK4MSL", payload=b"\xabThis is a test" ) - assert str(frame) == "VK4MSL>VK4BWI" + assert str(frame) == ( + "AX25RawFrame VK4MSL>VK4BWI:\n" "Payload=b'\\xabThis is a test'" + ) def test_ui_str(): @@ -193,7 +195,11 @@ def test_ui_str(): pid=0xF0, payload=b"This is a test", ) - assert str(frame) == ("VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'") + assert str(frame) == ( + "AX25UnnumberedInformationFrame VK4MSL>VK4BWI: " + "Control=0x03 P/F=False Modifier=0x03 PID=0xf0\n" + "Payload=b'This is a test'" + ) def test_ui_tnc2(): @@ -345,7 +351,8 @@ def test_rr_frame_str(): ) assert str(frame) == ( - "VK4MSL>VK4BWI: N(R)=6 P/F=False " "AX258BitReceiveReadyFrame" + "AX258BitReceiveReadyFrame VK4MSL>VK4BWI: N(R)=6 P/F=False " + "Code=0x00" ) @@ -433,8 +440,8 @@ def test_iframe_str(): ) assert str(frame) == ( - "VK4MSL>VK4BWI: " - "N(R)=6 P/F=True N(S)=2 PID=0xff " + "AX258BitInformationFrame VK4MSL>VK4BWI: " + "N(R)=6 P/F=True N(S)=2 PID=0xff\n" "Payload=b'Testing 1 2 3'" ) diff --git a/tests/test_frame/test_iframe.py b/tests/test_frame/test_iframe.py index 869883f..5839e27 100644 --- a/tests/test_frame/test_iframe.py +++ b/tests/test_frame/test_iframe.py @@ -72,7 +72,8 @@ def test_iframe_str(): ) assert str(frame) == ( - "VK4MSL>VK4BWI: N(R)=6 P/F=True N(S)=2 PID=0xff " + "AX258BitInformationFrame VK4MSL>VK4BWI: " + "N(R)=6 P/F=True N(S)=2 PID=0xff\n" "Payload=b'Testing 1 2 3'" ) diff --git a/tests/test_frame/test_sframe.py b/tests/test_frame/test_sframe.py index 9c6b78b..dbbd966 100644 --- a/tests/test_frame/test_sframe.py +++ b/tests/test_frame/test_sframe.py @@ -144,7 +144,10 @@ def test_rr_frame_str(): assert ( str(frame) - == "VK4MSL>VK4BWI: N(R)=6 P/F=False AX258BitReceiveReadyFrame" + == ( + "AX258BitReceiveReadyFrame VK4MSL>VK4BWI: N(R)=6 P/F=False " + "Code=0x00" + ) ) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index 761679e..54dd826 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -65,7 +65,7 @@ def test_decode_sabm_payload(): ) assert False, "This should not have worked" except ValueError as e: - assert str(e) == "Frame does not support payload" + assert str(e).startswith("Frame does not support payload") def test_decode_sabme(): @@ -99,7 +99,7 @@ def test_decode_sabme_payload(): ) assert False, "This should not have worked" except ValueError as e: - assert str(e) == "Frame does not support payload" + assert str(e).startswith("Frame does not support payload") def test_decode_uframe_payload(): @@ -689,4 +689,8 @@ def test_ui_str(): pid=0xF0, payload=b"This is a test", ) - assert str(frame) == "VK4MSL>VK4BWI: PID=0xf0 Payload=b'This is a test'" + assert str(frame) == ( + "AX25UnnumberedInformationFrame VK4MSL>VK4BWI: " + "Control=0x03 P/F=False Modifier=0x03 PID=0xf0\n" + "Payload=b'This is a test'" + ) From 147961d024d182325734832e49ab60999a6eaea2 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Thu, 19 Oct 2023 15:27:03 +1000 Subject: [PATCH 154/207] peer: Do not send SABM on incoming connection Observing the behaviour of `axcall` connecting to `ax25d` across a KISS connection, suggests this is wrong, we should just send `UA` to `SABM` and be done. --- aioax25/peer.py | 8 ++------ tests/test_peer/test_connection.py | 13 +++++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 89bb439..acbec12 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -334,8 +334,8 @@ def accept(self): # Send a UA and set ourselves as connected self._stop_ack_timer() self._send_ua() - self._uaframe_handler = self._on_connect_sabm_ua - self._send_sabm() + self._log.info("Connection accepted") + self._set_conn_state(self.AX25PeerState.CONNECTED) else: self._log.info( "Will not accept connection from peer now, " @@ -798,10 +798,6 @@ def _on_incoming_connect_timeout(self): "Incoming connection time-out in state %s", self._state.name ) - def _on_connect_sabm_ua(self): - self._log.info("Connection accepted by peer") - self._set_conn_state(self.AX25PeerState.CONNECTED) - def _on_connect_response(self, response, **kwargs): # Handle the connection result. self._log.debug("Connection response: %r", response) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 8cb1b2f..d741dd5 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -884,21 +884,18 @@ def _stop_ack_timer(): peer._stop_ack_timer = _stop_ack_timer def _send_ua(): + # At this time, we should be in the INCOMING_CONNECTION state + assert peer._state is peer.AX25PeerState.INCOMING_CONNECTION actions.append("sent-ua") peer._send_ua = _send_ua - def _send_sabm(): - actions.append("sent-sabm") - - peer._send_sabm = _send_sabm - # Try accepting a ficticious connection peer.accept() - assert peer._state is peer.AX25PeerState.INCOMING_CONNECTION - assert actions == ["stop-connect-timer", "sent-ua", "sent-sabm"] - assert peer._uaframe_handler == peer._on_connect_sabm_ua + assert peer._state is peer.AX25PeerState.CONNECTED + assert actions == ["stop-connect-timer", "sent-ua"] + assert peer._uaframe_handler is None def test_reject_connected_noop(): From ac15741d94d6228ccca4d1ccb26b45504bdb24bf Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Mon, 23 Oct 2023 16:41:44 +1000 Subject: [PATCH 155/207] doc: Snapshot the AX.25 2.0 spec page TAPR have taken this off their site. This was grabbed from the Wayback Machine, and shall be stored here for future reference. --- doc/ax25-2p0/index.html | 1995 +++++++++++++++++++ doc/ax25-2p0/index_files/analytics.js | 474 +++++ doc/ax25-2p0/index_files/banner-styles.css | 507 +++++ doc/ax25-2p0/index_files/bundle-playback.js | 3 + doc/ax25-2p0/index_files/iconochive.css | 116 ++ doc/ax25-2p0/index_files/pdf2.gif | Bin 0 -> 1638 bytes doc/ax25-2p0/index_files/ruffle.js | 3 + doc/ax25-2p0/index_files/seal.js | 114 ++ doc/ax25-2p0/index_files/secure90x72.gif | Bin 0 -> 2894 bytes doc/ax25-2p0/index_files/tapr-80px-logo.gif | Bin 0 -> 4922 bytes doc/ax25-2p0/index_files/tapr-logo.png | Bin 0 -> 9070 bytes doc/ax25-2p0/index_files/tapr_sub.css | 587 ++++++ doc/ax25-2p0/index_files/tl_curve_white.gif | Bin 0 -> 59 bytes doc/ax25-2p0/index_files/tr_curve_white.gif | Bin 0 -> 58 bytes doc/ax25-2p0/index_files/wombat.js | 21 + 15 files changed, 3820 insertions(+) create mode 100644 doc/ax25-2p0/index.html create mode 100644 doc/ax25-2p0/index_files/analytics.js create mode 100644 doc/ax25-2p0/index_files/banner-styles.css create mode 100644 doc/ax25-2p0/index_files/bundle-playback.js create mode 100644 doc/ax25-2p0/index_files/iconochive.css create mode 100644 doc/ax25-2p0/index_files/pdf2.gif create mode 100644 doc/ax25-2p0/index_files/ruffle.js create mode 100644 doc/ax25-2p0/index_files/seal.js create mode 100644 doc/ax25-2p0/index_files/secure90x72.gif create mode 100644 doc/ax25-2p0/index_files/tapr-80px-logo.gif create mode 100644 doc/ax25-2p0/index_files/tapr-logo.png create mode 100644 doc/ax25-2p0/index_files/tapr_sub.css create mode 100644 doc/ax25-2p0/index_files/tl_curve_white.gif create mode 100644 doc/ax25-2p0/index_files/tr_curve_white.gif create mode 100644 doc/ax25-2p0/index_files/wombat.js diff --git a/doc/ax25-2p0/index.html b/doc/ax25-2p0/index.html new file mode 100644 index 0000000..bc3b9a0 --- /dev/null +++ b/doc/ax25-2p0/index.html @@ -0,0 +1,1995 @@ + + + + + + + + + + + + + +AX.25 Amateur Packet-Radio Link-Layer Protocol + + + + + + + + + + +
+
The Wayback Machine - http://web.archive.org/web/20190831185324/https://tapr.org/pub_ax25.html
+ + + + + + + + + +
+ + +
+ Publications:

AX.25 Amateur Packet-Radio Link-Layer Protocol

+
+ + +
+
+ + + + +
AX.25 Amateur Packet-Radio Link-Layer Protocol
+

Version 2.2, 1998

+ Ver 2.2 Rev: 1998 (pdf format 3.0, 1.6M)
+ +

+


+

+ +

AX.25 Amateur Packet-Radio Link-Layer Protocol
+

Version 2.0, October 1984

+
    +
    +
    +PID Table Updated November, 1997. +
    +
    +
+

+ + +

2 AX.25 Link-Layer Protocol Specification

+ + +

2.1 Scope and Field of Operation

+ +In order to provide a mechanism for the reliable +transport of data between two signaling terminals, it is +necessary to define a protocol that can accept and deliver data +over a variety of types of communications links. The AX.25 Link- +Layer Protocol is designed to provide this service, independent +of any other level that may or may not exist. + +

This protocol conforms to ISO Recommendations 3309, 4335 +(including DAD 1&2) and 6256 high-level data link control (HDLC) +and uses some terminology found in these documents. It also +conforms with ANSI X3.66, which describes ADCCP, balanced mode. + +

This protocol follows, in principle, the CCITT X.25 +Recommendation, with the exception of an extended address field +and the addition of the Unnumbered Information (UI) frame. It +also follows the principles of CCITT Recommendation Q.921 (LAPD) +in the use of multiple links, distinguished by the address field, +on a single shared channel. + +

As defined, this protocol will work equally well in +either half- or full-duplex Amateur Radio environments. + +

This protocol has been designed to work equally well for +direct connections between two individual amateur packet-radio +stations or an individual station and a multiport controller. + +

This protocol allows for the establishment of more than +one link-layer connection per device, if the device is so +capable. + +

This protocol does not prohibit self-connections. A +self-connection is considered to be when a device establishes a +link to itself using its own address for both the source and +destination of the frame. + +

Most link-layer protocols assume that one primary (or +master) device (generally called a DCE, or data circuit- +terminating equipment) is connected to one or more secondary (or +slave) device(s) (usually called a DTE, or data terminating +equipment). This type of unbalanced operation is not practical +in a shared-RF Amateur Radio environment. Instead, AX.25 assumes +that both ends of the link are of the same class, thereby +eliminating the two different classes of devices. The term DXE +is used in this protocol specification to describe the balanced +type of device found in amateur packet radio. + + +

2.2 Frame Structure

+ +Link layer packet radio transmissions are sent in small +blocks of data, called frames. Each frame is made up of several +smaller groups, called fields. Fig.1 shows the three basic types +of frames. Note that the first bit to be transmitted is on the +left side. + +

+

+ + + + + + + +
First Bit Sent
FlagAddressControlFCSFlag
01111110112/560 Bits8 Bits16 Bits01111110
Fig. 1A -- U and S frame construction
+ +

+ + +

+ + + + +
First Bit Sent
FlagAddressControlPIDInfo.FCSFlag
01111110112/560 Bits8 Bits8 BitsN*8 Bits16 Bits01111110
Fig. 1B -- Information frame construction
+ +
+ +

Each field is made up of an integral number of octets (or +bytes), and serves a specific function as outlined below. + + +

2.2.1 Flag Field

+ +The flag field is one octet long. Since the flag is used +to delimit frames, it occurs at both the beginning and end of +each frame. Two frames may share one flag, which would denote +the end of the first frame, and the start of the next frame. A +flag consists of a zero followed by six ones followed by another +zero, or 01111110 (7E hex). As a result of bit stuffing (see +2.2.6, below), this sequence is not allowed +to occur anywhere else inside a complete frame. + + +

2.2.2 Address Field

+ +The address field is used to identify both the source of +the frame and its destination. In addition, the address field +contains the command/response information and facilities for +level 2 repeater operation. + +

The encoding of the address field is described in +2.2.13. + + +

2.2.3 Control Field

+ +The control field is used to identify the type of frame +being passed and control several attributes of the level 2 +connection. It is one octet in length, and its encoding is +discussed in +2.3.2.1, below. + + +

2.2.4 PID Field

+ +The Protocol Identifier (PID) field shall appear in +information frames (I and UI) only. It identifies what kind of +layer 3 protocol, if any, is in use. + +

The PID itself is not included as part of the octet count +of the information field. The encoding of the PID is as follows: + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HEX + M      L
+ S      S
+ B      B
Translation
0x0100000001 + ISO 8208/CCITT X.25 PLP +
0x0600000110 + Compressed TCP/IP packet. Van Jacobson (RFC 1144) +
0x0700000111 + Uncompressed TCP/IP packet. Van Jacobson (RFC 1144) +
0x0800001000 + Segmentation fragment +
**yy01yyyyAX.25 layer 3 implemented. +
**yy10yyyy + AX.25 layer 3 implemented. +
0xC311000011 + TEXNET datagram protocol +
0xC411000100 + Link Quality Protocol +
0xCA11001010 + Appletalk +
0xCB11001011 + Appletalk ARP +
0xCC11001100 + ARPA Internet Protocol +
0xCD11001101 + ARPA Address resolution +
0xCE11001110 + FlexNet +
0xCF11001111 + NET/ROM +
0xF011110000 + No layer 3 protocol implemented. +
0xFF11111111 + Escape character. Next octet contains more Level 3 + protocol information. +
+
+ +

Where:
+A y indicates all combinations used. + +

Note:
+All forms of yy11yyyy and yy00yyyy other than those +listed above are reserved at this time for future level 3 +protocols. The assignment of these formats is up to amateur +agreement. It is recommended that the creators of level 3 +protocols contact the ARRL Ad Hoc Committee on Digital +Communications for suggested encodings. + + +

2.2.5 Information Field

+ +The information field is used to convey user data from +one end of the link to the other. I fields are allowed in only +three types of frames: the I frame, the UI frame, and the FRMR +frame. The I field can be up to 256 octets long, and shall +contain an integral number of octets. These constraints apply +prior to the insertion of zero bits as specified in +2.2.6, below. +Any information in the I field shall be passed along the link +transparently, except for the zero-bit insertion (see +2.2.6) +necessary to prevent flags from accidentally appearing in the I +field. + + +

2.2.6 Bit Stuffing

+ +In order to assure that the flag bit sequence mentioned +above doesn't appear accidentally anywhere else in a frame, the +sending station shall monitor the bit sequence for a group of +five or more contiguous one bits. Any time five contiguous one +bits are sent the sending station shall insert a zero bit after +the fifth one bit. During frame reception, any time five +contiguous one bits are received, a zero bit immediately +following five one bits shall be discarded. + + +

2.2.7 Frame-Check Sequence

+ +The frame-check sequence (FCS) is a sixteen-bit number +calculated by both the sender and receiver of a frame. It is +used to insure that the frame was not corrupted by the medium +used to get the frame from the sender to the receiver. It shall +be calculated in accordance with ISO 3309 (HDLC) Recommendations. + + +

2.2.8 Order of Bit Transmission

+ +With the exception of the FCS field, all fields of an +AX.25 frame shall be sent with each octet's least-significant bit +first. The FCS shall be sent most-significant bit first. + + +

2.2.9 Invalid Frames

+ +Any frame consisting of less than 136 bits (including the +opening and closing flags), not bounded by opening and closing +flags, or not octet aligned (an integral number of octets), shall +be considered an invalid frame by the link layer. See also +2.4.4.4, below. + + +

2.2.10 Frame Abort

+ +If a frame must be prematurely aborted, at least fifteen +contiguous ones shall be sent with no bit stuffing added. + + +

2.2.11 Interframe Time Fill

+ +Whenever it is necessary for a DXE to keep its +transmitter on while not actually sending frames, the time +between frames should be filled with contiguous flags. + + +

2.2.12 Link Channel States

+ +Not applicable. + + +

2.2.13 Address-Field Encoding

+ +The address field of all frames shall be encoded with +both the destination and source amateur call signs for the frame. +Except for the Secondary Station Identifier (SSID), the address +field should be made up of upper-case alpha and numeric ASCII +characters only. If level 2 amateur "repeaters" are to be used, +their call signs shall also be in the address field. + +

The HDLC address field is extended beyond one octet by +assigning the least-significant bit of each octet to be an +"extension bit". The extension bit of each octet is set to zero, +to indicate the next octet contains more address information, or +one, to indicate this is the last octet of the HDLC address +field. To make room for this extension bit, the amateur Radio +call sign information is shifted one bit left. + + +

2.2.13.1 Nonrepeater Address-Field Encoding
+ +If level 2 repeaters are not being used, the address +field is encoded as shown in Fig. 2. The destination address is +the call sign and SSID of the amateur radio station to which the +frame is addressed, while the source address contains the amateur +call sign and SSID of the station that sent the frame. These +call signs are the call signs of the two ends of a level 2 AX.25 +link only. + +

+

+ + + + + + + + +
First Octet Sent
Address Field of Frame
Destination Address + Source Address
A1 A2 A3 A4 A5 A6 A7 +A8 A9 A10 A11 A12 A13 A14
Fig. 2 -- Nonrepeater Address-Field Encoding
+ +
+ +

A1 through A14 are the fourteen octets that make up the +two address subfields of the address field. The destination +subaddress is seven octets long (A1 thru A7), and is sent first. +This address sequence provides the receivers of frames time to +check the destination address subfield to see if the frame is +addressed to them while the rest of the frame is being received. +The source address subfield is then sent in octets A8 through +A14. Both of these subfields are encoded in the same manner, +except that the last octet of the address field has the HDLC +address extension bit set. + +

There is an octet at the end of each address subfield +that contains the Secondary Station Identifier (SSID). The SSID +subfield allows an Amateur Radio operator to have more than one +packet-radio station operating under the same call sign. This is +useful when an amateur wants to put up a repeater in addition to +a regular station, for example. The C bits (see +2.4.1.2, below) +and H bit (see +2.2.13.2, below) +are also contained in this octet, +along with two bits which are reserved for future use. + +

Fig. 3A shows a typical AX.25 frame in the nonrepeater +mode of operation. + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
OctetASCIIBin.DataHex Data
Flag011111107E
A1K1001011096
A280111000070
A3M100110109A
A4M100110109A
A5O100111109E
A6space0100000040
A7SSID11100000E0
A8W10101110AE
A9B1000010084
A1040110010068
A11J1001010094
A12F100011008C
A13I1001001092
A14SSID0110000161
ControlI001111103E
PIDnone11110000F0
FCSpart 1xxxxxxxxHH
FCSpart 2xxxxxxxxHH
Flag011111107E
Bit position76543210
Fig. 3A -- Nonrepeater AX.25 frame
+ +

The frame shown is an I frame, not going through a level +2 repeater, from WB4JFI (SSID=0) to K8MMO (SSID=0), with no level +3 protocol. The P/F bit is set; the receive sequence number +(N(R)) is 1; the send sequence number (N(S)) is 7. + + +

2.2.13.1.1 Destination Subfield Encoding

+ +Fig. 3 shows how an amateur call sign is placed in the +destination address subfield, occupying octets A1 thru A7. + +

+ + + + + + + + + + + + + +
OctetASCIIBin.DataHex Data
A1W10101110AE
A2B1000010084
A340110100068
A4J1001010094
A5F100011008C
A6I1001001092
A7SSIDCRRSSID0
Bit Position76543210
Fig. 3 -- Destination Field Encoding
+ +
+ +

Where:

    +
  1. The top octet (A1) is the first octet sent, with bit + 0 of each octet being the first bit sent, and bit 7 + being the last bit sent. +
  2. The first (low-order or bit 0) bit of each octet is + the HDLC address extension bit, which is set to zero + on all but the last octet in the address field, where + it is set to one. +
  3. The bits marked "r" are reserved bits. They may be + used in an agreed-upon manner in individual networks. + When not implemented, they should be set to one. +
  4. The bit marked "C" is used as the command/response + bit of an AX.25 frame, as outlined in +2.4.1.2 below. +
  5. The characters of the call sign should be standard + seven-bit ASCII (upper case only) placed in the + leftmost seven bits of the octet to make room for the + address extension bit. If the call sign contains + fewer than six characters, it should be padded with + ASCII spaces between the last call sign character and + the SSID octet. +
  6. The 0000 SSID is reserved for the first personal + AX.25 station. This establishes one standard SSID for + "normal" stations to use for the first station. +
+ + +
2.2.13.2 Level 2 Repeater-Address Encoding
+ +If a frame is to go through level 2 amateur packet +repeater(s), there is an additional address subfield appended to +the end of the address field. This additional subfield contains +the call sign(s) of the repeater(s) to be used. This allows more +than one repeater to share the same RF channel. If this subfield +exists, the last octet of the source subfield has its address +extension bit set to zero, indicating that more address-field +data follows. The repeater-address subfield is encoded in the +same manner as the destination and source address subfields, +except for the most-significant bit in the last octet, called the +"H" bit. The H bit is used to indicate whether a frame has been +repeated or not. + +

In order to provide some method of indicating when a +frame has been repeated, the H bit is set to zero on frames going +to a repeater. The repeater will set the H bit to one when the +frame is retransmitted. Stations should monitor the H bit, and +discard any frames going to the repeater (uplink frames), while +operating through a repeater. Fig. 4 shows how the repeater- +address subfield is encoded. Fig. 4A is an example of a complete +frame after being repeated. + +

+

+ + + + + + + + + + + + + +
OctetASCIIBin.DataHex Data
A15W10101110AE
A16B1000010084
A1740110100068
A18J1001010094
A19F100011008C
A20I1001001092
A21SSIDHRRSSID1
Bit Order -->76543210
Fig. 4 -- Repeater-address encoding
+
+ +Where: +
    +
  1. The top octet is the first octet sent, with bit 0 being +sent first and bit 7 sent last of each octet. +
  2. As with the source and destination address subfields +discussed above, bit 0 of each octet is the HDLC address +extension bit, which is set to zero on all but the last +address octet, where it is set to one. +
  3. The "R" bits are reserved in the same manner as in the +source and destination subfields. +
  4. The "H" bit is the has-been-repeated bit. It is set to +zero whenever a frame has not been repeated, and set to +one by the repeater when the frame has been repeated. +
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OctetASCIIBin.DataHex Data
Flag011111107E
A1K1001011096
A280111000070
A3M100110109A
A4M100110109A
A5O100111109E
A6space0100000040
A7SSID11100000E0
A8W10101110AE
A9B1000010084
A1040110010068
A11J1001010094
A12F100011008C
A13I1001001092
A14SSID0110000060
A15W10101110AE
A16B1000010084
A1740110100068
A18J1001010094
A19F100011008C
A20I1001001092
A21SSID11100011E3
ControlI001111103E
PIDnone11110000F0
FCSpart 1xxxxxxxxHH
FCSpart 2xxxxxxxxHH
Flag011111107E
Bit position76543210
Fig. 4A -- AX.25 frame in repeater mode
+ +
+ +

The above frame is the same as Fig. 3A, +except for the +addition of a repeater-address subfield (WB4JFI, SSID=1). The H +bit is set, indicating this is from the output of the repeater. + + +

2.2.13.3 Multiple Repeater Operation
+ +The link-layer AX.25 protocol allows operation through +more than one repeater, creating a primitive frame routing +mechanism. Up to eight repeaters may be used by extending the +repeater-address subfield. When there is more than one repeater +address, the repeater address immediately following the source +address subfield will be considered the address of the first +repeater of a multiple-repeater chain. As a frame progresses +through a chain of repeaters, each successive repeater will set +the H bit (has-been-repeated bit) in its SSID octet, indicating +that the frame has been successfully repeated through it. No +other changes to the frame are made (except for the necessary +recalculation of the FCS). The destination station can determine +the route the frame took to each it by examining the address +field. + +

The number of repeater addresses is variable. All but +the last repeater address will have the address extension bits of +all octets set to zero, as will all but the last octet (SSID +octet) of the last repeater address. The last octet of the last +repeater address will have the address extension bit set to one, +indicating the end of the address field. + +

It should be noted that various timers (see +2.4.7, below) +may have to be adjusted to accommodate the additional delays +encountered when a frame must pass through a multiple-repeater +chain, and the return acknowledgement must travel through the +same path before reaching the source device. + +

It is anticipated that multiple-repeater operation is a +temporary method of interconnecting stations over large distances +until such time that a layer 3 protocol is in use. Once this +layer 3 protocol becomes operational, repeater chaining should be +phased out. + + +

2.3 Elements of Procedure

+ + +

2.3.1

+ The elements of procedure are defined in terms of actions +that occur on receipt of frames. + + +

2.3.2 Control-Field Formats and State Variables

+ + +
2.3.2.1 Control-Field Formats
+ +The control field is responsible for identifying the type +of frame being sent, and is also used to convey commands and +responses from one end of the link to the other in order to +maintain proper link control. + +

The control fields used in AX.25 use the CCITT X.25 +control fields for balanced operation (LAPB), with an additional +control field taken from ADCCP to allow connectionless and round- +table operation. + +

There are three general types of AX.25 frames. They are +the Information frame (I frame), the Supervisory frame (S frame), +and the Unnumbered frame (U frame). Fig. 5 shows the basic +format of the control field associated with these types of +frames. + +

+ + + + + + + + + +
Control-Field
Type +
Control-Field Bits
7654 +3210
I FrameN(R)P +N(S)0
S FrameN(R)P/FSS01
U FrameMMMP/FMM11
Fig. 5 -- Control-field formats
+ +

Where: +

    +
  1. Bit 0 is the first bit sent and bit 7 is the last bit + sent of the control field. +
  2. N(S) is the send sequence number (bit 1 is the LSB). +
  3. N(R) is the receive sequence number (bit 5 is the + LSB). +
  4. The "S" bits are the supervisory function bits, and + their encoding is discussed in 2.3.4.2. +
  5. The "M" bits are the unnumbered frame modifier bits + and their encoding is discussed in 2.3.4.3. +
  6. The P/F bit is the Poll/Final bit. Its function is + described in 2.3.3. The distinction between command + and response, and therefore the distinction between P + bit and F bit, is made by addressing rules discussed + in 2.4.1.2. +
+ + +

2.3.2.1.1 Information-Transfer Format

+ +All I frames have bit 0 of the control field set to zero. +N(S) is the sender's send sequence number (the send sequence +number of this frame). N(R) is the sender's receive sequence +number (the sequence number of the next expected received frame). +These numbers are described in 2.3.2.4. +In addition, the P/F bit +is to be used as described in 2.4.2. + + + +

2.3.2.1.2 Supervisory Format

+ +Supervisory frames are denoted by having bit 0 of the +control field set to one, and bit 1 of the control field set to +zero. S frames provide supervisory link control such as +acknowledging or requesting retransmission of I frames, and link- +level window control. Since S frames do not have an information +field, the sender's send variable and the receiver's receive +variable are not incremented for S frames. In addition, the P/F +bit is used as described in +2.4.2. + + +

2.3.2.1.3 Unnumbered Format

+ +Unnumbered frames are distinguished by having both bits 0 +and 1 of the control field set to one. U frames are responsible +for maintaining additional control over the link beyond what is +accomplished with S frames. They are also responsible for +establishing and terminating link connections. U frames also +allow for the transmission and reception of information outside +of the normal flow control. Some U frames may contain +information and PID fields. The P/F bit is used as described in +2.4.2. + + +
2.3.2.2 Control-Field Parameters
+ + +
2.3.2.3 Sequence Numbers
+ +Every AX.25 I frame shall be assigned, modulo 8, a +sequential number from 0 to 7. This will allow up to seven +outstanding I frames per level 2 connection at a time. + + +
2.3.2.4 Frame Variables and Sequence Numbers
+ + +

2.3.2.4.1 Send State Variable V(S)

+ +The send state variable is a variable that is internal to +the DXE and is never sent. It contains the next sequential +number to be assigned to the next transmitted I frame. This +variable is updated upon the transmission of each I frame. + + +

2.3.2.4.2 Send Sequence Number N(S)

+ +The send sequence number is found in the control field of +all I frames. It contains the sequence number of the I frame +being sent. Just prior to the transmission of the I frame, N(S) +is updated to equal the send state variable. + + + +

2.3.2.4.3 Receive State Variable V(R)

+ +The receive state variable is a variable that is internal +to the DXE. It contains the sequence number of the next expected +received I frame. This variable is updated upon the reception of +an error-free I frame whose send sequence number equals the +present received state variable value. + + +

2.3.2.4.4 Received Sequence Number N(R)

+ +The received sequence number is in both I and S frames. +Prior to sending an I or S frame, this variable is updated to +equal that of the received state variable, thus implicitly +acknowledging the proper reception of all frames up to and +including N(R)-1. + + +

2.3.3 Functions of Poll/Final (P/F) Bit

+ +The P/F bit is used in all types of frames. It is used +in a command (poll) mode to request an immediate reply to a +frame. The reply to this poll is indicated by setting the +response (final) bit in the appropriate frame. Only one +outstanding poll condition per direction is allowed at a time. +The procedure for P/F bit operation is described in +2.4.2. + + +

2.3.4 Control Field Coding for Commands and Responses

+ +The following commands and responses, indicated by their +control field encoding, are to be use by the DXE: + + +
2.3.4.1 Information Command Frame Control Field
+ +The function of the information (I) command is to +transfer across a data link sequentially numbered frames +containing an information field. + +

The information-frame control field is encoded as shown +in Fig. 6. These frames are sequentially numbered by the N(S) +subfield to maintain control of their passage over the link-layer +connection. + +

+ + + + + + +
Control Field Bits
76543210
N(R) +P N(S) 0
Fig. 6 -- I frame control field
+ +
+ + +
2.3.4.2 Supervisory Frame Control Field
+ +The supervisory frame control fields are encoded as shown +in Fig. 7. + +

+ + + + + + + + +
Control Field Type +Control Field Bits
76543210
Receive ReadyRR +N(R)P/F0001
Receive Not ReadyRNR +N(R)P/F0101
RejectREJ +N(R)P/F1001
+Fig. 7 -- S frame control fields +
+ +

+ +

+ + + + + + + + + + +
The Frame identifiers:
C or SABMLayer 2 Connect Request
D or DISCLayer 2 Disconnect Request
I Information Frame
RR Receive Ready. System Ready To Receive
RNR or NRReceive Not Ready. TNC Buffer Full
RJ or REJReject Frame. Out of Sequence or Duplicate
FRMR Frame Reject. Fatal Error
UI Unnumbered Information Frame. "Unproto"
DM Disconnect Mode. System Busy or Disconnected.
+ +
+ + +

2.3.4.2.1 Receive Ready (RR) Command and Response

+ +Receive Ready is used to do the following: +
    +
  1. to indicate that the sender of the RR is now able to + receive more I frames. +
  2. to acknowledge properly received I frames up to, and + including N(R)-1, and +
  3. to clear a previously set busy condition created by an RNR + command having been sent. +
+

The status of the DXE at the other end of the link can be +requested by sending a RR command frame with the P-bit set to +one. + + +

2.3.4.2.2 Receive Not Ready (RNR) Command and Response

+ +Receive Not Ready is used to indicate to the sender of I +frames that the receiving DXE is temporarily busy and cannot +accept any more I frames. Frames up to N(R)-1 are acknowledged. +Any I frames numbered N(R) and higher that might have been caught +between states and not acknowledged when the RNR command was sent +are not acknowledged. + +

The RNR condition can be cleared by the sending of a UA, +RR, REJ, or SABM frame. + +

The status of the DXE at the other end of the link can be +requested by sending a RNR command frame with the P bit set to +one. + + +

2.3.4.2.3 Reject (REJ) Command and Response

+ +The reject frame is used to request retransmission of I +frames starting with N(R). Any frames that were sent with a +sequence number of N(R)-1 or less are acknowledged. Additional I +frames may be appended to the retransmission of the N(R) frame if +there are any. + +

Only one reject frame condition is allowed in each +direction at a time. The reject condition is cleared by the +proper reception of I frames up to the I frame that caused the +reject condition to be initiated. + +

The status of the DXE at the other end of the link can be +requested by sending a REJ command frame with the P bit set to +one. + + +

2.3.4.3 Unnumbered Frame Control Fields
+ +Unnumbered frame control fields are either commands or +responses. + +

Fig. 8 shows the layout of U frames implemented within +this protocol. + + +

+ + + + + + + + + + + + +
Control Field Type +Control Field Bits
76543210
Set Asynchronous Balanced ModeSABMRes +001P1111
DisconnectDISCCmd +010P0011
Disconnected ModeDMRes +000F1111
Unnumbered AcknowledgeUARes +011F0011
Frame RejectFRMRRes +100F0111
Unnumbered InformationUIEither +000P/F0011
+Fig. 8 -- U frame control fields +
+ +
+ + +

2.3.4.3.1 Set Asynchronous Balanced Mode (SABM) Command

+ +The SABM command is used to place 2 DXEs in the +asynchronous balanced mode. This is a balanced mode of operation +known as LAPB where both devices are treated as equals. + +

Information fields are not allowed in SABM commands. Any +outstanding I frames left when the SABM command is issued will +remain unacknowledged. + +

The DXE confirms reception and acceptance of a SABM +command by sending a UA response frame at the earliest +opportunity. If the DXE is not capable of accepting a SABM +command, it should respond with a DM frame if possible. + + +

2.3.4.3.2 Disconnect (DISC) Command

+ +The DISC command is used to terminate a link session +between two stations. No information field is permitted in a +DISC command frame. + +

Prior to acting on the DISC frame, the receiving DXE +confirms acceptance of the DISC by issuing a UA response frame at +its earliest opportunity. The DXE sending the DISC enters the +disconnected state when it receives the UA response. + +

Any unacknowledged I frames left when this command is +acted upon will remain unacknowledged. + + +

2.3.4.3.3 Frame Reject (FRMR) Response

+ + +

2.3.4.3.3.1

+ +

The FRMR response frame is sent to report that the receiver +of a frame cannot successfully process that frame and that the +error condition is not correctable by sending the offending frame +again. Typically this condition will appear when a frame without +an FCS error has been received with one of the following +conditions: + +

    +
  1. The reception of an invalid or not implemented command or + response frame. +
  2. The reception of an I frame whose information field exceeds + the agreed-upon length. +(See 2.4.7.3, below.) +
  3. The reception of an improper N(R). This usually happens + when the N(R) frame has already been sent and acknowledged, + or when N(R) is out of sequence with what was expected. +
  4. The reception of a frame with an information field where one + is not allowed, or the reception of a U or S frame whose + length is incorrect. Bits W and Y described in +2.3.4.3.3.2 + should both be set to one to indicate this condition. +
  5. The reception of a supervisory frame with the F bit set + to one, except during a timer recovery condition (see + 2.4.4.9), +or except as a reply to a command frame sent with + the P bit set to one. Bit W (described in +2.3.4.3.3.2) + should be set to one. +
  6. The reception of an unexpected UA or DM response frame. Bit + W should be set to one. +
  7. The reception of a frame with an invalid N(S). Bit W should be + set to one. +
+ +

An invalid N(R) is defined as one which points to an I +frame that previously has been transmitted and acknowledged, or +an I frame which has not been transmitted and is not the next +sequential I frame pending transmission. + +

An invalid N(S) is defined as an N(S) that is equal to +the last transmitted N(R)+k and is equal to the received state +variable V(R), where k is the maximum number of outstanding +information frames as defined in 2.4.7.4 below. + +

An invalid or not implemented command or response is +defined as a frame with a control field that is unknown to the +receiver of this frame. + + +

2.3.4.3.3.2

+When a FRMR frame is sent, an information field is added to +the frame that contains additional information indicating where +the problem occurred. This information field is three octets +long and is shown in Fig. 9. + +

+ + + + + +
Information Field Bits
2322212019181716 +15141312111098 +76543210
0000ZYXW +V(R)C
R +
V(S)0 +Rejected Frame
Control Field
+Fig. 9 -- FRMR frame information field +
+ +
+ +

Where: +

    +
  1. The rejected frame control field carries the control field + of the frame that caused the reject condition. It is in + bits 0-7 of the information field. +
  2. V(S) is the current send state variable of the device + reporting the rejection (bit 9 is the low bit). +
  3. The CR bit is set to zero to indicate the rejected frame + was a command, or one if it was a response. +
  4. V(R) is the current receive state variable of the device + reporting rejection (bit 13 is the low bit). +
  5. If W is set to 1, the control field received was invalid or + not implemented. +
  6. If X is set to 1, the frame that caused the reject + condition was considered invalid because it was a U or S + frame that had an information field that is not allowed. + Bit W must be set to 1 in addition to the X bit. +
  7. If Y is set to 1, the control field received and returned + in bits exceeded the maximum allowed under this + recommendation in 2.4.7.3, below. +
  8. If A is set to 1, the control field received and returned + in bits 1 to 8 contained an invalid N(R). +
  9. Bits 8, and 20 to 23 are set to 0. +
+ + + +

2.3.4.3.4 Unnumbered Acknowledge (UA) Response

+ +The UA response frame is sent to acknowledge the +reception and acceptance of a SABM or DISC command frame. A +received command is not actually processed until the UA response +frame is sent. Information fields are not permitted in a UA +frame. + + + +

2.3.4.3.5 Disconnected Mode (DM) Response

+ +The disconnected mode response is sent whenever a DXE +receives a frame other than a SABM or UI frame while in a +disconnected mode. It is also sent to request a set mode +command, or to indicate it cannot accept a connection at the +moment. The DM response does not have an information field. + +

Whenever a SABM frame is a received, and it is determined +that a connection is not possible, a DM frame shall be sent. +This will indicate that the called station cannot accept a +connection at that time. + +

While a DXE is in the disconnected mode, it will respond +to any command other than a SABM or UI frame with a DM response +with the P/F bit set to 1. + + + +

2.3.4.3.6 Unnumbered Information (UI) Frame

+ +

The Unnumbered Information frame contains PID and +information fields and is used to pass information along the link +outside the normal information controls. This allows information +fields to go back and forth on the link bypassing flow control. +Since these frames are not acknowledgeable, if one gets +obliterated, there is no way to recover it. A received UI frame +with the P bit set shall cause a response to be transmitted. +This response shall be a DM frame when in the disconnected state +or a RR (or RNR, if appropriate) frame in the information +transfer state. + + +

2.3.5 Link Error Reporting and Recovery

+ +There are several link-layer errors that are recoverable +without terminating the connection. These error situations may +occur as a result of malfunctions within the DXE, or if +transmission errors occur. + + +
2.3.5.1 DXE Busy Condition
+ +When a DXE becomes temporarily unable to receive I +frames, such as when receive buffers are full, it will send a +Receive Not Ready (RNR) frame. This informs the other DXE that +this DXE cannot handle any more I frames at the moment. This +condition is usually cleared by the sending of a UA, RR, REJ, or +SABM command frame. + + + +
2.3.5.2 Send Sequence Number Error
+ +If the send sequence number, N(S), of an otherwise error- +free received frame does not match the receive state variable, +V(R), a send sequence error has occurred, and the information +field will be discarded. The receiver will not acknowledge this +frame, or any other I frames, until N(S) matches V(R). + +

The control field of the erroneous I frame(s) will be +accepted so that link supervisory functions such as checking the +P/F bit can still be performed. Because of this updating, the +retransmitted I frame may have an updated P bit and N(R). + + + +

2.3.5.3 Reject (REJ) Recovery
+ +REJ is used to request a retransmission of I frames +following the detection of a N(S) sequence error. Only one +outstanding "sent REJ" condition is allowed at a time. This +condition is cleared when the requested I frame has been +received. + +

A DXE receiving the REJ command will clear the condition +by resending all outstanding I frames (up to the window size), +starting with the one indicated in N(R) of the REJ frame. + + + +

2.3.5.4 Time-out Error Recovery
+ + + +

2.3.5.4.1 T1 Timer Recovery

+ +If a DXE, due to a transmission error, does not receive +(or receives and discards) a single I frame or the last I frame +in a sequence of I frames, it will not detect a send-sequence- +number error, and therefore will not transmit a REJ. The DXE +which transmitted the unacknowledged I frame(s) shall, following +the completion of time-out period T1, take appropriate recovery +action to determine when I frame retransmission should begin as +described in 2.4.4.9, below. This condition is cleared by the +reception of an acknowledgement for the sent frame(s), or by the +link being reset. See 2.4.6. + + + +

2.3.5.4.2 Timer T3 Recovery

+ +Timer T3 is used to assure the link is still functional +during periods of low information transfer. Whenever T1 is not +running (no outstanding I frames), T3 is used to periodically +poll the other DXE of a link. When T3 times out, a RR or RNR +frame is transmitted as a command and with the P bit set. The +waiting acknowledgement procedure (2.4.4.9, below) is then +executed. + + + +
2.3.5.5 Invalid Frame or FCS Error
+ +If an invalid frame is received, or a frame is received +with an FCS error, that frame will be discarded with no action +taken. + + + +
2.3.5.6 Frame Rejection Condition
+ +A frame rejection condition occurs when an otherwise +error-free frame has been received with one of the conditions +listed in 2.3.4.3.3 above. + +

Once a rejection error occurs, no more I frames are +accepted (except for the examination of the P/F bit) until the +error is resolved. The error condition is reported to the other +DXE by sending a FRMR response frame. See 2.4.5. + + + +

2.4 Description of AX.25 Procedures

+ +The following describes the procedures used to setup, +use, and disconnect a balanced link between two DXE stations. + + + +

2.4.1 Address Field Operation

+ + + +
2.4.1.1 Address Information
+ +All transmitted frames shall have address fields +conforming to 2.2.13, above. All frames shall have both the +destination device and the source device addresses in the address +field, with the destination address coming first. This allows +many links to share the same RF channel. The destination address +is always the address of the station(s) to receive the frame, +while the source address contains the address of the device that +sent the frame. + +

The destination address can be a group name or club call +sign if the point-to-multipoint operation is allowed. Operation +with destination addresses other than actual amateur call signs +is a subject for further study. + + + +

2.4.1.2 Command/Response Procedure
+ +AX.25 Version 2.0 has implemented the command/response +information in the address field. In order to maintain +compatibility with previous versions of AX.25, the +command/response information is conveyed using two bits. + +

An upward-compatible AX.25 DXE can determine whether it +is communicating with a DXE using an older version of this +protocol by testing the command/response bit information located +in bit 7 of the SSID octets of both the destination and source +address subfields. If both C bits are set to zero, the device is +using the older protocol. The newer version of the protocol +always has one of these two bits set to one and the other set to +zero, depending on whether the frame is a command or a response. + +

The command/response information is encoded into the +address field as shown in Fig. 10. + +

+

+ + + + + + + + + +
Frame TypeDest. SSID C-BitSource SSID C-Bit
Previous versions00
Command (V.2.0)10
Response (V.2.0)01
Previous versions11
Fig. 10 -- Command/Response encoding
+ +
+ +

Since all frames are considered either commands or +responses, a device shall always have one of the bits set to one, +and the other bit set to zero. + +

The use of the command/response information in AX.25 +allows S frames to be either commands or responses. This aids +maintenance of proper control over the link during the +information transfer state. + + + +

2.4.2 P/F Bit Procedures

+ +The next response frame returned by the DXE to a SABM or +DISC command with the P bit set to 1 will be a UA or DM response +with the F bit set to 1. + +

The next response frame returned to an I frame with the P +bit set to 1, received during the information transfer state, +will be a RR, RNR, or REJ response with the F bit set to 1. + +

The next response frame returned to a supervisory command +frame with the P bit set to 1, received during the information +transfer state, will be a RR, RNR, or REJ response frame with the +F bit set to 1. + +

The next response frame returned to a S or I command +frame with the P bit set to 1, received in the disconnected +state, will be a DM response frame with the F bit set to 1. + +

The P bit is used in conjunction with the time-out +recovery condition discussed in 2.3.5.4, above. + +

When not used, the P/F bit is set to zero. + + + +

2.4.3 Procedures For Link Set-Up and Disconnection

+ + + +
2.4.3.1 LAPB Link Connection Establishment
+ +When one DXE wishes to connect to another DXE, it will +send a SABM command frame to that device and start timer (T1). +If the other DXE is there and able to connect, it will respond +with a UA response frame, and reset both of its internal state +variables (V(S) and V(R)). The reception of the UA response +frame at the other end will cause the DXE requesting the +connection to cancel the T1 timer and set its internal state +variables to 0. + +

If the other DXE doesn't respond before T1 times out, the +device requesting the connection will re-send the SABM frame, and +start T1 running again. The DXE should continue to try to +establish a connection until it has tried unsuccessfully N2 +times. N2 is defined in 2.4.7.2, below. + +

If, upon reception of a SABM command, the DXE decides +that it cannot enter the indicated state, it should send a DM +frame. + +

When receiving a DM response, the DXE sending the SABM +should cancel its T1 timer, and not enter the information- +transfer state. + +

The DXE sending a SABM command will ignore and discard +any frames except SABM, DISC, UA, and DM frames from the other +DXE. + +

Frames other than UA and DM in response to a received +SABM will be sent only after the link is set up and if no +outstanding SABM exists. + + +

2.4.3.2 Information-Transfer Phase
+ +After establishing a link connection, the DXE will enter +the information-transfer state. In this state, the DXE will +accept and transmit I and S frames according to the procedure +outlined in 2.4.4, below. + +

When receiving a SABM command while in the information- +transfer state, the DXE will follow the resetting procedure +outlined in 2.4.6 below. + + +

2.4.3.3 Link Disconnection
+ + +

2.4.3.3.1

+ While in the information-transfer state, either DXE may +indicate a request to disconnect the link by transmitting a DISC +command frame and starting timer T1 (see 2.4.7). + + +

2.4.3.3.2

+ A DXE, upon receiving a valid DISC command, shall send a UA +response frame and enter the disconnected state. A DXE, upon +receiving a UA or DM response to a sent DISC command, shall +cancel timer T1, and enter the disconnected state. + + +

2.4.3.3.3

+If a UA or DM response is not correctly received before T1 +times out, the DISC frame should be sent again and T1 restarted. +If this happens N2 times, the DXE should enter the disconnected +state. + + +
2.4.3.4 Disconnected State
+ + +

2.4.3.4.1

+A DXE in the disconnected state shall monitor received +commands and react upon the reception of a SABM as described in +2.4.3.1 above and will transmit a DM frame +in response to a DISC command. + + +

2.4.3.4.2

+In the disconnected state, a DXE may initiate a link set-up +as outlined in connection establishment above (2.4.3.1). It may +also respond to the reception of a SABM and establish a +connection, or it may ignore the SABM and send a DM instead. + + +

2.4.3.4.3

+Any DXE receiving a command frame other than a SABM or UI +frame with the P bit set to one should respond with a DM frame +with the F bit set to one. The offending frame should be +ignored. + + +

2.4.3.4.4

+When the DXE enters the disconnected state after an error +condition or if an internal error has resulted in the DXE being +in the disconnected state, the DXE should indicate this by +sending a DM response rather than a DISC frame and follow the +link disconnection procedure outlined in 2.4.3.3.3, above. The +DXE may then try to re-establish the link using the link set-up +procedure outlined in 2.4.3.1, above. + + +
2.4.3.5 Collision Recovery
+ + +

2.4.3.5.1 Collisions in a Half-Duplex Environment

+ +Collisions of frames in a half-duplex environment are +taken care of by the retry nature of the T1 timer and +retransmission count variable. No other special action needs to +be taken. + + +

2.4.3.5.2 Collisions of Unnumbered Commands

+ +If sent and received SABM or DISC command frames are the +same, both DXEs should send a UA response at the earliest +opportunity, and both devices should enter the indicated state. + +

If sent and received SABM or DISC commands are different, +both DXEs should enter the disconnected state and transmit a DM +frame at the earliest opportunity. + + +

2.4.3.5.3 Collision of a DM with a SABM or DISC

+ +When an unsolicited DM response frame is sent, a +collision between it and a SABM or DISC may occur. In order to +prevent this DM from being misinterpreted, all unsolicited DM +frames should be transmitted with the F bit set to zero. All +SABM and DISC frames should be sent with the P bit set to one. +This will prevent any confusion when a DM frame is received. + + +
2.4.3.6 Connectionless Operation
+ +In Amateur Radio, there is an additional type of +operation that is not feasible using level 2 connections. This +operation is the round table, where several amateurs may be +engaged in one conversation. This type of operation cannot be +accommodated by AX.25 link-layer connections. + +

The way round-table activity is implemented is +technically outside the AX.25 connection, but still using the +AX.25 frame structure. + +

AX.25 uses a special frame for this operation, called the +Unnumbered Information (UI) frame. When this type of operation +is used, the destination address should have a code word +installed in it to prevent the users of that particular round +table from seeing all frames going through the shared RF medium. +An example of this is if a group of amateurs are in a round-table +discussion about packet radio, they could put PACKET in the +destination address, so they would receive frames only from +others in the same discussion. An added advantage of the use of +AX.25 in this manner is that the source of each frame is in the +source address subfield, so software could be written to +automatically display who is making what comments. + +

Since this mode is connectionless, there will be no +requests for retransmissions of bad frames. Collisions will also +occur, with the potential of losing the frames that collided. + + +

2.4.4 Procedures for Information Transfer

+ +Once a connection has been established, as outlined +above, both devices are able to accept I, S, and U frames. + + +
2.4.4.1 Sending I Frames
+ +Whenever a DXE has an I frame to transmit, it will send +the I frame with N(S) of the control field equal to its current +send state variable V(S). Once the I frame is sent, the send +state variable is incremented by one. If timer T1 is not +running, it should be started. If timer T1 is running, it should +be restarted. + +

The DXE should not transmit any more I frames if its send +state variable equals the last received N(R) from the other side +of the link plus seven. If it were to send more I frames, the +flow control window would be exceed, and errors could result. + +

If a DXE is in a busy condition, it may still send I +frames as long as the other device is not also busy. + +

If a DXE is in the frame-rejection mode, it will stop +sending I frames. + + +

2.4.4.2 Receiving I Frames
+ + +

2.4.4.2.1

+If a DXE receives a valid I frame (one with a correct FCS +and whose send sequence number equals the receiver's receive +state variable) and is not in the busy condition, it will accept +the received I frame, increment its receive state variable, and +act in one of the following manners: + +
    +
  1. If it has an I frame to send, that I frame may be sent with the + transmitted N(R) equal to its receive state variable V(R) (thus + acknowledging the received frame). Alternately, the device may + send a RR frame with N(R) equal to V(R), and then send the I + frame. +
  2. If there are no outstanding I frames, the receiving device will + send a RR frame with N(R) equal to V(R). The receiving DXE may + wait a small period of time before sending the RR frame to be sure + additional I frames are not being transmitted. +
+ + +

2.4.4.2.2

+If the DXE is in a busy condition, it may ignore any +received I frames without reporting this condition other than +repeating the indication of the busy condition. + +

If a busy condition exists, the DXE receiving the busy +condition indication should poll the sender of the busy +indication periodically until the busy condition disappears. + +

A DXE may poll the busy DXE periodically with RR or RNR +frames with the P bit set to one. + +

The reception of I frames that contain zero-length +information fields shall be reported to the next level but no +information field will be transferred. + + +

2.4.4.3 Reception of Out of Sequence Frames
+ +When an I frame is received with a correct FCS, but its +send sequence number, N(S), does not match the current receiver's +receive state variable, the frame should be discarded. A REJ +frame shall be sent with a receive sequence number equal to one +higher (modulo 8) than the last correctly received I frame if an +uncleared N(S) sequence error condition has not been previously +established. The received state variable and poll bit of the +discarded frame should be checked and acted upon, if necessary, +before discarding the frame. + + +
2.4.4.4 Reception of Incorrect Frames
+ +When a DXE receives a frame with an incorrect FCS, an +invalid frame, or a frame with an improper address, that frame +shall be discarded. + + +
2.4.4.5 Receiving Acknowledgement
+ +Whenever an I or S frame is correctly received, even in a +busy condition, the N(R) of the received frame should be checked +to see if it includes an acknowledgement of outstanding sent I +frames. The T1 timer should be cancelled if the received frame +actually acknowledges previously unacknowledged frames. If the +T1 timer is cancelled and there are still some frames that have +been sent that are not acknowledged, T1 should be started again. +If the T1 timer runs out before an acknowledgement is received, +the device should proceed to the retransmission procedure in +2.4.4.9. + + +
2.4.4.6 Receiving Reject
+ +Upon receiving a REJ frame, the transmitting DXE will set +its send state variable to the same value as the REJ frame's +received sequence number in the control field. The DXE will then +retransmit any I frame(s) outstanding at the next available +opportunity conforming to the following: + +
    +
  1. If the DXE is not transmitting at the time, and the channel + is open, the device may commence to retransmit the I + frame(s) immediately. +
  2. If the DXE is operating on a full-duplex channel + transmitting a UI or S frame when it receives a REJ frame, + it may finish sending the UI or S frame and then retransmit + the I frame(s). +
  3. If the DXE is operating in a full-duplex channel + transmitting another I frame when it receives a REJ frame, + it may abort the I frame it was sending and start + retransmission of the requested I frames immediately. +
  4. The DXE may send just the one I frame outstanding, or it may + send more than the one indicated if more I frames followed + the first one not acknowledged, provided the total to be + sent does not exceed the flow-control window (7 frames). +
+ +

If the DXE receives a REJ frame with the poll bit set, it +should respond with either a RR or RNR frame with the final bit +set before retransmitting the outstanding I frame(s). + + +

2.4.4.7 Receiving a RNR Frame
+ +Whenever a DXE receives a RNR frame, it shall stop +transmission of I frames until the busy condition has been +cleared. If timer T1 runs out after the RNR was received, the +waiting acknowledgement procedure listed in 2.4.4.9, below, +should be performed. The poll bit may be used in conjunction +with S frames to test for a change in the condition of the +busied-out DXE. + + +
2.4.4.8 Sending a Busy Indication
+ +Whenever a DXE enters a busy condition, it will indicate +this by sending a RNR response at the next opportunity. While +the DXE is in the busy condition, it may receive and process S +frames, and if a received S frame has the P bit set to one, the +DXE should send a RNR frame with the F bit set to one at the next +possible opportunity. To clear the busy condition, the DXE +should send either a RR or REJ frame with the received sequence +number equal to the current receive state variable, depending on +whether the last received I frame was properly received or not. + + +
2.4.4.9 Waiting Acknowledgement
+ +If timer T1 runs out waiting the acknowledgement from the +other DXE for an I frame transmitted, the DXE will restart timer +T1 and transmit an appropriate supervisory command frame (RR or +RNR) with the P bit set. If the DXE receives correctly a +supervisory response frame with the F bit set and with an N(R) +within the range from the last N(R) received to the last N(S) +sent plus one, the DXE will restart timer T1 and will set its +send state variable V(S) to the received N(R). It may then +resume with I frame transmission or retransmission, as +appropriate. If, on the other hand, the DXE receives correctly a +supervisory response frame with the F bit not set, or an I frame +or supervisory command frame, and with an N(R) within the range +from the last N(R) received to the last N(S) sent plus one, the +DXE will not restart timer T1, but will use the received N(R) as +an indication of acknowledgement of transmitted I frames up to +and including I frame numbered N(R)-1. + +

If timer T1 runs out before a supervisory response frame +with the F bit set is received, the DXE will retransmit an +appropriate supervisory command frame (RR or RNR) with the P bit +set. After N2 attempts to get a supervisory response frame with +the F bit set from the other DXE, the DXE will initiate a link +resetting procedure as described in 2.4.6, below. + + +

2.4.5 Frame Rejection Conditions

+ +A DXE shall initiate the frame-reset procedure when a +frame is received with the correct FCS and address field during +the information-transfer state with one or more of the conditions +in 2.3.4.3.3, above. + +

Under these conditions, the DXE will ask the other DXE to +reset the link by transmitting a FRMR response as outlined in + +2.4.6.3, below. + +

After sending the FRMR frame, the sending DXE will enter +the frame reject condition. This condition is cleared when the +DXE that sent the FRMR frame receives a SABM or DISC command, or +a DM response frame. Any other command received while the DXE is +in the frame reject state will cause another FRMR to be sent out +with the same information field as originally sent. + +

In the frame rejection condition, additional I frames +will not be transmitted, and received I frames and S frames will +be discarded by the DXE. + +

The DXE that sent the FRMR frame shall start the T1 timer +when the FRMR is sent. If no SABM or DISC frame is received +before the timer runs out, the FRMR frame shall be retransmitted, +and the T1 timer restarted as described in the waiting +acknowledgement section (2.4.4.9) above. If the FRMR is sent N2 +times without success, the link shall be reset. + + +

2.4.6 Resetting Procedure

+ + +
2.4.6.1
+The resetting procedure is used to initialize both +directions of data flow after a nonrecoverable error has +occurred. This resetting procedure is used in the information- +transfer state of an AX.25 link only. + + +
2.4.6.2
+A DXE shall initiate a reset procedure whenever it receives +an unexpected UA response frame or an unsolicited response frame +with the F bit set to one. A DXE may also initiate the reset +procedure upon receipt of a FRMR frame. Alternatively, the DXE +may respond to a FRMR by terminating the connection with a DISC +frame. + + +
2.4.6.3
+A DXE shall reset the link by sending a SABM frame and +starting timer T1. Upon receiving a SABM frame from the DXE +previously connected to, the receiver of a SABM frame should send +a UA frame back at the earliest opportunity, set its send and +receive state variables, V(S) and V(R), to zero and stop T1 +unless it has sent a SABM or DISC itself. If the UA is correctly +received by the initial DXE, it resets its send and receive state +variables, V(S) and V(R), and stops timer T1. Any busy condition +that previously existed will also be cleared. + +

If a DM response is received, the DXE will enter the +disconnected state and stop timer T1. If timer T1 runs out +before a UA or DM response frame is received, the SABM will be +retransmitted and timer T1 restarted. If timer T1 runs out N2 +times, the DXE will enter the disconnected state, and any +previously existing link conditions will be cleared. + +

Other commands or responses received by the DXE before +completion of the reset procedure will be discarded. + + +

2.4.6.4
+One DXE may request that the other DXE reset the link by +sending a DM response frame. After the DM frame is sent, the +sending DXE will then enter the disconnected state. + + +

2.4.7 List of System Defined Parameters

+ + +
2.4.7.1 Timers
+ +To maintain the integrity of the AX.25 level 2 +connection, use of these timers is recommended. + + +

2.4.7.1.1 Acknowledgement Timer T1

+ +The first timer, T1, is used to make sure a DXE doesn't +wait forever for a response to a frame it sends. This timer +cannot be expressed in absolute time, since the time required to +send frames varies greatly with the signaling rate used at level +1. T1 should take at least twice the amount of time it would +take to send maximum length frame to the other DXE, and get the +proper response frame back from the other DXE. This would allow +time for the other DXE to do some processing before responding. + +

If level 2 repeaters are to be used, the value of T1 +should be adjusted according to the number of repeaters the frame +is being transferred through. + + +

2.4.7.1.2 Response Delay Timer T2

+ +The second timer, T2, may be implemented by the DXE to +specify a maximum amount of delay to be introduced between the +time an I frame is received, and the time the resulting response +frame is sent. This delay may be introduced to allow a receiving +DXE to wait a short period of time to determine if there is more +than one frame being sent to it. If more frames are received, +the DXE can acknowledge them at once (up to seven), rather than +acknowledge each individual frame. The use of timer T2 is not +mandatory, but is recommended to improve channel efficiency. +Note that, on full-duplex channels, acknowledgements should not +be delayed beyond k/2 frames to achieve maximum throughput. The +k parameter is defined in 2.4.7.4, below. + + +

2.4.7.1.3 Inactive Link Timer T3

+ +The third timer, T3, is used whenever T1 isn't running to +maintain link integrity. It is recommended that whenever there +are no outstanding unacknowledged I frames or P-bit frames +(during the information-transfer state), a RR or RNR frame with +the P bit set to one be sent every T3 time units to query the +status of the other DXE. The period of T3 is locally defined, +and depends greatly on level 1 operation. T3 should be greater +than T1, and may be very large on channels of high integrity. + + +
2.4.7.2 Maximum Number of Retries (N2)
+ +The maximum number of retries is used in conjunction with +the T1 timer. + + +
2.4.7.3 Maximum Number of Octets in an I Field (N1)
+ +The maximum number of octets allowed in the I field will +be 256. There shall also be an integral number of octets. + + +
2.4.7.4 Maximum Number of I Frames Outstanding (k)
+ +The maximum number of outstanding I frames at a time is +seven. + + +
+This document was originally downloaded from the +TAPR archives and +FTP site, +as ax25.doc. +HTML markup was done by Bill Buthod, N5RRS. Last updated: 27 Dec 1997 + + + + +
+
+ + +
+ +About Us | Privacy Policy | Contact Us | +©2005-2019 Tucson Amateur Packet Radio Corp unless otherwise noted. +

+

+

+ + +
+ + + +Authorize.Net Merchant - Click to Verify + + +Accept Credit Cards + +
+

+ +

+ +
+ + + + + + \ No newline at end of file diff --git a/doc/ax25-2p0/index_files/analytics.js b/doc/ax25-2p0/index_files/analytics.js new file mode 100644 index 0000000..ac86472 --- /dev/null +++ b/doc/ax25-2p0/index_files/analytics.js @@ -0,0 +1,474 @@ +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3.0 +/* eslint-disable no-var, semi, prefer-arrow-callback, prefer-template */ + +/** + * Collection of methods for sending analytics events to Archive.org's analytics server. + * + * These events are used for internal stats and sent (in anonymized form) to Google Analytics. + * + * @see analytics.md + * + * @type {Object} + */ +window.archive_analytics = (function defineArchiveAnalytics() { + // keep orignal Date object so as not to be affected by wayback's + // hijacking global Date object + var Date = window.Date; + var ARCHIVE_ANALYTICS_VERSION = 2; + var DEFAULT_SERVICE = 'ao_2'; + var NO_SAMPLING_SERVICE = 'ao_no_sampling'; // sends every event instead of a percentage + + var startTime = new Date(); + + /** + * @return {Boolean} + */ + function isPerformanceTimingApiSupported() { + return 'performance' in window && 'timing' in window.performance; + } + + /** + * Determines how many milliseconds elapsed between the browser starting to parse the DOM and + * the current time. + * + * Uses the Performance API or a fallback value if it's not available. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API + * + * @return {Number} + */ + function getLoadTime() { + var start; + + if (isPerformanceTimingApiSupported()) + start = window.performance.timing.domLoading; + else + start = startTime.getTime(); + + return new Date().getTime() - start; + } + + /** + * Determines how many milliseconds elapsed between the user navigating to the page and + * the current time. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API + * + * @return {Number|null} null if the browser doesn't support the Performance API + */ + function getNavToDoneTime() { + if (!isPerformanceTimingApiSupported()) + return null; + + return new Date().getTime() - window.performance.timing.navigationStart; + } + + /** + * Performs an arithmetic calculation on a string with a number and unit, while maintaining + * the unit. + * + * @param {String} original value to modify, with a unit + * @param {Function} doOperation accepts one Number parameter, returns a Number + * @returns {String} + */ + function computeWithUnit(original, doOperation) { + var number = parseFloat(original, 10); + var unit = original.replace(/(\d*\.\d+)|\d+/, ''); + + return doOperation(number) + unit; + } + + /** + * Computes the default font size of the browser. + * + * @returns {String|null} computed font-size with units (typically pixels), null if it cannot be computed + */ + function getDefaultFontSize() { + var fontSizeStr; + + if (!('getComputedStyle' in window)) + return null; + + var style = window.getComputedStyle(document.documentElement); + if (!style) + return null; + + fontSizeStr = style.fontSize; + + // Don't modify the value if tracking book reader. + if (document.querySelector('#BookReader')) + return fontSizeStr; + + return computeWithUnit(fontSizeStr, function reverseBootstrapFontSize(number) { + // Undo the 62.5% size applied in the Bootstrap CSS. + return number * 1.6; + }); + } + + /** + * Get the URL parameters for a given Location + * @param {Location} + * @return {Object} The URL parameters + */ + function getParams(location) { + if (!location) location = window.location; + var vars; + var i; + var pair; + var params = {}; + var query = location.search; + if (!query) return params; + vars = query.substring(1).split('&'); + for (i = 0; i < vars.length; i++) { + pair = vars[i].split('='); + params[pair[0]] = decodeURIComponent(pair[1]); + } + return params; + } + + function getMetaProp(name) { + var metaTag = document.querySelector('meta[property=' + name + ']'); + return metaTag ? metaTag.getAttribute('content') || null : null; + } + + var ArchiveAnalytics = { + /** + * @type {String|null} + */ + service: getMetaProp('service'), + mediaType: getMetaProp('mediatype'), + primaryCollection: getMetaProp('primary_collection'), + + /** + * Key-value pairs to send in pageviews (you can read this after a pageview to see what was + * sent). + * + * @type {Object} + */ + values: {}, + + /** + * Sends an analytics ping, preferably using navigator.sendBeacon() + * @param {Object} values + * @param {Function} [onload_callback] (deprecated) callback to invoke once ping to analytics server is done + * @param {Boolean} [augment_for_ao_site] (deprecated) if true, add some archive.org site-specific values + */ + send_ping: function send_ping(values, onload_callback, augment_for_ao_site) { + if (typeof window.navigator !== 'undefined' && typeof window.navigator.sendBeacon !== 'undefined') + this.send_ping_via_beacon(values); + else + this.send_ping_via_image(values); + }, + + /** + * Sends a ping via Beacon API + * NOTE: Assumes window.navigator.sendBeacon exists + * @param {Object} values Tracking parameters to pass + */ + send_ping_via_beacon: function send_ping_via_beacon(values) { + var url = this.generate_tracking_url(values || {}); + window.navigator.sendBeacon(url); + }, + + /** + * Sends a ping via Image object + * @param {Object} values Tracking parameters to pass + */ + send_ping_via_image: function send_ping_via_image(values) { + var url = this.generate_tracking_url(values || {}); + var loadtime_img = new Image(1, 1); + loadtime_img.src = url; + loadtime_img.alt = ''; + }, + + /** + * Construct complete tracking URL containing payload + * @param {Object} params Tracking parameters to pass + * @return {String} URL to use for tracking call + */ + generate_tracking_url: function generate_tracking_url(params) { + var baseUrl = '//analytics.archive.org/0.gif'; + var keys; + var outputParams = params; + var outputParamsArray = []; + + outputParams.service = outputParams.service || this.service || DEFAULT_SERVICE; + + // Build array of querystring parameters + keys = Object.keys(outputParams); + keys.forEach(function keyIteration(key) { + outputParamsArray.push(encodeURIComponent(key) + '=' + encodeURIComponent(outputParams[key])); + }); + outputParamsArray.push('version=' + ARCHIVE_ANALYTICS_VERSION); + outputParamsArray.push('count=' + (keys.length + 2)); // Include `version` and `count` in count + + return baseUrl + '?' + outputParamsArray.join('&'); + }, + + /** + * @param {int} page Page number + */ + send_scroll_fetch_event: function send_scroll_fetch_event(page) { + var additionalValues = { ev: page }; + var loadTime = getLoadTime(); + var navToDoneTime = getNavToDoneTime(); + if (loadTime) additionalValues.loadtime = loadTime; + if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime; + this.send_event('page_action', 'scroll_fetch', location.pathname, additionalValues); + }, + + send_scroll_fetch_base_event: function send_scroll_fetch_base_event() { + var additionalValues = {}; + var loadTime = getLoadTime(); + var navToDoneTime = getNavToDoneTime(); + if (loadTime) additionalValues.loadtime = loadTime; + if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime; + this.send_event('page_action', 'scroll_fetch_base', location.pathname, additionalValues); + }, + + /** + * @param {Object} [options] + * @param {String} [options.mediaType] + * @param {String} [options.mediaLanguage] + * @param {String} [options.page] The path portion of the page URL + */ + send_pageview: function send_pageview(options) { + var settings = options || {}; + + var defaultFontSize; + var loadTime = getLoadTime(); + var mediaType = settings.mediaType; + var primaryCollection = settings.primaryCollection; + var page = settings.page; + var navToDoneTime = getNavToDoneTime(); + + /** + * @return {String} + */ + function get_locale() { + if (navigator) { + if (navigator.language) + return navigator.language; + + else if (navigator.browserLanguage) + return navigator.browserLanguage; + + else if (navigator.systemLanguage) + return navigator.systemLanguage; + + else if (navigator.userLanguage) + return navigator.userLanguage; + } + return ''; + } + + defaultFontSize = getDefaultFontSize(); + + // Set field values + this.values.kind = 'pageview'; + this.values.timediff = (new Date().getTimezoneOffset()/60)*(-1); // *timezone* diff from UTC + this.values.locale = get_locale(); + this.values.referrer = (document.referrer == '' ? '-' : document.referrer); + + if (loadTime) + this.values.loadtime = loadTime; + + if (navToDoneTime) + this.values.nav_to_done_ms = navToDoneTime; + + if (settings.trackingId) { + this.values.ga_tid = settings.trackingId; + } + + /* START CUSTOM DIMENSIONS */ + if (defaultFontSize) + this.values.iaprop_fontSize = defaultFontSize; + + if ('devicePixelRatio' in window) + this.values.iaprop_devicePixelRatio = window.devicePixelRatio; + + if (mediaType) + this.values.iaprop_mediaType = mediaType; + + if (settings.mediaLanguage) { + this.values.iaprop_mediaLanguage = settings.mediaLanguage; + } + + if (primaryCollection) { + this.values.iaprop_primaryCollection = primaryCollection; + } + /* END CUSTOM DIMENSIONS */ + + if (page) + this.values.page = page; + + this.send_ping(this.values); + }, + + /** + * Sends a tracking "Event". + * @param {string} category + * @param {string} action + * @param {string} label + * @param {Object} additionalEventParams + */ + send_event: function send_event( + category, + action, + label, + additionalEventParams + ) { + if (!label) label = window.location.pathname; + if (!additionalEventParams) additionalEventParams = {}; + if (additionalEventParams.mediaLanguage) { + additionalEventParams.ga_cd4 = additionalEventParams.mediaLanguage; + delete additionalEventParams.mediaLanguage; + } + var eventParams = Object.assign( + { + kind: 'event', + ec: category, + ea: action, + el: label, + cache_bust: Math.random(), + }, + additionalEventParams + ); + this.send_ping(eventParams); + }, + + /** + * Sends every event instead of a small percentage. + * + * Use this sparingly as it can generate a lot of events. + * + * @param {string} category + * @param {string} action + * @param {string} label + * @param {Object} additionalEventParams + */ + send_event_no_sampling: function send_event_no_sampling( + category, + action, + label, + additionalEventParams + ) { + var extraParams = additionalEventParams || {}; + extraParams.service = NO_SAMPLING_SERVICE; + this.send_event(category, action, label, extraParams); + }, + + /** + * @param {Object} options see this.send_pageview options + */ + send_pageview_on_load: function send_pageview_on_load(options) { + var self = this; + window.addEventListener('load', function send_pageview_with_options() { + self.send_pageview(options); + }); + }, + + /** + * Handles tracking events passed in URL. + * Assumes category and action values are separated by a "|" character. + * NOTE: Uses the unsampled analytics property. Watch out for future high click links! + * @param {Location} + */ + process_url_events: function process_url_events(location) { + var eventValues; + var actionValue; + var eventValue = getParams(location).iax; + if (!eventValue) return; + eventValues = eventValue.split('|'); + actionValue = eventValues.length >= 1 ? eventValues[1] : ''; + this.send_event_no_sampling( + eventValues[0], + actionValue, + window.location.pathname + ); + }, + + /** + * Attaches handlers for event tracking. + * + * To enable click tracking for a link, add a `data-event-click-tracking` + * attribute containing the Google Analytics Event Category and Action, separated + * by a vertical pipe (|). + * e.g. `` + * + * To enable form submit tracking, add a `data-event-form-tracking` attribute + * to the `form` tag. + * e.g. `
` + * + * Additional tracking options can be added via a `data-event-tracking-options` + * parameter. This parameter, if included, should be a JSON string of the parameters. + * Valid parameters are: + * - service {string}: Corresponds to the Google Analytics property data values flow into + */ + set_up_event_tracking: function set_up_event_tracking() { + var self = this; + var clickTrackingAttributeName = 'event-click-tracking'; + var formTrackingAttributeName = 'event-form-tracking'; + var trackingOptionsAttributeName = 'event-tracking-options'; + + function handleAction(event, attributeName) { + var selector = '[data-' + attributeName + ']'; + var eventTarget = event.target; + if (!eventTarget) return; + var target = eventTarget.closest(selector); + if (!target) return; + var categoryAction; + var categoryActionParts; + var options; + categoryAction = target.dataset[toCamelCase(attributeName)]; + if (!categoryAction) return; + categoryActionParts = categoryAction.split('|'); + options = target.dataset[toCamelCase(trackingOptionsAttributeName)]; + options = options ? JSON.parse(options) : {}; + self.send_event( + categoryActionParts[0], + categoryActionParts[1], + categoryActionParts[2] || window.location.pathname, + options.service ? { service: options.service } : {} + ); + } + + function toCamelCase(str) { + return str.replace(/\W+(.)/g, function (match, chr) { + return chr.toUpperCase(); + }); + }; + + document.addEventListener('click', function(e) { + handleAction(e, clickTrackingAttributeName); + }); + + document.addEventListener('submit', function(e) { + handleAction(e, formTrackingAttributeName); + }); + }, + + /** + * @returns {Object[]} + */ + get_data_packets: function get_data_packets() { + return [this.values]; + }, + + /** + * Creates a tracking image for tracking JS compatibility. + * + * @param {string} type The type value for track_js_case in query params for 0.gif + */ + create_tracking_image: function create_tracking_image(type) { + this.send_ping_via_image({ + cache_bust: Math.random(), + kind: 'track_js', + track_js_case: type, + }); + } + }; + + return ArchiveAnalytics; +}()); +// @license-end diff --git a/doc/ax25-2p0/index_files/banner-styles.css b/doc/ax25-2p0/index_files/banner-styles.css new file mode 100644 index 0000000..0ab5af9 --- /dev/null +++ b/doc/ax25-2p0/index_files/banner-styles.css @@ -0,0 +1,507 @@ +@import 'record.css'; /* for SPN1 */ + +#wm-ipp-base { + height:65px;/* initial height just in case js code fails */ + padding:0; + margin:0; + border:none; + background:none transparent; +} +#wm-ipp { + z-index: 2147483647; +} +#wm-ipp, #wm-ipp * { + font-family:Lucida Grande, Helvetica, Arial, sans-serif; + font-size:12px; + line-height:1.2; + letter-spacing:0; + width:auto; + height:auto; + max-width:none; + max-height:none; + min-width:0 !important; + min-height:0; + outline:none; + float:none; + text-align:left; + border:none; + color: #000; + text-indent: 0; + position: initial; + background: none; +} +#wm-ipp div, #wm-ipp canvas { + display: block; +} +#wm-ipp div, #wm-ipp tr, #wm-ipp td, #wm-ipp a, #wm-ipp form { + padding:0; + margin:0; + border:none; + border-radius:0; + background-color:transparent; + background-image:none; + /*z-index:2147483640;*/ + height:auto; +} +#wm-ipp table { + border:none; + border-collapse:collapse; + margin:0; + padding:0; + width:auto; + font-size:inherit; +} +#wm-ipp form input { + padding:1px !important; + height:auto; + display:inline; + margin:0; + color: #000; + background: none #fff; + border: 1px solid #666; +} +#wm-ipp form input[type=submit] { + padding:0 8px !important; + margin:1px 0 1px 5px !important; + width:auto !important; + border: 1px solid #000 !important; + background: #fff !important; + color: #000 !important; +} +#wm-ipp form input[type=submit]:hover { + background: #eee !important; + cursor: pointer !important; +} +#wm-ipp form input[type=submit]:active { + transform: translateY(1px); +} +#wm-ipp a { + display: inline; +} +#wm-ipp a:hover{ + text-decoration:underline; +} +#wm-ipp a.wm-btn:hover { + text-decoration:none; + color:#ff0 !important; +} +#wm-ipp a.wm-btn:hover span { + color:#ff0 !important; +} +#wm-ipp #wm-ipp-inside { + margin: 0 6px; + border:5px solid #000; + border-top:none; + background-color:rgba(255,255,255,0.9); + -moz-box-shadow:1px 1px 4px #333; + -webkit-box-shadow:1px 1px 4px #333; + box-shadow:1px 1px 4px #333; + border-radius:0 0 8px 8px; +} +/* selectors are intentionally verbose to ensure priority */ +#wm-ipp #wm-logo { + padding:0 10px; + vertical-align:middle; + min-width:100px; + flex: 0 0 100px; +} +#wm-ipp .c { + padding-left: 4px; +} +#wm-ipp .c .u { + margin-top: 4px !important; +} +#wm-ipp .n { + padding:0 0 0 5px !important; + vertical-align: bottom; +} +#wm-ipp .n a { + text-decoration:none; + color:#33f; + font-weight:bold; +} +#wm-ipp .n .b { + padding:0 6px 0 0 !important; + text-align:right !important; + overflow:visible; + white-space:nowrap; + color:#99a; + vertical-align:middle; +} +#wm-ipp .n .y .b { + padding:0 6px 2px 0 !important; +} +#wm-ipp .n .c { + background:#000; + color:#ff0; + font-weight:bold; + padding:0 !important; + text-align:center; +} +#wm-ipp.hi .n td.c { + color:#ec008c; +} +#wm-ipp .n td.f { + padding:0 0 0 6px !important; + text-align:left !important; + overflow:visible; + white-space:nowrap; + color:#99a; + vertical-align:middle; +} +#wm-ipp .n tr.m td { + text-transform:uppercase; + white-space:nowrap; + padding:2px 0; +} +#wm-ipp .c .s { + padding:0 5px 0 0 !important; + vertical-align:bottom; +} +#wm-ipp #wm-nav-captures { + white-space: nowrap; +} +#wm-ipp .c .s a.t { + color:#33f; + font-weight:bold; + line-height: 1.8; +} +#wm-ipp .c .s div.r { + color: #666; + font-size:9px; + white-space:nowrap; +} +#wm-ipp .c .k { + padding-bottom:1px; +} +#wm-ipp .c .s { + padding:0 5px 2px 0 !important; +} +#wm-ipp td#displayMonthEl { + padding: 2px 0 !important; +} +#wm-ipp td#displayYearEl { + padding: 0 0 2px 0 !important; +} + +div#wm-ipp-sparkline { + position:relative;/* for positioning markers */ + white-space:nowrap; + background-color:#fff; + cursor:pointer; + line-height:0.9; +} +#sparklineImgId, #wm-sparkline-canvas { + position:relative; + z-index:9012; + max-width:none; +} +#wm-ipp-sparkline div.yt { + position:absolute; + z-index:9010 !important; + background-color:#ff0 !important; + top: 0; +} +#wm-ipp-sparkline div.mt { + position:absolute; + z-index:9013 !important; + background-color:#ec008c !important; + top: 0; +} +#wm-ipp .r { + margin-left: 4px; +} +#wm-ipp .r a { + color:#33f; + border:none; + position:relative; + background-color:transparent; + background-repeat:no-repeat !important; + background-position:100% 100% !important; + text-decoration: none; +} +#wm-ipp #wm-capinfo { + /* prevents notice div background from sticking into round corners of + #wm-ipp-inside */ + border-radius: 0 0 4px 4px; +} +#wm-ipp #wm-capinfo .c-logo { + display:block; + float:left; + margin-right:3px; + width:90px; + min-height:90px; + max-height: 290px; + border-radius:45px; + overflow:hidden; + background-position:50%; + background-size:auto 90px; + box-shadow: 0 0 2px 2px rgba(208,208,208,128) inset; +} +#wm-ipp #wm-capinfo .c-logo span { + display:inline-block; +} +#wm-ipp #wm-capinfo .c-logo img { + height:90px; + position:relative; + left:-50%; +} +#wm-ipp #wm-capinfo .wm-title { + font-size:130%; +} +#wm-ipp #wm-capinfo a.wm-selector { + display:inline-block; + color: #aaa; + text-decoration:none !important; + padding: 2px 8px; +} +#wm-ipp #wm-capinfo a.wm-selector.selected { + background-color:#666; +} +#wm-ipp #wm-capinfo a.wm-selector:hover { + color: #fff; +} +#wm-ipp #wm-capinfo.notice-only #wm-capinfo-collected-by, +#wm-ipp #wm-capinfo.notice-only #wm-capinfo-timestamps { + display: none; +} +#wm-ipp #wm-capinfo #wm-capinfo-notice .wm-capinfo-content { + background-color:#ff0; + padding:5px; + font-size:14px; + text-align:center; +} +#wm-ipp #wm-capinfo #wm-capinfo-notice .wm-capinfo-content * { + font-size:14px; + text-align:center; +} +#wm-ipp #wm-expand { + right: 1px; + bottom: -1px; + color: #ffffff; + background-color: #666 !important; + padding:0 5px 0 3px !important; + border-radius: 3px 3px 0 0 !important; +} +#wm-ipp #wm-expand span { + color: #ffffff; +} +#wm-ipp #wm-expand #wm-expand-icon { + display: inline-block; + transition: transform 0.5s; + transform-origin: 50% 45%; +} +#wm-ipp #wm-expand.wm-open #wm-expand-icon { + transform: rotate(180deg); +} +#wm-ipp #wmtb { + text-align:right; +} +#wm-ipp #wmtb #wmtbURL { + width: calc(100% - 45px); +} +#wm-ipp #wm-graph-anchor { + border-right:1px solid #ccc; +} +/* time coherence */ +html.wb-highlight { + box-shadow: inset 0 0 0 3px #a50e3a !important; +} +.wb-highlight { + outline: 3px solid #a50e3a !important; +} +#wm-ipp-print { + display:none !important; +} +@media print { +#wm-ipp-base { + display:none !important; +} +#wm-ipp-print { + display:block !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +} +@media (max-width:414px) { + #wm-ipp .xxs { + display:none !important; + } +} +@media (min-width:1055px) { +#wm-ipp #wm-graph-anchor { + display:block !important; +} +} +@media (max-width:1054px) { +#wm-ipp #wm-graph-anchor { + display:none !important; +} +} +@media (max-width:1163px) { +#wm-logo { + display:none !important; +} +} + +#wm-btns { + white-space: nowrap; + margin-top: -2px; +} + +#wm-btns #wm-save-snapshot-open { + margin-right: 7px; + top: -6px; +} + +#wm-btns #wm-sign-in { + box-sizing: content-box; + display: none; + margin-right: 7px; + top: -8px; + + /* + round border around sign in button + */ + border: 2px #000 solid; + border-radius: 14px; + padding-right: 2px; + padding-bottom: 2px; + width: 11px; + height: 11px; +} + +#wm-btns #wm-sign-in>.iconochive-person { + font-size: 12.5px; +} + +#wm-save-snapshot-open > .iconochive-web { + color:#000; + font-size:160%; +} + +#wm-ipp #wm-share { + display: flex; + align-items: flex-end; + justify-content: space-between; +} + +#wm-share > #wm-screenshot { + display: inline-block; + margin-right: 3px; + visibility: hidden; +} + +#wm-screenshot > .iconochive-image { + color:#000; + font-size:160%; +} + +#wm-share > #wm-video { + display: inline-block; + margin-right: 3px; + visibility: hidden; +} + +#wm-video > .iconochive-movies { + color: #000; + display: inline-block; + font-size: 150%; + margin-bottom: 2px; +} + +#wm-btns #wm-save-snapshot-in-progress { + display: none; + font-size:160%; + opacity: 0.5; + position: relative; + margin-right: 7px; + top: -5px; +} + +#wm-btns #wm-save-snapshot-success { + display: none; + color: green; + position: relative; + top: -7px; +} + +#wm-btns #wm-save-snapshot-fail { + display: none; + color: red; + position: relative; + top: -7px; +} + +.wm-icon-screen-shot { + background: url("../images/web-screenshot.svg") no-repeat !important; + background-size: contain !important; + width: 22px !important; + height: 19px !important; + + display: inline-block; +} +#donato { + /* transition effect is disable so as to simplify height adjustment */ + /*transition: height 0.5s;*/ + height: 0; + margin: 0; + padding: 0; + border-bottom: 1px solid #999 !important; +} +body.wm-modal { + height: auto !important; + overflow: hidden !important; +} +#donato #donato-base { + width: 100%; + height: 100%; + /*bottom: 0;*/ + margin: 0; + padding: 0; + position: absolute; + z-index: 2147483639; +} +body.wm-modal #donato #donato-base { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 2147483640; +} + +.wb-autocomplete-suggestions { + font-family: Lucida Grande, Helvetica, Arial, sans-serif; + font-size: 12px; + text-align: left; + cursor: default; + border: 1px solid #ccc; + border-top: 0; + background: #fff; + box-shadow: -1px 1px 3px rgba(0,0,0,.1); + position: absolute; + display: none; + z-index: 2147483647; + max-height: 254px; + overflow: hidden; + overflow-y: auto; + box-sizing: border-box; +} +.wb-autocomplete-suggestion { + position: relative; + padding: 0 .6em; + line-height: 23px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 1.02em; + color: #333; +} +.wb-autocomplete-suggestion b { + font-weight: bold; +} +.wb-autocomplete-suggestion.selected { + background: #f0f0f0; +} diff --git a/doc/ax25-2p0/index_files/bundle-playback.js b/doc/ax25-2p0/index_files/bundle-playback.js new file mode 100644 index 0000000..00c2744 --- /dev/null +++ b/doc/ax25-2p0/index_files/bundle-playback.js @@ -0,0 +1,3 @@ +// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 +!function(t){var e={};function n(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(o,r,function(e){return t[e]}.bind(null,r));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=9)}([function(t,e,n){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){for(var n=0;n=0}function v(t,e){var n=window["HTML".concat(t,"Element")];if(void 0!==n){var o=Object.getOwnPropertyDescriptor(n.prototype,e);void 0!==o&&Object.defineProperty(n.prototype,"_wm_".concat(e),o)}}function y(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"src",n="_wm_".concat(e);return n in t.__proto__?t[n]:t[e]}v("Image","src"),v("Media","src"),v("Embed","src"),v("IFrame","src"),v("Script","src"),v("Link","href"),v("Anchor","href")},function(t,e,n){"use strict";n.d(e,"c",(function(){return s})),n.d(e,"b",(function(){return a})),n.d(e,"a",(function(){return c}));var o=["January","February","March","April","May","June","July","August","September","October","November","December"],r=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],i={Y:function(t){return t.getUTCFullYear()},m:function(t){return t.getUTCMonth()+1},b:function(t){return r[t.getUTCMonth()]},B:function(t){return o[t.getUTCMonth()]},d:function(t){return t.getUTCDate()},H:function(t){return("0"+t.getUTCHours()).slice(-2)},M:function(t){return("0"+t.getUTCMinutes()).slice(-2)},S:function(t){return("0"+t.getUTCSeconds()).slice(-2)},"%":function(){return"%"}};function s(t){var e=function(t){return"number"==typeof t&&(t=t.toString()),[t.slice(-14,-10),t.slice(-10,-8),t.slice(-8,-6),t.slice(-6,-4),t.slice(-4,-2),t.slice(-2)]}(t);return new Date(Date.UTC(e[0],e[1]-1,e[2],e[3],e[4],e[5]))}function a(t){return r[t]}function c(t,e){return e.replace(/%./g,(function(e){var n=i[e[1]];return n?n(s(t)):e}))}},function(t,e,n){"use strict";n.d(e,"b",(function(){return a})),n.d(e,"a",(function(){return c}));var o=n(0);function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function i(t,e){for(var n=0;n=400?r.failure&&r.failure(t):r.success&&r.success(t)}),{"Content-Type":"application/json"},s.stringify({url:t,snapshot:e,tags:n||[]})),!1}var c=function(){function t(e,n,r){var i=this;!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.el=e,this.url=n,this.timestamp=r,e.onclick=this.save.bind(this),document.addEventListener("DOMContentLoaded",(function(){i.enableSaveSnapshot(Object(o.c)("logged-in-user"))}))}var e,n,r;return e=t,(n=[{key:"save",value:function(t){this.start(),a(this.url,this.timestamp,[],{failure:this.failure.bind(this),success:this.success.bind(this)})}},{key:"start",value:function(){this.hide(["wm-save-snapshot-fail","wm-save-snapshot-open","wm-save-snapshot-success"]),this.show(["wm-save-snapshot-in-progress"])}},{key:"failure",value:function(t){401==t.status?this.userNotLoggedIn(t):(this.hide(["wm-save-snapshot-in-progress","wm-save-snapshot-success"]),this.show(["wm-save-snapshot-fail","wm-save-snapshot-open"]),console.log("You have got an error."),console.log("If you think something wrong here please send it to support."),console.log('Response: "'+t.responseText+'"'),console.log('status: "'+t.status+'"'))}},{key:"success",value:function(t){this.hide(["wm-save-snapshot-fail","wm-save-snapshot-in-progress"]),this.show(["wm-save-snapshot-open","wm-save-snapshot-success"])}},{key:"enableSaveSnapshot",value:function(){var t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];t?(this.show("wm-save-snapshot-open"),this.hide("wm-sign-in")):(this.hide(["wm-save-snapshot-open","wm-save-snapshot-in-progress"]),this.show("wm-sign-in"))}},{key:"show",value:function(t){this.setDisplayStyle(t,"inline-block")}},{key:"hide",value:function(t){this.setDisplayStyle(t,"none")}},{key:"setDisplayStyle",value:function(t,e){var n=this;(Object(o.d)(t)?t:[t]).forEach((function(t){var o=n.el.getRootNode().getElementById(t);o&&(o.style.display=e)}))}}])&&i(e.prototype,n),r&&i(e,r),Object.defineProperty(e,"prototype",{writable:!1}),t}()},,,,,,,function(t,e,n){"use strict";var o;n.r(e);var r,i={createElementNS:document.createElementNS};var s=!0;function a(t){s=t}function c(t){try{a(!1),t()}finally{a(!0)}}function l(t){!function(t,e,n){if(n){var o=new Date;o.setTime(o.getTime()+24*n*60*60*1e3);var r="; expires="+o.toGMTString()}else r="";document.cookie=t+"="+e+r+"; path=/"}(t,"",-1)}var u=n(0),f=n(1),h=window.Date;function p(t,e){return(t=t.toString()).length>=e?t:"00000000".substring(0,e-t.length)+t}function d(t){for(var e=0,n=0;n3}(t)){var o=[];for(n=0;n=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,i=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw i}}}}function v(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n2&&void 0!==arguments[2]?arguments[2]:"src",i=window.location.origin,s=T(window,t),c=m(s);try{for(c.s();!(o=c.n()).done;){var l=o.value;if(!n||n(l)){var f=Object(u.b)(l,r);f&&!f.startsWith(e)&&f.startsWith(i)&&(f.startsWith("data:")||a.push(f))}}}catch(t){c.e(t)}finally{c.f()}}c("img"),c("frame"),c("iframe",(function(t){return"playback"!==t.id})),c("script"),c("link",(function(t){return"stylesheet"===t.rel}),"href");var l=a.filter((function(t,e,n){return n.indexOf(t)===e}));l.length>0?(s=0,l.map((function(t){t.match("^https?://")&&(s++,Object(u.a)("HEAD",t,(function(t){if(200==t.status){var e=t.getResponseHeader("Memento-Datetime");if(null==e)console.log("%s: no Memento-Datetime",u);else{var n=document.createElement("span"),a=function(t,e){var n=new Date(t).getTime()-e,o="";n<0?(o+="-",n=Math.abs(n)):o+="+";var r=!1;if(n<1e3)return{delta:n,text:"",highlight:r};var i=n,s=Math.floor(n/1e3/60/60/24/30/12);n-=1e3*s*60*60*24*30*12;var a=Math.floor(n/1e3/60/60/24/30);n-=1e3*a*60*60*24*30;var c=Math.floor(n/1e3/60/60/24);n-=1e3*c*60*60*24;var l=Math.floor(n/1e3/60/60);n-=1e3*l*60*60;var u=Math.floor(n/1e3/60);n-=1e3*u*60;var f=Math.floor(n/1e3),h=[];s>1?(h.push(s+" years"),r=!0):1==s&&(h.push(s+" year"),r=!0);a>1?(h.push(a+" months"),r=!0):1==a&&(h.push(a+" month"),r=!0);c>1?h.push(c+" days"):1==c&&h.push(c+" day");l>1?h.push(l+" hours"):1==l&&h.push(l+" hour");u>1?h.push(u+" minutes"):1==u&&h.push(u+" minute");f>1?h.push(f+" seconds"):1==f&&h.push(f+" second");h.length>2&&(h=h.slice(0,2));return{delta:i,text:o+h.join(" "),highlight:r}}(e,i),c=a.highlight?"color:red;":"";n.innerHTML=" "+a.text,n.title=e,n.setAttribute("style",c);var l=t.getResponseHeader("Content-Type"),u=t.responseURL.replace(window.location.origin,""),f=document.createElement("a");f.innerHTML=u.split("/").splice(3).join("/"),f._wm_href=u,f.title=l,f.onmouseover=w,f.onmouseout=S;var h=document.createElement("div");h.setAttribute("data-delta",a.delta),h.appendChild(f),h.append(n),o.appendChild(h);var p=Array.prototype.slice.call(o.childNodes,0);p.sort((function(t,e){return e.getAttribute("data-delta")-t.getAttribute("data-delta")})),o.innerHTML="";for(var d=0,m=p.length;d0)for(var n=0;n0)for(var n=0;n0?this.sc.scrollTop=r+this.sc.suggestionHeight+o-this.sc.maxHeight:r<0&&(this.sc.scrollTop=r+o)}}},{key:"blurHandler",value:function(){var t=this;try{var e=this.root.querySelector(".wb-autocomplete-suggestions:hover")}catch(t){e=null}e?this.input!==document.activeElement&&setTimeout((function(){return t.focus()}),20):(this.last_val=this.input.value,this.sc.style.display="none",setTimeout((function(){return t.sc.style.display="none"}),350))}},{key:"suggest",value:function(t){var e=this.input.value;if(this.cache[e]=t,t.length&&e.length>=this.minChars){for(var n="",o=0;o40)&&13!=n&&27!=n){var o=this.input.value;if(o.length>=this.minChars){if(o!=this.last_val){if(this.last_val=o,clearTimeout(this.timer),this.cache){if(o in this.cache)return void this.suggest(this.cache[o]);for(var r=1;r'+t.replace(n,"$1")+""}},{key:"onSelect",value:function(t,e,n){}}]),t}(),L=function(){function t(e,n){_(this,t);var o=e.getRootNode();if(o.querySelector){var r="object"==M(e)?[e]:o.querySelectorAll(e);this.elems=r.map((function(t){return new j(t,n)}))}}return x(t,[{key:"destroy",value:function(){for(;this.elems.length>0;)this.elems.pop().unload()}}]),t}(),A=n(2);function R(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=function(t,e){if(!t)return;if("string"==typeof t)return N(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return N(t,e)}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var o=0,r=function(){};return{s:r,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,i=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw i}}}}function N(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n0&&i<60,i)}))}window.__wm={init:function(t){!function(){var t=document.cookie.split(";");if(t.length>40)for(var e=0;e1?e-1:0),o=1;o0;)E.appendChild(O.children[0]);if(m)for(var H=0;H'+((""+n).replace(/\B(?=(\d{3})+$)/g,",")+" ")+(n>1?"captures":"capture")+"",h=f.a(r,"%d %b %Y");s!=r&&(h+=" - "+f.a(s,"%d %b %Y")),u+='
'+h+"
",e.innerHTML=u}(o),function(t,e,n,o,r,i,s){var a=o.getContext("2d");if(a){a.fillStyle="#FFF";var c=(new h).getUTCFullYear(),l=e/(c-r+1),u=d(t.years),f=u[0],p=n/u[1];if(i>=r){var m=_(i);a.fillStyle="#FFFFA5",a.fillRect(m,0,l,n)}for(var v=r;v<=c;v++){m=_(v);a.beginPath(),a.moveTo(m,0),a.lineTo(m,n),a.lineWidth=1,a.strokeStyle="#CCC",a.stroke()}s=parseInt(s)-1;for(var y=(l-1)/12,g=0;g0){var M=Math.ceil(T*p);a.fillStyle=v==i&&S==s?"#EC008C":"#000",a.fillRect(Math.round(w),Math.ceil(n-M),Math.ceil(y),Math.round(M))}w+=y}}}function _(t){return Math.ceil((t-r)*l)+.5}}(o,t,e,rt,a,M,_)}}))}else{var st=new Image;st.src="/__wb/sparkline?url="+encodeURIComponent(i)+"&width="+t+"&height="+e+"&selected_year="+M+"&selected_month="+_+(r&&"&collection="+r||""),st.alt="sparkline",st.width=t,st.height=e,st.id="sparklineImgId",st.border="0",ot.parentNode.replaceChild(st,ot)}function at(t){for(var e=[],n=t.length,o=0;o0)try{var o=document.createElement("div");o.setAttribute("style","background-color:#666;color:#fff;font-weight:bold;text-align:center"),o.textContent="NOTICE";var r=document.createElement("div");r.className="wm-capinfo-content";var i,s=R(n);try{var a=function(){var t=i.value;"string"==typeof t.notice&&c((function(){var e=document.createElement("div");e.innerHTML=t.notice,r.appendChild(e)}))};for(s.s();!(i=s.n()).done;)a()}catch(t){s.e(t)}finally{s.f()}ct.appendChild(o),c((function(){return ct.appendChild(r)})),J(!0)}catch(t){console.error("failed to build content of %o - maybe notice text is malformed: %s",ct,n)}}))}else J(!0);new A.a(X("wm-save-snapshot-open"),i,Y)},ex:function(t){t.stopPropagation(),J(!1)},ajax:u.a,sp:function(){return $}}}]); +// @license-end diff --git a/doc/ax25-2p0/index_files/iconochive.css b/doc/ax25-2p0/index_files/iconochive.css new file mode 100644 index 0000000..7a95ea7 --- /dev/null +++ b/doc/ax25-2p0/index_files/iconochive.css @@ -0,0 +1,116 @@ +@font-face{font-family:'Iconochive-Regular';src:url('https://archive.org/includes/fonts/Iconochive-Regular.eot?-ccsheb');src:url('https://archive.org/includes/fonts/Iconochive-Regular.eot?#iefix-ccsheb') format('embedded-opentype'),url('https://archive.org/includes/fonts/Iconochive-Regular.woff?-ccsheb') format('woff'),url('https://archive.org/includes/fonts/Iconochive-Regular.ttf?-ccsheb') format('truetype'),url('https://archive.org/includes/fonts/Iconochive-Regular.svg?-ccsheb#Iconochive-Regular') format('svg');font-weight:normal;font-style:normal} +[class^="iconochive-"],[class*=" iconochive-"]{font-family:'Iconochive-Regular'!important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} +.iconochive-Uplevel:before{content:"\21b5"} +.iconochive-exit:before{content:"\1f6a3"} +.iconochive-beta:before{content:"\3b2"} +.iconochive-logo:before{content:"\1f3db"} +.iconochive-audio:before{content:"\1f568"} +.iconochive-movies:before{content:"\1f39e"} +.iconochive-software:before{content:"\1f4be"} +.iconochive-texts:before{content:"\1f56e"} +.iconochive-etree:before{content:"\1f3a4"} +.iconochive-image:before{content:"\1f5bc"} +.iconochive-web:before{content:"\1f5d4"} +.iconochive-collection:before{content:"\2211"} +.iconochive-folder:before{content:"\1f4c2"} +.iconochive-data:before{content:"\1f5c3"} +.iconochive-tv:before{content:"\1f4fa"} +.iconochive-article:before{content:"\1f5cf"} +.iconochive-question:before{content:"\2370"} +.iconochive-question-dark:before{content:"\3f"} +.iconochive-info:before{content:"\69"} +.iconochive-info-small:before{content:"\24d8"} +.iconochive-comment:before{content:"\1f5e9"} +.iconochive-comments:before{content:"\1f5ea"} +.iconochive-person:before{content:"\1f464"} +.iconochive-people:before{content:"\1f465"} +.iconochive-eye:before{content:"\1f441"} +.iconochive-rss:before{content:"\221e"} +.iconochive-time:before{content:"\1f551"} +.iconochive-quote:before{content:"\275d"} +.iconochive-disc:before{content:"\1f4bf"} +.iconochive-tv-commercial:before{content:"\1f4b0"} +.iconochive-search:before{content:"\1f50d"} +.iconochive-search-star:before{content:"\273d"} +.iconochive-tiles:before{content:"\229e"} +.iconochive-list:before{content:"\21f6"} +.iconochive-list-bulleted:before{content:"\2317"} +.iconochive-latest:before{content:"\2208"} +.iconochive-left:before{content:"\2c2"} +.iconochive-right:before{content:"\2c3"} +.iconochive-left-solid:before{content:"\25c2"} +.iconochive-right-solid:before{content:"\25b8"} +.iconochive-up-solid:before{content:"\25b4"} +.iconochive-down-solid:before{content:"\25be"} +.iconochive-dot:before{content:"\23e4"} +.iconochive-dots:before{content:"\25a6"} +.iconochive-columns:before{content:"\25af"} +.iconochive-sort:before{content:"\21d5"} +.iconochive-atoz:before{content:"\1f524"} +.iconochive-ztoa:before{content:"\1f525"} +.iconochive-upload:before{content:"\1f4e4"} +.iconochive-download:before{content:"\1f4e5"} +.iconochive-favorite:before{content:"\2605"} +.iconochive-heart:before{content:"\2665"} +.iconochive-play:before{content:"\25b6"} +.iconochive-play-framed:before{content:"\1f3ac"} +.iconochive-fullscreen:before{content:"\26f6"} +.iconochive-mute:before{content:"\1f507"} +.iconochive-unmute:before{content:"\1f50a"} +.iconochive-share:before{content:"\1f381"} +.iconochive-edit:before{content:"\270e"} +.iconochive-reedit:before{content:"\2710"} +.iconochive-gear:before{content:"\2699"} +.iconochive-remove-circle:before{content:"\274e"} +.iconochive-plus-circle:before{content:"\1f5d6"} +.iconochive-minus-circle:before{content:"\1f5d5"} +.iconochive-x:before{content:"\1f5d9"} +.iconochive-fork:before{content:"\22d4"} +.iconochive-trash:before{content:"\1f5d1"} +.iconochive-warning:before{content:"\26a0"} +.iconochive-flash:before{content:"\1f5f2"} +.iconochive-world:before{content:"\1f5fa"} +.iconochive-lock:before{content:"\1f512"} +.iconochive-unlock:before{content:"\1f513"} +.iconochive-twitter:before{content:"\1f426"} +.iconochive-facebook:before{content:"\66"} +.iconochive-googleplus:before{content:"\67"} +.iconochive-reddit:before{content:"\1f47d"} +.iconochive-tumblr:before{content:"\54"} +.iconochive-pinterest:before{content:"\1d4df"} +.iconochive-popcorn:before{content:"\1f4a5"} +.iconochive-email:before{content:"\1f4e7"} +.iconochive-embed:before{content:"\1f517"} +.iconochive-gamepad:before{content:"\1f579"} +.iconochive-Zoom_In:before{content:"\2b"} +.iconochive-Zoom_Out:before{content:"\2d"} +.iconochive-RSS:before{content:"\1f4e8"} +.iconochive-Light_Bulb:before{content:"\1f4a1"} +.iconochive-Add:before{content:"\2295"} +.iconochive-Tab_Activity:before{content:"\2318"} +.iconochive-Forward:before{content:"\23e9"} +.iconochive-Backward:before{content:"\23ea"} +.iconochive-No_Audio:before{content:"\1f508"} +.iconochive-Pause:before{content:"\23f8"} +.iconochive-No_Favorite:before{content:"\2606"} +.iconochive-Unike:before{content:"\2661"} +.iconochive-Song:before{content:"\266b"} +.iconochive-No_Flag:before{content:"\2690"} +.iconochive-Flag:before{content:"\2691"} +.iconochive-Done:before{content:"\2713"} +.iconochive-Check:before{content:"\2714"} +.iconochive-Refresh:before{content:"\27f3"} +.iconochive-Headphones:before{content:"\1f3a7"} +.iconochive-Chart:before{content:"\1f4c8"} +.iconochive-Bookmark:before{content:"\1f4d1"} +.iconochive-Documents:before{content:"\1f4da"} +.iconochive-Newspaper:before{content:"\1f4f0"} +.iconochive-Podcast:before{content:"\1f4f6"} +.iconochive-Radio:before{content:"\1f4fb"} +.iconochive-Cassette:before{content:"\1f4fc"} +.iconochive-Shuffle:before{content:"\1f500"} +.iconochive-Loop:before{content:"\1f501"} +.iconochive-Low_Audio:before{content:"\1f509"} +.iconochive-First:before{content:"\1f396"} +.iconochive-Invisible:before{content:"\1f576"} +.iconochive-Computer:before{content:"\1f5b3"} diff --git a/doc/ax25-2p0/index_files/pdf2.gif b/doc/ax25-2p0/index_files/pdf2.gif new file mode 100644 index 0000000000000000000000000000000000000000..2d238dee3058051a8c24fe618c14c7a5216e3763 GIT binary patch literal 1638 zcmV-s2ATOsNk%w1VITk>0Qdg@0000?007|N;r$B^{uUVO>g=VZsNn_)+1lONO-{zf z%KzKj&;bSIf`jhg;qdVB{|*uD=jqD<1XEL1|J>Z}?(qNr|K;T8-Q3>sAmgnc{=AfeN+uhaG)&3F{!NJGh-s0`;@8{>~wY9gp zy1uQgve=4^p`oGb1_{*E*vZPy($d$ZrKaiW>*;E1UL3sIHotp8qZ} zd3k&O{{FPIxz5hc=UiO-ZEs~|X3fpg|2I0p!NLD1D(2?s;kCBO$>%+Xz4 zUjNtLIl&(9qWDd3x;#4DP|g>FMn2 z>+bgo4aCI6|KH#3>+Rj$;f98Z|0*p>NlfpKj{h(+|1~!3qNC_vVBN5@#bRXVdwh|R zl5cuCVXt=}t~k=ccLZ>g)gD;LFR==boUdr?2JZ?B(U@ zYin)OouKU4*+xc4>~wWtU}C7KtN#WGtEsNt1qkKE z$moWL0|Nu=wzu!Lw%&Ss>hbpfEHBsB-1G*}_+@Z{v{#>n2?;rb2{?d7cqjMvxM($dv0Ff!)m=~!4>T3TKI zd3*nPeE+5GfGIP~r;(kFIzDq}sB=Mx_d#9yypK#2!vj z8-uJufgD@Rtn&bcjJY+h>P(o>V?r?>s9OSP0&taF@rL9w9rBg>TnYo1_-3_Moc4Af&>+SR8qhQ7x1B&5={6qLvVSG zAOH?46mY~5-cZ3)H1%Yn0uozzNCPC0kVp^!l;nU08q#bNi7SQhG-Cn!fS^eqxiFIq zQIUm{`0{nT5!UN zEa6BLD+L-1AjUZVg)|@wQnc}hBi6tG-7};FAnv&2mTRsi;?Qsf6Dat>fdM)W1b__p zsFKECfelvJVTmo)m@-VnTaW-4q=3T(XdIG9FXTjW2v0WPkVYRg%rc7}0{;Vmi748D z&I7K5p-VUCuv39+7fd|F${7Pt2@YGdlgSbA1ak^c5!?`l5UsQV%P2I&flA9(=0HgS zx+D-v5=q2U0zF6+piK}B{P2PTP^Pwu4I={var e,n,t={297:(e,n,t)=>{e.exports=function e(n,t,r){function a(o,s){if(!t[o]){if(!n[o]){if(i)return i(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var u=t[o]={exports:{}};n[o][0].call(u.exports,(function(e){return a(n[o][1][e]||e)}),u,u.exports,e,n,t,r)}return t[o].exports}for(var i=void 0,o=0;o>2,s=(3&n)<<4|t>>4,l=1>6:64,u=2>4,t=(15&o)<<4|(s=i.indexOf(e.charAt(u++)))>>2,r=(3&s)<<6|(l=i.indexOf(e.charAt(u++))),f[c++]=n,64!==s&&(f[c++]=t),64!==l&&(f[c++]=r);return f}},{"./support":30,"./utils":32}],2:[function(e,n,t){"use strict";var r=e("./external"),a=e("./stream/DataWorker"),i=e("./stream/Crc32Probe"),o=e("./stream/DataLengthProbe");function s(e,n,t,r,a){this.compressedSize=e,this.uncompressedSize=n,this.crc32=t,this.compression=r,this.compressedContent=a}s.prototype={getContentWorker:function(){var e=new a(r.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new o("data_length")),n=this;return e.on("end",(function(){if(this.streamInfo.data_length!==n.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")})),e},getCompressedWorker:function(){return new a(r.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},s.createWorkerFrom=function(e,n,t){return e.pipe(new i).pipe(new o("uncompressedSize")).pipe(n.compressWorker(t)).pipe(new o("compressedSize")).withStreamInfo("compression",n)},n.exports=s},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,n,t){"use strict";var r=e("./stream/GenericWorker");t.STORE={magic:"\0\0",compressWorker:function(){return new r("STORE compression")},uncompressWorker:function(){return new r("STORE decompression")}},t.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,n,t){"use strict";var r=e("./utils"),a=function(){for(var e,n=[],t=0;t<256;t++){e=t;for(var r=0;r<8;r++)e=1&e?3988292384^e>>>1:e>>>1;n[t]=e}return n}();n.exports=function(e,n){return void 0!==e&&e.length?"string"!==r.getTypeOf(e)?function(e,n,t,r){var i=a,o=r+t;e^=-1;for(var s=r;s>>8^i[255&(e^n[s])];return-1^e}(0|n,e,e.length,0):function(e,n,t,r){var i=a,o=r+t;e^=-1;for(var s=r;s>>8^i[255&(e^n.charCodeAt(s))];return-1^e}(0|n,e,e.length,0):0}},{"./utils":32}],5:[function(e,n,t){"use strict";t.base64=!1,t.binary=!1,t.dir=!1,t.createFolders=!0,t.date=null,t.compression=null,t.compressionOptions=null,t.comment=null,t.unixPermissions=null,t.dosPermissions=null},{}],6:[function(e,n,t){"use strict";var r=null;r="undefined"!=typeof Promise?Promise:e("lie"),n.exports={Promise:r}},{lie:37}],7:[function(e,n,t){"use strict";var r="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,a=e("pako"),i=e("./utils"),o=e("./stream/GenericWorker"),s=r?"uint8array":"array";function l(e,n){o.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=n,this.meta={}}t.magic="\b\0",i.inherits(l,o),l.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(i.transformTo(s,e.data),!1)},l.prototype.flush=function(){o.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},l.prototype.cleanUp=function(){o.prototype.cleanUp.call(this),this._pako=null},l.prototype._createPako=function(){this._pako=new a[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(n){e.push({data:n,meta:e.meta})}},t.compressWorker=function(e){return new l("Deflate",e)},t.uncompressWorker=function(){return new l("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,n,t){"use strict";function r(e,n){var t,r="";for(t=0;t>>=8;return r}function a(e,n,t,a,o,c){var d,f,h=e.file,m=e.compression,p=c!==s.utf8encode,v=i.transformTo("string",c(h.name)),g=i.transformTo("string",s.utf8encode(h.name)),b=h.comment,w=i.transformTo("string",c(b)),k=i.transformTo("string",s.utf8encode(b)),y=g.length!==h.name.length,_=k.length!==b.length,R="",x="",S="",z=h.dir,E=h.date,j={crc32:0,compressedSize:0,uncompressedSize:0};n&&!t||(j.crc32=e.crc32,j.compressedSize=e.compressedSize,j.uncompressedSize=e.uncompressedSize);var A=0;n&&(A|=8),p||!y&&!_||(A|=2048);var C=0,I=0;z&&(C|=16),"UNIX"===o?(I=798,C|=function(e,n){var t=e;return e||(t=n?16893:33204),(65535&t)<<16}(h.unixPermissions,z)):(I=20,C|=function(e){return 63&(e||0)}(h.dosPermissions)),d=E.getUTCHours(),d<<=6,d|=E.getUTCMinutes(),d<<=5,d|=E.getUTCSeconds()/2,f=E.getUTCFullYear()-1980,f<<=4,f|=E.getUTCMonth()+1,f<<=5,f|=E.getUTCDate(),y&&(x=r(1,1)+r(l(v),4)+g,R+="up"+r(x.length,2)+x),_&&(S=r(1,1)+r(l(w),4)+k,R+="uc"+r(S.length,2)+S);var O="";return O+="\n\0",O+=r(A,2),O+=m.magic,O+=r(d,2),O+=r(f,2),O+=r(j.crc32,4),O+=r(j.compressedSize,4),O+=r(j.uncompressedSize,4),O+=r(v.length,2),O+=r(R.length,2),{fileRecord:u.LOCAL_FILE_HEADER+O+v+R,dirRecord:u.CENTRAL_FILE_HEADER+r(I,2)+O+r(w.length,2)+"\0\0\0\0"+r(C,4)+r(a,4)+v+R+w}}var i=e("../utils"),o=e("../stream/GenericWorker"),s=e("../utf8"),l=e("../crc32"),u=e("../signature");function c(e,n,t,r){o.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=n,this.zipPlatform=t,this.encodeFileName=r,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}i.inherits(c,o),c.prototype.push=function(e){var n=e.meta.percent||0,t=this.entriesCount,r=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,o.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:t?(n+100*(t-r-1))/t:100}}))},c.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var n=this.streamFiles&&!e.file.dir;if(n){var t=a(e,n,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:t.fileRecord,meta:{percent:0}})}else this.accumulate=!0},c.prototype.closedSource=function(e){this.accumulate=!1;var n=this.streamFiles&&!e.file.dir,t=a(e,n,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(t.dirRecord),n)this.push({data:function(e){return u.DATA_DESCRIPTOR+r(e.crc32,4)+r(e.compressedSize,4)+r(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:t.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},c.prototype.flush=function(){for(var e=this.bytesWritten,n=0;n=this.index;n--)t=(t<<8)+this.byteAt(n);return this.index+=e,t},readString:function(e){return r.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},n.exports=a},{"../utils":32}],19:[function(e,n,t){"use strict";var r=e("./Uint8ArrayReader");function a(e){r.call(this,e)}e("../utils").inherits(a,r),a.prototype.readData=function(e){this.checkOffset(e);var n=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,n},n.exports=a},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,n,t){"use strict";var r=e("./DataReader");function a(e){r.call(this,e)}e("../utils").inherits(a,r),a.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},a.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},a.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},a.prototype.readData=function(e){this.checkOffset(e);var n=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,n},n.exports=a},{"../utils":32,"./DataReader":18}],21:[function(e,n,t){"use strict";var r=e("./ArrayReader");function a(e){r.call(this,e)}e("../utils").inherits(a,r),a.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var n=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,n},n.exports=a},{"../utils":32,"./ArrayReader":17}],22:[function(e,n,t){"use strict";var r=e("../utils"),a=e("../support"),i=e("./ArrayReader"),o=e("./StringReader"),s=e("./NodeBufferReader"),l=e("./Uint8ArrayReader");n.exports=function(e){var n=r.getTypeOf(e);return r.checkSupport(n),"string"!==n||a.uint8array?"nodebuffer"===n?new s(e):a.uint8array?new l(r.transformTo("uint8array",e)):new i(r.transformTo("array",e)):new o(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,n,t){"use strict";t.LOCAL_FILE_HEADER="PK\x03\x04",t.CENTRAL_FILE_HEADER="PK\x01\x02",t.CENTRAL_DIRECTORY_END="PK\x05\x06",t.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK\x06\x07",t.ZIP64_CENTRAL_DIRECTORY_END="PK\x06\x06",t.DATA_DESCRIPTOR="PK\x07\b"},{}],24:[function(e,n,t){"use strict";var r=e("./GenericWorker"),a=e("../utils");function i(e){r.call(this,"ConvertWorker to "+e),this.destType=e}a.inherits(i,r),i.prototype.processChunk=function(e){this.push({data:a.transformTo(this.destType,e.data),meta:e.meta})},n.exports=i},{"../utils":32,"./GenericWorker":28}],25:[function(e,n,t){"use strict";var r=e("./GenericWorker"),a=e("../crc32");function i(){r.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(i,r),i.prototype.processChunk=function(e){this.streamInfo.crc32=a(e.data,this.streamInfo.crc32||0),this.push(e)},n.exports=i},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,n,t){"use strict";var r=e("../utils"),a=e("./GenericWorker");function i(e){a.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}r.inherits(i,a),i.prototype.processChunk=function(e){if(e){var n=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=n+e.data.length}a.prototype.processChunk.call(this,e)},n.exports=i},{"../utils":32,"./GenericWorker":28}],27:[function(e,n,t){"use strict";var r=e("../utils"),a=e("./GenericWorker");function i(e){a.call(this,"DataWorker");var n=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then((function(e){n.dataIsReady=!0,n.data=e,n.max=e&&e.length||0,n.type=r.getTypeOf(e),n.isPaused||n._tickAndRepeat()}),(function(e){n.error(e)}))}r.inherits(i,a),i.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this.data=null},i.prototype.resume=function(){return!!a.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,r.delay(this._tickAndRepeat,[],this)),!0)},i.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(r.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},i.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,n=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,n);break;case"uint8array":e=this.data.subarray(this.index,n);break;case"array":case"nodebuffer":e=this.data.slice(this.index,n)}return this.index=n,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},n.exports=i},{"../utils":32,"./GenericWorker":28}],28:[function(e,n,t){"use strict";function r(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}r.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,n){return this._listeners[e].push(n),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,n){if(this._listeners[e])for(var t=0;t "+e:e}},n.exports=r},{}],29:[function(e,n,t){"use strict";var r=e("../utils"),a=e("./ConvertWorker"),i=e("./GenericWorker"),o=e("../base64"),s=e("../support"),l=e("../external"),u=null;if(s.nodestream)try{u=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function c(e,n){return new l.Promise((function(t,a){var i=[],s=e._internalType,l=e._outputType,u=e._mimeType;e.on("data",(function(e,t){i.push(e),n&&n(t)})).on("error",(function(e){i=[],a(e)})).on("end",(function(){try{var e=function(e,n,t){switch(e){case"blob":return r.newBlob(r.transformTo("arraybuffer",n),t);case"base64":return o.encode(n);default:return r.transformTo(e,n)}}(l,function(e,n){var t,r=0,a=null,i=0;for(t=0;t>>6:(t<65536?n[o++]=224|t>>>12:(n[o++]=240|t>>>18,n[o++]=128|t>>>12&63),n[o++]=128|t>>>6&63),n[o++]=128|63&t);return n}(e)},t.utf8decode=function(e){return a.nodebuffer?r.transformTo("nodebuffer",e).toString("utf-8"):function(e){var n,t,a,i,o=e.length,l=new Array(2*o);for(n=t=0;n>10&1023,l[t++]=56320|1023&a)}return l.length!==t&&(l.subarray?l=l.subarray(0,t):l.length=t),r.applyFromCharCode(l)}(e=r.transformTo(a.uint8array?"uint8array":"array",e))},r.inherits(u,o),u.prototype.processChunk=function(e){var n=r.transformTo(a.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(a.uint8array){var i=n;(n=new Uint8Array(i.length+this.leftOver.length)).set(this.leftOver,0),n.set(i,this.leftOver.length)}else n=this.leftOver.concat(n);this.leftOver=null}var o=function(e,n){var t;for((n=n||e.length)>e.length&&(n=e.length),t=n-1;0<=t&&128==(192&e[t]);)t--;return t<0||0===t?n:t+s[e[t]]>n?t:n}(n),l=n;o!==n.length&&(a.uint8array?(l=n.subarray(0,o),this.leftOver=n.subarray(o,n.length)):(l=n.slice(0,o),this.leftOver=n.slice(o,n.length))),this.push({data:t.utf8decode(l),meta:e.meta})},u.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:t.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},t.Utf8DecodeWorker=u,r.inherits(c,o),c.prototype.processChunk=function(e){this.push({data:t.utf8encode(e.data),meta:e.meta})},t.Utf8EncodeWorker=c},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,n,t){"use strict";var r=e("./support"),a=e("./base64"),i=e("./nodejsUtils"),o=e("./external");function s(e){return e}function l(e,n){for(var t=0;t>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=r(this.extraFields[1].value);this.uncompressedSize===a.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===a.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===a.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===a.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var n,t,r,a=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(t<65536?n[o++]=224|t>>>12:(n[o++]=240|t>>>18,n[o++]=128|t>>>12&63),n[o++]=128|t>>>6&63),n[o++]=128|63&t);return n},t.buf2binstring=function(e){return l(e,e.length)},t.binstring2buf=function(e){for(var n=new r.Buf8(e.length),t=0,a=n.length;t>10&1023,u[r++]=56320|1023&a)}return l(u,r)},t.utf8border=function(e,n){var t;for((n=n||e.length)>e.length&&(n=e.length),t=n-1;0<=t&&128==(192&e[t]);)t--;return t<0||0===t?n:t+o[e[t]]>n?t:n}},{"./common":41}],43:[function(e,n,t){"use strict";n.exports=function(e,n,t,r){for(var a=65535&e|0,i=e>>>16&65535|0,o=0;0!==t;){for(t-=o=2e3>>1:e>>>1;n[t]=e}return n}();n.exports=function(e,n,t,a){var i=r,o=a+t;e^=-1;for(var s=a;s>>8^i[255&(e^n[s])];return-1^e}},{}],46:[function(e,n,t){"use strict";var r,a=e("../utils/common"),i=e("./trees"),o=e("./adler32"),s=e("./crc32"),l=e("./messages"),u=0,c=4,d=0,f=-2,h=-1,m=4,p=2,v=8,g=9,b=286,w=30,k=19,y=2*b+1,_=15,R=3,x=258,S=x+R+1,z=42,E=113,j=1,A=2,C=3,I=4;function O(e,n){return e.msg=l[n],n}function D(e){return(e<<1)-(4e.avail_out&&(t=e.avail_out),0!==t&&(a.arraySet(e.output,n.pending_buf,n.pending_out,t,e.next_out),e.next_out+=t,n.pending_out+=t,e.total_out+=t,e.avail_out-=t,n.pending-=t,0===n.pending&&(n.pending_out=0))}function T(e,n){i._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,n),e.block_start=e.strstart,P(e.strm)}function $(e,n){e.pending_buf[e.pending++]=n}function B(e,n){e.pending_buf[e.pending++]=n>>>8&255,e.pending_buf[e.pending++]=255&n}function M(e,n){var t,r,a=e.max_chain_length,i=e.strstart,o=e.prev_length,s=e.nice_match,l=e.strstart>e.w_size-S?e.strstart-(e.w_size-S):0,u=e.window,c=e.w_mask,d=e.prev,f=e.strstart+x,h=u[i+o-1],m=u[i+o];e.prev_length>=e.good_match&&(a>>=2),s>e.lookahead&&(s=e.lookahead);do{if(u[(t=n)+o]===m&&u[t+o-1]===h&&u[t]===u[i]&&u[++t]===u[i+1]){i+=2,t++;do{}while(u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&il&&0!=--a);return o<=e.lookahead?o:e.lookahead}function L(e){var n,t,r,i,l,u,c,d,f,h,m=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=m+(m-S)){for(a.arraySet(e.window,e.window,m,m,0),e.match_start-=m,e.strstart-=m,e.block_start-=m,n=t=e.hash_size;r=e.head[--n],e.head[n]=m<=r?r-m:0,--t;);for(n=t=m;r=e.prev[--n],e.prev[n]=m<=r?r-m:0,--t;);i+=m}if(0===e.strm.avail_in)break;if(u=e.strm,c=e.window,d=e.strstart+e.lookahead,h=void 0,(f=i)<(h=u.avail_in)&&(h=f),t=0===h?0:(u.avail_in-=h,a.arraySet(c,u.input,u.next_in,h,d),1===u.state.wrap?u.adler=o(u.adler,c,h,d):2===u.state.wrap&&(u.adler=s(u.adler,c,h,d)),u.next_in+=h,u.total_in+=h,h),e.lookahead+=t,e.lookahead+e.insert>=R)for(l=e.strstart-e.insert,e.ins_h=e.window[l],e.ins_h=(e.ins_h<=R&&(e.ins_h=(e.ins_h<=R)if(r=i._tr_tally(e,e.strstart-e.match_start,e.match_length-R),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=R){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=R&&(e.ins_h=(e.ins_h<=R&&e.match_length<=e.prev_length){for(a=e.strstart+e.lookahead-R,r=i._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-R),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=a&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(t=e.pending_buf_size-5);;){if(e.lookahead<=1){if(L(e),0===e.lookahead&&n===u)return j;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var r=e.block_start+t;if((0===e.strstart||e.strstart>=r)&&(e.lookahead=e.strstart-r,e.strstart=r,T(e,!1),0===e.strm.avail_out))return j;if(e.strstart-e.block_start>=e.w_size-S&&(T(e,!1),0===e.strm.avail_out))return j}return e.insert=0,n===c?(T(e,!0),0===e.strm.avail_out?C:I):(e.strstart>e.block_start&&(T(e,!1),e.strm.avail_out),j)})),new q(4,4,8,4,N),new q(4,5,16,8,N),new q(4,6,32,32,N),new q(4,4,16,16,U),new q(8,16,32,32,U),new q(8,16,128,128,U),new q(8,32,128,256,U),new q(32,128,258,1024,U),new q(32,258,258,4096,U)],t.deflateInit=function(e,n){return V(e,n,v,15,8,0)},t.deflateInit2=V,t.deflateReset=H,t.deflateResetKeep=Z,t.deflateSetHeader=function(e,n){return e&&e.state?2!==e.state.wrap?f:(e.state.gzhead=n,d):f},t.deflate=function(e,n){var t,a,o,l;if(!e||!e.state||5>8&255),$(a,a.gzhead.time>>16&255),$(a,a.gzhead.time>>24&255),$(a,9===a.level?2:2<=a.strategy||a.level<2?4:0),$(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&($(a,255&a.gzhead.extra.length),$(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(e.adler=s(e.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69):($(a,0),$(a,0),$(a,0),$(a,0),$(a,0),$(a,9===a.level?2:2<=a.strategy||a.level<2?4:0),$(a,3),a.status=E);else{var h=v+(a.w_bits-8<<4)<<8;h|=(2<=a.strategy||a.level<2?0:a.level<6?1:6===a.level?2:3)<<6,0!==a.strstart&&(h|=32),h+=31-h%31,a.status=E,B(a,h),0!==a.strstart&&(B(a,e.adler>>>16),B(a,65535&e.adler)),e.adler=1}if(69===a.status)if(a.gzhead.extra){for(o=a.pending;a.gzindex<(65535&a.gzhead.extra.length)&&(a.pending!==a.pending_buf_size||(a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),P(e),o=a.pending,a.pending!==a.pending_buf_size));)$(a,255&a.gzhead.extra[a.gzindex]),a.gzindex++;a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),a.gzindex===a.gzhead.extra.length&&(a.gzindex=0,a.status=73)}else a.status=73;if(73===a.status)if(a.gzhead.name){o=a.pending;do{if(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),P(e),o=a.pending,a.pending===a.pending_buf_size)){l=1;break}l=a.gzindexo&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),0===l&&(a.gzindex=0,a.status=91)}else a.status=91;if(91===a.status)if(a.gzhead.comment){o=a.pending;do{if(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),P(e),o=a.pending,a.pending===a.pending_buf_size)){l=1;break}l=a.gzindexo&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),0===l&&(a.status=103)}else a.status=103;if(103===a.status&&(a.gzhead.hcrc?(a.pending+2>a.pending_buf_size&&P(e),a.pending+2<=a.pending_buf_size&&($(a,255&e.adler),$(a,e.adler>>8&255),e.adler=0,a.status=E)):a.status=E),0!==a.pending){if(P(e),0===e.avail_out)return a.last_flush=-1,d}else if(0===e.avail_in&&D(n)<=D(t)&&n!==c)return O(e,-5);if(666===a.status&&0!==e.avail_in)return O(e,-5);if(0!==e.avail_in||0!==a.lookahead||n!==u&&666!==a.status){var m=2===a.strategy?function(e,n){for(var t;;){if(0===e.lookahead&&(L(e),0===e.lookahead)){if(n===u)return j;break}if(e.match_length=0,t=i._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,t&&(T(e,!1),0===e.strm.avail_out))return j}return e.insert=0,n===c?(T(e,!0),0===e.strm.avail_out?C:I):e.last_lit&&(T(e,!1),0===e.strm.avail_out)?j:A}(a,n):3===a.strategy?function(e,n){for(var t,r,a,o,s=e.window;;){if(e.lookahead<=x){if(L(e),e.lookahead<=x&&n===u)return j;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=R&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=R?(t=i._tr_tally(e,1,e.match_length-R),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(t=i._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),t&&(T(e,!1),0===e.strm.avail_out))return j}return e.insert=0,n===c?(T(e,!0),0===e.strm.avail_out?C:I):e.last_lit&&(T(e,!1),0===e.strm.avail_out)?j:A}(a,n):r[a.level].func(a,n);if(m!==C&&m!==I||(a.status=666),m===j||m===C)return 0===e.avail_out&&(a.last_flush=-1),d;if(m===A&&(1===n?i._tr_align(a):5!==n&&(i._tr_stored_block(a,0,0,!1),3===n&&(F(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),P(e),0===e.avail_out))return a.last_flush=-1,d}return n!==c?d:a.wrap<=0?1:(2===a.wrap?($(a,255&e.adler),$(a,e.adler>>8&255),$(a,e.adler>>16&255),$(a,e.adler>>24&255),$(a,255&e.total_in),$(a,e.total_in>>8&255),$(a,e.total_in>>16&255),$(a,e.total_in>>24&255)):(B(a,e.adler>>>16),B(a,65535&e.adler)),P(e),0=t.w_size&&(0===s&&(F(t.head),t.strstart=0,t.block_start=0,t.insert=0),h=new a.Buf8(t.w_size),a.arraySet(h,n,m-t.w_size,t.w_size,0),n=h,m=t.w_size),l=e.avail_in,u=e.next_in,c=e.input,e.avail_in=m,e.next_in=0,e.input=n,L(t);t.lookahead>=R;){for(r=t.strstart,i=t.lookahead-(R-1);t.ins_h=(t.ins_h<>>=k=w>>>24,m-=k,0==(k=w>>>16&255))z[i++]=65535&w;else{if(!(16&k)){if(0==(64&k)){w=p[(65535&w)+(h&(1<>>=k,m-=k),m<15&&(h+=S[r++]<>>=k=w>>>24,m-=k,!(16&(k=w>>>16&255))){if(0==(64&k)){w=v[(65535&w)+(h&(1<>>=k,m-=k,(k=i-o)<_){if(c<(k=_-k)&&t.sane){e.msg="invalid distance too far back",t.mode=30;break e}if(x=f,(R=0)===d){if(R+=u-k,k>3,h&=(1<<(m-=y<<3))-1,e.next_in=r,e.next_out=i,e.avail_in=r>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function v(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new r.Buf16(320),this.work=new r.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function g(e){var n;return e&&e.state?(n=e.state,e.total_in=e.total_out=n.total=0,e.msg="",n.wrap&&(e.adler=1&n.wrap),n.mode=f,n.last=0,n.havedict=0,n.dmax=32768,n.head=null,n.hold=0,n.bits=0,n.lencode=n.lendyn=new r.Buf32(h),n.distcode=n.distdyn=new r.Buf32(m),n.sane=1,n.back=-1,c):d}function b(e){var n;return e&&e.state?((n=e.state).wsize=0,n.whave=0,n.wnext=0,g(e)):d}function w(e,n){var t,r;return e&&e.state?(r=e.state,n<0?(t=0,n=-n):(t=1+(n>>4),n<48&&(n&=15)),n&&(n<8||15=o.wsize?(r.arraySet(o.window,n,t-o.wsize,o.wsize,0),o.wnext=0,o.whave=o.wsize):(a<(i=o.wsize-o.wnext)&&(i=a),r.arraySet(o.window,n,t-a,i,o.wnext),(a-=i)?(r.arraySet(o.window,n,t-a,a,0),o.wnext=a,o.whave=o.wsize):(o.wnext+=i,o.wnext===o.wsize&&(o.wnext=0),o.whave>>8&255,t.check=i(t.check,L,2,0),y=k=0,t.mode=2;break}if(t.flags=0,t.head&&(t.head.done=!1),!(1&t.wrap)||(((255&k)<<8)+(k>>8))%31){e.msg="incorrect header check",t.mode=30;break}if(8!=(15&k)){e.msg="unknown compression method",t.mode=30;break}if(y-=4,P=8+(15&(k>>>=4)),0===t.wbits)t.wbits=P;else if(P>t.wbits){e.msg="invalid window size",t.mode=30;break}t.dmax=1<>8&1),512&t.flags&&(L[0]=255&k,L[1]=k>>>8&255,t.check=i(t.check,L,2,0)),y=k=0,t.mode=3;case 3:for(;y<32;){if(0===b)break e;b--,k+=h[v++]<>>8&255,L[2]=k>>>16&255,L[3]=k>>>24&255,t.check=i(t.check,L,4,0)),y=k=0,t.mode=4;case 4:for(;y<16;){if(0===b)break e;b--,k+=h[v++]<>8),512&t.flags&&(L[0]=255&k,L[1]=k>>>8&255,t.check=i(t.check,L,2,0)),y=k=0,t.mode=5;case 5:if(1024&t.flags){for(;y<16;){if(0===b)break e;b--,k+=h[v++]<>>8&255,t.check=i(t.check,L,2,0)),y=k=0}else t.head&&(t.head.extra=null);t.mode=6;case 6:if(1024&t.flags&&(b<(z=t.length)&&(z=b),z&&(t.head&&(P=t.head.extra_len-t.length,t.head.extra||(t.head.extra=new Array(t.head.extra_len)),r.arraySet(t.head.extra,h,v,z,P)),512&t.flags&&(t.check=i(t.check,h,z,v)),b-=z,v+=z,t.length-=z),t.length))break e;t.length=0,t.mode=7;case 7:if(2048&t.flags){if(0===b)break e;for(z=0;P=h[v+z++],t.head&&P&&t.length<65536&&(t.head.name+=String.fromCharCode(P)),P&&z>9&1,t.head.done=!0),e.adler=t.check=0,t.mode=12;break;case 10:for(;y<32;){if(0===b)break e;b--,k+=h[v++]<>>=7&y,y-=7&y,t.mode=27;break}for(;y<3;){if(0===b)break e;b--,k+=h[v++]<>>=1)){case 0:t.mode=14;break;case 1:if(x(t),t.mode=20,6!==n)break;k>>>=2,y-=2;break e;case 2:t.mode=17;break;case 3:e.msg="invalid block type",t.mode=30}k>>>=2,y-=2;break;case 14:for(k>>>=7&y,y-=7&y;y<32;){if(0===b)break e;b--,k+=h[v++]<>>16^65535)){e.msg="invalid stored block lengths",t.mode=30;break}if(t.length=65535&k,y=k=0,t.mode=15,6===n)break e;case 15:t.mode=16;case 16:if(z=t.length){if(b>>=5,y-=5,t.ndist=1+(31&k),k>>>=5,y-=5,t.ncode=4+(15&k),k>>>=4,y-=4,286>>=3,y-=3}for(;t.have<19;)t.lens[N[t.have++]]=0;if(t.lencode=t.lendyn,t.lenbits=7,$={bits:t.lenbits},T=s(0,t.lens,0,19,t.lencode,0,t.work,$),t.lenbits=$.bits,T){e.msg="invalid code lengths set",t.mode=30;break}t.have=0,t.mode=19;case 19:for(;t.have>>16&255,I=65535&M,!((A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>>=A,y-=A,t.lens[t.have++]=I;else{if(16===I){for(B=A+2;y>>=A,y-=A,0===t.have){e.msg="invalid bit length repeat",t.mode=30;break}P=t.lens[t.have-1],z=3+(3&k),k>>>=2,y-=2}else if(17===I){for(B=A+3;y>>=A)),k>>>=3,y-=3}else{for(B=A+7;y>>=A)),k>>>=7,y-=7}if(t.have+z>t.nlen+t.ndist){e.msg="invalid bit length repeat",t.mode=30;break}for(;z--;)t.lens[t.have++]=P}}if(30===t.mode)break;if(0===t.lens[256]){e.msg="invalid code -- missing end-of-block",t.mode=30;break}if(t.lenbits=9,$={bits:t.lenbits},T=s(l,t.lens,0,t.nlen,t.lencode,0,t.work,$),t.lenbits=$.bits,T){e.msg="invalid literal/lengths set",t.mode=30;break}if(t.distbits=6,t.distcode=t.distdyn,$={bits:t.distbits},T=s(u,t.lens,t.nlen,t.ndist,t.distcode,0,t.work,$),t.distbits=$.bits,T){e.msg="invalid distances set",t.mode=30;break}if(t.mode=20,6===n)break e;case 20:t.mode=21;case 21:if(6<=b&&258<=w){e.next_out=g,e.avail_out=w,e.next_in=v,e.avail_in=b,t.hold=k,t.bits=y,o(e,R),g=e.next_out,m=e.output,w=e.avail_out,v=e.next_in,h=e.input,b=e.avail_in,k=t.hold,y=t.bits,12===t.mode&&(t.back=-1);break}for(t.back=0;C=(M=t.lencode[k&(1<>>16&255,I=65535&M,!((A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>O)])>>>16&255,I=65535&M,!(O+(A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>>=O,y-=O,t.back+=O}if(k>>>=A,y-=A,t.back+=A,t.length=I,0===C){t.mode=26;break}if(32&C){t.back=-1,t.mode=12;break}if(64&C){e.msg="invalid literal/length code",t.mode=30;break}t.extra=15&C,t.mode=22;case 22:if(t.extra){for(B=t.extra;y>>=t.extra,y-=t.extra,t.back+=t.extra}t.was=t.length,t.mode=23;case 23:for(;C=(M=t.distcode[k&(1<>>16&255,I=65535&M,!((A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>O)])>>>16&255,I=65535&M,!(O+(A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>>=O,y-=O,t.back+=O}if(k>>>=A,y-=A,t.back+=A,64&C){e.msg="invalid distance code",t.mode=30;break}t.offset=I,t.extra=15&C,t.mode=24;case 24:if(t.extra){for(B=t.extra;y>>=t.extra,y-=t.extra,t.back+=t.extra}if(t.offset>t.dmax){e.msg="invalid distance too far back",t.mode=30;break}t.mode=25;case 25:if(0===w)break e;if(z=R-w,t.offset>z){if((z=t.offset-z)>t.whave&&t.sane){e.msg="invalid distance too far back",t.mode=30;break}E=z>t.wnext?(z-=t.wnext,t.wsize-z):t.wnext-z,z>t.length&&(z=t.length),j=t.window}else j=m,E=g-t.offset,z=t.length;for(wb?(k=$[B+d[x]],D[F+d[x]]):(k=96,0),h=1<>A)+(m-=h)]=w<<24|k<<16|y|0,0!==m;);for(h=1<>=1;if(0!==h?(O&=h-1,O+=h):O=0,x++,0==--P[R]){if(R===z)break;R=n[t+d[x]]}if(E>>7)]}function $(e,n){e.pending_buf[e.pending++]=255&n,e.pending_buf[e.pending++]=n>>>8&255}function B(e,n,t){e.bi_valid>p-t?(e.bi_buf|=n<>p-e.bi_valid,e.bi_valid+=t-p):(e.bi_buf|=n<>>=1,t<<=1,0<--n;);return t>>>1}function N(e,n,t){var r,a,i=new Array(m+1),o=0;for(r=1;r<=m;r++)i[r]=o=o+t[r-1]<<1;for(a=0;a<=n;a++){var s=e[2*a+1];0!==s&&(e[2*a]=L(i[s]++,s))}}function U(e){var n;for(n=0;n>1;1<=t;t--)Z(e,i,t);for(a=l;t=e.heap[1],e.heap[1]=e.heap[e.heap_len--],Z(e,i,1),r=e.heap[1],e.heap[--e.heap_max]=t,e.heap[--e.heap_max]=r,i[2*a]=i[2*t]+i[2*r],e.depth[a]=(e.depth[t]>=e.depth[r]?e.depth[t]:e.depth[r])+1,i[2*t+1]=i[2*r+1]=a,e.heap[1]=a++,Z(e,i,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,n){var t,r,a,i,o,s,l=n.dyn_tree,u=n.max_code,c=n.stat_desc.static_tree,d=n.stat_desc.has_stree,f=n.stat_desc.extra_bits,p=n.stat_desc.extra_base,v=n.stat_desc.max_length,g=0;for(i=0;i<=m;i++)e.bl_count[i]=0;for(l[2*e.heap[e.heap_max]+1]=0,t=e.heap_max+1;t>=7;r>>=1)if(1&t&&0!==e.dyn_ltree[2*n])return a;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return i;for(n=32;n>>3,(s=e.static_len+3+7>>>3)<=o&&(o=s)):o=s=t+5,t+4<=o&&-1!==n?Y(e,n,t,r):4===e.strategy||s===o?(B(e,2+(r?1:0),3),H(e,S,z)):(B(e,4+(r?1:0),3),function(e,n,t,r){var a;for(B(e,n-257,5),B(e,t-1,5),B(e,r-4,4),a=0;a>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&n,e.pending_buf[e.l_buf+e.last_lit]=255&t,e.last_lit++,0===n?e.dyn_ltree[2*t]++:(e.matches++,n--,e.dyn_ltree[2*(j[t]+u+1)]++,e.dyn_dtree[2*T(n)]++),e.last_lit===e.lit_bufsize-1},t._tr_align=function(e){B(e,2,3),M(e,g,S),function(e){16===e.bi_valid?($(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,n,t){"use strict";n.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,n,r){(function(e){!function(e,n){"use strict";if(!e.setImmediate){var t,r,a,i,o=1,s={},l=!1,u=e.document,c=Object.getPrototypeOf&&Object.getPrototypeOf(e);c=c&&c.setTimeout?c:e,t="[object process]"==={}.toString.call(e.process)?function(e){process.nextTick((function(){f(e)}))}:function(){if(e.postMessage&&!e.importScripts){var n=!0,t=e.onmessage;return e.onmessage=function(){n=!1},e.postMessage("","*"),e.onmessage=t,n}}()?(i="setImmediate$"+Math.random()+"$",e.addEventListener?e.addEventListener("message",h,!1):e.attachEvent("onmessage",h),function(n){e.postMessage(i+n,"*")}):e.MessageChannel?((a=new MessageChannel).port1.onmessage=function(e){f(e.data)},function(e){a.port2.postMessage(e)}):u&&"onreadystatechange"in u.createElement("script")?(r=u.documentElement,function(e){var n=u.createElement("script");n.onreadystatechange=function(){f(e),n.onreadystatechange=null,r.removeChild(n),n=null},r.appendChild(n)}):function(e){setTimeout(f,0,e)},c.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var n=new Array(arguments.length-1),r=0;r{"use strict";e.exports=t.p+"ad9ce752523ea9fa4f5d.wasm"},878:(e,n,t)=>{"use strict";e.exports=t.p+"44d967c3c705de5b1d1b.wasm"}},r={};function a(e){var n=r[e];if(void 0!==n)return n.exports;var i=r[e]={id:e,loaded:!1,exports:{}};return t[e](i,i.exports,a),i.loaded=!0,i.exports}a.m=t,a.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return a.d(n,{a:n}),n},a.d=(e,n)=>{for(var t in n)a.o(n,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((n,t)=>(a.f[t](e,n),n)),[])),a.u=e=>"core.ruffle."+{159:"8c80148e5adc80f63dfe",339:"92e6746d93be1e95cd77"}[e]+".js",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.hmd=e=>((e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:()=>{throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e),a.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),e={},n="ruffle-selfhosted:",a.l=(t,r,i,o)=>{if(e[t])e[t].push(r);else{var s,l;if(void 0!==i)for(var u=document.getElementsByTagName("script"),c=0;c{s.onerror=s.onload=null,clearTimeout(h);var a=e[t];if(delete e[t],s.parentNode&&s.parentNode.removeChild(s),a&&a.forEach((e=>e(r))),n)return n(r)},h=setTimeout(f.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=f.bind(null,s.onerror),s.onload=f.bind(null,s.onload),l&&document.head.appendChild(s)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.p="",(()=>{a.b=document.baseURI||self.location.href;var e={179:0};a.f.j=(n,t)=>{var r=a.o(e,n)?e[n]:void 0;if(0!==r)if(r)t.push(r[2]);else{var i=new Promise(((t,a)=>r=e[n]=[t,a]));t.push(r[2]=i);var o=a.p+a.u(n),s=new Error;a.l(o,(t=>{if(a.o(e,n)&&(0!==(r=e[n])&&(e[n]=void 0),r)){var i=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;s.message="Loading chunk "+n+" failed.\n("+i+": "+o+")",s.name="ChunkLoadError",s.type=i,s.request=o,r[1](s)}}),"chunk-"+n,n)}};var n=(n,t)=>{var r,i,[o,s,l]=t,u=0;if(o.some((n=>0!==e[n]))){for(r in s)a.o(s,r)&&(a.m[r]=s[r]);if(l)l(a)}for(n&&n(t);u{"use strict";class e{constructor(e,n,t,r,a){this.major=e,this.minor=n,this.patch=t,this.prIdent=r,this.buildIdent=a}static fromSemver(n){const t=n.split("+"),r=t[0].split("-"),a=r[0].split("."),i=parseInt(a[0],10);let o=0,s=0,l=null,u=null;return void 0!==a[1]&&(o=parseInt(a[1],10)),void 0!==a[2]&&(s=parseInt(a[2],10)),void 0!==r[1]&&(l=r[1].split(".")),void 0!==t[1]&&(u=t[1].split(".")),new e(i,o,s,l,u)}isCompatibleWith(e){return 0!==this.major&&this.major===e.major||0===this.major&&0===e.major&&0!==this.minor&&this.minor===e.minor||0===this.major&&0===e.major&&0===this.minor&&0===e.minor&&0!==this.patch&&this.patch===e.patch}hasPrecedenceOver(e){if(this.major>e.major)return!0;if(this.majore.minor)return!0;if(this.minore.patch)return!0;if(this.patchparseInt(e.prIdent[t],10))return!0;if(parseInt(this.prIdent[t],10)e.prIdent[t])return!0;if(this.prIdent[t]e.prIdent.length}return!1}isEqual(e){return this.major===e.major&&this.minor===e.minor&&this.patch===e.patch}isStableOrCompatiblePrerelease(e){return null===e.prIdent||this.major===e.major&&this.minor===e.minor&&this.patch===e.patch}}class n{constructor(e){this.requirements=e}satisfiedBy(e){for(const n of this.requirements){let t=!0;for(const{comparator:r,version:a}of n)t=t&&a.isStableOrCompatiblePrerelease(e),""===r||"="===r?t=t&&a.isEqual(e):">"===r?t=t&&e.hasPrecedenceOver(a):">="===r?t=t&&(e.hasPrecedenceOver(a)||a.isEqual(e)):"<"===r?t=t&&a.hasPrecedenceOver(e):"<="===r?t=t&&(a.hasPrecedenceOver(e)||a.isEqual(e)):"^"===r&&(t=t&&a.isCompatibleWith(e));if(t)return!0}return!1}static fromRequirementString(t){const r=t.split(" ");let a=[];const i=[];for(const n of r)if("||"===n)a.length>0&&(i.push(a),a=[]);else if(n.length>0){const t=/[0-9]/.exec(n);if(t){const r=n.slice(0,t.index).trim(),i=e.fromSemver(n.slice(t.index).trim());a.push({comparator:r,version:i})}}return a.length>0&&i.push(a),new n(i)}}const t=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),r=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,7,1,5,0,208,112,26,11])),i=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,12,1,10,0,67,0,0,0,0,252,0,26,11])),o=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,65,0,192,26,11])),s=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]));function l(e){const n="function"==typeof Function.prototype.toString?Function.prototype.toString():null;return"string"==typeof n&&n.indexOf("[native code]")>=0&&Function.prototype.toString.call(e).indexOf("[native code]")>=0}function u(){"function"==typeof Array.prototype.reduce&&l(Array.prototype.reduce)||Object.defineProperty(Array.prototype,"reduce",{value(...e){if(0===e.length&&window.Prototype&&window.Prototype.Version&&window.Prototype.Version<"1.6.1")return this.length>1?this:this[0];const n=e[0];if(null===this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!=typeof n)throw new TypeError(`${n} is not a function`);const t=Object(this),r=t.length>>>0;let a,i=0;if(e.length>=2)a=e[1];else{for(;i=r)throw new TypeError("Reduce of empty array with no initial value");a=t[i++]}for(;ie[n]}),"function"!=typeof Reflect.set&&Object.defineProperty(Reflect,"set",{value(e,n,t){e[n]=t}}),"function"!=typeof Reflect.has&&Object.defineProperty(Reflect,"has",{value:(e,n)=>n in e}),"function"!=typeof Reflect.ownKeys&&Object.defineProperty(Reflect,"ownKeys",{value:e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]})}let c=null,d=!1;try{if(void 0!==document.currentScript&&null!==document.currentScript&&"src"in document.currentScript&&""!==document.currentScript.src){let e=document.currentScript.src;e.endsWith(".js")||e.endsWith("/")||(e+="/"),c=new URL(".",e),d=c.protocol.includes("extension")}}catch(e){console.warn("Unable to get currentScript URL")}function f(e){var n;let t=null!==(n=null==c?void 0:c.href)&&void 0!==n?n:"";return!d&&"publicPath"in e&&null!==e.publicPath&&void 0!==e.publicPath&&(t=e.publicPath),""===t||t.endsWith("/")||(t+="/"),t}let h=null;function m(e,n){return null===h&&(h=async function(e,n){var l;u();const c=(await Promise.all([t(),s(),i(),o(),r()])).every(Boolean);c||console.log("Some WebAssembly extensions are NOT available, falling back to the vanilla WebAssembly module"),a.p=f(e);const{default:d,Ruffle:h}=await(c?a.e(339).then(a.bind(a,339)):a.e(159).then(a.bind(a,159)));let m;const p=c?new URL(a(899),a.b):new URL(a(878),a.b),v=await fetch(p),g="function"==typeof ReadableStream;if(n&&g){const e=(null===(l=null==v?void 0:v.headers)||void 0===l?void 0:l.get("content-length"))||"";let t=0;const r=parseInt(e);m=new Response(new ReadableStream({async start(e){var a;const i=null===(a=v.body)||void 0===a?void 0:a.getReader();if(!i)throw"Response had no body";for(n(t,r);;){const{done:a,value:o}=await i.read();if(a)break;(null==o?void 0:o.byteLength)&&(t+=null==o?void 0:o.byteLength),e.enqueue(o),n(t,r)}e.close()}}),v)}else m=v;return await d(m),h}(e,n)),h}const p=document.createElement("template");p.innerHTML='\n \n \n\n
\n
\n
\n \n
\n\n \n\n \n\n \n';const v={};function g(e,n){const t=v[e];if(void 0!==t){if(t.class!==n)throw new Error("Internal naming conflict on "+e);return t.name}let r=0;if(void 0!==window.customElements)for(;r<999;){let t=e;if(r>0&&(t=t+"-"+r),void 0===window.customElements.get(t))return window.customElements.define(t,n),v[e]={class:n,name:t,internalName:e},t;r+=1}throw new Error("Failed to assign custom element "+e)}const b={allowScriptAccess:!1,parameters:{},autoplay:"auto",backgroundColor:null,letterbox:"fullscreen",unmuteOverlay:"visible",upgradeToHttps:!0,compatibilityRules:!0,favorFlash:!0,warnOnUnsupportedContent:!0,logLevel:"error",showSwfDownload:!1,contextMenu:"on",preloader:!0,splashScreen:!0,maxExecutionDuration:15,base:null,menu:!0,salign:"",forceAlign:!1,quality:"high",scale:"showAll",forceScale:!1,frameRate:null,wmode:"opaque",publicPath:null,polyfills:!0,playerVersion:null,preferredRenderer:null,openUrlMode:"allow",allowNetworking:"all"},w="application/x-shockwave-flash",k="application/futuresplash",y="application/x-shockwave-flash2-preview",_="application/vnd.adobe.flash.movie";function R(e,n){const t=function(e){let n="";try{n=new URL(e,"https://example.com").pathname}catch(e){}if(n&&n.length>=4){const e=n.slice(-4).toLowerCase();if(".swf"===e||".spl"===e)return!0}return!1}(e);return n?function(e,n){switch(e=e.toLowerCase()){case w.toLowerCase():case k.toLowerCase():case y.toLowerCase():case _.toLowerCase():return!0;default:if(n)switch(e){case"application/octet-stream":case"binary/octet-stream":return!0}}return!1}(n,t):t}const x="0.1.0",S="nightly 2023-08-02",z="nightly",E="2023-08-02T00:22:54.390Z",j="006393c5816e68b54c6720fcd8e657009cfb90d8";class A{constructor(e){this.value=e}valueOf(){return this.value}}class C extends A{constructor(e="???"){super(e)}toString(e){return`{${this.value}}`}}class I extends A{constructor(e,n={}){super(e),this.opts=n}toString(e){try{return e.memoizeIntlObject(Intl.NumberFormat,this.opts).format(this.value)}catch(n){return e.reportError(n),this.value.toString(10)}}}class O extends A{constructor(e,n={}){super(e),this.opts=n}toString(e){try{return e.memoizeIntlObject(Intl.DateTimeFormat,this.opts).format(this.value)}catch(n){return e.reportError(n),new Date(this.value).toISOString()}}}const D=100,F="\u2068",P="\u2069";function T(e,n,t){if(t===n)return!0;if(t instanceof I&&n instanceof I&&t.value===n.value)return!0;if(n instanceof I&&"string"==typeof t){if(t===e.memoizeIntlObject(Intl.PluralRules,n.opts).select(n.value))return!0}return!1}function $(e,n,t){return n[t]?N(e,n[t].value):(e.reportError(new RangeError("No default")),new C)}function B(e,n){const t=[],r=Object.create(null);for(const a of n)"narg"===a.type?r[a.name]=M(e,a.value):t.push(M(e,a));return{positional:t,named:r}}function M(e,n){switch(n.type){case"str":return n.value;case"num":return new I(n.value,{minimumFractionDigits:n.precision});case"var":return function(e,{name:n}){let t;if(e.params){if(!Object.prototype.hasOwnProperty.call(e.params,n))return new C(`$${n}`);t=e.params[n]}else{if(!e.args||!Object.prototype.hasOwnProperty.call(e.args,n))return e.reportError(new ReferenceError(`Unknown variable: $${n}`)),new C(`$${n}`);t=e.args[n]}if(t instanceof A)return t;switch(typeof t){case"string":return t;case"number":return new I(t);case"object":if(t instanceof Date)return new O(t.getTime());default:return e.reportError(new TypeError(`Variable type not supported: $${n}, ${typeof t}`)),new C(`$${n}`)}}(e,n);case"mesg":return function(e,{name:n,attr:t}){const r=e.bundle._messages.get(n);if(!r)return e.reportError(new ReferenceError(`Unknown message: ${n}`)),new C(n);if(t){const a=r.attributes[t];return a?N(e,a):(e.reportError(new ReferenceError(`Unknown attribute: ${t}`)),new C(`${n}.${t}`))}if(r.value)return N(e,r.value);return e.reportError(new ReferenceError(`No value: ${n}`)),new C(n)}(e,n);case"term":return function(e,{name:n,attr:t,args:r}){const a=`-${n}`,i=e.bundle._terms.get(a);if(!i)return e.reportError(new ReferenceError(`Unknown term: ${a}`)),new C(a);if(t){const n=i.attributes[t];if(n){e.params=B(e,r).named;const t=N(e,n);return e.params=null,t}return e.reportError(new ReferenceError(`Unknown attribute: ${t}`)),new C(`${a}.${t}`)}e.params=B(e,r).named;const o=N(e,i.value);return e.params=null,o}(e,n);case"func":return function(e,{name:n,args:t}){let r=e.bundle._functions[n];if(!r)return e.reportError(new ReferenceError(`Unknown function: ${n}()`)),new C(`${n}()`);if("function"!=typeof r)return e.reportError(new TypeError(`Function ${n}() is not callable`)),new C(`${n}()`);try{let n=B(e,t);return r(n.positional,n.named)}catch(t){return e.reportError(t),new C(`${n}()`)}}(e,n);case"select":return function(e,{selector:n,variants:t,star:r}){let a=M(e,n);if(a instanceof C)return $(e,t,r);for(const n of t){if(T(e,a,M(e,n.key)))return N(e,n.value)}return $(e,t,r)}(e,n);default:return new C}}function L(e,n){if(e.dirty.has(n))return e.reportError(new RangeError("Cyclic reference")),new C;e.dirty.add(n);const t=[],r=e.bundle._useIsolating&&n.length>1;for(const a of n)if("string"!=typeof a){if(e.placeables++,e.placeables>D)throw e.dirty.delete(n),new RangeError(`Too many placeables expanded: ${e.placeables}, max allowed is ${D}`);r&&t.push(F),t.push(M(e,a).toString(e)),r&&t.push(P)}else t.push(e.bundle._transform(a));return e.dirty.delete(n),t.join("")}function N(e,n){return"string"==typeof n?e.bundle._transform(n):L(e,n)}class U{constructor(e,n,t){this.dirty=new WeakSet,this.params=null,this.placeables=0,this.bundle=e,this.errors=n,this.args=t}reportError(e){if(!(this.errors&&e instanceof Error))throw e;this.errors.push(e)}memoizeIntlObject(e,n){let t=this.bundle._intls.get(e);t||(t={},this.bundle._intls.set(e,t));let r=JSON.stringify(n);return t[r]||(t[r]=new e(this.bundle.locales,n)),t[r]}}function q(e,n){const t=Object.create(null);for(const[r,a]of Object.entries(e))n.includes(r)&&(t[r]=a.valueOf());return t}const W=["unitDisplay","currencyDisplay","useGrouping","minimumIntegerDigits","minimumFractionDigits","maximumFractionDigits","minimumSignificantDigits","maximumSignificantDigits"];function Z(e,n){let t=e[0];if(t instanceof C)return new C(`NUMBER(${t.valueOf()})`);if(t instanceof I)return new I(t.valueOf(),{...t.opts,...q(n,W)});if(t instanceof O)return new I(t.valueOf(),{...q(n,W)});throw new TypeError("Invalid argument to NUMBER")}const H=["dateStyle","timeStyle","fractionalSecondDigits","dayPeriod","hour12","weekday","era","year","month","day","hour","minute","second","timeZoneName"];function V(e,n){let t=e[0];if(t instanceof C)return new C(`DATETIME(${t.valueOf()})`);if(t instanceof O)return new O(t.valueOf(),{...t.opts,...q(n,H)});if(t instanceof I)return new O(t.valueOf(),{...q(n,H)});throw new TypeError("Invalid argument to DATETIME")}const J=new Map;class K{constructor(e,{functions:n,useIsolating:t=!0,transform:r=(e=>e)}={}){this._terms=new Map,this._messages=new Map,this.locales=Array.isArray(e)?e:[e],this._functions={NUMBER:Z,DATETIME:V,...n},this._useIsolating=t,this._transform=r,this._intls=function(e){const n=Array.isArray(e)?e.join(" "):e;let t=J.get(n);return void 0===t&&(t=new Map,J.set(n,t)),t}(e)}hasMessage(e){return this._messages.has(e)}getMessage(e){return this._messages.get(e)}addResource(e,{allowOverrides:n=!1}={}){const t=[];for(let r=0;r\s*/y,ge=/\s*:\s*/y,be=/\s*,?\s*/y,we=/\s+/y;class ke{constructor(e){this.body=[],G.lastIndex=0;let n=0;for(;;){let t=G.exec(e);if(null===t)break;n=G.lastIndex;try{this.body.push(s(t[1]))}catch(e){if(e instanceof SyntaxError)continue;throw e}}function t(t){return t.lastIndex=n,t.test(e)}function r(t,r){if(e[n]===t)return n++,!0;if(r)throw new r(`Expected ${t}`);return!1}function a(e,r){if(t(e))return n=e.lastIndex,!0;if(r)throw new r(`Expected ${e.toString()}`);return!1}function i(t){t.lastIndex=n;let r=t.exec(e);if(null===r)throw new SyntaxError(`Expected ${t.toString()}`);return n=t.lastIndex,r}function o(e){return i(e)[1]}function s(e){let n=l(),r=function(){let e=Object.create(null);for(;t(Y);){let n=o(Y),t=l();if(null===t)throw new SyntaxError("Expected attribute value");e[n]=t}return e}();if(null===n&&0===Object.keys(r).length)throw new SyntaxError("Expected message value or attributes");return{id:e,value:n,attributes:r}}function l(){let r;if(t(re)&&(r=o(re)),"{"===e[n]||"}"===e[n])return u(r?[r]:[],1/0);let a=g();return a?r?u([r,a],a.length):(a.value=b(a.value,se),u([a],a.length)):r?b(r,le):null}function u(r=[],a){for(;;){if(t(re)){r.push(o(re));continue}if("{"===e[n]){r.push(c());continue}if("}"===e[n])throw new SyntaxError("Unbalanced closing brace");let i=g();if(!i)break;r.push(i),a=Math.min(a,i.length)}let i=r.length-1,s=r[i];"string"==typeof s&&(r[i]=b(s,le));let l=[];for(let e of r)e instanceof ye&&(e=e.value.slice(0,e.value.length-a)),e&&l.push(e);return l}function c(){a(de,SyntaxError);let e=d();if(a(fe))return e;if(a(ve)){let n=function(){let e,n=[],a=0;for(;t(X);){r("*")&&(e=a);let t=h(),i=l();if(null===i)throw new SyntaxError("Expected variant value");n[a++]={key:t,value:i}}if(0===a)return null;if(void 0===e)throw new SyntaxError("Expected default variant");return{variants:n,star:e}}();return a(fe,SyntaxError),{type:"select",selector:e,...n}}throw new SyntaxError("Unclosed placeable")}function d(){if("{"===e[n])return c();if(t(ne)){let[,t,r,o=null]=i(ne);if("$"===t)return{type:"var",name:r};if(a(pe)){let i=function(){let t=[];for(;;){switch(e[n]){case")":return n++,t;case void 0:throw new SyntaxError("Unclosed argument list")}t.push(f()),a(be)}}();if("-"===t)return{type:"term",name:r,attr:o,args:i};if(te.test(r))return{type:"func",name:r,args:i};throw new SyntaxError("Function names must be all upper-case")}return"-"===t?{type:"term",name:r,attr:o,args:[]}:{type:"mesg",name:r,attr:o}}return m()}function f(){let e=d();return"mesg"!==e.type?e:a(ge)?{type:"narg",name:e.name,value:m()}:e}function h(){let e;return a(he,SyntaxError),e=t(Q)?p():{type:"str",value:o(ee)},a(me,SyntaxError),e}function m(){if(t(Q))return p();if('"'===e[n])return function(){r('"',SyntaxError);let t="";for(;;){if(t+=o(ae),"\\"!==e[n]){if(r('"'))return{type:"str",value:t};throw new SyntaxError("Unclosed string literal")}t+=v()}}();throw new SyntaxError("Invalid expression")}function p(){let[,e,n=""]=i(Q),t=n.length;return{type:"num",value:parseFloat(e),precision:t}}function v(){if(t(ie))return o(ie);if(t(oe)){let[,e,n]=i(oe),t=parseInt(e||n,16);return t<=55295||57344<=t?String.fromCodePoint(t):"\ufffd"}throw new SyntaxError("Unknown escape sequence")}function g(){let t=n;switch(a(we),e[n]){case".":case"[":case"*":case"}":case void 0:return!1;case"{":return w(e.slice(t,n))}return" "===e[n-1]&&w(e.slice(t,n))}function b(e,n){return e.replace(n,"")}function w(e){let n=e.replace(ue,"\n"),t=ce.exec(e)[1].length;return new ye(n,t)}}}class ye{constructor(e,n){this.value=e,this.length=n}}const _e=new RegExp("^([a-z]{2,3}|\\*)(?:-([a-z]{4}|\\*))?(?:-([a-z]{2}|\\*))?(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))?$","i");class Re{constructor(e){const n=_e.exec(e.replace(/_/g,"-"));if(!n)return void(this.isWellFormed=!1);let[,t,r,a,i]=n;t&&(this.language=t.toLowerCase()),r&&(this.script=r[0].toUpperCase()+r.slice(1)),a&&(this.region=a.toUpperCase()),this.variant=i,this.isWellFormed=!0}isEqual(e){return this.language===e.language&&this.script===e.script&&this.region===e.region&&this.variant===e.variant}matches(e,n=!1,t=!1){return(this.language===e.language||n&&void 0===this.language||t&&void 0===e.language)&&(this.script===e.script||n&&void 0===this.script||t&&void 0===e.script)&&(this.region===e.region||n&&void 0===this.region||t&&void 0===e.region)&&(this.variant===e.variant||n&&void 0===this.variant||t&&void 0===e.variant)}toString(){return[this.language,this.script,this.region,this.variant].filter((e=>void 0!==e)).join("-")}clearVariants(){this.variant=void 0}clearRegion(){this.region=void 0}addLikelySubtags(){const e=function(e){if(Object.prototype.hasOwnProperty.call(xe,e))return new Re(xe[e]);const n=new Re(e);if(n.language&&Se.includes(n.language))return n.region=n.language.toUpperCase(),n;return null}(this.toString().toLowerCase());return!!e&&(this.language=e.language,this.script=e.script,this.region=e.region,this.variant=e.variant,!0)}}const xe={ar:"ar-arab-eg","az-arab":"az-arab-ir","az-ir":"az-arab-ir",be:"be-cyrl-by",da:"da-latn-dk",el:"el-grek-gr",en:"en-latn-us",fa:"fa-arab-ir",ja:"ja-jpan-jp",ko:"ko-kore-kr",pt:"pt-latn-br",sr:"sr-cyrl-rs","sr-ru":"sr-latn-ru",sv:"sv-latn-se",ta:"ta-taml-in",uk:"uk-cyrl-ua",zh:"zh-hans-cn","zh-hant":"zh-hant-tw","zh-hk":"zh-hant-hk","zh-mo":"zh-hant-mo","zh-tw":"zh-hant-tw","zh-gb":"zh-hant-gb","zh-us":"zh-hant-us"},Se=["az","bg","cs","de","es","fi","fr","hu","it","lt","lv","nl","pl","ro","ru"];function ze(e,n,{strategy:t="filtering",defaultLocale:r}={}){const a=function(e,n,t){const r=new Set,a=new Map;for(let e of n)new Re(e).isWellFormed&&a.set(e,new Re(e));e:for(const n of e){const e=n.toLowerCase(),i=new Re(e);if(void 0!==i.language){for(const n of a.keys())if(e===n.toLowerCase()){if(r.add(n),a.delete(n),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}for(const[e,n]of a.entries())if(n.matches(i,!0,!1)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}if(i.addLikelySubtags())for(const[e,n]of a.entries())if(n.matches(i,!0,!1)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}i.clearVariants();for(const[e,n]of a.entries())if(n.matches(i,!0,!0)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}if(i.clearRegion(),i.addLikelySubtags())for(const[e,n]of a.entries())if(n.matches(i,!0,!1)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}i.clearRegion();for(const[e,n]of a.entries())if(n.matches(i,!0,!0)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}}}return Array.from(r)}(Array.from(null!=e?e:[]).map(String),Array.from(null!=n?n:[]).map(String),t);if("lookup"===t){if(void 0===r)throw new Error("defaultLocale cannot be undefined for strategy `lookup`");0===a.length&&a.push(r)}else r&&!a.includes(r)&&a.push(r);return a}const Ee={"ar-SA":{"context_menu.ftl":"context-menu-download-swf = \u062a\u062d\u0645\u064a\u0644 .swf\ncontext-menu-copy-debug-info = \u0646\u0633\u062e \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u062a\u0635\u062d\u064a\u062d\ncontext-menu-open-save-manager = \u0641\u062a\u062d \u0645\u062f\u064a\u0631 \u0627\u0644\u062d\u0641\u0638\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u062d\u0648\u0644 \u0645\u0644\u062d\u0642 \u0631\u0641\u0644 ({ $version })\n *[other] \u062d\u0648\u0644 \u0631\u0641\u0644 ({ $version })\n }\ncontext-menu-hide = \u0625\u062e\u0641\u0627\u0621 \u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0626\u0645\u0629\ncontext-menu-exit-fullscreen = \u0627\u0644\u062e\u0631\u0648\u062c \u0645\u0646 \u0648\u0636\u0639\u064a\u0629 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629\ncontext-menu-enter-fullscreen = \u062a\u0641\u0639\u064a\u0644 \u0648\u0636\u0639\u064a\u0629 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629\n","messages.ftl":'message-cant-embed =\n \u0644\u0645 \u062a\u0643\u0646 \u0631\u0641\u0644 \u0642\u0627\u062f\u0631\u0629 \u0639\u0644\u0649 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0641\u0644\u0627\u0634 \u0627\u0644\u0645\u0636\u0645\u0646\u0629 \u0641\u064a \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062d\u0629.\n \u064a\u0645\u0643\u0646\u0643 \u0645\u062d\u0627\u0648\u0644\u0629 \u0641\u062a\u062d \u0627\u0644\u0645\u0644\u0641 \u0641\u064a \u0639\u0644\u0627\u0645\u0629 \u062a\u0628\u0648\u064a\u0628 \u0645\u0646\u0641\u0635\u0644\u0629\u060c \u0644\u062a\u062c\u0627\u0648\u0632 \u0647\u0630\u0647 \u0627\u0644\u0645\u0634\u0643\u0644\u0629.\npanic-title = \u0644\u0642\u062f \u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0627 :(\nmore-info = \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u0643\u062b\u0631\nrun-anyway = \u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0639\u0644\u0649 \u0623\u064a \u062d\u0627\u0644\ncontinue = \u0627\u0644\u0627\u0633\u062a\u0645\u0631\u0627\u0631\nreport-bug = \u0625\u0628\u0644\u0627\u063a \u0639\u0646 \u062e\u0644\u0644\nupdate-ruffle = \u062a\u062d\u062f\u064a\u062b \u0631\u0641\u0644\nruffle-demo = \u0648\u064a\u0628 \u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a\nruffle-desktop = \u0628\u0631\u0646\u0627\u0645\u062c \u0633\u0637\u062d \u0627\u0644\u0645\u0643\u062a\u0628\nruffle-wiki = \u0639\u0631\u0636 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a\nview-error-details = \u0639\u0631\u0636 \u062a\u0641\u0627\u0635\u064a\u0644 \u0627\u0644\u062e\u0637\u0623\nopen-in-new-tab = \u0641\u062a\u062d \u0641\u064a \u0639\u0644\u0627\u0645\u0629 \u062a\u0628\u0648\u064a\u0628 \u062c\u062f\u064a\u062f\u0629\nclick-to-unmute = \u0627\u0646\u0642\u0631 \u0644\u0625\u0644\u063a\u0627\u0621 \u0627\u0644\u0643\u062a\u0645\nerror-file-protocol =\n \u064a\u0628\u062f\u0648 \u0623\u0646\u0643 \u062a\u0642\u0648\u0645 \u0628\u062a\u0634\u063a\u064a\u0644 \u0631\u0641\u0644 \u0639\u0644\u0649 \u0628\u0631\u0648\u062a\u0648\u0643\u0648\u0644 "\u0627\u0644\u0645\u0644\u0641:".\n \u0647\u0630\u0627 \u0644\u0646 \u064a\u0639\u0645\u0644 \u0644\u0623\u0646 \u0627\u0644\u0645\u062a\u0635\u0641\u062d\u0627\u062a \u062a\u0645\u0646\u0639 \u0627\u0644\u0639\u062f\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u064a\u0632\u0627\u062a \u0645\u0646 \u0627\u0644\u0639\u0645\u0644 \u0644\u0623\u0633\u0628\u0627\u0628 \u0623\u0645\u0646\u064a\u0629.\n \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0630\u0644\u0643\u060c \u0646\u062f\u0639\u0648\u0643 \u0625\u0644\u0649 \u0625\u0639\u062f\u0627\u062f \u062e\u0627\u062f\u0645 \u0645\u062d\u0644\u064a \u0623\u0648 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0639\u0631\u0636 \u0627\u0644\u0648\u064a\u0628 \u0623\u0648 \u062a\u0637\u0628\u064a\u0642 \u0633\u0637\u062d \u0627\u0644\u0645\u0643\u062a\u0628.\nerror-javascript-config =\n \u062a\u0639\u0631\u0636 \u0631\u0641\u0644 \u0625\u0644\u0649 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0628\u0633\u0628\u0628 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062e\u0627\u0637\u0626\u0629 \u0644\u0644\u062c\u0627\u0641\u0627 \u0633\u0643\u0631\u064a\u0628\u062a.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0646\u062d\u0646 \u0646\u062f\u0639\u0648\u0643 \u0625\u0644\u0649 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u062a\u0641\u0627\u0635\u064a\u0644 \u0627\u0644\u062e\u0637\u0623 \u0644\u0645\u0639\u0631\u0641\u0629 \u0633\u0628\u0628 \u0627\u0644\u0645\u0634\u0643\u0644\u0629.\n \u064a\u0645\u0643\u0646\u0643 \u0623\u064a\u0636\u0627 \u0627\u0644\u0631\u062c\u0648\u0639 \u0625\u0644\u0649 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-not-found =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0643\u0648\u0646 \u0627\u0644\u0645\u0644\u0641 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0627\u0644\u0631\u062c\u0627\u0621 \u0627\u0644\u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646 \u0627\u0644\u0645\u0644\u0641 \u0642\u062f \u062a\u0645 \u062a\u062d\u0645\u064a\u0644\u0647 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d.\n \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631\u062a \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u060c \u0642\u062f \u062a\u062d\u062a\u0627\u062c \u0625\u0644\u0649 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0625\u0639\u062f\u0627\u062f\u0627\u062a "\u0627\u0644\u0645\u0633\u0627\u0631 \u0627\u0644\u0639\u0627\u0645": \u0627\u0644\u0631\u062c\u0627\u0621 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-mime-type =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u062e\u0627\u062f\u0645 \u0627\u0644\u0648\u064a\u0628 \u0647\u0630\u0627 \u0644\u0627 \u064a\u062e\u062f\u0645 \u0645\u0644\u0641\u0627\u062a ". wasm" \u0645\u0639 \u0646\u0648\u0639 MIME \u0627\u0644\u0635\u062d\u064a\u062d.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-swf-fetch =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0644\u0641 \u0641\u0644\u0627\u0634 SWF.\n \u0627\u0644\u0633\u0628\u0628 \u0627\u0644\u0623\u0643\u062b\u0631 \u0627\u062d\u062a\u0645\u0627\u0644\u0627 \u0647\u0648 \u0623\u0646 \u0627\u0644\u0645\u0644\u0641 \u0644\u0645 \u064a\u0639\u062f \u0645\u0648\u062c\u0648\u062f\u0627\u060c \u0644\u0630\u0644\u0643 \u0644\u0627 \u064a\u0648\u062c\u062f \u0634\u064a\u0621 \u0644\u064a\u062d\u0645\u0644\u0647 \u0631\u0641\u0644.\n \u062d\u0627\u0648\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u0645\u0648\u0642\u0639 \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-swf-cors =\n \u0641\u0634\u0644 \u0627\u0644\u0631\u0648\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0644\u0641 \u0641\u0644\u0627\u0634 SWF.\n \u0645\u0646 \u0627\u0644\u0645\u062d\u062a\u0645\u0644 \u0623\u0646 \u062a\u0645 \u062d\u0638\u0631 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0645\u0646\u0627\u0644 \u0628\u0648\u0627\u0633\u0637\u0629 \u0633\u064a\u0627\u0633\u0629 CORS.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-cors =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0643\u0648\u0646 \u0645\u0644\u0641 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0645\u0646 \u0627\u0644\u0645\u062d\u062a\u0645\u0644 \u0623\u0646 \u062a\u0645 \u062d\u0638\u0631 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0645\u0646\u0627\u0644 \u0628\u0648\u0627\u0633\u0637\u0629 \u0633\u064a\u0627\u0633\u0629 CORS.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-invalid =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u062e\u0627\u062f\u0645 \u0627\u0644\u0648\u064a\u0628 \u0647\u0630\u0627 \u0644\u0627 \u064a\u062e\u062f\u0645 \u0645\u0644\u0641\u0627\u062a ". wasm" \u0645\u0639 \u0646\u0648\u0639 MIME \u0627\u0644\u0635\u062d\u064a\u062d.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-download =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u062a\u0647\u0627 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u0647\u0630\u0627 \u064a\u0645\u0643\u0646 \u0623\u0646 \u064a\u062d\u0644 \u0646\u0641\u0633\u0647 \u0641\u064a \u0643\u062b\u064a\u0631 \u0645\u0646 \u0627\u0644\u0623\u062d\u064a\u0627\u0646\u060c \u0644\u0630\u0644\u0643 \u064a\u0645\u0643\u0646\u0643 \u0645\u062d\u0627\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0635\u0641\u062d\u0629.\n \u062e\u0644\u0627\u0641 \u0630\u0644\u0643\u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0645\u062f\u064a\u0631 \u0627\u0644\u0645\u0648\u0642\u0639.\nerror-wasm-disabled-on-edge =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0643\u0648\u0646 \u0627\u0644\u0645\u0644\u0641 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0644\u0625\u0635\u0644\u0627\u062d \u0647\u0630\u0647 \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u060c \u062d\u0627\u0648\u0644 \u0641\u062a\u062d \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u0645\u062a\u0635\u0641\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u0643\u060c \u0627\u0646\u0642\u0631 \u0641\u0648\u0642 "\u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629\u060c \u0627\u0644\u0628\u062d\u062b\u060c \u0627\u0644\u062e\u062f\u0645\u0627\u062a"\u060c \u0648\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0644\u0623\u0633\u0641\u0644\u060c \u0648\u0625\u064a\u0642\u0627\u0641 "\u062a\u0639\u0632\u064a\u0632 \u0623\u0645\u0627\u0646\u0643 \u0639\u0644\u0649 \u0627\u0644\u0648\u064a\u0628".\n \u0647\u0630\u0627 \u0633\u064a\u0633\u0645\u062d \u0644\u0644\u0645\u062a\u0635\u0641\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641\u0627\u062a ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629.\n \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631\u062a \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u060c \u0642\u062f \u062a\u062d\u062a\u0627\u062c \u0625\u0644\u0649 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u062a\u0635\u0641\u062d \u0623\u062e\u0631.\nerror-javascript-conflict =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u064a\u0628\u062f\u0648 \u0623\u0646 \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062d\u0629 \u062a\u0633\u062a\u062e\u062f\u0645 \u0643\u0648\u062f \u062c\u0627\u0641\u0627 \u0633\u0643\u0631\u064a\u0628\u062a \u0627\u0644\u0630\u064a \u064a\u062a\u0639\u0627\u0631\u0636 \u0645\u0639 \u0631\u0641\u0644.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0641\u0625\u0646\u0646\u0627 \u0646\u062f\u0639\u0648\u0643 \u0625\u0644\u0649 \u0645\u062d\u0627\u0648\u0644\u0629 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641 \u0639\u0644\u0649 \u0635\u0641\u062d\u0629 \u0641\u0627\u0631\u063a\u0629.\nerror-javascript-conflict-outdated = \u064a\u0645\u0643\u0646\u0643 \u0623\u064a\u0636\u064b\u0627 \u0645\u062d\u0627\u0648\u0644\u0629 \u062a\u062d\u0645\u064a\u0644 \u0646\u0633\u062e\u0629 \u0623\u062d\u062f\u062b \u0645\u0646 \u0631\u0641\u0644 \u0627\u0644\u062a\u064a \u0642\u062f \u062a\u062d\u0644 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 (\u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u062d\u0627\u0644\u064a\u0629 \u0642\u062f\u064a\u0645\u0629: { $buildDate }).\nerror-csp-conflict =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u0644\u0627 \u062a\u0633\u0645\u062d \u0633\u064a\u0627\u0633\u0629 \u0623\u0645\u0627\u0646 \u0627\u0644\u0645\u062d\u062a\u0648\u0649 \u0644\u062e\u0627\u062f\u0645 \u0627\u0644\u0648\u064a\u0628 \u0647\u0630\u0627 \u0628\u062a\u0634\u063a\u064a\u0644 \u0645\u0643\u0648\u0646 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0631\u062c\u0648\u0639 \u0625\u0644\u0649 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-unknown =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0648\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0639\u0631\u0636 \u0645\u062d\u062a\u0648\u0649 \u0627\u0644\u0641\u0644\u0627\u0634 \u0647\u0630\u0627.\n { $outdated ->\n [true] \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0627\u0644\u0631\u062c\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u062a\u062d\u0645\u064a\u0644 \u0625\u0635\u062f\u0627\u0631 \u0623\u062d\u062f\u062b \u0645\u0646 \u0631\u0641\u0644 (\u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u062d\u0627\u0644\u064a\u0629 \u0642\u062f\u064a\u0645\u0629: { $buildDate }).\n *[false] \u0644\u064a\u0633 \u0645\u0646 \u0627\u0644\u0645\u0641\u062a\u0631\u0636 \u0623\u0646 \u064a\u062d\u062f\u062b \u0647\u0630\u0627\u060c \u0644\u0630\u0644\u0643 \u0646\u062d\u0646 \u0646\u0642\u062f\u0631 \u062d\u0642\u064b\u0627 \u0625\u0630\u0627 \u0642\u0645\u062a \u0628\u0627\u0644\u062a\u0628\u0644\u064a\u063a \u0639\u0646 \u0627\u0644\u062e\u0637\u0623!\n }\n',"save-manager.ftl":"save-delete-prompt = \u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 \u0645\u0644\u0641 \u062d\u0641\u0638 \u0627\u0644\u0644\u0639\u0628\u0629 \u0647\u0630\u0627\u061f\nsave-reload-prompt =\n \u0627\u0644\u0637\u0631\u064a\u0642\u0629 \u0627\u0644\u0648\u062d\u064a\u062f\u0629 \u0644\u0640 { $action ->\n [delete] \u062d\u0630\u0641\n *[replace] \u0627\u0633\u062a\u0628\u062f\u0627\u0644\n } \u0647\u0630\u0627 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u062d\u0641\u0638 \u062f\u0648\u0646 \u062a\u0636\u0627\u0631\u0628 \u0645\u062d\u062a\u0645\u0644 \u0647\u064a \u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 \u0647\u0630\u0627 \u0627\u0644\u0645\u062d\u062a\u0648\u0649. \u0647\u0644 \u062a\u0631\u063a\u0628 \u0641\u064a \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629 \u0639\u0644\u0649 \u0623\u064a \u062d\u0627\u0644\u061f\nsave-download = \u062a\u062d\u0645\u064a\u0644\nsave-replace = \u0627\u0633\u062a\u0628\u062f\u0627\u0644\nsave-delete = \u062d\u0630\u0641\nsave-backup-all = \u062a\u062d\u0645\u064a\u0644 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629\n"},"ca-ES":{"context_menu.ftl":"context-menu-download-swf = Baixa el fitxer .swf\ncontext-menu-copy-debug-info = Copia la informaci\xf3 de depuraci\xf3\ncontext-menu-open-save-manager = Obre el gestor d'emmagatzematge\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Quant a l'extensi\xf3 de Ruffle ({ $version })\n *[other] Quant a Ruffle ({ $version })\n }\ncontext-menu-hide = Amaga aquest men\xfa\ncontext-menu-exit-fullscreen = Surt de la pantalla completa\ncontext-menu-enter-fullscreen = Pantalla completa\n","messages.ftl":"panic-title = Alguna cosa ha fallat :(\nmore-info = M\xe9s informaci\xf3\nrun-anyway = Reprodueix igualment\ncontinue = Continua\nreport-bug = Informa d'un error\nupdate-ruffle = Actualitza Ruffle\nruffle-demo = Demostraci\xf3 web\nruffle-desktop = Aplicaci\xf3 d'escriptori\nruffle-wiki = Obre la wiki de Ruffle\nview-error-details = Mostra detalls de l'error\nopen-in-new-tab = Obre en una pestanya nova\nclick-to-unmute = Feu clic per activar el so\n","save-manager.ftl":""},"cs-CZ":{"context_menu.ftl":"context-menu-download-swf = St\xe1hnout .swf\ncontext-menu-copy-debug-info = Zkop\xedrovat debug info\ncontext-menu-open-save-manager = Otev\u0159\xedt spr\xe1vce ulo\u017een\xed\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] O Ruffle roz\u0161\xed\u0159en\xed ({ $version })\n *[other] O Ruffle ({ $version })\n }\ncontext-menu-hide = Skr\xfdt menu\ncontext-menu-exit-fullscreen = Ukon\u010dit re\u017eim cel\xe9 obrazovky\ncontext-menu-enter-fullscreen = P\u0159ej\xedt do re\u017eimu cel\xe9 obrazovky\n","messages.ftl":'message-cant-embed =\n Ruffle nemohl spustit Flash vlo\u017een\xfd na t\xe9to str\xe1nce.\n M\u016f\u017eete se pokusit otev\u0159\xedt soubor na samostatn\xe9 kart\u011b, abyste se vyhnuli tomuto probl\xe9mu.\npanic-title = N\u011bco se pokazilo :(\nmore-info = Dal\u0161\xed informace\nrun-anyway = P\u0159esto spustit\ncontinue = Pokra\u010dovat\nreport-bug = Nahl\xe1sit chybu\nupdate-ruffle = Aktualizovat Ruffle\nruffle-demo = Web Demo\nruffle-desktop = Desktopov\xe1 aplikace\nruffle-wiki = Zobrazit Ruffle Wiki\nview-error-details = Zobrazit podrobnosti o chyb\u011b\nopen-in-new-tab = Otev\u0159\xedt na nov\xe9 kart\u011b\nclick-to-unmute = Kliknut\xedm zru\u0161\xedte ztlumen\xed\nerror-file-protocol =\n Zd\xe1 se, \u017ee pou\u017e\xedv\xe1te Ruffle na protokolu "file:".\n To nen\xed mo\u017en\xe9, proto\u017ee prohl\xed\u017ee\u010de blokuj\xed fungov\xe1n\xed mnoha funkc\xed z bezpe\u010dnostn\xedch d\u016fvod\u016f.\n Nam\xedsto toho v\xe1m doporu\u010dujeme nastavit lok\xe1ln\xed server nebo pou\u017e\xedt web demo \u010di desktopovou aplikaci.\nerror-javascript-config =\n Ruffle narazil na probl\xe9m v d\u016fsledku nespr\xe1vn\xe9 konfigurace JavaScriptu.\n Pokud jste spr\xe1vcem serveru, doporu\u010dujeme v\xe1m zkontrolovat podrobnosti o chyb\u011b, abyste zjistili, kter\xfd parametr je vadn\xfd.\n Pomoc m\u016f\u017eete z\xedskat tak\xe9 na wiki Ruffle.\nerror-wasm-not-found =\n Ruffle se nepoda\u0159ilo na\u010d\xedst po\u017eadovanou komponentu souboru \u201e.wasm\u201c.\n Pokud jste spr\xe1vcem serveru, zkontrolujte, zda byl soubor spr\xe1vn\u011b nahr\xe1n.\n Pokud probl\xe9m p\u0159etrv\xe1v\xe1, mo\u017en\xe1 budete muset pou\u017e\xedt nastaven\xed \u201epublicPath\u201c: pomoc naleznete na wiki Ruffle.\nerror-wasm-mime-type =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Tento webov\xfd server neposkytuje soubory \u201e.wasm\u201c se spr\xe1vn\xfdm typem MIME.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-swf-fetch =\n Ruffle se nepoda\u0159ilo na\u010d\xedst SWF soubor Flash.\n Nejpravd\u011bpodobn\u011bj\u0161\xedm d\u016fvodem je, \u017ee soubor ji\u017e neexistuje, tak\u017ee Ruffle nem\xe1 co na\u010d\xedst.\n Zkuste po\u017e\xe1dat o pomoc spr\xe1vce webu.\nerror-swf-cors =\n Ruffle se nepoda\u0159ilo na\u010d\xedst SWF soubor Flash.\n P\u0159\xedstup k na\u010d\xedt\xe1n\xed byl pravd\u011bpodobn\u011b zablokov\xe1n politikou CORS.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-wasm-cors =\n Ruffle se nepoda\u0159ilo na\u010d\xedst po\u017eadovanou komponentu souboru \u201e.wasm\u201c.\n P\u0159\xedstup k na\u010d\xedt\xe1n\xed byl pravd\u011bpodobn\u011b zablokov\xe1n politikou CORS.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-wasm-invalid =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Zd\xe1 se, \u017ee na t\xe9to str\xe1nce chyb\xed nebo jsou neplatn\xe9 soubory ke spu\u0161t\u011bn\xed Ruffle.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-wasm-download =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Probl\xe9m se m\u016f\u017ee vy\u0159e\u0161it i s\xe1m, tak\u017ee m\u016f\u017eete zkusit str\xe1nku na\u010d\xedst znovu.\n V opa\u010dn\xe9m p\u0159\xedpad\u011b kontaktujte administr\xe1tora str\xe1nky.\nerror-wasm-disabled-on-edge =\n Ruffle se nepoda\u0159ilo na\u010d\xedst po\u017eadovanou komponentu souboru \u201e.wasm\u201c.\n Chcete-li tento probl\xe9m vy\u0159e\u0161it, zkuste otev\u0159\xedt nastaven\xed prohl\xed\u017ee\u010de, klikn\u011bte na polo\u017eku \u201eOchrana osobn\xedch \xfadaj\u016f, vyhled\xe1v\xe1n\xed a slu\u017eby\u201c, p\u0159ejd\u011bte dol\u016f a vypn\u011bte mo\u017enost \u201eZvy\u0161te svou bezpe\u010dnost na webu\u201c.\n Va\u0161emu prohl\xed\u017ee\u010di to umo\u017en\xed na\u010d\xedst po\u017eadovan\xe9 soubory \u201e.wasm\u201c.\n Pokud probl\xe9m p\u0159etrv\xe1v\xe1, budete mo\u017en\xe1 muset pou\u017e\xedt jin\xfd prohl\xed\u017ee\u010d.\nerror-javascript-conflict =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Zd\xe1 se, \u017ee tato str\xe1nka pou\u017e\xedv\xe1 k\xf3d JavaScript, kter\xfd je v konfliktu s Ruffle.\n Pokud jste spr\xe1vcem serveru, doporu\u010dujeme v\xe1m zkusit na\u010d\xedst soubor na pr\xe1zdnou str\xe1nku.\nerror-javascript-conflict-outdated = M\u016f\u017eete se tak\xe9 pokusit nahr\xe1t nov\u011bj\u0161\xed verzi Ruffle, kter\xe1 m\u016f\u017ee dan\xfd probl\xe9m vy\u0159e\u0161it (aktu\xe1ln\xed build je zastaral\xfd: { $buildDate }).\nerror-csp-conflict =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Z\xe1sady zabezpe\u010den\xed obsahu tohoto webov\xe9ho serveru nepovoluj\xed spu\u0161t\u011bn\xed po\u017eadovan\xe9 komponenty \u201e.wasm\u201c.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-unknown =\n Ruffle narazil na probl\xe9m p\u0159i pokusu zobrazit tento Flash obsah.\n { $outdated ->\n [true] Pokud jste spr\xe1vcem serveru, zkuste nahr\xe1t nov\u011bj\u0161\xed verzi Ruffle (aktu\xe1ln\xed build je zastaral\xfd: { $buildDate }).\n *[false] Toto by se nem\u011blo st\xe1t, tak\u017ee bychom opravdu ocenili, kdybyste mohli nahl\xe1sit chybu!\n }\n',"save-manager.ftl":"save-delete-prompt = Opravdu chcete odstranit tento soubor s ulo\u017een\xfdmi pozicemi?\nsave-reload-prompt =\n Jedin\xfd zp\u016fsob, jak { $action ->\n [delete] vymazat\n *[replace] nahradit\n } tento soubor s ulo\u017een\xfdmi pozicemi bez potenci\xe1ln\xedho konfliktu je op\u011btovn\xe9 na\u010dten\xed tohoto obsahu. Chcete p\u0159esto pokra\u010dovat?\nsave-download = St\xe1hnout\nsave-replace = Nahradit\nsave-delete = Vymazat\nsave-backup-all = St\xe1hnout v\u0161echny soubory s ulo\u017een\xfdmi pozicemi\n"},"de-DE":{"context_menu.ftl":"context-menu-download-swf = .swf herunterladen\ncontext-menu-copy-debug-info = Debug-Info kopieren\ncontext-menu-open-save-manager = Dateimanager \xf6ffnen\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \xdcber Ruffle Erweiterung ({ $version })\n *[other] \xdcber Ruffle ({ $version })\n }\ncontext-menu-hide = Men\xfc ausblenden\ncontext-menu-exit-fullscreen = Vollbild verlassen\ncontext-menu-enter-fullscreen = Vollbildmodus aktivieren\n","messages.ftl":'message-cant-embed =\n Ruffle konnte den Flash in dieser Seite nicht ausf\xfchren.\n Du kannst versuchen, die Datei in einem separaten Tab zu \xf6ffnen, um dieses Problem zu umgehen.\npanic-title = Etwas ist schief gelaufen\nmore-info = Weitere Informationen\nrun-anyway = Trotzdem ausf\xfchren\ncontinue = Fortfahren\nreport-bug = Fehler melden\nupdate-ruffle = Ruffle aktuallisieren\nruffle-demo = Web-Demo\nruffle-desktop = Desktop-Anwendung\nruffle-wiki = Ruffle-Wiki anzeigen\nview-error-details = Fehlerdetails anzeigen\nopen-in-new-tab = In einem neuen Tab \xf6ffnen\nclick-to-unmute = Klicke zum Entmuten\nerror-file-protocol =\n Es scheint, dass Sie Ruffle auf dem "file:"-Protokoll ausf\xfchren.\n Dies funktioniert nicht so, als Browser viele Funktionen aus Sicherheitsgr\xfcnden blockieren.\n Stattdessen laden wir Sie ein, einen lokalen Server einzurichten oder entweder die Webdemo oder die Desktop-Anwendung zu verwenden.\nerror-javascript-config =\n Ruffle ist aufgrund einer falschen JavaScript-Konfiguration auf ein gro\xdfes Problem gesto\xdfen.\n Wenn du der Server-Administrator bist, laden wir dich ein, die Fehlerdetails zu \xfcberpr\xfcfen, um herauszufinden, welcher Parameter fehlerhaft ist.\n Sie k\xf6nnen auch das Ruffle-Wiki f\xfcr Hilfe konsultieren.\nerror-wasm-not-found =\n Ruffle konnte die erforderliche ".wasm"-Datei-Komponente nicht laden.\n Wenn Sie der Server-Administrator sind, stellen Sie bitte sicher, dass die Datei korrekt hochgeladen wurde.\n Wenn das Problem weiterhin besteht, m\xfcssen Sie unter Umst\xe4nden die "publicPath"-Einstellung verwenden: Bitte konsultieren Sie das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-mime-type =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Dieser Webserver dient nicht ". asm"-Dateien mit dem korrekten MIME-Typ.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-swf-fetch =\n Ruffle konnte die Flash-SWF-Datei nicht laden.\n Der wahrscheinlichste Grund ist, dass die Datei nicht mehr existiert, so dass Ruffle nicht geladen werden kann.\n Kontaktieren Sie den Website-Administrator f\xfcr Hilfe.\nerror-swf-cors =\n Ruffle konnte die Flash-SWF-Datei nicht laden.\n Der Zugriff auf den Abruf wurde wahrscheinlich durch die CORS-Richtlinie blockiert.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-cors =\n Ruffle konnte die Flash-SWF-Datei nicht laden.\n Der Zugriff auf den Abruf wurde wahrscheinlich durch die CORS-Richtlinie blockiert.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-invalid =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Dieser Webserver dient nicht ". asm"-Dateien mit dem korrekten MIME-Typ.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-download =\n Ruffle ist auf ein gro\xdfes Problem gesto\xdfen, w\xe4hrend er versucht hat zu initialisieren.\n Dies kann sich oft selbst beheben, so dass Sie versuchen k\xf6nnen, die Seite neu zu laden.\n Andernfalls kontaktieren Sie bitte den Website-Administrator.\nerror-wasm-disabled-on-edge =\n Ruffle konnte die erforderliche ".wasm"-Datei-Komponente nicht laden.\n Um dies zu beheben, versuche die Einstellungen deines Browsers zu \xf6ffnen, klicke auf "Privatsph\xe4re, Suche und Dienste", scrollen nach unten und schalte "Verbessere deine Sicherheit im Web" aus.\n Dies erlaubt Ihrem Browser die erforderlichen ".wasm"-Dateien zu laden.\n Wenn das Problem weiterhin besteht, m\xfcssen Sie m\xf6glicherweise einen anderen Browser verwenden.\nerror-javascript-conflict =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Es scheint, als ob diese Seite JavaScript-Code verwendet, der mit Ruffle kollidiert.\n Wenn Sie der Server-Administrator sind, laden wir Sie ein, die Datei auf einer leeren Seite zu laden.\nerror-javascript-conflict-outdated = Du kannst auch versuchen, eine neuere Version von Ruffle hochzuladen, die das Problem umgehen k\xf6nnte (aktuelle Version ist veraltet: { $buildDate }).\nerror-csp-conflict =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Dieser Webserver dient nicht ". asm"-Dateien mit dem korrekten MIME-Typ.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-unknown =\n Bei dem Versuch, diesen Flash-Inhalt anzuzeigen, ist Ruffle auf ein gro\xdfes Problem gesto\xdfen.\n { $outdated ->\n [true] Wenn Sie der Server-Administrator sind, Bitte versuchen Sie, eine neuere Version von Ruffle hochzuladen (aktuelle Version ist veraltet: { $buildDate }).\n *[false] Dies soll nicht passieren, deshalb w\xfcrden wir uns sehr dar\xfcber freuen, wenn Sie einen Fehler melden k\xf6nnten!\n }\n',"save-manager.ftl":"save-delete-prompt = Sind Sie sicher, dass Sie diese Speicherdatei l\xf6schen m\xf6chten?\nsave-reload-prompt =\n Der einzige Weg zu { $action ->\n [delete] l\xf6schen\n *[replace] ersetzen\n } diese Speicherdatei ohne m\xf6glichen Konflikt ist das erneute Laden dieses Inhalts. M\xf6chten Sie trotzdem fortfahren?\nsave-download = Herunterladen\nsave-replace = Ersetzen\nsave-delete = L\xf6schen\nsave-backup-all = Alle gespeicherten Dateien herunterladen\n"},"en-US":{"context_menu.ftl":"context-menu-download-swf = Download .swf\ncontext-menu-copy-debug-info = Copy debug info\ncontext-menu-open-save-manager = Open Save Manager\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] About Ruffle Extension ({$version})\n *[other] About Ruffle ({$version})\n }\ncontext-menu-hide = Hide this menu\ncontext-menu-exit-fullscreen = Exit fullscreen\ncontext-menu-enter-fullscreen = Enter fullscreen","messages.ftl":'message-cant-embed =\n Ruffle wasn\'t able to run the Flash embedded in this page.\n You can try to open the file in a separate tab, to sidestep this issue.\npanic-title = Something went wrong :(\nmore-info = More info\nrun-anyway = Run anyway\ncontinue = Continue\nreport-bug = Report Bug\nupdate-ruffle = Update Ruffle\nruffle-demo = Web Demo\nruffle-desktop = Desktop Application\nruffle-wiki = View Ruffle Wiki\nview-error-details = View Error Details\nopen-in-new-tab = Open in a new tab\nclick-to-unmute = Click to unmute\nerror-file-protocol =\n It appears you are running Ruffle on the "file:" protocol.\n This doesn\'t work as browsers block many features from working for security reasons.\n Instead, we invite you to setup a local server or either use the web demo or the desktop application.\nerror-javascript-config =\n Ruffle has encountered a major issue due to an incorrect JavaScript configuration.\n If you are the server administrator, we invite you to check the error details to find out which parameter is at fault.\n You can also consult the Ruffle wiki for help.\nerror-wasm-not-found =\n Ruffle failed to load the required ".wasm" file component.\n If you are the server administrator, please ensure the file has correctly been uploaded.\n If the issue persists, you may need to use the "publicPath" setting: please consult the Ruffle wiki for help.\nerror-wasm-mime-type =\n Ruffle has encountered a major issue whilst trying to initialize.\n This web server is not serving ".wasm" files with the correct MIME type.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-swf-fetch =\n Ruffle failed to load the Flash SWF file.\n The most likely reason is that the file no longer exists, so there is nothing for Ruffle to load.\n Try contacting the website administrator for help.\nerror-swf-cors =\n Ruffle failed to load the Flash SWF file.\n Access to fetch has likely been blocked by CORS policy.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-wasm-cors =\n Ruffle failed to load the required ".wasm" file component.\n Access to fetch has likely been blocked by CORS policy.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-wasm-invalid =\n Ruffle has encountered a major issue whilst trying to initialize.\n It seems like this page has missing or invalid files for running Ruffle.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-wasm-download =\n Ruffle has encountered a major issue whilst trying to initialize.\n This can often resolve itself, so you can try reloading the page.\n Otherwise, please contact the website administrator.\nerror-wasm-disabled-on-edge =\n Ruffle failed to load the required ".wasm" file component.\n To fix this, try opening your browser\'s settings, clicking "Privacy, search, and services", scrolling down, and turning off "Enhance your security on the web".\n This will allow your browser to load the required ".wasm" files.\n If the issue persists, you might have to use a different browser.\nerror-javascript-conflict =\n Ruffle has encountered a major issue whilst trying to initialize.\n It seems like this page uses JavaScript code that conflicts with Ruffle.\n If you are the server administrator, we invite you to try loading the file on a blank page.\nerror-javascript-conflict-outdated = You can also try to upload a more recent version of Ruffle that may circumvent the issue (current build is outdated: {$buildDate}).\nerror-csp-conflict =\n Ruffle has encountered a major issue whilst trying to initialize.\n This web server\'s Content Security Policy does not allow the required ".wasm" component to run.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-unknown =\n Ruffle has encountered a major issue whilst trying to display this Flash content.\n {$outdated ->\n [true] If you are the server administrator, please try to upload a more recent version of Ruffle (current build is outdated: {$buildDate}).\n *[false] This isn\'t supposed to happen, so we\'d really appreciate if you could file a bug!\n }',"save-manager.ftl":"save-delete-prompt = Are you sure you want to delete this save file?\nsave-reload-prompt =\n The only way to {$action ->\n [delete] delete\n *[replace] replace\n } this save file without potential conflict is to reload this content. Do you wish to continue anyway?\nsave-download = Download\nsave-replace = Replace\nsave-delete = Delete\nsave-backup-all = Download all save files"},"es-ES":{"context_menu.ftl":"context-menu-download-swf = Descargar .swf\ncontext-menu-copy-debug-info = Copiar Informaci\xf3n de depuraci\xf3n\ncontext-menu-open-save-manager = Abrir gestor de guardado\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Sobre la extensi\xf3n de Ruffle ({ $version })\n *[other] Sobre Ruffle ({ $version })\n }\ncontext-menu-hide = Ocultar este men\xfa\ncontext-menu-exit-fullscreen = Salir de pantalla completa\ncontext-menu-enter-fullscreen = Entrar a pantalla completa\n","messages.ftl":'message-cant-embed =\n Ruffle no pudo ejecutar el Flash incrustado en esta p\xe1gina.\n Puedes intentar abrir el archivo en una pesta\xf1a aparte, para evitar este problema.\npanic-title = Algo sali\xf3 mal :(\nmore-info = M\xe1s info\nrun-anyway = Ejecutar de todos modos\ncontinue = Continuar\nreport-bug = Reportar un Error\nupdate-ruffle = Actualizar Ruffle\nruffle-demo = Demostraci\xf3n de web\nruffle-desktop = Aplicaci\xf3n de Desktop\nruffle-wiki = Ver la p\xe1gina wiki\nview-error-details = Ver los detalles del error\nopen-in-new-tab = Abrir en una pesta\xf1a nueva\nclick-to-unmute = Haz clic para dejar de silenciar\nerror-file-protocol =\n Parece que est\xe1 ejecutando Ruffle en el protocolo "archivo:".\n Esto no funciona porque los navegadores bloquean que muchas caracter\xedsticas funcionen por razones de seguridad.\n En su lugar, le invitamos a configurar un servidor local o bien usar la demostraci\xf3n web o la aplicaci\xf3n de desktop.\nerror-javascript-config =\n Ruffle ha encontrado un problema cr\xedtico debido a una configuraci\xf3n JavaScript incorrecta.\n Si usted es el administrador del servidor, le invitamos a comprobar los detalles del error para averiguar qu\xe9 par\xe1metro est\xe1 en falta.\n Tambi\xe9n puedes consultar la wiki de Ruffle para obtener ayuda.\nerror-wasm-not-found =\n Ruffle no pudo cargar el componente de archivo ".wasm" requerido.\n Si usted es el administrador del servidor, aseg\xfarese de que el archivo ha sido subido correctamente.\n Si el problema persiste, puede que necesite usar la configuraci\xf3n "publicPath": por favor consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-mime-type =\n Ruffle ha encontrado un problema cr\xedtico al intentar inicializar.\n Este servidor web no est\xe1 sirviendo archivos wasm" con el tipo MIME correcto.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-swf-fetch =\n Ruffle no pudo cargar el archivo Flash SWF.\n La raz\xf3n m\xe1s probable es que el archivo ya no existe, as\xed que no hay nada para cargar Ruffle.\n Intente ponerse en contacto con el administrador del sitio web para obtener ayuda.\nerror-swf-cors =\n Ruffle no pudo cargar el archivo Flash SWF.\n Es probable que el acceso a la b\xfasqueda haya sido bloqueado por la pol\xedtica CORS.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-cors =\n Ruffle no pudo cargar el archivo ".wasm."\n Es probable que el acceso a la b\xfasqueda o la llamada a la funci\xf3n fetch haya sido bloqueado por la pol\xedtica CORS.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-invalid =\n Ruffle ha encontrado un problema cr\xedtico al intentar inicializar.\n Este servidor web no est\xe1 sirviendo archivos wasm" con el tipo Mime correcto.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-download =\n Ruffle ha encontrado un problema cr\xedtico mientras intentaba inicializarse.\n Esto a menudo puede resolverse por s\xed mismo, as\xed que puede intentar recargar la p\xe1gina.\n De lo contrario, p\xf3ngase en contacto con el administrador del sitio web.\nerror-wasm-disabled-on-edge =\n Ruffle no pudo cargar el componente de archivo ".wasm" requerido.\n Para solucionar esto, intenta abrir la configuraci\xf3n de tu navegador, haciendo clic en "Privacidad, b\xfasqueda y servicios", desplaz\xe1ndote y apagando "Mejore su seguridad en la web".\n Esto permitir\xe1 a su navegador cargar los archivos ".wasm" necesarios.\n Si el problema persiste, puede que tenga que utilizar un navegador diferente.\nerror-javascript-conflict =\n Ruffle ha encontrado un problema cr\xedtico mientras intentaba inicializarse.\n Parece que esta p\xe1gina utiliza c\xf3digo JavaScript que entra en conflicto con Ruffle.\n Si usted es el administrador del servidor, le invitamos a intentar cargar el archivo en una p\xe1gina en blanco.\nerror-javascript-conflict-outdated = Tambi\xe9n puedes intentar subir una versi\xf3n m\xe1s reciente de Ruffle que puede eludir el problema (la versi\xf3n actual est\xe1 desactualizada: { $buildDate }).\nerror-csp-conflict =\n Ruffle encontr\xf3 un problema al intentar inicializarse.\n La Pol\xedtica de Seguridad de Contenido de este servidor web no permite el componente requerido ".wasm". \n Si usted es el administrador del servidor, por favor consulta la wiki de Ruffle para obtener ayuda.\nerror-unknown =\n Ruffle ha encontrado un problema al tratar de mostrar el contenido Flash.\n { $outdated ->\n [true] Si usted es el administrador del servidor, intenta cargar una version m\xe1s reciente de Ruffle (la version actual esta desactualizada: { $buildDate }).\n *[false] Esto no deberia suceder! apreciariamos que reportes el error!\n }\n',"save-manager.ftl":"save-delete-prompt = \xbfEst\xe1 seguro de querer eliminar este archivo de guardado?\nsave-reload-prompt =\n La \xfanica forma de { $action ->\n [delete] eliminar\n *[replace] sobreescribir\n } este archivo de guardado sin conflictos potenciales es reiniciando el contenido. \xbfDesea continuar de todos modos?\nsave-download = Descargar\nsave-replace = Sobreescribir\nsave-delete = Borrar\nsave-backup-all = Borrar todos los archivos de guardado\n"},"fr-FR":{"context_menu.ftl":"context-menu-download-swf = T\xe9l\xe9charger en tant que .swf\ncontext-menu-copy-debug-info = Copier les infos de d\xe9bogage\ncontext-menu-open-save-manager = Ouvrir le gestionnaire de stockage\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \xc0 propos de Ruffle Extension ({ $version })\n *[other] \xc0 propos de Ruffle ({ $version })\n }\ncontext-menu-hide = Masquer ce menu\ncontext-menu-exit-fullscreen = Sortir du mode plein \xe9cran\ncontext-menu-enter-fullscreen = Afficher en plein \xe9cran\n","messages.ftl":"message-cant-embed =\n Ruffle n'a pas \xe9t\xe9 en mesure de lire le fichier Flash int\xe9gr\xe9 dans cette page.\n Vous pouvez essayer d'ouvrir le fichier dans un onglet isol\xe9, pour contourner le probl\xe8me.\npanic-title = Une erreur est survenue :(\nmore-info = Plus d'infos\nrun-anyway = Ex\xe9cuter quand m\xeame\ncontinue = Continuer\nreport-bug = Signaler le bug\nupdate-ruffle = Mettre \xe0 jour Ruffle\nruffle-demo = D\xe9mo en ligne\nruffle-desktop = Application de bureau\nruffle-wiki = Wiki de Ruffle\nview-error-details = D\xe9tails de l'erreur\nopen-in-new-tab = Ouvrir dans un nouvel onglet\nclick-to-unmute = Cliquez pour activer le son\nerror-file-protocol =\n Il semblerait que vous ex\xe9cutiez Ruffle sur le protocole \"file:\".\n Cela ne fonctionne pas car les navigateurs bloquent de nombreuses fonctionnalit\xe9s pour des raisons de s\xe9curit\xe9.\n Nous vous invitons soit \xe0 configurer un serveur local, soit \xe0 utiliser la d\xe9mo en ligne ou l'application de bureau.\nerror-javascript-config =\n Ruffle a rencontr\xe9 un probl\xe8me majeur en raison d'une configuration JavaScript incorrecte.\n Si vous \xeates l'administrateur du serveur, nous vous invitons \xe0 v\xe9rifier les d\xe9tails de l'erreur pour savoir quel est le param\xe8tre en cause.\n Vous pouvez \xe9galement consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-not-found =\n Ruffle n'a pas r\xe9ussi \xe0 charger son fichier \".wasm\".\n Si vous \xeates l'administrateur du serveur, veuillez vous assurer que ce fichier a bien \xe9t\xe9 mis en ligne.\n Si le probl\xe8me persiste, il vous faudra peut-\xeatre utiliser le param\xe8tre \"publicPath\" : veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-mime-type =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Ce serveur web ne renvoie pas le bon type MIME pour les fichiers \".wasm\".\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-swf-fetch =\n Ruffle n'a pas r\xe9ussi \xe0 charger le fichier Flash.\n La raison la plus probable est que le fichier n'existe pas ou plus.\n Vous pouvez essayer de prendre contact avec l'administrateur du site pour obtenir plus d'informations.\nerror-swf-cors =\n Ruffle n'a pas r\xe9ussi \xe0 charger le fichier Flash.\n La requ\xeate a probablement \xe9t\xe9 rejet\xe9e en raison de la configuration du CORS.\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-cors =\n Ruffle n'a pas r\xe9ussi \xe0 charger son fichier \".wasm\".\n La requ\xeate a probablement \xe9t\xe9 rejet\xe9e en raison de la configuration du CORS.\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-invalid =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Il semblerait que cette page comporte des fichiers manquants ou invalides pour ex\xe9cuter Ruffle.\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-download =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Le probl\xe8me d\xe9tect\xe9 peut souvent se r\xe9soudre de lui-m\xeame, donc vous pouvez essayer de recharger la page.\n Si le probl\xe8me persiste, veuillez prendre contact avec l'administrateur du site.\nerror-wasm-disabled-on-edge =\n Ruffle n'a pas r\xe9ussi \xe0 charger son fichier \".wasm\".\n Pour r\xe9soudre ce probl\xe8me, essayez d'ouvrir les param\xe8tres de votre navigateur et de cliquer sur \"Confidentialit\xe9, recherche et services\". Puis, vers le bas de la page, d\xe9sactivez l'option \"Am\xe9liorez votre s\xe9curit\xe9 sur le web\".\n Cela permettra \xe0 votre navigateur de charger les fichiers \".wasm\".\n Si le probl\xe8me persiste, vous devrez peut-\xeatre utiliser un autre navigateur.\nerror-javascript-conflict =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Il semblerait que cette page contienne du code JavaScript qui entre en conflit avec Ruffle.\n Si vous \xeates l'administrateur du serveur, nous vous invitons \xe0 essayer de charger le fichier dans une page vide.\nerror-javascript-conflict-outdated = Vous pouvez \xe9galement essayer de mettre en ligne une version plus r\xe9cente de Ruffle qui pourrait avoir corrig\xe9 le probl\xe8me (la version que vous utilisez est obsol\xe8te : { $buildDate }).\nerror-csp-conflict =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n La strat\xe9gie de s\xe9curit\xe9 du contenu (CSP) de ce serveur web n'autorise pas l'ex\xe9cution de fichiers \".wasm\".\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-unknown =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant l'ex\xe9cution de ce contenu Flash.\n { $outdated ->\n [true] Si vous \xeates l'administrateur du serveur, veuillez essayer de mettre en ligne une version plus r\xe9cente de Ruffle (la version que vous utilisez est obsol\xe8te : { $buildDate }).\n *[false] Cela n'est pas cens\xe9 se produire, donc nous vous serions reconnaissants si vous pouviez nous signaler ce bug !\n }\n","save-manager.ftl":"save-delete-prompt = Voulez-vous vraiment supprimer ce fichier de sauvegarde ?\nsave-reload-prompt =\n La seule fa\xe7on de { $action ->\n [delete] supprimer\n *[replace] remplacer\n } ce fichier de sauvegarde sans conflit potentiel est de recharger ce contenu. Souhaitez-vous quand m\xeame continuer ?\nsave-download = T\xe9l\xe9charger\nsave-replace = Remplacer\nsave-delete = Supprimer\nsave-backup-all = T\xe9l\xe9charger tous les fichiers de sauvegarde\n"},"he-IL":{"context_menu.ftl":"context-menu-download-swf = \u05d4\u05d5\u05e8\u05d3\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4swf.\ncontext-menu-copy-debug-info = \u05d4\u05e2\u05ea\u05e7\u05ea \u05e0\u05ea\u05d5\u05e0\u05d9 \u05e0\u05d9\u05e4\u05d5\u05d9 \u05e9\u05d2\u05d9\u05d0\u05d5\u05ea\ncontext-menu-open-save-manager = \u05e4\u05ea\u05d7 \u05d0\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05e9\u05de\u05d9\u05e8\u05d5\u05ea\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u05d0\u05d5\u05d3\u05d5\u05ea \u05d4\u05ea\u05d5\u05e1\u05e3 Ruffle ({ $version })\n *[other] \u05d0\u05d5\u05d3\u05d5\u05ea Ruffle ({ $version })\n }\ncontext-menu-hide = \u05d4\u05e1\u05ea\u05e8 \u05ea\u05e4\u05e8\u05d9\u05d8 \u05d6\u05d4\ncontext-menu-exit-fullscreen = \u05d9\u05e6\u05d9\u05d0\u05d4 \u05de\u05de\u05e1\u05da \u05de\u05dc\u05d0\ncontext-menu-enter-fullscreen = \u05de\u05e1\u05da \u05de\u05dc\u05d0\n","messages.ftl":'message-cant-embed =\n Ruffle \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d4\u05e8\u05d9\u05e5 \u05d0\u05ea \u05ea\u05d5\u05db\u05df \u05d4\u05e4\u05dc\u05d0\u05e9 \u05d4\u05de\u05d5\u05d8\u05de\u05e2 \u05d1\u05d3\u05e3 \u05d6\u05d4.\n \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea \u05d4\u05e7\u05d5\u05d1\u05e5 \u05d1\u05dc\u05e9\u05d5\u05e0\u05d9\u05ea \u05e0\u05e4\u05e8\u05d3\u05ea, \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5.\npanic-title = \u05de\u05e9\u05d4\u05d5 \u05d4\u05e9\u05ea\u05d1\u05e9 :(\nmore-info = \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3\nrun-anyway = \u05d4\u05e4\u05e2\u05dc \u05d1\u05db\u05dc \u05d6\u05d0\u05ea\ncontinue = \u05d4\u05de\u05e9\u05da\nreport-bug = \u05d3\u05d5\u05d5\u05d7 \u05e2\u05dc \u05ea\u05e7\u05dc\u05d4\nupdate-ruffle = \u05e2\u05d3\u05db\u05df \u05d0\u05ea Ruffle\nruffle-demo = \u05d4\u05d3\u05d2\u05de\u05d4\nruffle-desktop = \u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d9\u05ea \u05e9\u05d5\u05dc\u05d7\u05df \u05e2\u05d1\u05d5\u05d3\u05d4\nruffle-wiki = \u05e8\u05d0\u05d4 \u05d0\u05ea Ruffle wiki\nview-error-details = \u05e8\u05d0\u05d4 \u05e4\u05e8\u05d8\u05d9 \u05e9\u05d2\u05d9\u05d0\u05d4\nopen-in-new-tab = \u05e4\u05ea\u05d7 \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d7\u05d3\u05e9\u05d4\nclick-to-unmute = \u05dc\u05d7\u05e5 \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05d1\u05d8\u05dc \u05d4\u05e9\u05ea\u05e7\u05d4\nerror-file-protocol =\n \u05e0\u05d3\u05de\u05d4 \u05e9\u05d0\u05ea\u05d4 \u05de\u05e8\u05d9\u05e5 \u05d0\u05ea Ruffle \u05ea\u05d7\u05ea \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc "file:".\n \u05d6\u05d4 \u05dc\u05d0 \u05d9\u05e2\u05d1\u05d5\u05d3 \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d3\u05e4\u05d3\u05e4\u05e0\u05d9\u05dd \u05d7\u05d5\u05e1\u05de\u05d9\u05dd \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e8\u05d1\u05d5\u05ea \u05de\u05dc\u05e2\u05d1\u05d5\u05d3 \u05e2\u05e7\u05d1 \u05e1\u05d9\u05d1\u05d5\u05ea \u05d0\u05d1\u05d8\u05d7\u05d4.\n \u05d1\u05de\u05e7\u05d5\u05dd \u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d0\u05d5\u05ea\u05da \u05dc\u05d0\u05d7\u05e1\u05df \u05d0\u05ea\u05e8 \u05d6\u05d4 \u05ea\u05d7\u05ea \u05e9\u05e8\u05ea \u05de\u05e7\u05d5\u05de\u05d9 \u05d0\u05d5 \u05d4\u05d3\u05d2\u05de\u05d4 \u05d1\u05e8\u05e9\u05ea \u05d0\u05d5 \u05d3\u05e8\u05da \u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d9\u05ea \u05e9\u05d5\u05dc\u05d7\u05df \u05d4\u05e2\u05d1\u05d5\u05d3\u05d4.\nerror-javascript-config =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05ea\u05e7\u05dc\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05e2\u05e7\u05d1 \u05d4\u05d2\u05d3\u05e8\u05ea JavaScript \u05e9\u05d2\u05d5\u05d9\u05d4.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d5 \u05de\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d0\u05d5\u05ea\u05da \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d9\u05d6\u05d4 \u05e4\u05e8\u05de\u05d8\u05e8 \u05d4\u05d5\u05d0 \u05e9\u05d2\u05d5\u05d9.\n \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e2\u05d9\u05d9\u05df \u05d5\u05dc\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-not-found =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4"wasm." \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05d5\u05d5\u05d3\u05d0 \u05db\u05d9 \u05d4\u05e7\u05d5\u05d1\u05e5 \u05d4\u05d5\u05e2\u05dc\u05d4 \u05db\u05e9\u05d5\u05e8\u05d4.\n \u05d0\u05dd \u05d4\u05d1\u05e2\u05d9\u05d4 \u05de\u05de\u05e9\u05d9\u05db\u05d4, \u05d9\u05d9\u05ea\u05db\u05df \u05d5\u05ea\u05e6\u05d8\u05e8\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05d2\u05d3\u05e8\u05ea "publicPath": \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-mime-type =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05e9\u05e8\u05ea\u05d5 \u05e9\u05dc \u05d0\u05ea\u05e8 \u05d6\u05d4 \u05dc\u05d0 \u05de\u05e9\u05d9\u05d9\u05da \u05e7\u05d1\u05e6\u05d9 ".wasm" \u05e2\u05dd \u05e1\u05d5\u05d2 \u05d4MIME \u05d4\u05e0\u05db\u05d5\u05df.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-swf-fetch =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4\u05e4\u05dc\u05d0\u05e9/swf. .\n \u05d6\u05d4 \u05e0\u05d5\u05d1\u05e2 \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05d5\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e7\u05d9\u05d9\u05dd \u05d9\u05d5\u05ea\u05e8, \u05d0\u05d6 \u05d0\u05d9\u05df \u05dcRuffle \u05de\u05d4 \u05dc\u05d8\u05e2\u05d5\u05df.\n \u05e0\u05e1\u05d4 \u05dc\u05d9\u05e6\u05d5\u05e8 \u05e7\u05e9\u05e8 \u05e2\u05dd \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8 \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-swf-cors =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4\u05e4\u05dc\u05d0\u05e9/swf. .\n \u05d2\u05d9\u05e9\u05d4 \u05dcfetch \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05e0\u05d7\u05e1\u05de\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea CORS.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-cors =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4".wasm" \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05d2\u05d9\u05e9\u05d4 \u05dcfetch \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05e0\u05d7\u05e1\u05de\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea CORS.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-invalid =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05e0\u05d3\u05de\u05d4 \u05db\u05d9 \u05d1\u05d3\u05e3 \u05d6\u05d4 \u05d7\u05e1\u05e8\u05d9\u05dd \u05d0\u05d5 \u05dc\u05d0 \u05e2\u05d5\u05d1\u05d3\u05d9\u05dd \u05db\u05e8\u05d0\u05d5\u05d9 \u05e7\u05d1\u05e6\u05d9\u05dd \u05d0\u05e9\u05e8 \u05de\u05e9\u05de\u05e9\u05d9\u05dd \u05d0\u05ea Ruffle \u05db\u05d3\u05d9 \u05dc\u05e4\u05e2\u05d5\u05dc\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-download =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05dc\u05e2\u05d9\u05ea\u05d9\u05dd \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5 \u05d9\u05db\u05d5\u05dc\u05d4 \u05dc\u05e4\u05ea\u05d5\u05e8 \u05d0\u05ea \u05e2\u05e6\u05de\u05d4, \u05d0\u05d6 \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e0\u05e1\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d3\u05e3 \u05d6\u05d4.\n \u05d0\u05dd \u05dc\u05d0, \u05d0\u05e0\u05d0 \u05e4\u05e0\u05d4 \u05dc\u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8.\nerror-wasm-disabled-on-edge =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4".wasm" \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05ea\u05e7\u05df \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5, \u05e0\u05e1\u05d4 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d3\u05e4\u05d3\u05e4\u05df \u05e9\u05dc\u05da, \u05dc\u05d7\u05e5 \u05e2\u05dc "\u05d0\u05d1\u05d8\u05d7\u05d4, \u05d7\u05d9\u05e4\u05d5\u05e9 \u05d5\u05e9\u05d9\u05e8\u05d5\u05ea",\n \u05d2\u05dc\u05d5\u05dc \u05de\u05d8\u05d4, \u05d5\u05db\u05d1\u05d4 \u05d0\u05ea "\u05d4\u05d2\u05d1\u05e8 \u05d0\u05ea \u05d4\u05d0\u05d1\u05d8\u05d7\u05d4 \u05e9\u05dc\u05da \u05d1\u05e8\u05e9\u05ea".\n \u05d6\u05d4 \u05d9\u05d0\u05e4\u05e9\u05e8 \u05dc\u05d3\u05e4\u05d3\u05e4\u05df \u05e9\u05dc\u05da \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4".wasm" \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05d0\u05dd \u05d4\u05d1\u05e2\u05d9\u05d4 \u05de\u05de\u05e9\u05d9\u05db\u05d4, \u05d9\u05d9\u05ea\u05db\u05df \u05d5\u05e2\u05dc\u05d9\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d3\u05e4\u05d3\u05e4\u05df \u05d0\u05d7\u05e8.\nerror-javascript-conflict =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05e0\u05d3\u05de\u05d4 \u05db\u05d9 \u05d3\u05e3 \u05d6\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d5\u05d3 JavaScript \u05d0\u05e9\u05e8 \u05de\u05ea\u05e0\u05d2\u05e9 \u05e2\u05dd Ruffle.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d5 \u05de\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d0\u05d5\u05ea\u05da \u05dc\u05e0\u05e1\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05d4\u05d3\u05e3 \u05ea\u05d7\u05ea \u05e2\u05de\u05d5\u05d3 \u05e8\u05d9\u05e7.\nerror-javascript-conflict-outdated = \u05d1\u05e0\u05d5\u05e1\u05e3, \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e0\u05e1\u05d5\u05ea \u05d5\u05dc\u05d4\u05e2\u05dc\u05d5\u05ea \u05d2\u05e8\u05e1\u05d0\u05d5\u05ea \u05e2\u05d3\u05db\u05e0\u05d9\u05d5\u05ea \u05e9\u05dc Ruffle \u05d0\u05e9\u05e8 \u05e2\u05dc\u05d5\u05dc\u05d9\u05dd \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5 (\u05d2\u05e8\u05e1\u05d4 \u05d6\u05d5 \u05d4\u05d9\u05e0\u05d4 \u05de\u05d9\u05d5\u05e9\u05e0\u05ea : { $buildDate }).\nerror-csp-conflict =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea \u05d0\u05d1\u05d8\u05d7\u05ea \u05d4\u05ea\u05d5\u05db\u05df \u05e9\u05dc \u05e9\u05e8\u05ea\u05d5 \u05e9\u05dc \u05d0\u05ea\u05e8 \u05d6\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d0\u05e4\u05e9\u05e8\u05ea \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4"wasm." \u05d4\u05d3\u05e8\u05d5\u05e9 \u05dc\u05e4\u05e2\u05d5\u05dc.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-unknown =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05d1\u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05e6\u05d9\u05d2 \u05d0\u05ea \u05ea\u05d5\u05db\u05df \u05e4\u05dc\u05d0\u05e9 \u05d6\u05d4.\n { $outdated ->\n [true] \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e2\u05dc\u05d5\u05ea \u05d2\u05e8\u05e1\u05d4 \u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d9\u05d5\u05ea\u05e8 \u05e9\u05dc Ruffle (\u05d2\u05e8\u05e1\u05d4 \u05d6\u05d5 \u05d4\u05d9\u05e0\u05d4 \u05de\u05d9\u05d5\u05e9\u05e0\u05ea: { $buildDate }).\n *[false] \u05d6\u05d4 \u05dc\u05d0 \u05d0\u05de\u05d5\u05e8 \u05dc\u05e7\u05e8\u05d5\u05ea, \u05e0\u05e9\u05de\u05d7 \u05d0\u05dd \u05ea\u05d5\u05db\u05dc \u05dc\u05e9\u05ea\u05e3 \u05ea\u05e7\u05dc\u05d4 \u05d6\u05d5!\n }\n',"save-manager.ftl":"save-delete-prompt = \u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05d1\u05d8\u05d5\u05d7 \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05de\u05d7\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05e9\u05de\u05d9\u05e8\u05d4 \u05d6\u05d4?\nsave-reload-prompt =\n \u05d4\u05d3\u05e8\u05da \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4 { $action ->\n [delete] \u05dc\u05de\u05d7\u05d5\u05e7\n *[replace] \u05dc\u05d4\u05d7\u05dc\u05d9\u05e3\n } \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4\u05e9\u05de\u05d9\u05e8\u05d4 \u05d4\u05d6\u05d4 \u05de\u05d1\u05dc\u05d9 \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d5 \u05dc\u05d4\u05ea\u05e0\u05d2\u05e9 \u05d4\u05d9\u05d0 \u05dc\u05d8\u05e2\u05d5\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05ea\u05d5\u05db\u05df \u05d6\u05d4. \u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05d1\u05db\u05dc \u05d6\u05d0\u05ea?\nsave-download = \u05d4\u05d5\u05e8\u05d3\u05d4\nsave-replace = \u05d4\u05d7\u05dc\u05e4\u05d4\nsave-delete = \u05de\u05d7\u05d9\u05e7\u05d4\nsave-backup-all = \u05d4\u05d5\u05e8\u05d3\u05ea \u05db\u05dc \u05e7\u05d1\u05e6\u05d9 \u05d4\u05e9\u05de\u05d9\u05e8\u05d4\n"},"hu-HU":{"context_menu.ftl":"context-menu-download-swf = .swf f\xe1jl let\xf6lt\xe9se\ncontext-menu-copy-debug-info = Hibakeres\xe9si inform\xe1ci\xf3k m\xe1sol\xe1sa\ncontext-menu-open-save-manager = Ment\xe9skezel\u0151 megnyit\xe1sa\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] A Ruffle kieg\xe9sz\xedt\u0151 ({ $version }) n\xe9vjegye\n *[other] A Ruffle ({ $version }) n\xe9vjegye\n }\ncontext-menu-hide = Ezen men\xfc elrejt\xe9se\ncontext-menu-exit-fullscreen = Kil\xe9p\xe9s a teljes k\xe9perny\u0151b\u0151l\ncontext-menu-enter-fullscreen = V\xe1lt\xe1s teljes k\xe9perny\u0151re\n","messages.ftl":'message-cant-embed =\n A Ruffle nem tudta futtatni az oldalba \xe1gyazott Flash tartalmat.\n A probl\xe9ma kiker\xfcl\xe9s\xe9hez megpr\xf3b\xe1lhatod megnyitni a f\xe1jlt egy k\xfcl\xf6n lapon.\npanic-title = Valami baj t\xf6rt\xe9nt :(\nmore-info = Tov\xe1bbi inform\xe1ci\xf3\nrun-anyway = Futtat\xe1s m\xe9gis\ncontinue = Folytat\xe1s\nreport-bug = Hiba jelent\xe9se\nupdate-ruffle = Ruffle friss\xedt\xe9se\nruffle-demo = Webes dem\xf3\nruffle-desktop = Asztali alkalmaz\xe1s\nruffle-wiki = Ruffle Wiki megnyit\xe1sa\nview-error-details = Hiba r\xe9szletei\nopen-in-new-tab = Megnyit\xe1s \xfaj lapon\nclick-to-unmute = Kattints a n\xe9m\xedt\xe1s felold\xe1s\xe1hoz\nerror-file-protocol =\n \xdagy t\u0171nik, a Ruffle-t a "file:" protokollon futtatod.\n Ez nem m\u0171k\xf6dik, mivel \xedgy a b\xf6ng\xe9sz\u0151k biztons\xe1gi okokb\xf3l sz\xe1mos funkci\xf3 m\u0171k\xf6d\xe9s\xe9t letiltj\xe1k.\n Ehelyett azt aj\xe1nljuk hogy ind\xedts egy helyi kiszolg\xe1l\xf3t, vagy haszn\xe1ld a webes dem\xf3t vagy az asztali alkalmaz\xe1st.\nerror-javascript-config =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt egy helytelen JavaScript-konfigur\xe1ci\xf3 miatt.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, ellen\u0151rizd a hiba r\xe9szleteit, hogy megtudd, melyik param\xe9ter a hib\xe1s.\n A Ruffle wikiben is tal\xe1lhatsz ehhez seg\xedts\xe9get.\nerror-wasm-not-found =\n A Ruffle nem tudta bet\xf6lteni a sz\xfcks\xe9ges ".wasm" \xf6sszetev\u0151t.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck ellen\u0151rizd, hogy a f\xe1jl megfelel\u0151en lett-e felt\xf6ltve.\n Ha a probl\xe9ma tov\xe1bbra is fenn\xe1ll, el\u0151fordulhat, hogy a "publicPath" be\xe1ll\xedt\xe1st kell haszn\xe1lnod: seg\xedts\xe9g\xe9rt keresd fel a Ruffle wikit.\nerror-wasm-mime-type =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n Ez a webszerver a ".wasm" f\xe1jlokat nem a megfelel\u0151 MIME-t\xedpussal szolg\xe1lja ki.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-swf-fetch =\n A Ruffle nem tudta bet\xf6lteni a Flash SWF f\xe1jlt.\n A legval\xf3sz\xedn\u0171bb ok az, hogy a f\xe1jl m\xe1r nem l\xe9tezik, \xedgy a Ruffle sz\xe1m\xe1ra nincs mit bet\xf6lteni.\n Pr\xf3b\xe1ld meg felvenni a kapcsolatot a webhely rendszergazd\xe1j\xe1val seg\xedts\xe9g\xe9rt.\nerror-swf-cors =\n A Ruffle nem tudta bet\xf6lteni a Flash SWF f\xe1jlt.\n A lek\xe9r\xe9shez val\xf3 hozz\xe1f\xe9r\xe9st val\xf3sz\xedn\u0171leg letiltotta a CORS-h\xe1zirend.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-wasm-cors =\n A Ruffle nem tudta bet\xf6lteni a sz\xfcks\xe9ges ".wasm" \xf6sszetev\u0151t.\n A lek\xe9r\xe9shez val\xf3 hozz\xe1f\xe9r\xe9st val\xf3sz\xedn\u0171leg letiltotta a CORS-h\xe1zirend.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-wasm-invalid =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n \xdagy t\u0171nik, hogy ezen az oldalon hi\xe1nyoznak vagy hib\xe1sak a Ruffle futtat\xe1s\xe1hoz sz\xfcks\xe9ges f\xe1jlok.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-wasm-download =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n Ez gyakran mag\xe1t\xf3l megold\xf3dik, ez\xe9rt megpr\xf3b\xe1lhatod \xfajrat\xf6lteni az oldalt.\n Ellenkez\u0151 esetben fordulj a webhely rendszergazd\xe1j\xe1hoz.\nerror-wasm-disabled-on-edge =\n A Ruffle nem tudta bet\xf6lteni a sz\xfcks\xe9ges ".wasm" \xf6sszetev\u0151t.\n A probl\xe9ma megold\xe1s\xe1hoz nyisd meg a b\xf6ng\xe9sz\u0151 be\xe1ll\xedt\xe1sait, kattints az \u201eAdatv\xe9delem, keres\xe9s \xe9s szolg\xe1ltat\xe1sok\u201d elemre, g\xf6rgess le, \xe9s kapcsold ki a \u201eFokozott biztons\xe1g a weben\u201d opci\xf3t.\n Ez lehet\u0151v\xe9 teszi a b\xf6ng\xe9sz\u0151 sz\xe1m\xe1ra, hogy bet\xf6ltse a sz\xfcks\xe9ges ".wasm" f\xe1jlokat.\n Ha a probl\xe9ma tov\xe1bbra is fenn\xe1ll, lehet, hogy m\xe1sik b\xf6ng\xe9sz\u0151t kell haszn\xe1lnod.\nerror-javascript-conflict =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n \xdagy t\u0171nik, ez az oldal olyan JavaScript-k\xf3dot haszn\xe1l, amely \xfctk\xf6zik a Ruffle-lel.\n Ha a kiszolg\xe1l\xf3 rendszergazd\xe1ja vagy, k\xe9rj\xfck, pr\xf3b\xe1ld meg a f\xe1jlt egy \xfcres oldalon bet\xf6lteni.\nerror-javascript-conflict-outdated = Megpr\xf3b\xe1lhatod tov\xe1bb\xe1 felt\xf6lteni a Ruffle egy \xfajabb verzi\xf3j\xe1t is, amely megker\xfclheti a probl\xe9m\xe1t (a jelenlegi elavult: { $buildDate }).\nerror-csp-conflict =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n A kiszolg\xe1l\xf3 tartalombiztons\xe1gi h\xe1zirendje nem teszi lehet\u0151v\xe9 a sz\xfcks\xe9ges \u201e.wasm\u201d \xf6sszetev\u0151k futtat\xe1s\xe1t.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-unknown =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt, mik\xf6zben megpr\xf3b\xe1lta megjelen\xedteni ezt a Flash-tartalmat.\n { $outdated ->\n [true] Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, pr\xf3b\xe1ld meg felt\xf6lteni a Ruffle egy \xfajabb verzi\xf3j\xe1t (a jelenlegi elavult: { $buildDate }).\n *[false] Ennek nem lett volna szabad megt\xf6rt\xe9nnie, ez\xe9rt nagyon h\xe1l\xe1sak lenn\xe9nk, ha jelezn\xe9d a hib\xe1t!\n }\n',"save-manager.ftl":"save-delete-prompt = Biztosan t\xf6r\xf6lni akarod ezt a ment\xe9st?\nsave-reload-prompt =\n Ennek a ment\xe9snek az esetleges konfliktus n\xe9lk\xfcli { $action ->\n [delete] t\xf6rl\xe9s\xe9hez\n *[replace] cser\xe9j\xe9hez\n } \xfajra kell t\xf6lteni a tartalmat. M\xe9gis szeretn\xe9d folytatni?\nsave-download = Let\xf6lt\xe9s\nsave-replace = Csere\nsave-delete = T\xf6rl\xe9s\nsave-backup-all = Az \xf6sszes f\xe1jl let\xf6lt\xe9se\n"},"id-ID":{"context_menu.ftl":"context-menu-download-swf = Unduh .swf\ncontext-menu-copy-debug-info = Salin info debug\ncontext-menu-open-save-manager = Buka Manager Save\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Tentang Ekstensi Ruffle ({ $version })\n *[other] Tentang Ruffle ({ $version })\n }\ncontext-menu-hide = Sembunyikan Menu ini\ncontext-menu-exit-fullscreen = Keluar dari layar penuh\ncontext-menu-enter-fullscreen = Masuk mode layar penuh\n","messages.ftl":'message-cant-embed =\n Ruffle tidak dapat menjalankan Flash yang disematkan di halaman ini.\n Anda dapat mencoba membuka file di tab terpisah, untuk menghindari masalah ini.\npanic-title = Terjadi kesalahan :(\nmore-info = Info lebih lanjut\nrun-anyway = Jalankan\ncontinue = Lanjutkan\nreport-bug = Laporkan Bug\nupdate-ruffle = Perbarui Ruffle\nruffle-demo = Demo Web\nruffle-desktop = Aplikasi Desktop\nruffle-wiki = Kunjungi Wiki Ruffle\nview-error-details = Tunjukan Detail Error\nopen-in-new-tab = Buka di Tab Baru\nclick-to-unmute = Tekan untuk menyalakan suara\nerror-file-protocol =\n Sepertinya anda menjalankan Ruffle di protokol "file:". \n Ini tidak berfungsi karena browser memblokir fitur ini dengan alasan keamanan.\n Sebagai gantinya, kami mengajak anda untuk membuat server lokal, menggunakan demo web atau aplikasi desktop.\nerror-javascript-config =\n Ruffle mengalami masalah besar karena konfigurasi JavaScript yang salah.\n Jika Anda adalah administrator server ini, kami mengajak Anda untuk memeriksa detail kesalahan untuk mengetahui parameter mana yang salah.\n Anda juga dapat membaca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-not-found =\n Ruffle gagal memuat komponen file ".wasm" yang diperlukan.\n Jika Anda adalah administrator server ini, pastikan file telah diunggah dengan benar.\n Jika masalah terus berlanjut, Anda mungkin perlu menggunakan pengaturan "publicPath": silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-mime-type =\n Ruffle mengalami masalah ketika mencoba melakukan inisialisasi.\n Server web ini tidak melayani file ".wasm" dengan tipe MIME yang benar.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-swf-fetch =\n Ruffle gagal memuat file SWF Flash.\n Kemungkinan file tersebut sudah tidak ada, sehingga tidak dapat dimuat oleh Ruffle.\n Coba hubungi administrator situs web ini untuk mendapatkan bantuan.\nerror-swf-cors =\n Ruffle gagal memuat file SWF Flash.\n Akses untuk memuat kemungkinan telah diblokir oleh kebijakan CORS.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-cors =\n Ruffle gagal memuat komponen file ".wasm" yang diperlukan.\n Akses untuk mengambil kemungkinan telah diblokir oleh kebijakan CORS.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-invalid =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Sepertinya halaman ini memiliki file yang hilang atau tidak valid untuk menjalankan Ruffle.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-download =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Hal ini sering kali dapat teratasi dengan sendirinya, sehingga Anda dapat mencoba memuat ulang halaman.\n Jika tidak, silakan hubungi administrator situs web ini.\nerror-wasm-disabled-on-edge =\n Ruffle gagal memuat komponen file ".wasm" yang diperlukan.\n Untuk mengatasinya, coba buka pengaturan peramban Anda, klik "Privasi, pencarian, dan layanan", turun ke bawah, dan matikan "Tingkatkan keamanan Anda di web".\n Ini akan memungkinkan browser Anda memuat file ".wasm" yang diperlukan.\n Jika masalah berlanjut, Anda mungkin harus menggunakan browser yang berbeda.\nerror-javascript-conflict =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Sepertinya situs web ini menggunakan kode JavaScript yang bertentangan dengan Ruffle.\n Jika Anda adalah administrator server ini, kami mengajak Anda untuk mencoba memuat file pada halaman kosong.\nerror-javascript-conflict-outdated = Anda juga dapat mencoba mengunggah versi Ruffle yang lebih baru yang mungkin dapat mengatasi masalah ini (versi saat ini sudah kedaluwarsa: { $buildDate }).\nerror-csp-conflict =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Kebijakan Keamanan Konten server web ini tidak mengizinkan komponen ".wasm" yang diperlukan untuk dijalankan.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-unknown =\n Ruffle telah mengalami masalah besar saat menampilkan konten Flash ini.\n { $outdated ->\n [true] Jika Anda administrator server ini, cobalah untuk mengganti versi Ruffle yang lebih baru (versi saat ini sudah kedaluwarsa: { $buildDate }).\n *[false] Hal ini seharusnya tidak terjadi, jadi kami sangat menghargai jika Anda dapat melaporkan bug ini!\n }\n',"save-manager.ftl":"save-delete-prompt = Anda yakin ingin menghapus berkas ini?\nsave-reload-prompt =\n Satu-satunya cara untuk { $action ->\n [delete] menghapus\n *[replace] mengganti\n } berkas penyimpanan ini tanpa potensi konflik adalah dengan memuat ulang konten ini. Apakah Anda ingin melanjutkannya?\nsave-download = Unduh\nsave-replace = Ganti\nsave-delete = Hapus\nsave-backup-all = Unduh semua berkas penyimpanan\n"},"it-IT":{"context_menu.ftl":"context-menu-download-swf = Scarica .swf\ncontext-menu-copy-debug-info = Copia informazioni di debug\ncontext-menu-open-save-manager = Apri Gestione salvataggi\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Informazioni su Ruffle Extension ({ $version })\n *[other] Informazioni su Ruffle ({ $version })\n }\ncontext-menu-hide = Nascondi questo menu\ncontext-menu-exit-fullscreen = Esci dallo schermo intero\ncontext-menu-enter-fullscreen = Entra a schermo intero\n","messages.ftl":"message-cant-embed =\n Ruffle non \xe8 stato in grado di eseguire il Flash incorporato in questa pagina.\n Puoi provare ad aprire il file in una scheda separata, per evitare questo problema.\npanic-title = Qualcosa \xe8 andato storto :(\nmore-info = Maggiori informazioni\nrun-anyway = Esegui comunque\ncontinue = Continua\nreport-bug = Segnala Un Bug\nupdate-ruffle = Aggiorna Ruffle\nruffle-demo = Demo Web\nruffle-desktop = Applicazione Desktop\nruffle-wiki = Visualizza Ruffle Wiki\nview-error-details = Visualizza Dettagli Errore\nopen-in-new-tab = Apri in una nuova scheda\nclick-to-unmute = Clicca per riattivare l'audio\nerror-file-protocol =\n Sembra che tu stia eseguendo Ruffle sul protocollo \"file:\".\n Questo non funziona come browser blocca molte funzionalit\xe0 di lavoro per motivi di sicurezza.\n Invece, ti invitiamo a configurare un server locale o a utilizzare la demo web o l'applicazione desktop.\nerror-javascript-config =\n Ruffle ha incontrato un problema importante a causa di una configurazione JavaScript non corretta.\n Se sei l'amministratore del server, ti invitiamo a controllare i dettagli dell'errore per scoprire quale parametro \xe8 in errore.\n Puoi anche consultare il wiki Ruffle per aiuto.\nerror-wasm-not-found =\n Ruffle non \xe8 riuscito a caricare il componente di file \".wasm\".\n Se sei l'amministratore del server, assicurati che il file sia stato caricato correttamente.\n Se il problema persiste, potrebbe essere necessario utilizzare l'impostazione \"publicPath\": si prega di consultare il wiki Ruffle per aiuto.\nerror-wasm-mime-type =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzazione.\n Questo server web non serve \". asm\" file con il tipo MIME corretto.\n Se sei l'amministratore del server, consulta la wiki Ruffle per aiuto.\nerror-swf-fetch =\n Ruffle non \xe8 riuscito a caricare il file Flash SWF.\n La ragione pi\xf9 probabile \xe8 che il file non esiste pi\xf9, quindi non c'\xe8 nulla che Ruffle possa caricare.\n Prova a contattare l'amministratore del sito web per aiuto.\nerror-swf-cors =\n Ruffle non \xe8 riuscito a caricare il file SWF Flash.\n L'accesso al recupero probabilmente \xe8 stato bloccato dalla politica CORS.\n Se sei l'amministratore del server, consulta la wiki Ruffle per ricevere aiuto.\nerror-wasm-cors =\n Ruffle non \xe8 riuscito a caricare il componente di file \".wasm\".\n L'accesso al recupero probabilmente \xe8 stato bloccato dalla politica CORS.\n Se sei l'amministratore del server, consulta la wiki Ruffle per ricevere aiuto.\nerror-wasm-invalid =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzazione.\n Sembra che questa pagina abbia file mancanti o non validi per l'esecuzione di Ruffle.\n Se sei l'amministratore del server, consulta la wiki Ruffle per ricevere aiuto.\nerror-wasm-download =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzazione.\n Questo pu\xf2 spesso risolversi da solo, quindi puoi provare a ricaricare la pagina.\n Altrimenti, contatta l'amministratore del sito.\nerror-wasm-disabled-on-edge =\n Ruffle non ha caricato il componente di file \".wasm\" richiesto.\n Per risolvere il problema, prova ad aprire le impostazioni del tuo browser, facendo clic su \"Privacy, search, and services\", scorrendo verso il basso e disattivando \"Migliora la tua sicurezza sul web\".\n Questo permetter\xe0 al tuo browser di caricare i file \".wasm\" richiesti.\n Se il problema persiste, potresti dover usare un browser diverso.\nerror-javascript-conflict =\n Ruffle ha riscontrato un problema importante durante il tentativo di inizializzazione.\n Sembra che questa pagina utilizzi il codice JavaScript che \xe8 in conflitto con Ruffle.\n Se sei l'amministratore del server, ti invitiamo a provare a caricare il file su una pagina vuota.\nerror-javascript-conflict-outdated = Puoi anche provare a caricare una versione pi\xf9 recente di Ruffle che potrebbe aggirare il problema (l'attuale build \xe8 obsoleta: { $buildDate }).\nerror-csp-conflict =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzare.\n La Politica di Sicurezza dei Contenuti di questo server web non consente l'impostazione richiesta\". asm\" componente da eseguire.\n Se sei l'amministratore del server, consulta la Ruffle wiki per aiuto.\nerror-unknown =\n Ruffle ha incontrato un problema importante durante il tentativo di visualizzare questo contenuto Flash.\n { $outdated ->\n [true] Se sei l'amministratore del server, prova a caricare una versione pi\xf9 recente di Ruffle (la versione attuale \xe8 obsoleta: { $buildDate }).\n *[false] Questo non dovrebbe accadere, quindi ci piacerebbe molto se si potesse inviare un bug!\n }\n","save-manager.ftl":"save-delete-prompt = Sei sicuro di voler eliminare questo file di salvataggio?\nsave-reload-prompt =\n L'unico modo per { $action ->\n [delete] delete\n *[replace] replace\n } questo salvataggio file senza potenziali conflitti \xe8 quello di ricaricare questo contenuto. Volete continuare comunque?\nsave-download = Scarica\nsave-replace = Sostituisci\nsave-delete = Elimina\nsave-backup-all = Scarica tutti i file di salvataggio\n"},"ja-JP":{"context_menu.ftl":"context-menu-download-swf = .swf\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\ncontext-menu-copy-debug-info = \u30c7\u30d0\u30c3\u30b0\u60c5\u5831\u3092\u30b3\u30d4\u30fc\ncontext-menu-open-save-manager = \u30bb\u30fc\u30d6\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u3092\u958b\u304f\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Ruffle\u62e1\u5f35\u6a5f\u80fd\u306b\u3064\u3044\u3066 ({ $version })\n *[other] Ruffle\u306b\u3064\u3044\u3066 ({ $version })\n }\ncontext-menu-hide = \u30e1\u30cb\u30e5\u30fc\u3092\u96a0\u3059\ncontext-menu-exit-fullscreen = \u30d5\u30eb\u30b9\u30af\u30ea\u30fc\u30f3\u3092\u7d42\u4e86\ncontext-menu-enter-fullscreen = \u30d5\u30eb\u30b9\u30af\u30ea\u30fc\u30f3\u306b\u3059\u308b\n","messages.ftl":'message-cant-embed =\n Ruffle\u306f\u3053\u306e\u30da\u30fc\u30b8\u306b\u57cb\u3081\u8fbc\u307e\u308c\u305f Flash \u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n \u5225\u306e\u30bf\u30d6\u3067\u30d5\u30a1\u30a4\u30eb\u3092\u958b\u304f\u3053\u3068\u3067\u3001\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3067\u304d\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\npanic-title = \u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f :(\nmore-info = \u8a73\u7d30\u60c5\u5831\nrun-anyway = \u3068\u306b\u304b\u304f\u5b9f\u884c\u3059\u308b\ncontinue = \u7d9a\u884c\nreport-bug = \u30d0\u30b0\u3092\u5831\u544a\nupdate-ruffle = Ruffle\u3092\u66f4\u65b0\nruffle-demo = Web\u30c7\u30e2\nruffle-desktop = \u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\nruffle-wiki = Ruffle Wiki\u3092\u898b\u308b\nview-error-details = \u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u3092\u8868\u793a\nopen-in-new-tab = \u65b0\u3057\u3044\u30bf\u30d6\u3067\u958b\u304f\nclick-to-unmute = \u30af\u30ea\u30c3\u30af\u3067\u30df\u30e5\u30fc\u30c8\u3092\u89e3\u9664\nerror-file-protocol =\n Ruffle\u3092"file:"\u30d7\u30ed\u30c8\u30b3\u30eb\u3067\u4f7f\u7528\u3057\u3066\u3044\u308b\u3088\u3046\u3067\u3059\u3002\n \u30d6\u30e9\u30a6\u30b6\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u4e0a\u306e\u7406\u7531\u304b\u3089\u6b86\u3069\u306e\u6a5f\u80fd\u3092\u5236\u9650\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002\n \u30ed\u30fc\u30ab\u30eb\u30b5\u30fc\u30d0\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u304b\u3001\u30a6\u30a7\u30d6\u30c7\u30e2\u307e\u305f\u306f\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u3092\u3054\u5229\u7528\u304f\u3060\u3055\u3044\u3002\nerror-javascript-config =\n JavaScript\u306e\u8a2d\u5b9a\u304c\u6b63\u3057\u304f\u306a\u3044\u305f\u3081\u3001Ruffle\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u30b5\u30fc\u30d0\u30fc\u7ba1\u7406\u8005\u306e\u65b9\u306f\u3001\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u304b\u3089\u3001\u3069\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u306b\u554f\u984c\u304c\u3042\u308b\u306e\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n Ruffle\u306ewiki\u3092\u53c2\u7167\u3059\u308b\u3053\u3068\u3067\u3001\u89e3\u6c7a\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\nerror-wasm-not-found =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306eWeb\u30b5\u30fc\u30d0\u30fc\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30dd\u30ea\u30b7\u30fc\u304c\u3001\u5b9f\u884c\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u5b9f\u884c\u3092\u8a31\u53ef\u3057\u3066\u3044\u307e\u305b\u3093\u3002\u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306e\u5834\u5408\u306f\u3001\u30d5\u30a1\u30a4\u30eb\u304c\u6b63\u3057\u304f\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3055\u308c\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3092\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u306e\u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u300cpublicPath\u300d\u306e\u8a2d\u5b9a\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-mime-type =\n Ruffle\u306e\u521d\u671f\u5316\u306b\u5931\u6557\u3059\u308b\u5927\u304d\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306eWeb\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u3044MIME\u30bf\u30a4\u30d7\u306e\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u3092\u63d0\u4f9b\u3057\u3066\u3044\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-swf-fetch =\n Ruffle\u304cFlash SWF\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n \u6700\u3082\u8003\u3048\u3089\u308c\u308b\u539f\u56e0\u306f\u3001SWF\u30d5\u30a1\u30a4\u30eb\u304c\u65e2\u306b\u5b58\u5728\u3057\u306a\u3044\u4e8b\u3067Ruffle\u304c\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3059\u308b\u3068\u3044\u3046\u554f\u984c\u3067\u3059\u3002\n Web\u30b5\u30a4\u30c8\u306e\u7ba1\u7406\u8005\u306b\u304a\u554f\u3044\u5408\u308f\u305b\u304f\u3060\u3055\u3044\u3002\nerror-swf-cors =\n Ruffle\u306fSWF\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n CORS\u30dd\u30ea\u30b7\u30fc\u306e\u8a2d\u5b9a\u306b\u3088\u308a\u3001fetch\u3078\u306e\u30a2\u30af\u30bb\u30b9\u304c\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\n \u30b5\u30fc\u30d0\u30fc\u7ba1\u7406\u8005\u306e\u65b9\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-cors =\n Ruffle\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n CORS\u30dd\u30ea\u30b7\u30fc\u306b\u3088\u3063\u3066fetch\u3078\u306e\u30a2\u30af\u30bb\u30b9\u304c\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle wiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-invalid =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u30da\u30fc\u30b8\u306b\u306fRuffle\u3092\u5b9f\u884c\u3059\u308b\u305f\u3081\u306e\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u7121\u52b9\u306a\u30d5\u30a1\u30a4\u30eb\u304c\u3042\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-download =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u554f\u984c\u306f\u30da\u30fc\u30b8\u3092\u518d\u8aad\u307f\u8fbc\u307f\u3059\u308b\u4e8b\u3067\u5927\u62b5\u306f\u89e3\u6c7a\u3059\u308b\u306f\u305a\u306a\u306e\u3067\u884c\u306a\u3063\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n \u3082\u3057\u3082\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001Web\u30b5\u30a4\u30c8\u306e\u7ba1\u7406\u8005\u306b\u304a\u554f\u3044\u5408\u308f\u305b\u304f\u3060\u3055\u3044\u3002\nerror-wasm-disabled-on-edge =\n Ruffle\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u30d6\u30e9\u30a6\u30b6\u30fc\u306e\u8a2d\u5b9a\u3092\u958b\u304d\u3001\u300c\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3001\u691c\u7d22\u3001\u30b5\u30fc\u30d3\u30b9\u300d\u3092\u30af\u30ea\u30c3\u30af\u3057\u3001\u4e0b\u306b\u30b9\u30af\u30ed\u30fc\u30eb\u3067\u300cWeb\u4e0a\u306e\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5f37\u5316\u3059\u308b\u300d\u3092\u30aa\u30d5\u306b\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n \u3053\u308c\u3067\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u304c\u8aad\u307f\u8fbc\u307e\u308c\u308b\u3088\u3046\u306b\u306a\u308a\u307e\u3059\u3002\n \u305d\u308c\u3067\u3082\u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u3001\u5225\u306e\u30d6\u30e9\u30a6\u30b6\u30fc\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\nerror-javascript-conflict =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u30da\u30fc\u30b8\u3067\u306fRuffle\u3068\u7af6\u5408\u3059\u308bJavaScript\u30b3\u30fc\u30c9\u304c\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001\u7a7a\u767d\u306e\u30da\u30fc\u30b8\u3067\u30d5\u30a1\u30a4\u30eb\u3092\u8aad\u307f\u8fbc\u307f\u3057\u76f4\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\nerror-csp-conflict =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306eWeb\u30b5\u30fc\u30d0\u30fc\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30dd\u30ea\u30b7\u30fc\u304c\u5b9f\u884c\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u5b9f\u884c\u3092\u8a31\u53ef\u3057\u3066\u3044\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-unknown =\n Flash\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u8868\u793a\u3059\u308b\u969b\u306bRuffle\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n { $outdated ->\n [true] \u73fe\u5728\u4f7f\u7528\u3057\u3066\u3044\u308b\u30d3\u30eb\u30c9\u306f\u6700\u65b0\u3067\u306f\u306a\u3044\u305f\u3081\u3001\u30b5\u30fc\u30d0\u30fc\u7ba1\u7406\u8005\u306e\u65b9\u306f\u3001\u6700\u65b0\u7248\u306eRuffle\u306b\u66f4\u65b0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044(\u73fe\u5728\u5229\u7528\u4e2d\u306e\u30d3\u30eb\u30c9: { $buildDate })\u3002\n *[false] \u60f3\u5b9a\u5916\u306e\u554f\u984c\u306a\u306e\u3067\u3001\u30d0\u30b0\u3068\u3057\u3066\u5831\u544a\u3057\u3066\u3044\u305f\u3060\u3051\u308b\u3068\u5b09\u3057\u3044\u3067\u3059!\n }\n',"save-manager.ftl":"save-delete-prompt = \u3053\u306e\u30bb\u30fc\u30d6\u30d5\u30a1\u30a4\u30eb\u3092\u524a\u9664\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b?\nsave-reload-prompt =\n \u30bb\u30fc\u30d6\u30d5\u30a1\u30a4\u30eb\u3092\u7af6\u5408\u306e\u53ef\u80fd\u6027\u306a\u304f { $action ->\n [delete] \u524a\u9664\u3059\u308b\n *[replace] \u7f6e\u304d\u63db\u3048\u308b\n } \u305f\u3081\u306b\u3001\u3053\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u518d\u8aad\u307f\u8fbc\u307f\u3059\u308b\u3053\u3068\u3092\u63a8\u5968\u3057\u307e\u3059\u3002\u7d9a\u884c\u3057\u307e\u3059\u304b\uff1f\nsave-download = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\nsave-replace = \u7f6e\u304d\u63db\u3048\nsave-delete = \u524a\u9664\nsave-backup-all = \u5168\u3066\u306e\u30bb\u30fc\u30d6\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\n"},"ko-KR":{"context_menu.ftl":"context-menu-download-swf = .swf \ub2e4\uc6b4\ub85c\ub4dc\ncontext-menu-copy-debug-info = \ub514\ubc84\uadf8 \uc815\ubcf4 \ubcf5\uc0ac\ncontext-menu-open-save-manager = \uc800\uc7a5 \uad00\ub9ac\uc790 \uc5f4\uae30\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Ruffle \ud655\uc7a5 \ud504\ub85c\uadf8\ub7a8 \uc815\ubcf4 ({ $version })\n *[other] Ruffle \uc815\ubcf4 ({ $version })\n }\ncontext-menu-hide = \uc774 \uba54\ub274 \uc228\uae30\uae30\ncontext-menu-exit-fullscreen = \uc804\uccb4\ud654\uba74 \ub098\uac00\uae30\ncontext-menu-enter-fullscreen = \uc804\uccb4\ud654\uba74\uc73c\ub85c \uc5f4\uae30\n","messages.ftl":'message-cant-embed = Ruffle\uc774 \uc774 \ud398\uc774\uc9c0\uc5d0 \ud3ec\ud568\ub41c \ud50c\ub798\uc2dc\ub97c \uc2e4\ud589\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4. \ubcc4\ub3c4\uc758 \ud0ed\uc5d0\uc11c \ud30c\uc77c\uc744 \uc5f4\uc5b4\ubd04\uc73c\ub85c\uc11c \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\npanic-title = \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4 :(\nmore-info = \ucd94\uac00 \uc815\ubcf4\nrun-anyway = \uadf8\ub798\ub3c4 \uc2e4\ud589\ud558\uae30\ncontinue = \uacc4\uc18d\ud558\uae30\nreport-bug = \ubc84\uadf8 \uc81c\ubcf4\nupdate-ruffle = Ruffle \uc5c5\ub370\uc774\ud2b8\nruffle-demo = \uc6f9 \ub370\ubaa8\nruffle-desktop = \ub370\uc2a4\ud06c\ud1b1 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\nruffle-wiki = Ruffle \uc704\ud0a4 \ubcf4\uae30\nview-error-details = \uc624\ub958 \uc138\ubd80 \uc815\ubcf4 \ubcf4\uae30\nopen-in-new-tab = \uc0c8 \ud0ed\uc5d0\uc11c \uc5f4\uae30\nclick-to-unmute = \ud074\ub9ad\ud558\uc5ec \uc74c\uc18c\uac70 \ud574\uc81c\nerror-file-protocol =\n Ruffle\uc744 "file:" \ud504\ub85c\ud1a0\ucf5c\uc5d0\uc11c \uc2e4\ud589\ud558\uace0 \uc788\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4.\n \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c\ub294 \uc774 \ud504\ub85c\ud1a0\ucf5c\uc744 \ubcf4\uc548\uc0c1\uc758 \uc774\uc720\ub85c \ub9ce\uc740 \uae30\ub2a5\uc744 \uc791\ub3d9\ud558\uc9c0 \uc54a\uac8c \ucc28\ub2e8\ud558\ubbc0\ub85c \uc774 \ubc29\ubc95\uc740 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n \ub300\uc2e0, \ub85c\uceec \uc11c\ubc84\ub97c \uc9c1\uc811 \uc5f4\uc5b4\uc11c \uc124\uc815\ud558\uac70\ub098 \uc6f9 \ub370\ubaa8 \ub610\ub294 \ub370\uc2a4\ud06c\ud1b1 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc2dc\uae30 \ubc14\ub78d\ub2c8\ub2e4.\nerror-javascript-config =\n \uc798\ubabb\ub41c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \uc124\uc815\uc73c\ub85c \uc778\ud574 Ruffle\uc5d0\uc11c \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\uc778 \uacbd\uc6b0, \uc624\ub958 \uc138\ubd80\uc0ac\ud56d\uc744 \ud655\uc778\ud558\uc5ec \uc5b4\ub5a4 \ub9e4\uac1c\ubcc0\uc218\uac00 \uc798\ubabb\ub418\uc5c8\ub294\uc9c0 \uc54c\uc544\ubcf4\uc138\uc694.\n \ub610\ub294 Ruffle \uc704\ud0a4\ub97c \ud1b5\ud574 \ub3c4\uc6c0\uc744 \ubc1b\uc544 \ubcfc \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-not-found =\n Ruffle\uc774 ".wasm" \ud544\uc218 \ud30c\uc77c \uad6c\uc131\uc694\uc18c\ub97c \ub85c\ub4dc\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 \ud30c\uc77c\uc774 \uc62c\ubc14\ub974\uac8c \uc5c5\ub85c\ub4dc\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud558\uc138\uc694.\n \ubb38\uc81c\uac00 \uc9c0\uc18d\ub41c\ub2e4\uba74 "publicPath" \uc635\uc158\uc744 \uc0ac\uc6a9\ud574\uc57c \ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4: Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc73c\uc138\uc694.\nerror-wasm-mime-type =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \uc6f9 \uc11c\ubc84\ub294 \uc62c\ubc14\ub978 MIME \uc720\ud615\uc758 ".wasm" \ud30c\uc77c\uc744 \uc81c\uacf5\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ud1b5\ud574 \ub3c4\uc6c0\uc744 \ubc1b\uc73c\uc138\uc694.\nerror-swf-fetch =\n Ruffle\uc774 \ud50c\ub798\uc2dc SWF \ud30c\uc77c\uc744 \ub85c\ub4dc\ud558\ub294 \ub370 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4.\n \uc774\ub294 \uc8fc\ub85c \ud30c\uc77c\uc774 \ub354 \uc774\uc0c1 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc544 Ruffle\uc774 \ub85c\ub4dc\ud560 \uc218 \uc788\ub294 \uac83\uc774 \uc5c6\uc744 \uac00\ub2a5\uc131\uc774 \ub192\uc2b5\ub2c8\ub2e4.\n \uc6f9\uc0ac\uc774\ud2b8 \uad00\ub9ac\uc790\uc5d0\uac8c \ubb38\uc758\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcf4\uc138\uc694.\nerror-swf-cors =\n Ruffle\uc774 \ud50c\ub798\uc2dc SWF \ud30c\uc77c\uc744 \ub85c\ub4dc\ud558\ub294 \ub370 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4.\n CORS \uc815\ucc45\uc5d0 \uc758\ud574 \ub370\uc774\ud130 \uac00\uc838\uc624\uae30\uc5d0 \ub300\ud55c \uc561\uc138\uc2a4\uac00 \ucc28\ub2e8\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-cors =\n Ruffle\uc774 ".wasm" \ud544\uc218 \ud30c\uc77c \uad6c\uc131\uc694\uc18c\ub97c \ub85c\ub4dc\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\n CORS \uc815\ucc45\uc5d0 \uc758\ud574 \ub370\uc774\ud130 \uac00\uc838\uc624\uae30\uc5d0 \ub300\ud55c \uc561\uc138\uc2a4\uac00 \ucc28\ub2e8\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-invalid =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \ud398\uc774\uc9c0\uc5d0 Ruffle\uc744 \uc2e4\ud589\ud558\uae30 \uc704\ud55c \ud30c\uc77c\uc774 \ub204\ub77d\ub418\uc5c8\uac70\ub098 \uc798\ubabb\ub41c \uac83 \uac19\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-download =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \ubb38\uc81c\ub294 \ub54c\ub54c\ub85c \ubc14\ub85c \ud574\uacb0\ub420 \uc218 \uc788\uc73c\ubbc0\ub85c \ud398\uc774\uc9c0\ub97c \uc0c8\ub85c\uace0\uce68\ud558\uc5ec \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694.\n \uadf8\ub798\ub3c4 \ubb38\uc81c\uac00 \uc9c0\uc18d\ub41c\ub2e4\uba74, \uc6f9\uc0ac\uc774\ud2b8 \uad00\ub9ac\uc790\uc5d0\uac8c \ubb38\uc758\ud574\uc8fc\uc138\uc694.\nerror-wasm-disabled-on-edge =\n Ruffle\uc774 ".wasm" \ud544\uc218 \ud30c\uc77c \uad6c\uc131\uc694\uc18c\ub97c \ub85c\ub4dc\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\n \uc774\ub97c \ud574\uacb0\ud558\ub824\uba74 \ube0c\ub77c\uc6b0\uc800 \uc124\uc815\uc5d0\uc11c "\uac1c\uc778 \uc815\ubcf4, \uac80\uc0c9 \ubc0f \uc11c\ube44\uc2a4"\ub97c \ud074\ub9ad\ud55c \ud6c4, \ud558\ub2e8\uc73c\ub85c \uc2a4\ud06c\ub864\ud558\uc5ec "\uc6f9\uc5d0\uc11c \ubcf4\uc548 \uac15\ud654" \uae30\ub2a5\uc744 \uaebc\uc57c \ud569\ub2c8\ub2e4.\n \uc774\ub294 \ud544\uc694\ud55c ".wasm" \ud30c\uc77c\uc744 \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c \ub85c\ub4dc\ud560 \uc218 \uc788\ub3c4\ub85d \ud5c8\uc6a9\ud569\ub2c8\ub2e4.\n \uc774 \ubb38\uc81c\uac00 \uc9c0\uc18d\ub420 \uacbd\uc6b0 \ub2e4\ub978 \ube0c\ub77c\uc6b0\uc800\ub97c \uc0ac\uc6a9\ud574\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-javascript-conflict =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \ud398\uc774\uc9c0\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \ucf54\ub4dc\uac00 Ruffle\uacfc \ucda9\ub3cc\ud558\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 \ube48 \ud398\uc774\uc9c0\uc5d0\uc11c \ud30c\uc77c\uc744 \ub85c\ub4dc\ud574\ubcf4\uc138\uc694.\nerror-javascript-conflict-outdated = \ub610\ud55c Ruffle\uc758 \ucd5c\uc2e0 \ubc84\uc804\uc744 \uc5c5\ub85c\ub4dc\ud558\ub294 \uac83\uc744 \uc2dc\ub3c4\ud558\uc5ec \ubb38\uc81c\ub97c \uc6b0\ud68c\ud574\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. (\ud604\uc7ac \ube4c\ub4dc\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4: { $buildDate }).\nerror-csp-conflict =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \uc6f9 \uc11c\ubc84\uc758 CSP(Content Security Policy) \uc815\ucc45\uc774 ".wasm" \ud544\uc218 \uad6c\uc131\uc694\uc18c\ub97c \uc2e4\ud589\ud558\ub294 \uac83\uc744 \ud5c8\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-unknown =\n Ruffle\uc774 \ud50c\ub798\uc2dc \ucf58\ud150\uce20\ub97c \ud45c\uc2dc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n { $outdated ->\n [true] \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74, Ruffle\uc758 \ucd5c\uc2e0 \ubc84\uc804\uc744 \uc5c5\ub85c\ub4dc\ud558\uc5ec \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694. (\ud604\uc7ac \ube4c\ub4dc\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4: { $buildDate }).\n *[false] \uc774\ub7f0 \ud604\uc0c1\uc774 \ubc1c\uc0dd\ud574\uc11c\ub294 \uc548\ub418\ubbc0\ub85c, \ubc84\uadf8\ub97c \uc81c\ubcf4\ud574\uc8fc\uc2e0\ub2e4\uba74 \uac10\uc0ac\ud558\uaca0\uc2b5\ub2c8\ub2e4!\n }\n',"save-manager.ftl":"save-delete-prompt = \uc815\ub9d0\ub85c \uc774 \uc138\uc774\ube0c \ud30c\uc77c\uc744 \uc0ad\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\nsave-reload-prompt =\n \b\uc774 \ud30c\uc77c\uc744 \uc7a0\uc7ac\uc801\uc778 \ucda9\ub3cc \uc5c6\uc774 { $action ->\n [delete] \uc0ad\uc81c\n *[replace] \uad50\uccb4\n }\ud558\ub824\uba74 \ucf58\ud150\uce20\ub97c \ub2e4\uc2dc \ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub798\ub3c4 \uacc4\uc18d\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\nsave-download = \ub2e4\uc6b4\ub85c\ub4dc\nsave-replace = \uad50\uccb4\nsave-delete = \uc0ad\uc81c\nsave-backup-all = \ubaa8\ub4e0 \uc800\uc7a5 \ud30c\uc77c \ub2e4\uc6b4\ub85c\ub4dc\n"},"nl-NL":{"context_menu.ftl":"context-menu-download-swf = .swf downloaden\ncontext-menu-copy-debug-info = Kopieer debuginformatie\ncontext-menu-open-save-manager = Open opgeslagen-data-manager\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Over Ruffle Uitbreiding ({ $version })\n *[other] Over Ruffle ({ $version })\n }\ncontext-menu-hide = Verberg dit menu\ncontext-menu-exit-fullscreen = Verlaat volledig scherm\ncontext-menu-enter-fullscreen = Naar volledig scherm\n","messages.ftl":'message-cant-embed =\n Ruffle kon de Flash-inhoud op de pagina niet draaien.\n Je kan proberen het bestand in een apart tabblad te openen, om hier omheen te werken.\npanic-title = Er ging iets mis :(\nmore-info = Meer informatie\nrun-anyway = Toch starten\ncontinue = Doorgaan\nreport-bug = Bug rapporteren\nupdate-ruffle = Ruffle updaten\nruffle-demo = Web Demo\nruffle-desktop = Desktopapplicatie\nruffle-wiki = Bekijk de Ruffle Wiki\nview-error-details = Foutdetails tonen\nopen-in-new-tab = Openen in een nieuw tabblad\nclick-to-unmute = Klik om te ontdempen\nerror-file-protocol =\n Het lijkt erop dat je Ruffle gebruikt met het "file" protocol.\n De meeste browsers blokkeren dit om veiligheidsredenen, waardoor het niet werkt.\n In plaats hiervan raden we aan om een lokale server te draaien, de web demo te gebruiken, of de desktopapplicatie.\nerror-javascript-config =\n Ruffle heeft een groot probleem ondervonden vanwege een onjuiste JavaScript configuratie.\n Als je de serverbeheerder bent, kijk dan naar de foutdetails om te zien wat er verkeerd is.\n Je kan ook in de Ruffle wiki kijken voor hulp.\nerror-wasm-not-found =\n Ruffle kon het vereiste ".wasm" bestandscomponent niet laden.\n Als je de serverbeheerder bent, controleer dan of het bestaand juist is ge\xfcpload.\n Mocht het probleem blijven voordoen, moet je misschien de "publicPath" instelling gebruiken: zie ook de Ruffle wiki voor hulp.\nerror-wasm-mime-type =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Deze webserver serveert ".wasm" bestanden niet met het juiste MIME type.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-swf-fetch =\n Ruffle kon het Flash SWF bestand niet inladen.\n De meest waarschijnlijke reden is dat het bestand niet langer bestaat, en er dus niets is om in te laden.\n Probeer contact op te nemen met de websitebeheerder voor hulp.\nerror-swf-cors =\n Ruffle kon het Flash SWD bestand niet inladen.\n Toegang is waarschijnlijk geblokeerd door het CORS beleid.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-wasm-cors =\n Ruffle kon het vereiste ".wasm" bestandscomponent niet laden.\n Toegang is waarschijnlijk geblokeerd door het CORS beleid.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-wasm-invalid =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Het lijkt erop dat de Ruffle bestanden ontbreken of ongeldig zijn.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-wasm-download =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Dit lost zichzelf vaak op als je de bladzijde opnieuw inlaadt.\n Zo niet, neem dan contact op met de websitebeheerder.\nerror-wasm-disabled-on-edge =\n Ruffle kon het vereiste ".wasm" bestandscomponent niet laden.\n Om dit op te lossen, ga naar je browserinstellingen, klik op "Privacy, zoeken en diensten", scroll omlaag, en schakel "Verbeter je veiligheid op he web" uit.\n Dan kan je browser wel de vereiste ".wasm" bestanden inladen.\n Als het probleem zich blijft voordoen, moet je misschien een andere browser gebruiken.\nerror-javascript-conflict =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Het lijkt erop dat deze pagina JavaScript code gebruikt die conflicteert met Ruffle.\n Als je de serverbeheerder bent, raden we aan om het bestand op een lege pagina te proberen in te laden.\nerror-javascript-conflict-outdated = Je kan ook proberen een nieuwe versie van Ruffle te installeren, om om het probleem heen te werken (huidige versie is oud: { $buildDate }).\nerror-csp-conflict =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Het CSP-beleid staat niet toe dat het vereiste ".wasm" component kan draaien.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-unknown =\n Ruffle heeft een groot probleem onderbonden tijdens het weergeven van deze Flash-inhoud.\n { $outdated ->\n [true] Als je de serverbeheerder bent, upload dan een nieuwe versie van Ruffle (huidige versie is oud: { $buildDate }).\n *[false] Dit hoort niet te gebeuren, dus we stellen het op prijs als je de fout aan ons rapporteert!\n }\n',"save-manager.ftl":"save-delete-prompt = Weet je zeker dat je deze opgeslagen data wilt verwijderen?\nsave-reload-prompt =\n De enige manier om deze opgeslagen data te { $action ->\n [delete] verwijderen\n *[replace] vervangen\n } zonder potenti\xeble problemen is door de inhoud opnieuw te laden. Toch doorgaan?\nsave-download = Downloaden\nsave-replace = Vervangen\nsave-delete = Verwijderen\nsave-backup-all = Download alle opgeslagen data\n"},"pt-BR":{"context_menu.ftl":"context-menu-download-swf = Baixar .swf\ncontext-menu-copy-debug-info = Copiar informa\xe7\xe3o de depura\xe7\xe3o\ncontext-menu-open-save-manager = Abrir o Gerenciador de Salvamento\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Sobre a extens\xe3o do Ruffle ({ $version })\n *[other] Sobre o Ruffle ({ $version })\n }\ncontext-menu-hide = Esconder este menu\ncontext-menu-exit-fullscreen = Sair da tela cheia\ncontext-menu-enter-fullscreen = Entrar em tela cheia\n","messages.ftl":'message-cant-embed =\n Ruffle n\xe3o conseguiu executar o Flash incorporado nesta p\xe1gina.\n Voc\xea pode tentar abrir o arquivo em uma guia separada para evitar esse problema.\npanic-title = Algo deu errado :(\nmore-info = Mais informa\xe7\xe3o\nrun-anyway = Executar mesmo assim\ncontinue = Continuar\nreport-bug = Reportar Bug\nupdate-ruffle = Atualizar Ruffle\nruffle-demo = Demo Web\nruffle-desktop = Aplicativo de Desktop\nruffle-wiki = Ver Wiki do Ruffle\nview-error-details = Ver detalhes do erro\nopen-in-new-tab = Abrir em uma nova guia\nclick-to-unmute = Clique para ativar o som\nerror-file-protocol =\n Parece que voc\xea est\xe1 executando o Ruffle no protocolo "file:".\n Isto n\xe3o funciona como navegadores bloqueiam muitos recursos de funcionar por raz\xf5es de seguran\xe7a.\n Ao inv\xe9s disso, convidamos voc\xea a configurar um servidor local ou a usar a demonstra\xe7\xe3o da web, ou o aplicativo de desktop.\nerror-javascript-config =\n O Ruffle encontrou um grande problema devido a uma configura\xe7\xe3o incorreta do JavaScript.\n Se voc\xea for o administrador do servidor, convidamos voc\xea a verificar os detalhes do erro para descobrir qual par\xe2metro est\xe1 com falha.\n Voc\xea tamb\xe9m pode consultar o wiki do Ruffle para obter ajuda.\nerror-wasm-not-found =\n Ruffle falhou ao carregar o componente de arquivo ".wasm" necess\xe1rio.\n Se voc\xea \xe9 o administrador do servidor, por favor, certifique-se de que o arquivo foi carregado corretamente.\n Se o problema persistir, voc\xea pode precisar usar a configura\xe7\xe3o "publicPath": por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-mime-type =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Este servidor de web n\xe3o est\xe1 servindo ".wasm" arquivos com o tipo MIME correto.\n Se voc\xea \xe9 o administrador do servidor, por favor consulte o wiki do Ruffle para obter ajuda.\nerror-swf-fetch =\n Ruffle falhou ao carregar o arquivo Flash SWF.\n A raz\xe3o prov\xe1vel \xe9 que o arquivo n\xe3o existe mais, ent\xe3o n\xe3o h\xe1 nada para o Ruffle carregar.\n Tente contatar o administrador do site para obter ajuda.\nerror-swf-cors =\n Ruffle falhou ao carregar o arquivo Flash SWF.\n O acesso para fetch provavelmente foi bloqueado pela pol\xedtica CORS.\n Se voc\xea for o administrador do servidor, consulte o wiki do Ruffle para obter ajuda.\nerror-wasm-cors =\n Ruffle falhou ao carregar o componente de arquivo ".wasm" necess\xe1rio.\n O acesso para fetch foi provavelmente bloqueado pela pol\xedtica CORS.\n Se voc\xea \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-invalid =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Parece que esta p\xe1gina tem arquivos ausentes ou inv\xe1lidos para executar o Ruffle.\n Se voc\xea for o administrador do servidor, consulte o wiki do Ruffle para obter ajuda.\nerror-wasm-download =\n O Ruffle encontrou um grande problema ao tentar inicializar.\n Muitas vezes isso pode se resolver sozinho, ent\xe3o voc\xea pode tentar recarregar a p\xe1gina.\n Caso contr\xe1rio, contate o administrador do site.\nerror-wasm-disabled-on-edge =\n O Ruffle falhou ao carregar o componente de arquivo ".wasm" necess\xe1rio.\n Para corrigir isso, tente abrir configura\xe7\xf5es do seu navegador, clicando em "Privacidade, pesquisa e servi\xe7os", rolando para baixo e desativando "Melhore sua seguran\xe7a na web".\n Isso permitir\xe1 que seu navegador carregue os arquivos ".wasm" necess\xe1rios.\n Se o problema persistir, talvez seja necess\xe1rio usar um navegador diferente.\nerror-javascript-conflict =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Parece que esta p\xe1gina usa c\xf3digo JavaScript que entra em conflito com o Ruffle.\n Se voc\xea for o administrador do servidor, convidamos voc\xea a tentar carregar o arquivo em uma p\xe1gina em branco.\nerror-javascript-conflict-outdated = Voc\xea tamb\xe9m pode tentar fazer o upload de uma vers\xe3o mais recente do Ruffle que pode contornar o problema (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\nerror-csp-conflict =\n Ruffle encontrou um grande problema ao tentar inicializar.\n A pol\xedtica de seguran\xe7a de conte\xfado deste servidor da web n\xe3o permite a execu\xe7\xe3o do componente ".wasm" necess\xe1rio.\n Se voc\xea for o administrador do servidor, consulte o wiki do Ruffle para obter ajuda.\nerror-unknown =\n O Ruffle encontrou um grande problema enquanto tentava exibir este conte\xfado em Flash.\n { $outdated ->\n [true] Se voc\xea \xe9 o administrador do servidor, por favor tente fazer o upload de uma vers\xe3o mais recente do Ruffle (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\n *[false] Isso n\xe3o deveria acontecer, ent\xe3o apreciar\xedamos muito se voc\xea pudesse arquivar um bug!\n }\n',"save-manager.ftl":"save-delete-prompt = Tem certeza que deseja excluir este arquivo de salvamento?\nsave-reload-prompt =\n A \xfanica maneira de { $action ->\n [delete] excluir\n *[replace] substituir\n } este arquivo sem potencial conflito \xe9 recarregar este conte\xfado. Deseja continuar mesmo assim?\nsave-download = Baixar\nsave-replace = Substituir\nsave-delete = Excluir\nsave-backup-all = Baixar todos os arquivos de salvamento\n"},"pt-PT":{"context_menu.ftl":"context-menu-download-swf = Descarga.swf\ncontext-menu-copy-debug-info = Copiar informa\xe7\xf5es de depura\xe7\xe3o\ncontext-menu-open-save-manager = Abrir Gestor de Grava\xe7\xf5es\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Sobre a extens\xe3o do Ruffle ({ $version })\n *[other] Sobre o Ruffle ({ $version })\n }\ncontext-menu-hide = Esconder este menu\ncontext-menu-exit-fullscreen = Fechar Ecr\xe3 Inteiro\ncontext-menu-enter-fullscreen = Abrir Ecr\xe3 Inteiro\n","messages.ftl":'message-cant-embed =\n O Ruffle n\xe3o conseguiu abrir o Flash integrado nesta p\xe1gina.\n Para tentar resolver o problema, pode abrir o ficheiro num novo separador.\npanic-title = Algo correu mal :(\nmore-info = Mais informa\xe7\xf5es\nrun-anyway = Executar mesmo assim\ncontinue = Continuar\nreport-bug = Reportar falha\nupdate-ruffle = Atualizar o Ruffle\nruffle-demo = Demonstra\xe7\xe3o na Web\nruffle-desktop = Aplica\xe7\xe3o para Desktop\nruffle-wiki = Ver a Wiki do Ruffle\nview-error-details = Ver detalhes do erro\nopen-in-new-tab = Abrir num novo separador\nclick-to-unmute = Clique para ativar o som\nerror-file-protocol =\n Parece que executa o Ruffle no protocolo "file:".\n Isto n\xe3o funciona, j\xe1 que os navegadores bloqueiam muitas funcionalidades por raz\xf5es de seguran\xe7a.\n Em vez disto, recomendados configurar um servidor local ou usar a demonstra\xe7\xe3o na web, ou a aplica\xe7\xe3o para desktop.\nerror-javascript-config =\n O Ruffle encontrou um problema maior devido a uma configura\xe7\xe3o de JavaScript incorreta.\n Se \xe9 o administrador do servidor, convidamo-lo a verificar os detalhes do erro para descobrir o par\xe2metro problem\xe1tico.\n Pode ainda consultar a wiki do Ruffle para obter ajuda.\nerror-wasm-not-found =\n O Ruffle falhou ao carregar o componente de ficheiro ".wasm" necess\xe1rio.\n Se \xe9 o administrador do servidor, por favor certifique-se de que o ficheiro foi devidamente carregado.\n Se o problema persistir, poder\xe1 querer usar a configura\xe7\xe3o "publicPath": consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-mime-type =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n Este servidor de web n\xe3o suporta ficheiros ".wasm" com o tipo MIME correto.\n Se \xe9 o administrador do servidor, por favor consulte o wiki do Ruffle para obter ajuda.\nerror-swf-fetch =\n Ruffle falhou ao carregar o arquivo SWF do Flash\n A raz\xe3o mais prov\xe1vel \xe9 que o arquivo n\xe3o existe mais, ent\xe3o n\xe3o h\xe1 nada para o Ruffle carregar.\n Tente contactar o administrador do site para obter ajuda.\nerror-swf-cors =\n O Ruffle falhou ao carregar o ficheiro Flash SWF.\n Acesso a buscar foi provavelmente bloqueado pela pol\xedtica de CORS.\n Se \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-cors =\n O Ruffle falhou ao carregar o componente de ficheiro ".wasm" necess\xe1rio.\n O acesso a buscar foi provavelmente bloqueado pela pol\xedtica CORS.\n Se \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-invalid =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Parece que esta p\xe1gina est\xe1 ausente ou arquivos inv\xe1lidos para executar o Ruffle.\n Se voc\xea \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-download =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n Isto frequentemente resolve-se sozinho, portanto experimente recarregar a p\xe1gina.\n Caso contr\xe1rio, por favor contacte o administrador do site.\nerror-wasm-disabled-on-edge =\n O Ruffle falhou ao carregar o componente de ficheiro ".wasm" necess\xe1rio.\n Para corrigir isso, tente abrir as op\xe7\xf5es do seu navegador, clicando em "Privacidade, pesquisa e servi\xe7os", rolando para baixo e desativando "Melhore a sua seguran\xe7a na web".\n Isto permitir\xe1 ao seu navegador carregar os ficheiros ".wasm" necess\xe1rios.\n Se o problema persistir, talvez seja necess\xe1rio usar um navegador diferente.\nerror-javascript-conflict =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n Parece que esta p\xe1gina usa c\xf3digo JavaScript que entra em conflito com o Ruffle.\n Se \xe9 o administrador do servidor, convidamo-lo a tentar carregar o ficheiro em numa p\xe1gina em branco.\nerror-javascript-conflict-outdated = Pode ainda tentar carregar uma vers\xe3o mais recente do Ruffle que talvez contorne o problema (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\nerror-csp-conflict =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n A Pol\xedtica de Seguran\xe7a de Conte\xfado deste servidor n\xe3o permite que o componente ".wasm" necess\xe1rio seja executado.\n Se \xe9 o administrador do servidor, por favor consulte o wiki do Ruffle para obter ajuda.\nerror-unknown =\n O Ruffle encontrou um problema maior enquanto tentava mostrar este conte\xfado em Flash.\n { $outdated ->\n [true] Se \xe9 o administrador do servidor, por favor tente carregar uma vers\xe3o mais recente do Ruffle (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\n *[false] N\xe3o era suposto isto ter acontecido, por isso agradecer\xedamos muito se pudesse reportar a falha!\n }\n',"save-manager.ftl":"save-delete-prompt = Tem a certeza de que quer apagar esta grava\xe7\xe3o?\nsave-reload-prompt =\n A \xfanica forma de { $action ->\n [delete] apagar\n *[replace] substituir\n } esta grava\xe7\xe3o sem um potencial conflito \xe9 recarregar este conte\xfado. Deseja continuar mesmo assim?\nsave-download = Descarregar\nsave-replace = Substituir\nsave-delete = Apagar\nsave-backup-all = Descarregar todas as grava\xe7\xf5es\n"},"ro-RO":{"context_menu.ftl":"context-menu-download-swf = Descarc\u0103 .swf\ncontext-menu-copy-debug-info = Copia\u021bi informa\u021biile de depanare\ncontext-menu-open-save-manager = Deschide manager de salv\u0103ri\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Despre extensia Ruffle ({ $version })\n *[other] Despre Ruffle ({ $version })\n }\ncontext-menu-hide = Ascunde acest meniu\ncontext-menu-exit-fullscreen = Ie\u0219i\u021bi din ecranul complet\ncontext-menu-enter-fullscreen = Intr\u0103 \xeen ecran complet\n","messages.ftl":'message-cant-embed =\n Ruffle nu a putut rula Flash \xeencorporat \xeen aceast\u0103 pagin\u0103.\n Pute\u021bi \xeencerca s\u0103 deschide\u021bi fi\u0219ierul \xeentr-o fil\u0103 separat\u0103, pentru a evita aceast\u0103 problem\u0103.\npanic-title = Ceva a mers prost :(\nmore-info = Mai multe informatii\nrun-anyway = Ruleaz\u0103 oricum\ncontinue = Continuare\nreport-bug = Raporteaz\u0103 o eroare\nupdate-ruffle = Actualizeaz\u0103\nruffle-demo = Demo Web\nruffle-desktop = Aplica\u021bie desktop\nruffle-wiki = Vezi Ruffle Wiki\nview-error-details = Vezi detaliile de eroare\nopen-in-new-tab = Deschidere in fil\u0103 nou\u0103\nclick-to-unmute = \xcenl\u0103tur\u0103 amu\u021birea\nerror-file-protocol =\n Se pare c\u0103 rula\u021bi Ruffle pe protocolul "fi\u0219ier:".\n Aceasta nu func\u021bioneaz\u0103 ca browsere blocheaz\u0103 multe caracteristici din motive de securitate.\n \xcen schimb, v\u0103 invit\u0103m s\u0103 configura\u021bi un server local sau s\u0103 folosi\u021bi aplica\u021bia web demo sau desktop.\nerror-javascript-config =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 din cauza unei configur\u0103ri incorecte a JavaScript.\n Dac\u0103 sunte\u021bi administratorul serverului, v\u0103 invit\u0103m s\u0103 verifica\u021bi detaliile de eroare pentru a afla care parametru este defect.\n Pute\u021bi consulta \u0219i Ruffle wiki pentru ajutor.\nerror-wasm-not-found =\n Ruffle a e\u0219uat la \xeenc\u0103rcarea componentei de fi\u0219ier ".wasm".\n Dac\u0103 sunte\u021bi administratorul serverului, v\u0103 rug\u0103m s\u0103 v\u0103 asigura\u021bi c\u0103 fi\u0219ierul a fost \xeenc\u0103rcat corect.\n Dac\u0103 problema persist\u0103, poate fi necesar s\u0103 utiliza\u0163i setarea "publicPath": v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-mime-type =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencerca ini\u021bializarea.\n Acest server web nu serve\u0219te ". asm" fi\u0219iere cu tipul corect MIME.\n Dac\u0103 sunte\u021bi administrator de server, v\u0103 rug\u0103m s\u0103 consulta\u021bi Ruffle wiki pentru ajutor.\nerror-swf-fetch =\n Ruffle a e\u0219uat la \xeenc\u0103rcarea fi\u0219ierului Flash SWF.\n Motivul cel mai probabil este c\u0103 fi\u015fierul nu mai exist\u0103, deci nu exist\u0103 nimic pentru Ruffle s\u0103 se \xeencarce.\n \xcencerca\u021bi s\u0103 contacta\u021bi administratorul site-ului web pentru ajutor.\nerror-swf-cors =\n Ruffle a e\u0219uat la \xeenc\u0103rcarea fi\u0219ierului Flash SWF.\n Accesul la preluare a fost probabil blocat de politica CORS.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-cors =\n Ruffle a e\u0219uat \xeen \xeenc\u0103rcarea componentei de fi\u0219ier ".wasm".\n Accesul la preluare a fost probabil blocat de politica CORS.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-invalid =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencearc\u0103 ini\u021bializarea.\n Se pare c\u0103 aceast\u0103 pagin\u0103 are fi\u0219iere lips\u0103 sau invalide pentru rularea Ruffle.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-download =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce \xeencerca s\u0103 ini\u021bializeze.\n Acest lucru se poate rezolva adesea, astfel \xeenc\xe2t pute\u0163i \xeencerca s\u0103 re\xeenc\u0103rca\u0163i pagina.\n Altfel, v\u0103 rug\u0103m s\u0103 contacta\u0163i administratorul site-ului.\nerror-wasm-disabled-on-edge =\n Ruffle nu a putut \xeenc\u0103rca componenta de fi\u0219ier ".wasm".\n Pentru a remedia acest lucru, \xeencerca\u021bi s\u0103 deschide\u021bi set\u0103rile browser-ului dvs., ap\u0103s\xe2nd pe "Confiden\u021bialitate, c\u0103utare \u0219i servicii", derul\xe2nd \xeen jos \u0219i \xeenchiz\xe2nd "\xcembun\u0103t\u0103\u021be\u0219te-\u021bi securitatea pe web".\n Acest lucru va permite browser-ului s\u0103 \xeencarce fi\u0219ierele ".wasm" necesare.\n Dac\u0103 problema persist\u0103, ar putea fi necesar s\u0103 folosi\u021bi un browser diferit.\nerror-javascript-conflict =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce \xeencerca s\u0103 ini\u021bializeze.\n Se pare c\u0103 aceast\u0103 pagin\u0103 folose\u0219te codul JavaScript care intr\u0103 \xeen conflict cu Ruffle.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 invit\u0103m s\u0103 \xeenc\u0103rca\u0163i fi\u015fierul pe o pagin\u0103 goal\u0103.\nerror-javascript-conflict-outdated = De asemenea, po\u021bi \xeencerca s\u0103 \xeencarci o versiune mai recent\u0103 de Ruffle care poate ocoli problema (versiunea curent\u0103 este expirat\u0103: { $buildDate }).\nerror-csp-conflict =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencerca ini\u021bializarea.\n Politica de securitate a con\u021binutului acestui server web nu permite serviciul necesar". asm" component\u0103 pentru a rula.\n Dac\u0103 sunte\u021bi administratorul de server, consulta\u021bi Ruffle wiki pentru ajutor.\nerror-unknown =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencerca afi\u0219area con\u021binutului Flash.\n { $outdated ->\n [true] Dac\u0103 sunte\u021bi administratorul de server, v\u0103 rug\u0103m s\u0103 \xeencerca\u0163i s\u0103 \xeenc\u0103rca\u0163i o versiune mai recent\u0103 de Ruffle (versiunea curent\u0103 este dep\u0103\u015fit\u0103: { $buildDate }).\n *[false] Acest lucru nu ar trebui s\u0103 se \xeent\xe2mple, a\u0219a c\u0103 am aprecia foarte mult dac\u0103 ai putea trimite un bug!\n }\n',"save-manager.ftl":"save-delete-prompt = Sunte\u0163i sigur c\u0103 dori\u0163i s\u0103 \u015fterge\u0163i acest fi\u015fier salvat?\nsave-reload-prompt =\n Singura cale de a { $action ->\n [delete] \u0219terge\n *[replace] \xeenlocuie\u0219te\n } acest fi\u0219ier de salvare f\u0103r\u0103 un conflict poten\u021bial este de a re\xeenc\u0103rca acest con\u021binut. Dori\u021bi s\u0103 continua\u021bi oricum?\nsave-download = Desc\u0103rcare\nsave-replace = \xcenlocuie\u0219te\nsave-delete = \u0218tergere\n"},"ru-RU":{"context_menu.ftl":"context-menu-download-swf = \u0421\u043a\u0430\u0447\u0430\u0442\u044c .swf\ncontext-menu-copy-debug-info = \u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043b\u0430\u0434\u043e\u0447\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e\ncontext-menu-open-save-manager = \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u041e \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0438 Ruffle ({ $version })\n *[other] \u041e Ruffle ({ $version })\n }\ncontext-menu-hide = \u0421\u043a\u0440\u044b\u0442\u044c \u044d\u0442\u043e \u043c\u0435\u043d\u044e\ncontext-menu-exit-fullscreen = \u041e\u043a\u043e\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c\ncontext-menu-enter-fullscreen = \u041f\u043e\u043b\u043d\u043e\u044d\u043a\u0440\u0430\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c\n","messages.ftl":'message-cant-embed =\n Ruffle \u043d\u0435 \u0441\u043c\u043e\u0433 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Flash, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435.\n \u0427\u0442\u043e\u0431\u044b \u043e\u0431\u043e\u0439\u0442\u0438 \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0444\u0430\u0439\u043b \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0435.\npanic-title = \u0427\u0442\u043e-\u0442\u043e \u043f\u043e\u0448\u043b\u043e \u043d\u0435 \u0442\u0430\u043a :(\nmore-info = \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435\nrun-anyway = \u0412\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c\ncontinue = \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c\nreport-bug = \u0421\u043e\u043e\u0431\u0449\u0438\u0442\u044c \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435\nupdate-ruffle = \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c Ruffle\nruffle-demo = \u0412\u0435\u0431-\u0434\u0435\u043c\u043e\nruffle-desktop = \u041d\u0430\u0441\u0442\u043e\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\nruffle-wiki = \u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0438\u043a\u0438 Ruffle\nview-error-details = \u0421\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435\nopen-in-new-tab = \u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432 \u043d\u043e\u0432\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0435\nclick-to-unmute = \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0432\u0443\u043a\nerror-file-protocol =\n \u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u0432\u044b \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0435 Ruffle \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 "file:".\n \u042d\u0442\u043e \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u044b \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u044e\u0442 \u0440\u0430\u0431\u043e\u0442\u0443 \u043c\u043d\u043e\u0433\u0438\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u043f\u043e \u0441\u043e\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.\n \u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u043e\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432\u0435\u0431-\u0434\u0435\u043c\u043e \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440.\nerror-javascript-config =\n \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 JavaScript.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u044f\u0432\u043b\u044f\u0435\u0442\u0435\u0441\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043c\u044b \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0434\u0435\u0442\u0430\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u044f\u0441\u043d\u0438\u0442\u044c, \u043a\u0430\u043a\u043e\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0434\u0430\u043b \u0441\u0431\u043e\u0439.\n \u0412\u044b \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-not-found =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0444\u0430\u0439\u043b\u0430 ".wasm".\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0444\u0430\u0439\u043b \u0431\u044b\u043b \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.\n \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f, \u0432\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 "publicPath": \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-mime-type =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u042d\u0442\u043e\u0442 \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0444\u0430\u0439\u043b\u044b ".wasm" \u0441 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u043c \u0442\u0438\u043f\u043e\u043c MIME.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-swf-fetch =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c SWF-\u0444\u0430\u0439\u043b Flash.\n \u0412\u0435\u0440\u043e\u044f\u0442\u043d\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0444\u0430\u0439\u043b \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 Ruffle \u043d\u0435\u0447\u0435\u0433\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044c.\n \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f \u0441 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0430\u0439\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u043c\u043e\u0449\u0438.\nerror-swf-cors =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c SWF-\u0444\u0430\u0439\u043b Flash.\n \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0444\u0430\u0439\u043b\u0443 \u0431\u044b\u043b \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u043e\u0439 CORS.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-cors =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0444\u0430\u0439\u043b\u0430 ".wasm".\n \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0444\u0430\u0439\u043b\u0443 \u0431\u044b\u043b \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u043e\u0439 CORS.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-invalid =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0444\u0430\u0439\u043b\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 Ruffle \u0438\u043b\u0438 \u043e\u043d\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-download =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u0427\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043e \u044d\u0442\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0443\u0441\u0442\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0441\u0430\u043c\u0430 \u0441\u043e\u0431\u043e\u044e, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.\n \u0415\u0441\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442 \u043f\u043e\u044f\u0432\u043b\u044f\u0442\u044c\u0441\u044f, \u0441\u0432\u044f\u0436\u0438\u0442\u0435\u0441\u044c \u0441 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0430\u0439\u0442\u0430.\nerror-wasm-disabled-on-edge =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0444\u0430\u0439\u043b\u0430 ".wasm".\n \u0427\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0443 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 WASM-\u0444\u0430\u0439\u043b\u044b.\n \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043e\u0441\u0442\u0430\u043b\u0430\u0441\u044c, \u0432\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0431\u0440\u0430\u0443\u0437\u0435\u0440.\nerror-javascript-conflict =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u044d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0438\u0439 \u0441 Ruffle \u043a\u043e\u0434 JavaScript.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u044f\u0432\u043b\u044f\u0435\u0442\u0435\u0441\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043c\u044b \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u043d\u0430 \u043f\u0443\u0441\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435.\nerror-javascript-conflict-outdated = \u0412\u044b \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044e\u044e \u0432\u0435\u0440\u0441\u0438\u044e Ruffle, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u043e\u0439\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 (\u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430: { $buildDate }).\nerror-csp-conflict =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u044d\u0442\u043e\u0433\u043e \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 ".wasm".\n \u0415\u0441\u043b\u0438 \u0432\u044b \u044f\u0432\u043b\u044f\u0435\u0442\u0435\u0441\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-unknown =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0437\u0438\u0442\u044c \u044d\u0442\u043e\u0442 Flash-\u043a\u043e\u043d\u0442\u0435\u043d\u0442.\n { $outdated ->\n [true] \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e Ruffle (\u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430: { $buildDate }).\n *[false] \u042d\u0442\u043e\u0433\u043e \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u0438\u0437\u043d\u0430\u0442\u0435\u043b\u044c\u043d\u044b, \u0435\u0441\u043b\u0438 \u0432\u044b \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043d\u0430\u043c \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435!\n }\n',"save-manager.ftl":"save-delete-prompt = \u0423\u0434\u0430\u043b\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f?\nsave-reload-prompt =\n \u0415\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 { $action ->\n [delete] \u0443\u0434\u0430\u043b\u0438\u0442\u044c\n *[replace] \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c\n } \u044d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0430 \u2013 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442. \u0412\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c?\nsave-download = \u0421\u043a\u0430\u0447\u0430\u0442\u044c\nsave-replace = \u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c\nsave-delete = \u0423\u0434\u0430\u043b\u0438\u0442\u044c\nsave-backup-all = \u0421\u043a\u0430\u0447\u0430\u0442\u044c \u0432\u0441\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\n"},"sk-SK":{"context_menu.ftl":"context-menu-download-swf = Stiahnu\u0165 .swf\ncontext-menu-copy-debug-info = Skop\xedrova\u0165 debug info\ncontext-menu-open-save-manager = Otvori\u0165 spr\xe1vcu ulo\u017een\xed\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] O Ruffle roz\u0161\xedren\xed ({ $version })\n *[other] O Ruffle ({ $version })\n }\ncontext-menu-hide = Skry\u0165 menu\ncontext-menu-exit-fullscreen = Ukon\u010di\u0165 re\u017eim celej obrazovky\ncontext-menu-enter-fullscreen = Prejs\u0165 do re\u017eimu celej obrazovky\n","messages.ftl":'message-cant-embed =\n Ruffle nemohol spusti\u0165 Flash vlo\u017een\xfd na tejto str\xe1nke.\n M\xf4\u017eete sa pok\xfasi\u0165 otvori\u0165 s\xfabor na samostatnej karte, aby ste sa vyhli tomuto probl\xe9mu.\npanic-title = Nie\u010do sa pokazilo :(\nmore-info = Viac inform\xe1ci\xed\nrun-anyway = Spusti\u0165 aj tak\ncontinue = Pokra\u010dova\u0165\nreport-bug = Nahl\xe1si\u0165 chybu\nupdate-ruffle = Aktualizova\u0165 Ruffle\nruffle-demo = Web Demo\nruffle-desktop = Desktopov\xe1 aplik\xe1cia\nruffle-wiki = Zobrazi\u0165 Ruffle Wiki\nview-error-details = Zobrazi\u0165 podrobnosti o chybe\nopen-in-new-tab = Otvori\u0165 na novej karte\nclick-to-unmute = Kliknut\xedm zapnete zvuk\nerror-file-protocol =\n Zd\xe1 sa, \u017ee pou\u017e\xedvate Ruffle na protokole "file:".\n To nie je mo\u017en\xe9, preto\u017ee prehliada\u010de blokuj\xfa fungovanie mnoh\xfdch funkci\xed z bezpe\u010dnostn\xfdch d\xf4vodov.\n Namiesto toho v\xe1m odpor\xfa\u010dame nastavi\u0165 lok\xe1lny server alebo pou\u017ei\u0165 web demo \u010di desktopov\xfa aplik\xe1ciu.\nerror-javascript-config =\n Ruffle narazil na probl\xe9m v d\xf4sledku nespr\xe1vnej konfigur\xe1cie JavaScriptu.\n Ak ste spr\xe1vcom servera, odpor\xfa\u010dame v\xe1m skontrolova\u0165 podrobnosti o chybe, aby ste zistili, ktor\xfd parameter je chybn\xfd.\n Pomoc m\xf4\u017eete z\xedska\u0165 aj na wiki Ruffle.\nerror-wasm-not-found =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 po\u017eadovan\xfd komponent s\xfaboru \u201e.wasm\u201c.\n Ak ste spr\xe1vcom servera, skontrolujte, \u010di bol s\xfabor spr\xe1vne nahran\xfd.\n Ak probl\xe9m pretrv\xe1va, mo\u017eno budete musie\u0165 pou\u017ei\u0165 nastavenie \u201epublicPath\u201c: pomoc n\xe1jdete na wiki Ruffle.\nerror-wasm-mime-type =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Tento webov\xfd server neposkytuje s\xfabory \u201e.wasm\u201c so spr\xe1vnym typom MIME.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-swf-fetch =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 SWF s\xfabor Flash.\n Najpravdepodobnej\u0161\xedm d\xf4vodom je, \u017ee s\xfabor u\u017e neexistuje, tak\u017ee Ruffle nem\xe1 \u010do na\u010d\xedta\u0165.\n Sk\xfaste po\u017eiada\u0165 o pomoc spr\xe1vcu webovej lokality.\nerror-swf-cors =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 SWF s\xfabor Flash.\n Pr\xedstup k na\u010d\xedtaniu bol pravdepodobne zablokovan\xfd politikou CORS.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-wasm-cors =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 po\u017eadovan\xfd komponent s\xfaboru \u201e.wasm\u201c.\n Pr\xedstup k na\u010d\xedtaniu bol pravdepodobne zablokovan\xfd politikou CORS.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-wasm-invalid =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Zd\xe1 sa, \u017ee na tejto str\xe1nke ch\xfdbaj\xfa alebo s\xfa neplatn\xe9 s\xfabory na spustenie Ruffle.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-wasm-download =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Probl\xe9m sa m\xf4\u017ee vyrie\u0161i\u0165 aj s\xe1m, tak\u017ee m\xf4\u017eete sk\xfasi\u0165 str\xe1nku na\u010d\xedta\u0165 znova.\n V opa\u010dnom pr\xedpade kontaktujte administr\xe1tora str\xe1nky.\nerror-wasm-disabled-on-edge =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 po\u017eadovan\xfd komponent s\xfaboru \u201e.wasm\u201c.\n Ak chcete tento probl\xe9m vyrie\u0161i\u0165, sk\xfaste otvori\u0165 nastavenia prehliada\u010da, kliknite na polo\u017eku \u201eOchrana osobn\xfdch \xfadajov, vyh\u013ead\xe1vanie a slu\u017eby\u201c, prejdite nadol a vypnite mo\u017enos\u0165 \u201eZv\xfd\u0161te svoju bezpe\u010dnos\u0165 na webe\u201c.\n V\xe1\u0161mu prehliada\u010du to umo\u017en\xed na\u010d\xedta\u0165 po\u017eadovan\xe9 s\xfabory \u201e.wasm\u201c.\n Ak probl\xe9m pretrv\xe1va, mo\u017eno budete musie\u0165 pou\u017ei\u0165 in\xfd prehliada\u010d.\nerror-javascript-conflict =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Zd\xe1 sa, \u017ee t\xe1to str\xe1nka pou\u017e\xedva k\xf3d JavaScript, ktor\xfd je v konflikte s Ruffle.\n Ak ste spr\xe1vcom servera, odpor\xfa\u010dame v\xe1m sk\xfasi\u0165 na\u010d\xedta\u0165 s\xfabor na pr\xe1zdnu str\xe1nku.\nerror-javascript-conflict-outdated = M\xf4\u017eete sa tie\u017e pok\xfasi\u0165 nahra\u0165 nov\u0161iu verziu Ruffle, ktor\xe1 m\xf4\u017ee dan\xfd probl\xe9m vyrie\u0161i\u0165 (aktu\xe1lny build je zastaran\xfd: { $buildDate }).\nerror-csp-conflict =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Z\xe1sady zabezpe\u010denia obsahu tohto webov\xe9ho servera nepovo\u013euj\xfa spustenie po\u017eadovan\xe9ho komponentu \u201e.wasm\u201c.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-unknown =\n Ruffle narazil na probl\xe9m pri pokuse zobrazi\u0165 tento Flash obsah.\n { $outdated ->\n [true] Ak ste spr\xe1vcom servera, sk\xfaste nahra\u0165 nov\u0161iu verziu Ruffle (aktu\xe1lny build je zastaran\xfd: { $buildDate }).\n *[false] Toto by sa nemalo sta\u0165, tak\u017ee by sme naozaj ocenili, keby ste mohli nahl\xe1si\u0165 chybu!\n }\n',"save-manager.ftl":"save-delete-prompt = Naozaj chcete odstr\xe1ni\u0165 tento s\xfabor s ulo\u017een\xfdmi poz\xedciami?\nsave-reload-prompt =\n Jedin\xfd sp\xf4sob, ako { $action ->\n [delete] vymaza\u0165\n *[replace] nahradi\u0165\n } tento s\xfabor s ulo\u017een\xfdmi poz\xedciami bez potenci\xe1lneho konfliktu je op\xe4tovn\xe9 na\u010d\xedtanie tohto obsahu. Chcete napriek tomu pokra\u010dova\u0165?\nsave-download = Stiahnu\u0165\nsave-replace = Nahradi\u0165\nsave-delete = Vymaza\u0165\nsave-backup-all = Stiahnu\u0165 v\u0161etky s\xfabory s ulo\u017een\xfdmi poz\xedciami\n"},"sv-SE":{"context_menu.ftl":"context-menu-download-swf = Ladda ner .swf\ncontext-menu-copy-debug-info = Kopiera fels\xf6kningsinfo\ncontext-menu-open-save-manager = \xd6ppna Sparhanteraren\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Om Ruffletill\xe4gget ({ $version })\n *[other] Om Ruffle ({ $version })\n }\ncontext-menu-hide = D\xf6lj denna meny\ncontext-menu-exit-fullscreen = Avsluta helsk\xe4rm\ncontext-menu-enter-fullscreen = G\xe5 in i helsk\xe4rm\n","messages.ftl":'message-cant-embed =\n Ruffle kunde inte k\xf6ra Flashinneh\xe5llet som \xe4r inb\xe4ddad p\xe5 denna sida.\n Du kan f\xf6rs\xf6ka \xf6ppna filen i en separat flik f\xf6r att kringg\xe5 problemet.\npanic-title = N\xe5got gick fel :(\nmore-info = Mer info\nrun-anyway = K\xf6r \xe4nd\xe5\ncontinue = Forts\xe4tt\nreport-bug = Rapportera Bugg\nupdate-ruffle = Uppdatera Ruffle\nruffle-demo = Webbdemo\nruffle-desktop = Skrivbordsprogram\nruffle-wiki = Se Rufflewiki\nview-error-details = Visa Felinformation\nopen-in-new-tab = \xd6ppna i ny flik\nclick-to-unmute = Klicka f\xf6r ljud\nerror-file-protocol =\n Det verkar som att du k\xf6r Ruffle p\xe5 "fil:"-protokollet.\n Detta fungerar inte eftersom webbl\xe4sare blockerar m\xe5nga funktioner fr\xe5n att fungera av s\xe4kerhetssk\xe4l.\n Ist\xe4llet bjuder vi in dig att s\xe4tta upp en lokal server eller antingen anv\xe4nda webbdemon eller skrivbordsprogrammet.\nerror-javascript-config =\n Ruffle har st\xf6tt p\xe5 ett stort fel p\xe5 grund av en felaktig JavaScriptkonfiguration.\n Om du \xe4r serveradministrat\xf6ren bjuder vi in dig att kontrollera feldetaljerna f\xf6r att ta reda p\xe5 vilken parameter som \xe4r felaktig.\n Du kan ocks\xe5 konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-not-found =\n Ruffle misslyckades ladda ".wasm"-filkomponenten.\n Om du \xe4r serveradministrat\xf6ren, se till att filen har laddats upp korrekt.\n Om problemet kvarst\xe5r kan du beh\xf6va anv\xe4nda inst\xe4llningen "publicPath": v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-mime-type =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Denna webbserver serverar inte ".wasm"-filer med korrekt MIME-typ.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-swf-fetch =\n Ruffle misslyckades ladda SWF-filen.\n Det mest sannolika sk\xe4let \xe4r att filen inte l\xe4ngre existerar, s\xe5 det finns inget f\xf6r Ruffle att ladda.\n F\xf6rs\xf6k att kontakta webbplatsadministrat\xf6ren f\xf6r hj\xe4lp.\nerror-swf-cors =\n Ruffle misslyckades ladda SWF-filen.\n \xc5tkomst att h\xe4mta har sannolikt blockerats av CORS-policy.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-cors =\n Ruffle misslyckades ladda ".wasm"-filkomponenten.\n \xc5tkomst att h\xe4mta har sannolikt blockerats av CORS-policy.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-invalid =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen\n Det verkar som att den h\xe4r sidan har saknade eller ogiltiga filer f\xf6r att k\xf6ra Ruffle.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-download =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Detta kan ofta l\xf6sas av sig sj\xe4lv, s\xe5 du kan prova att ladda om sidan.\n Annars, kontakta webbplatsens administrat\xf6r.\nerror-wasm-disabled-on-edge =\n Ruffle misslyckades ladda ".wasm"-filkomponenten.\n F\xf6r att \xe5tg\xe4rda detta, f\xf6rs\xf6k att \xf6ppna webbl\xe4sarens inst\xe4llningar, klicka p\xe5 "Sekretess, s\xf6kning och tj\xe4nster", bl\xe4ddra ner och st\xe4ng av "F\xf6rb\xe4ttra s\xe4kerheten p\xe5 webben".\n Detta till\xe5ter din webbl\xe4sare ladda ".wasm"-filerna.\n Om problemet kvarst\xe5r kan du beh\xf6va anv\xe4nda en annan webbl\xe4sare.\nerror-javascript-conflict =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Det verkar som att den h\xe4r sidan anv\xe4nder JavaScriptkod som st\xf6r Ruffle.\n Om du \xe4r serveradministrat\xf6ren bjuder vi in dig att f\xf6rs\xf6ka ladda filen p\xe5 en blank sida.\nerror-javascript-conflict-outdated = Du kan ocks\xe5 f\xf6rs\xf6ka ladda upp en nyare version av Ruffle, vilket kan kringg\xe5 problemet (nuvarande version \xe4r utdaterad: { $buildDate }).\nerror-csp-conflict =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Denna webbservers Content Security Policy till\xe5ter inte ".wasm"-komponenten att k\xf6ra.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-unknown =\n Ruffle har st\xf6tt p\xe5 ett stort fel medan den f\xf6rs\xf6kte visa Flashinneh\xe5llet.\n { $outdated ->\n [true] Om du \xe4r serveradministrat\xf6ren, f\xf6rs\xf6k att ladda upp en nyare version av Ruffle (nuvarande version \xe4r utdaterad: { $buildDate }).\n *[false] Detta \xe4r inte t\xe4nkt att h\xe4nda s\xe5 vi skulle verkligen uppskatta om du kunde rapportera in en bugg!\n }\n',"save-manager.ftl":"save-delete-prompt = \xc4r du s\xe4ker p\xe5 att du vill radera sparfilen?\nsave-reload-prompt =\n Det enda s\xe4ttet att { $action ->\n [delete] radera\n *[replace] ers\xe4tta\n } denna sparfil utan potentiell konflikt \xe4r att ladda om inneh\xe5llet. Vill du forts\xe4tta \xe4nd\xe5?\nsave-download = Ladda ner\nsave-replace = Ers\xe4tt\nsave-delete = Radera\nsave-backup-all = Ladda ner alla sparfiler\n"},"tr-TR":{"context_menu.ftl":"context-menu-download-swf = \u0130ndir .swf\ncontext-menu-copy-debug-info = Hata ay\u0131klama bilgisini kopyala\ncontext-menu-open-save-manager = Kay\u0131t Y\xf6neticisini A\xe7\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Ruffle Uzant\u0131s\u0131 Hakk\u0131nda ({ $version })\n *[other] Ruffle Hakk\u0131nda ({ $version })\n }\ncontext-menu-hide = Bu men\xfcy\xfc gizle\ncontext-menu-exit-fullscreen = Tam ekrandan \xe7\u0131k\ncontext-menu-enter-fullscreen = Tam ekran yap\n","messages.ftl":'message-cant-embed =\n Ruffle, bu sayfaya g\xf6m\xfcl\xfc Flash\'\u0131 \xe7al\u0131\u015ft\u0131ramad\u0131.\n Bu sorunu ortadan kald\u0131rmak i\xe7in dosyay\u0131 ayr\u0131 bir sekmede a\xe7may\u0131 deneyebilirsiniz.\npanic-title = Bir \u015feyler yanl\u0131\u015f gitti :(\nmore-info = Daha fazla bilgi\nrun-anyway = Yine de \xe7al\u0131\u015ft\u0131r\ncontinue = Devam et\nreport-bug = Hata Bildir\nupdate-ruffle = Ruffle\'\u0131 G\xfcncelle\nruffle-demo = A\u011f Demosu\nruffle-desktop = Masa\xfcst\xfc Uygulamas\u0131\nruffle-wiki = Ruffle Wiki\'yi G\xf6r\xfcnt\xfcle\nview-error-details = Hata Ayr\u0131nt\u0131lar\u0131n\u0131 G\xf6r\xfcnt\xfcle\nopen-in-new-tab = Yeni sekmede a\xe7\nclick-to-unmute = Sesi a\xe7mak i\xe7in t\u0131klay\u0131n\nerror-file-protocol =\n G\xf6r\xfcn\xfc\u015fe g\xf6re Ruffle\'\u0131 "dosya:" protokol\xfcnde \xe7al\u0131\u015ft\u0131r\u0131yorsunuz.\n Taray\u0131c\u0131lar g\xfcvenlik nedenleriyle bir\xe7ok \xf6zelli\u011fin \xe7al\u0131\u015fmas\u0131n\u0131 engelledi\u011finden bu i\u015fe yaramaz.\n Bunun yerine, sizi yerel bir sunucu kurmaya veya a\u011f\u0131n demosunu ya da masa\xfcst\xfc uygulamas\u0131n\u0131 kullanmaya davet ediyoruz.\nerror-javascript-config =\n Ruffle, yanl\u0131\u015f bir JavaScript yap\u0131land\u0131rmas\u0131 nedeniyle \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Sunucu y\xf6neticisiyseniz, hangi parametrenin hatal\u0131 oldu\u011funu bulmak i\xe7in sizi hata ayr\u0131nt\u0131lar\u0131n\u0131 kontrol etmeye davet ediyoruz.\n Yard\u0131m i\xe7in Ruffle wiki\'sine de ba\u015fvurabilirsiniz.\nerror-wasm-not-found =\n Ruffle gerekli ".wasm" dosya bile\u015fenini y\xfckleyemedi.\n Sunucu y\xf6neticisi iseniz, l\xfctfen dosyan\u0131n do\u011fru bir \u015fekilde y\xfcklendi\u011finden emin olun.\n Sorun devam ederse, "publicPath" ayar\u0131n\u0131 kullanman\u0131z gerekebilir: yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-mime-type =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Bu web sunucusu, do\u011fru MIME tipinde ".wasm" dosyalar\u0131 sunmuyor.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-swf-fetch =\n Ruffle, Flash SWF dosyas\u0131n\u0131 y\xfckleyemedi.\n Bunun en olas\u0131 nedeni, dosyan\u0131n art\u0131k mevcut olmamas\u0131 ve bu nedenle Ruffle\'\u0131n y\xfckleyece\u011fi hi\xe7bir \u015feyin olmamas\u0131d\u0131r.\n Yard\u0131m i\xe7in web sitesi y\xf6neticisiyle ileti\u015fime ge\xe7meyi deneyin.\nerror-swf-cors =\n Ruffle, Flash SWF dosyas\u0131n\u0131 y\xfckleyemedi.\n Getirme eri\u015fimi muhtemelen CORS politikas\u0131 taraf\u0131ndan engellenmi\u015ftir.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-cors =\n Ruffle gerekli ".wasm" dosya bile\u015fenini y\xfckleyemedi.\n Getirme eri\u015fimi muhtemelen CORS politikas\u0131 taraf\u0131ndan engellenmi\u015ftir.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-invalid =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n G\xf6r\xfcn\xfc\u015fe g\xf6re bu sayfada Ruffle\'\u0131 \xe7al\u0131\u015ft\u0131rmak i\xe7in eksik veya ge\xe7ersiz dosyalar var.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-download =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Bu genellikle kendi kendine \xe7\xf6z\xfclebilir, bu nedenle sayfay\u0131 yeniden y\xfcklemeyi deneyebilirsiniz.\n Aksi takdirde, l\xfctfen site y\xf6neticisiyle ileti\u015fime ge\xe7in.\nerror-wasm-disabled-on-edge =\n Ruffle gerekli ".wasm" dosya bile\u015fenini y\xfckleyemedi.\n Bunu d\xfczeltmek i\xe7in taray\u0131c\u0131n\u0131z\u0131n ayarlar\u0131n\u0131 a\xe7\u0131n, "Gizlilik, arama ve hizmetler"i t\u0131klay\u0131n, a\u015fa\u011f\u0131 kayd\u0131r\u0131n ve "Web\'de g\xfcvenli\u011finizi art\u0131r\u0131n"\u0131 kapatmay\u0131 deneyin.\n Bu, taray\u0131c\u0131n\u0131z\u0131n gerekli ".wasm" dosyalar\u0131n\u0131 y\xfcklemesine izin verecektir.\n Sorun devam ederse, farkl\u0131 bir taray\u0131c\u0131 kullanman\u0131z gerekebilir.\nerror-javascript-conflict =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n G\xf6r\xfcn\xfc\u015fe g\xf6re bu sayfa, Ruffle ile \xe7ak\u0131\u015fan JavaScript kodu kullan\u0131yor.\n Sunucu y\xf6neticisiyseniz, sizi dosyay\u0131 bo\u015f bir sayfaya y\xfcklemeyi denemeye davet ediyoruz.\nerror-javascript-conflict-outdated = Ayr\u0131ca sorunu giderebilecek daha yeni bir Ruffle s\xfcr\xfcm\xfc y\xfcklemeyi de deneyebilirsiniz (mevcut yap\u0131m eskimi\u015f: { $buildDate }).\nerror-csp-conflict =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Bu web sunucusunun \u0130\xe7erik G\xfcvenli\u011fi Politikas\u0131, gerekli ".wasm" bile\u015feninin \xe7al\u0131\u015fmas\u0131na izin vermiyor.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine bak\u0131n.\nerror-unknown =\n Ruffle, bu Flash i\xe7eri\u011fini g\xf6r\xfcnt\xfclemeye \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n { $outdated ->\n [true] Sunucu y\xf6neticisiyseniz, l\xfctfen Ruffle\'\u0131n daha yeni bir s\xfcr\xfcm\xfcn\xfc y\xfcklemeyi deneyin (mevcut yap\u0131m eskimi\u015f: { $buildDate }).\n *[false] Bunun olmamas\u0131 gerekiyor, bu y\xfczden bir hata bildirebilirseniz \xe7ok memnun oluruz!\n }\n',"save-manager.ftl":"save-delete-prompt = Bu kay\u0131t dosyas\u0131n\u0131 silmek istedi\u011finize emin misiniz?\nsave-reload-prompt =\n Bu kaydetme dosyas\u0131n\u0131 potansiyel \xe7ak\u0131\u015fma olmadan { $action ->\n [delete] silmenin\n *[replace] de\u011fi\u015ftirmenin\n } tek yolu, bu i\xe7eri\u011fi yeniden y\xfcklemektir. Yine de devam etmek istiyor musunuz?\nsave-download = \u0130ndir\nsave-replace = De\u011fi\u015ftir\nsave-delete = Sil\nsave-backup-all = T\xfcm kay\u0131t dosyalar\u0131n\u0131 indir\n"},"zh-CN":{"context_menu.ftl":"context-menu-download-swf = \u4e0b\u8f7d .swf\ncontext-menu-copy-debug-info = \u590d\u5236\u8c03\u8bd5\u4fe1\u606f\ncontext-menu-open-save-manager = \u6253\u5f00\u5b58\u6863\u7ba1\u7406\u5668\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u5173\u4e8e Ruffle \u6269\u5c55 ({ $version })\n *[other] \u5173\u4e8e Ruffle ({ $version })\n }\ncontext-menu-hide = \u9690\u85cf\u6b64\u83dc\u5355\ncontext-menu-exit-fullscreen = \u9000\u51fa\u5168\u5c4f\ncontext-menu-enter-fullscreen = \u8fdb\u5165\u5168\u5c4f\n","messages.ftl":'message-cant-embed =\n Ruffle \u65e0\u6cd5\u8fd0\u884c\u5d4c\u5165\u5728\u6b64\u9875\u9762\u4e2d\u7684 Flash\u3002\n \u60a8\u53ef\u4ee5\u5c1d\u8bd5\u5728\u5355\u72ec\u7684\u6807\u7b7e\u9875\u4e2d\u6253\u5f00\u8be5\u6587\u4ef6\uff0c\u4ee5\u56de\u907f\u6b64\u95ee\u9898\u3002\npanic-title = \u51fa\u4e86\u4e9b\u95ee\u9898 :(\nmore-info = \u66f4\u591a\u4fe1\u606f\nrun-anyway = \u4ecd\u7136\u8fd0\u884c\ncontinue = \u7ee7\u7eed\nreport-bug = \u53cd\u9988\u95ee\u9898\nupdate-ruffle = \u66f4\u65b0 Ruffle\nruffle-demo = \u7f51\u9875\u6f14\u793a\nruffle-desktop = \u684c\u9762\u5e94\u7528\u7a0b\u5e8f\nruffle-wiki = \u67e5\u770b Ruffle Wiki\nview-error-details = \u67e5\u770b\u9519\u8bef\u8be6\u60c5\nopen-in-new-tab = \u5728\u65b0\u6807\u7b7e\u9875\u4e2d\u6253\u5f00\nclick-to-unmute = \u70b9\u51fb\u53d6\u6d88\u9759\u97f3\nerror-file-protocol =\n \u770b\u6765\u60a8\u6b63\u5728 "file:" \u534f\u8bae\u4e0a\u4f7f\u7528 Ruffle\u3002\n \u7531\u4e8e\u6d4f\u89c8\u5668\u4ee5\u5b89\u5168\u539f\u56e0\u963b\u6b62\u8bb8\u591a\u529f\u80fd\uff0c\u56e0\u6b64\u8fd9\u4e0d\u8d77\u4f5c\u7528\u3002\n \u76f8\u53cd\u6211\u4eec\u9080\u8bf7\u60a8\u8bbe\u7f6e\u672c\u5730\u670d\u52a1\u5668\u6216\u4f7f\u7528\u7f51\u9875\u6f14\u793a\u6216\u684c\u9762\u5e94\u7528\u7a0b\u5e8f\u3002\nerror-javascript-config =\n \u7531\u4e8e\u9519\u8bef\u7684 JavaScript \u914d\u7f6e\uff0cRuffle \u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u6211\u4eec\u9080\u8bf7\u60a8\u68c0\u67e5\u9519\u8bef\u8be6\u7ec6\u4fe1\u606f\uff0c\u4ee5\u627e\u51fa\u54ea\u4e2a\u53c2\u6570\u6709\u6545\u969c\u3002\n \u60a8\u4e5f\u53ef\u4ee5\u67e5\u9605 Ruffle \u7684 Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-not-found =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d\u6240\u9700\u7684 \u201c.wasm\u201d \u6587\u4ef6\u7ec4\u4ef6\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u786e\u4fdd\u6587\u4ef6\u5df2\u6b63\u786e\u4e0a\u4f20\u3002\n \u5982\u679c\u95ee\u9898\u4ecd\u7136\u5b58\u5728\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u4f7f\u7528 \u201cpublicPath\u201d \u8bbe\u7f6e\uff1a\u8bf7\u67e5\u770b Ruffle \u7684 Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-mime-type =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8be5\u7f51\u7ad9\u670d\u52a1\u5668\u6ca1\u6709\u63d0\u4f9b ".asm\u201d \u6587\u4ef6\u6b63\u786e\u7684 MIME \u7c7b\u578b\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-swf-fetch =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d Flash SWF \u6587\u4ef6\u3002\n \u6700\u53ef\u80fd\u7684\u539f\u56e0\u662f\u6587\u4ef6\u4e0d\u518d\u5b58\u5728\u6240\u4ee5 Ruffle \u6ca1\u6709\u8981\u52a0\u8f7d\u7684\u5185\u5bb9\u3002\n \u8bf7\u5c1d\u8bd5\u8054\u7cfb\u7f51\u7ad9\u7ba1\u7406\u5458\u5bfb\u6c42\u5e2e\u52a9\u3002\nerror-swf-cors =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d Flash SWF \u6587\u4ef6\u3002\n \u83b7\u53d6\u6743\u9650\u53ef\u80fd\u88ab CORS \u7b56\u7565\u963b\u6b62\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u53c2\u8003 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-cors =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d\u6240\u9700\u7684\u201c.wasm\u201d\u6587\u4ef6\u7ec4\u4ef6\u3002\n \u83b7\u53d6\u6743\u9650\u53ef\u80fd\u88ab CORS \u7b56\u7565\u963b\u6b62\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-invalid =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8fd9\u4e2a\u9875\u9762\u4f3c\u4e4e\u7f3a\u5c11\u6587\u4ef6\u6765\u8fd0\u884c Curl\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-download =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8fd9\u901a\u5e38\u53ef\u4ee5\u81ea\u884c\u89e3\u51b3\uff0c\u56e0\u6b64\u60a8\u53ef\u4ee5\u5c1d\u8bd5\u91cd\u65b0\u52a0\u8f7d\u9875\u9762\u3002\n \u5426\u5219\u8bf7\u8054\u7cfb\u7f51\u7ad9\u7ba1\u7406\u5458\u3002\nerror-wasm-disabled-on-edge =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d\u6240\u9700\u7684 \u201c.wasm\u201d \u6587\u4ef6\u7ec4\u4ef6\u3002\n \u8981\u89e3\u51b3\u8fd9\u4e2a\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6253\u5f00\u60a8\u7684\u6d4f\u89c8\u5668\u8bbe\u7f6e\uff0c\u5355\u51fb"\u9690\u79c1\u3001\u641c\u7d22\u548c\u670d\u52a1"\uff0c\u5411\u4e0b\u6eda\u52a8\u5e76\u5173\u95ed"\u589e\u5f3a\u60a8\u7684\u7f51\u7edc\u5b89\u5168"\u3002\n \u8fd9\u5c06\u5141\u8bb8\u60a8\u7684\u6d4f\u89c8\u5668\u52a0\u8f7d\u6240\u9700\u7684 \u201c.wasm\u201d \u6587\u4ef6\u3002\n \u5982\u679c\u95ee\u9898\u4ecd\u7136\u5b58\u5728\uff0c\u60a8\u53ef\u80fd\u5fc5\u987b\u4f7f\u7528\u4e0d\u540c\u7684\u6d4f\u89c8\u5668\u3002\nerror-javascript-conflict =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8fd9\u4e2a\u9875\u9762\u4f3c\u4e4e\u4f7f\u7528\u4e86\u4e0e Ruffle \u51b2\u7a81\u7684 JavaScript \u4ee3\u7801\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u6211\u4eec\u5efa\u8bae\u60a8\u5c1d\u8bd5\u5728\u7a7a\u767d\u9875\u9762\u4e0a\u52a0\u8f7d\u6587\u4ef6\u3002\nerror-javascript-conflict-outdated = \u60a8\u8fd8\u53ef\u4ee5\u5c1d\u8bd5\u4e0a\u4f20\u53ef\u80fd\u89c4\u907f\u8be5\u95ee\u9898\u7684\u6700\u65b0\u7248\u672c\u7684 (\u5f53\u524d\u6784\u5efa\u5df2\u8fc7\u65f6: { $buildDate })\u3002\nerror-csp-conflict =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8be5\u7f51\u7ad9\u670d\u52a1\u5668\u7684\u5185\u5bb9\u5b89\u5168\u7b56\u7565\u4e0d\u5141\u8bb8\u8fd0\u884c\u6240\u9700\u7684 \u201c.wasm\u201d \u7ec4\u4ef6\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-unknown =\n Ruffle \u5728\u8bd5\u56fe\u663e\u793a\u6b64 Flash \u5185\u5bb9\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n { $outdated ->\n [true] \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u5c1d\u8bd5\u4e0a\u4f20\u66f4\u65b0\u7684 Ruffle \u7248\u672c (\u5f53\u524d\u7248\u672c\u5df2\u8fc7\u65f6: { $buildDate }).\n *[false] \u8fd9\u4e0d\u5e94\u8be5\u53d1\u751f\uff0c\u56e0\u6b64\u5982\u679c\u60a8\u53ef\u4ee5\u62a5\u544a\u9519\u8bef\uff0c\u6211\u4eec\u5c06\u975e\u5e38\u611f\u8c22\uff01\n }\n',"save-manager.ftl":"save-delete-prompt = \u786e\u5b9a\u8981\u5220\u9664\u6b64\u5b58\u6863\u5417\uff1f\nsave-reload-prompt =\n \u4e3a\u4e86\u907f\u514d\u6f5c\u5728\u7684\u51b2\u7a81\uff0c{ $action ->\n [delete] \u5220\u9664\n *[replace] \u66ff\u6362\n } \u6b64\u5b58\u6863\u6587\u4ef6\u9700\u8981\u91cd\u65b0\u52a0\u8f7d\u5f53\u524d\u5185\u5bb9\u3002\u662f\u5426\u4ecd\u7136\u7ee7\u7eed\uff1f\nsave-download = \u4e0b\u8f7d\nsave-replace = \u66ff\u6362\nsave-delete = \u5220\u9664\nsave-backup-all = \u4e0b\u8f7d\u6240\u6709\u5b58\u6863\u6587\u4ef6\n"},"zh-TW":{"context_menu.ftl":"context-menu-download-swf = \u4e0b\u8f09SWF\u6a94\u6848\ncontext-menu-copy-debug-info = \u8907\u88fd\u9664\u932f\u8cc7\u8a0a\ncontext-menu-open-save-manager = \u6253\u958b\u5b58\u6a94\u7ba1\u7406\u5668\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u95dc\u65bcRuffle\u64f4\u5145\u529f\u80fd ({ $version })\n *[other] \u95dc\u65bcRuffle ({ $version })\n }\ncontext-menu-hide = \u96b1\u85cf\u83dc\u55ae\ncontext-menu-exit-fullscreen = \u9000\u51fa\u5168\u87a2\u5e55\ncontext-menu-enter-fullscreen = \u9032\u5165\u5168\u87a2\u5e55\n","messages.ftl":'message-cant-embed =\n \u76ee\u524dRuffle\u6c92\u8fa6\u6cd5\u57f7\u884c\u5d4c\u5165\u5f0fFlash\u3002\n \u4f60\u53ef\u4ee5\u5728\u65b0\u5206\u9801\u4e2d\u958b\u555f\u4f86\u89e3\u6c7a\u9019\u500b\u554f\u984c\u3002\npanic-title = \u5b8c\u86cb\uff0c\u51fa\u554f\u984c\u4e86 :(\nmore-info = \u66f4\u591a\u8cc7\u8a0a\nrun-anyway = \u76f4\u63a5\u57f7\u884c\ncontinue = \u7e7c\u7e8c\nreport-bug = \u56de\u5831BUG\nupdate-ruffle = \u66f4\u65b0Ruffle\nruffle-demo = \u7db2\u9801\u5c55\u793a\nruffle-desktop = \u684c\u9762\u61c9\u7528\u7a0b\u5f0f\nruffle-wiki = \u67e5\u770bRuffle Wiki\nview-error-details = \u6aa2\u8996\u932f\u8aa4\u8a73\u7d30\u8cc7\u6599\nopen-in-new-tab = \u958b\u555f\u65b0\u589e\u5206\u9801\nclick-to-unmute = \u9ede\u64ca\u4ee5\u53d6\u6d88\u975c\u97f3\nerror-file-protocol =\n \u770b\u8d77\u4f86\u4f60\u60f3\u8981\u7528Ruffle\u4f86\u57f7\u884c"file:"\u7684\u5354\u8b70\u3002\n \u56e0\u70ba\u700f\u89bd\u5668\u7981\u4e86\u5f88\u591a\u529f\u80fd\u4ee5\u8cc7\u5b89\u7684\u7406\u7531\u4f86\u8b1b\u3002\n \u6211\u5011\u5efa\u8b70\u4f60\u5efa\u7acb\u672c\u5730\u4f3a\u670d\u5668\u6216\u8457\u76f4\u63a5\u4f7f\u7528\u7db2\u9801\u5c55\u793a\u6216\u684c\u9762\u61c9\u7528\u7a0b\u5f0f\u3002\nerror-javascript-config =\n \u76ee\u524dRuffle\u9047\u5230\u4e0d\u6b63\u78ba\u7684JavaScript\u914d\u7f6e\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u6211\u5011\u5efa\u8b70\u4f60\u6aa2\u67e5\u54ea\u500b\u74b0\u7bc0\u51fa\u932f\u3002\n \u6216\u8457\u4f60\u53ef\u4ee5\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-not-found =\n \u76ee\u524dRuffle\u627e\u4e0d\u5230".wasm"\u6a94\u6848\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u78ba\u4fdd\u6a94\u6848\u662f\u5426\u653e\u5c0d\u4f4d\u7f6e\u3002\n \u5982\u679c\u9084\u662f\u6709\u554f\u984c\u7684\u8a71\uff0c\u4f60\u8981\u7528"publicPath"\u4f86\u8a2d\u5b9a: \u6216\u8457\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-mime-type =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u9019\u7db2\u9801\u4f3a\u670d\u5668\u4e26\u6c92\u6709\u670d\u52d9".wasm"\u6a94\u6848\u6216\u6b63\u78ba\u7684\u7db2\u969b\u7db2\u8def\u5a92\u9ad4\u985e\u578b\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-swf-fetch =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6Flash\u7684SWF\u6a94\u6848\u3002\n \u5f88\u6709\u53ef\u80fd\u8981\u8b80\u53d6\u7684\u6a94\u6848\u4e0d\u5b58\u5728\uff0c\u6240\u4ee5Ruffle\u8b80\u4e0d\u5230\u6771\u897f\u3002\n \u8acb\u5617\u8a66\u6e9d\u901a\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-swf-cors =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6Flash\u7684SWF\u6a94\u6848\u3002\n \u770b\u8d77\u4f86\u662f\u4f7f\u7528\u6b0a\u88ab\u8de8\u4f86\u6e90\u8cc7\u6e90\u5171\u7528\u6a5f\u5236\u88ab\u64cb\u5230\u4e86\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-cors =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6".wasm"\u6a94\u6848\u3002\n \u770b\u8d77\u4f86\u662f\u4f7f\u7528\u6b0a\u88ab\u8de8\u4f86\u6e90\u8cc7\u6e90\u5171\u7528\u6a5f\u5236\u88ab\u64cb\u5230\u4e86\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-invalid =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u770b\u8d77\u4f86\u9019\u7db2\u9801\u6709\u7f3a\u5931\u6a94\u6848\u5c0e\u81f4Ruffle\u7121\u6cd5\u904b\u884c\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-download =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u9019\u53ef\u4ee5\u4f60\u81ea\u5df1\u89e3\u6c7a\uff0c\u4f60\u53ea\u8981\u91cd\u65b0\u6574\u7406\u5c31\u597d\u4e86\u3002\n \u5426\u5247\uff0c\u8acb\u5617\u8a66\u6e9d\u901a\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-disabled-on-edge =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6".wasm"\u6a94\u6848\u3002\n \u8981\u4fee\u6b63\u7684\u8a71\uff0c\u6253\u958b\u4f60\u7684\u700f\u89bd\u5668\u8a2d\u5b9a\uff0c\u9ede\u9078"\u96b1\u79c1\u6b0a\u3001\u641c\u5c0b\u8207\u670d\u52d9"\uff0c\u628a"\u9632\u6b62\u8ffd\u8e64"\u7d66\u95dc\u6389\u3002\n \u9019\u6a23\u4e00\u4f86\u4f60\u7684\u700f\u89bd\u5668\u6703\u8b80\u53d6\u9700\u8981\u7684".wasm"\u6a94\u6848\u3002\n \u5982\u679c\u554f\u984c\u4e00\u76f4\u9084\u5728\u7684\u8a71\uff0c\u4f60\u5fc5\u9808\u8981\u63db\u700f\u89bd\u5668\u4e86\u3002\nerror-javascript-conflict =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u770b\u8d77\u4f86\u9019\u7db2\u9801\u4f7f\u7528\u7684JavaScript\u6703\u8ddfRuffle\u8d77\u885d\u7a81\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u6211\u5011\u5efa\u8b70\u4f60\u958b\u500b\u7a7a\u767d\u9801\u4f86\u6e2c\u8a66\u3002\nerror-javascript-conflict-outdated = \u4f60\u4e5f\u53ef\u4ee5\u4e0a\u50b3\u6700\u65b0\u7248\u7684Ruffle\uff0c\u8aaa\u4e0d\u5b9a\u4f60\u8981\u8aaa\u7684\u7684\u554f\u984c\u5df2\u7d93\u4e0d\u898b\u4e86(\u73fe\u5728\u4f7f\u7528\u7684\u7248\u672c\u5df2\u7d93\u904e\u6642: { $buildDate })\u3002\nerror-csp-conflict =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u9019\u7db2\u9801\u4f3a\u670d\u5668\u88ab\u8de8\u4f86\u6e90\u8cc7\u6e90\u5171\u7528\u6a5f\u5236\u7981\u6b62\u8b80\u53d6".wasm"\u6a94\u6848\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-unknown =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u8981\u8b80\u53d6Flash\u5167\u5bb9\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\n { $outdated ->\n [true] \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c \u8acb\u4e0a\u50b3\u6700\u65b0\u7248\u7684Ruffle(\u73fe\u5728\u4f7f\u7528\u7684\u7248\u672c\u5df2\u7d93\u904e\u6642: { $buildDate }).\n *[false] \u9019\u4e0d\u61c9\u8a72\u767c\u751f\u7684\uff0c\u6211\u5011\u4e5f\u5f88\u9ad8\u8208\u4f60\u544a\u77e5bug!\n }\n',"save-manager.ftl":"save-delete-prompt = \u4f60\u78ba\u5b9a\u8981\u522a\u9664\u9019\u500b\u5b58\u6a94\u55ce\uff1f\nsave-reload-prompt =\n \u552f\u4e00\u65b9\u6cd5\u53ea\u6709 { $action ->\n [delete] \u522a\u9664\n *[replace] \u53d6\u4ee3\n } \u9019\u500b\u5b58\u6a94\u4e0d\u6703\u5b8c\u5168\u53d6\u4ee3\u76f4\u5230\u91cd\u65b0\u555f\u52d5. \u4f60\u9700\u8981\u7e7c\u7e8c\u55ce?\nsave-download = \u4e0b\u8f09\nsave-replace = \u53d6\u4ee3\nsave-delete = \u522a\u9664\nsave-backup-all = \u4e0b\u8f09\u6240\u6709\u5b58\u6a94\u6a94\u6848\u3002\n"}},je={};for(const[e,n]of Object.entries(Ee)){const t=new K(e);if(n)for(const[r,a]of Object.entries(n))if(a)for(const n of t.addResource(new ke(a)))console.error(`Error in text for ${e} ${r}: ${n}`);je[e]=t}function Ae(e,n,t){const r=je[e];if(void 0!==r){const e=r.getMessage(n);if(void 0!==e&&e.value)return r.formatPattern(e.value,t)}return null}function Ce(e,n){const t=ze(navigator.languages,Object.keys(je),{defaultLocale:"en-US"});for(const r in t){const a=Ae(t[r],e,n);if(a)return a}return console.error(`Unknown text key '${e}'`),e}function Ie(e,n){const t=document.createElement("div");return Ce(e,n).split("\n").forEach((e=>{const n=document.createElement("p");n.innerText=e,t.appendChild(n)})),t.innerHTML}var Oe=a(297),De=a.n(Oe);const Fe="https://ruffle.rs",Pe=/^\s*(\d+(\.\d+)?(%)?)/;let Te=!1;function $e(e){if(null==e)return{};e instanceof URLSearchParams||(e=new URLSearchParams(e));const n={};for(const[t,r]of e)n[t]=r.toString();return n}class Be{constructor(e,n){this.x=e,this.y=n}distanceTo(e){const n=e.x-this.x,t=e.y-this.y;return Math.sqrt(n*n+t*t)}}class Me extends HTMLElement{get readyState(){return this._readyState}get metadata(){return this._metadata}constructor(){super(),this.contextMenuForceDisabled=!1,this.isTouch=!1,this.contextMenuSupported=!1,this.panicked=!1,this.rendererDebugInfo="",this.longPressTimer=null,this.pointerDownPosition=null,this.pointerMoveMaxDistance=0,this.config={},this.shadow=this.attachShadow({mode:"open"}),this.shadow.appendChild(p.content.cloneNode(!0)),this.dynamicStyles=this.shadow.getElementById("dynamic_styles"),this.container=this.shadow.getElementById("container"),this.playButton=this.shadow.getElementById("play_button"),this.playButton.addEventListener("click",(()=>this.play())),this.unmuteOverlay=this.shadow.getElementById("unmute_overlay"),this.splashScreen=this.shadow.getElementById("splash-screen"),this.virtualKeyboard=this.shadow.getElementById("virtual-keyboard"),this.virtualKeyboard.addEventListener("input",this.virtualKeyboardInput.bind(this)),this.saveManager=this.shadow.getElementById("save-manager"),this.saveManager.addEventListener("click",(()=>this.saveManager.classList.add("hidden")));const e=this.saveManager.querySelector("#modal-area");e&&e.addEventListener("click",(e=>e.stopPropagation()));const n=this.saveManager.querySelector("#close-modal");n&&n.addEventListener("click",(()=>this.saveManager.classList.add("hidden")));const t=this.saveManager.querySelector("#backup-saves");t&&(t.addEventListener("click",this.backupSaves.bind(this)),t.innerText=Ce("save-backup-all"));const r=this.unmuteOverlay.querySelector("#unmute_overlay_svg");if(r){r.querySelector("#unmute_text").textContent=Ce("click-to-unmute")}this.contextMenuOverlay=this.shadow.getElementById("context-menu-overlay"),this.contextMenuElement=this.shadow.getElementById("context-menu"),window.addEventListener("pointerdown",this.checkIfTouch.bind(this)),this.addEventListener("contextmenu",this.showContextMenu.bind(this)),this.container.addEventListener("pointerdown",this.pointerDown.bind(this)),this.container.addEventListener("pointermove",this.checkLongPressMovement.bind(this)),this.container.addEventListener("pointerup",this.checkLongPress.bind(this)),this.container.addEventListener("pointercancel",this.clearLongPressTimer.bind(this)),this.addEventListener("fullscreenchange",this.fullScreenChange.bind(this)),this.addEventListener("webkitfullscreenchange",this.fullScreenChange.bind(this)),this.instance=null,this.onFSCommand=null,this._readyState=0,this._metadata=null,this.lastActivePlayingState=!1,this.setupPauseOnTabHidden()}setupPauseOnTabHidden(){document.addEventListener("visibilitychange",(()=>{this.instance&&(document.hidden&&(this.lastActivePlayingState=this.instance.is_playing(),this.instance.pause()),document.hidden||!0!==this.lastActivePlayingState||this.instance.play())}),!1)}connectedCallback(){this.updateStyles()}static get observedAttributes(){return["width","height"]}attributeChangedCallback(e,n,t){"width"!==e&&"height"!==e||this.updateStyles()}disconnectedCallback(){this.destroy()}updateStyles(){if(this.dynamicStyles.sheet){if(this.dynamicStyles.sheet.rules)for(let e=0;e{if(console.error(`Serious error loading Ruffle: ${e}`),"file:"===window.location.protocol)e.ruffleIndexError=2;else{e.ruffleIndexError=9;const n=String(e.message).toLowerCase();n.includes("mime")?e.ruffleIndexError=8:n.includes("networkerror")||n.includes("failed to fetch")?e.ruffleIndexError=6:n.includes("disallowed by embedder")?e.ruffleIndexError=1:"CompileError"===e.name?e.ruffleIndexError=3:n.includes("could not download wasm module")&&"TypeError"===e.name?e.ruffleIndexError=7:"TypeError"===e.name?e.ruffleIndexError=5:navigator.userAgent.includes("Edg")&&n.includes("webassembly is not defined")&&(e.ruffleIndexError=10)}throw this.panic(e),e}));this.instance=await new n(this.container,this,this.loadedConfig),this.rendererDebugInfo=this.instance.renderer_debug_info();const t=this.instance.renderer_name();if(console.log("%cNew Ruffle instance created (WebAssembly extensions: "+(n.is_wasm_simd_used()?"ON":"OFF")+" | Used renderer: "+(null!=t?t:"")+")","background: #37528C; color: #FFAD33"),"running"!==this.audioState()&&(this.container.style.visibility="hidden",await new Promise((e=>{window.setTimeout((()=>{e()}),200)})),this.container.style.visibility=""),this.unmuteAudioContext(),navigator.userAgent.toLowerCase().includes("android")&&this.container.addEventListener("click",(()=>this.virtualKeyboard.blur())),!this.loadedConfig||"on"===this.loadedConfig.autoplay||"off"!==this.loadedConfig.autoplay&&"running"===this.audioState()){if(this.play(),"running"!==this.audioState()){this.loadedConfig&&"hidden"===this.loadedConfig.unmuteOverlay||(this.unmuteOverlay.style.display="block"),this.container.addEventListener("click",this.unmuteOverlayClicked.bind(this),{once:!0});const n=null===(e=this.instance)||void 0===e?void 0:e.audio_context();n&&(n.onstatechange=()=>{"running"===n.state&&this.unmuteOverlayClicked(),n.onstatechange=null})}}else this.playButton.style.display="block"}onRuffleDownloadProgress(e,n){const t=this.splashScreen.querySelector(".loadbar-inner"),r=this.splashScreen.querySelector(".loadbar");Number.isNaN(n)?r&&(r.style.display="none"):t.style.width=e/n*100+"%"}destroy(){this.instance&&(this.instance.destroy(),this.instance=null,this._metadata=null,this._readyState=0,console.log("Ruffle instance destroyed."))}checkOptions(e){if("string"==typeof e)return{url:e};const n=(e,n)=>{if(!e){const e=new TypeError(n);throw e.ruffleIndexError=4,this.panic(e),e}};return n(null!==e&&"object"==typeof e,"Argument 0 must be a string or object"),n("url"in e||"data"in e,"Argument 0 must contain a `url` or `data` key"),n(!("url"in e)||"string"==typeof e.url,"`url` must be a string"),e}getExtensionConfig(){var e;return window.RufflePlayer&&window.RufflePlayer.conflict&&("extension"===window.RufflePlayer.conflict.newestName||"extension"===window.RufflePlayer.newestName)?null===(e=window.RufflePlayer)||void 0===e?void 0:e.conflict.config:{}}async load(e){var n,t;if(e=this.checkOptions(e),this.isConnected&&!this.isUnusedFallbackObject()){if(!We(this))try{const r=this.getExtensionConfig();this.loadedConfig=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},b),r),null!==(t=null===(n=window.RufflePlayer)||void 0===n?void 0:n.config)&&void 0!==t?t:{}),this.config),e),this.loadedConfig.backgroundColor&&"transparent"!==this.loadedConfig.wmode&&(this.container.style.backgroundColor=this.loadedConfig.backgroundColor),await this.ensureFreshInstance(),"url"in e?(console.log(`Loading SWF file ${e.url}`),this.swfUrl=new URL(e.url,document.baseURI),this.instance.stream_from(this.swfUrl.href,$e(e.parameters))):"data"in e&&(console.log("Loading SWF data"),this.instance.load_data(new Uint8Array(e.data),$e(e.parameters),e.swfFileName||"movie.swf"))}catch(e){console.error(`Serious error occurred loading SWF file: ${e}`);const n=new Error(e);throw n.message.includes("Error parsing config")&&(n.ruffleIndexError=4),this.panic(n),n}}else console.warn("Ignoring attempt to play a disconnected or suspended Ruffle element")}play(){this.instance&&(this.instance.play(),this.playButton.style.display="none")}get isPlaying(){return!!this.instance&&this.instance.is_playing()}get volume(){return this.instance?this.instance.volume():1}set volume(e){this.instance&&this.instance.set_volume(e)}get fullscreenEnabled(){return!(!document.fullscreenEnabled&&!document.webkitFullscreenEnabled)}get isFullscreen(){return(document.fullscreenElement||document.webkitFullscreenElement)===this}setFullscreen(e){this.fullscreenEnabled&&(e?this.enterFullscreen():this.exitFullscreen())}enterFullscreen(){const e={navigationUI:"hide"};this.requestFullscreen?this.requestFullscreen(e):this.webkitRequestFullscreen?this.webkitRequestFullscreen(e):this.webkitRequestFullScreen&&this.webkitRequestFullScreen(e)}exitFullscreen(){document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.webkitCancelFullScreen&&document.webkitCancelFullScreen()}fullScreenChange(){var e;null===(e=this.instance)||void 0===e||e.set_fullscreen(this.isFullscreen)}saveFile(e,n){const t=URL.createObjectURL(e),r=document.createElement("a");r.href=t,r.style.display="none",r.download=n,document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(t)}checkIfTouch(e){this.isTouch="touch"===e.pointerType||"pen"===e.pointerType}base64ToBlob(e,n){const t=atob(e),r=new ArrayBuffer(t.length),a=new Uint8Array(r);for(let e=0;e{if(r.result&&"string"==typeof r.result){const e=new RegExp("data:.*;base64,"),t=r.result.replace(e,"");this.confirmReloadSave(n,t,!0)}})),t&&t.files&&t.files.length>0&&t.files[0]&&r.readAsDataURL(t.files[0])}deleteSave(e){const n=localStorage.getItem(e);n&&this.confirmReloadSave(e,n,!1)}populateSaves(){const e=this.saveManager.querySelector("#local-saves");if(e){try{if(null===localStorage)return}catch(e){return}e.textContent="",Object.keys(localStorage).forEach((n=>{const t=n.split("/").pop(),r=localStorage.getItem(n);if(t&&r&&this.isB64SOL(r)){const a=document.createElement("TR"),i=document.createElement("TD");i.textContent=t,i.title=n;const o=document.createElement("TD"),s=document.createElement("SPAN");s.textContent=Ce("save-download"),s.className="save-option",s.addEventListener("click",(()=>{const e=this.base64ToBlob(r,"application/octet-stream");this.saveFile(e,t+".sol")})),o.appendChild(s);const l=document.createElement("TD"),u=document.createElement("INPUT");u.type="file",u.accept=".sol",u.className="replace-save",u.id="replace-save-"+n;const c=document.createElement("LABEL");c.htmlFor="replace-save-"+n,c.textContent=Ce("save-replace"),c.className="save-option",u.addEventListener("change",(e=>this.replaceSOL(e,n))),l.appendChild(u),l.appendChild(c);const d=document.createElement("TD"),f=document.createElement("SPAN");f.textContent=Ce("save-delete"),f.className="save-option",f.addEventListener("click",(()=>this.deleteSave(n))),d.appendChild(f),a.appendChild(i),a.appendChild(o),a.appendChild(l),a.appendChild(d),e.appendChild(a)}}))}}async backupSaves(){const e=new(De()),n=[];Object.keys(localStorage).forEach((t=>{let r=String(t.split("/").pop());const a=localStorage.getItem(t);if(a&&this.isB64SOL(a)){const t=this.base64ToBlob(a,"application/octet-stream"),i=n.filter((e=>e===r)).length;n.push(r),i>0&&(r+=` (${i+1})`),e.file(r+".sol",t)}}));const t=await e.generateAsync({type:"blob"});this.saveFile(t,"saves.zip")}openSaveManager(){this.saveManager.classList.remove("hidden")}async downloadSwf(){try{if(this.swfUrl){console.log("Downloading SWF: "+this.swfUrl);const e=await fetch(this.swfUrl.href);if(!e.ok)return void console.error("SWF download failed");const n=await e.blob();this.saveFile(n,function(e){const n=e.pathname;return n.substring(n.lastIndexOf("/")+1)}(this.swfUrl))}else console.error("SWF download failed")}catch(e){console.error("SWF download failed")}}virtualKeyboardInput(){const e=this.virtualKeyboard,n=e.value;for(const e of n)for(const n of["keydown","keyup"])this.dispatchEvent(new KeyboardEvent(n,{key:e,bubbles:!0}));e.value=""}openVirtualKeyboard(){navigator.userAgent.toLowerCase().includes("android")?setTimeout((()=>{this.virtualKeyboard.focus({preventScroll:!0})}),100):this.virtualKeyboard.focus({preventScroll:!0})}isVirtualKeyboardFocused(){return this.shadow.activeElement===this.virtualKeyboard}contextMenuItems(){const e=String.fromCharCode(10003),n=[],t=()=>{n.length>0&&null!==n[n.length-1]&&n.push(null)};if(this.instance&&this.isPlaying){this.instance.prepare_context_menu().forEach(((r,a)=>{r.separatorBefore&&t(),n.push({text:r.caption+(r.checked?` (${e})`:""),onClick:()=>{var e;return null===(e=this.instance)||void 0===e?void 0:e.run_context_menu_callback(a)},enabled:r.enabled})})),t()}this.fullscreenEnabled&&(this.isFullscreen?n.push({text:Ce("context-menu-exit-fullscreen"),onClick:()=>{var e;return null===(e=this.instance)||void 0===e?void 0:e.set_fullscreen(!1)}}):n.push({text:Ce("context-menu-enter-fullscreen"),onClick:()=>{var e;return null===(e=this.instance)||void 0===e?void 0:e.set_fullscreen(!0)}})),this.instance&&this.swfUrl&&this.loadedConfig&&!0===this.loadedConfig.showSwfDownload&&(t(),n.push({text:Ce("context-menu-download-swf"),onClick:this.downloadSwf.bind(this)})),window.isSecureContext&&n.push({text:Ce("context-menu-copy-debug-info"),onClick:()=>navigator.clipboard.writeText(this.getPanicData())}),this.populateSaves();const r=this.saveManager.querySelector("#local-saves");return r&&""!==r.textContent&&n.push({text:Ce("context-menu-open-save-manager"),onClick:this.openSaveManager.bind(this)}),t(),n.push({text:Ce("context-menu-about-ruffle",{flavor:d?"extension":"",version:S}),onClick(){window.open(Fe,"_blank")}}),this.isTouch&&(t(),n.push({text:Ce("context-menu-hide"),onClick:()=>this.contextMenuForceDisabled=!0})),n}pointerDown(e){this.pointerDownPosition=new Be(e.pageX,e.pageY),this.pointerMoveMaxDistance=0,this.startLongPressTimer()}clearLongPressTimer(){this.longPressTimer&&(clearTimeout(this.longPressTimer),this.longPressTimer=null)}startLongPressTimer(){this.clearLongPressTimer(),this.longPressTimer=setTimeout((()=>this.clearLongPressTimer()),800)}checkLongPressMovement(e){if(null!==this.pointerDownPosition){const n=new Be(e.pageX,e.pageY),t=this.pointerDownPosition.distanceTo(n);t>this.pointerMoveMaxDistance&&(this.pointerMoveMaxDistance=t)}}checkLongPress(e){this.longPressTimer?this.clearLongPressTimer():!this.contextMenuSupported&&"mouse"!==e.pointerType&&this.pointerMoveMaxDistance<15&&this.showContextMenu(e)}showContextMenu(e){var n,t,r;if(this.panicked||!this.saveManager.classList.contains("hidden"))return;if(e.preventDefault(),"contextmenu"===e.type?(this.contextMenuSupported=!0,window.addEventListener("click",this.hideContextMenu.bind(this),{once:!0})):(window.addEventListener("pointerup",this.hideContextMenu.bind(this),{once:!0}),e.stopPropagation()),[!1,"off"].includes(null!==(t=null===(n=this.loadedConfig)||void 0===n?void 0:n.contextMenu)&&void 0!==t?t:"on")||this.isTouch&&"rightClickOnly"===(null===(r=this.loadedConfig)||void 0===r?void 0:r.contextMenu)||this.contextMenuForceDisabled)return;for(;this.contextMenuElement.firstChild;)this.contextMenuElement.removeChild(this.contextMenuElement.firstChild);for(const e of this.contextMenuItems())if(null===e){const e=document.createElement("li");e.className="menu_separator";const n=document.createElement("hr");e.appendChild(n),this.contextMenuElement.appendChild(e)}else{const{text:n,onClick:t,enabled:r}=e,a=document.createElement("li");a.className="menu_item",a.textContent=n,this.contextMenuElement.appendChild(a),!1!==r?a.addEventListener(this.contextMenuSupported?"click":"pointerup",t):a.classList.add("disabled")}this.contextMenuElement.style.left="0",this.contextMenuElement.style.top="0",this.contextMenuOverlay.classList.remove("hidden");const a=this.getBoundingClientRect(),i=e.clientX-a.x,o=e.clientY-a.y,s=a.width-this.contextMenuElement.clientWidth-1,l=a.height-this.contextMenuElement.clientHeight-1;this.contextMenuElement.style.left=Math.floor(Math.min(i,s))+"px",this.contextMenuElement.style.top=Math.floor(Math.min(o,l))+"px"}hideContextMenu(){var e;null===(e=this.instance)||void 0===e||e.clear_custom_menu_items(),this.contextMenuOverlay.classList.add("hidden")}pause(){this.instance&&(this.instance.pause(),this.playButton.style.display="block")}audioState(){if(this.instance){const e=this.instance.audio_context();return e&&e.state||"running"}return"suspended"}unmuteOverlayClicked(){if(this.instance){if("running"!==this.audioState()){const e=this.instance.audio_context();e&&e.resume()}this.unmuteOverlay.style.display="none"}}unmuteAudioContext(){Te||(navigator.maxTouchPoints<1?Te=!0:this.container.addEventListener("click",(()=>{var e;if(Te)return;const n=null===(e=this.instance)||void 0===e?void 0:e.audio_context();if(!n)return;const t=new Audio;t.src=(()=>{const e=new ArrayBuffer(10),t=new DataView(e),r=n.sampleRate;t.setUint32(0,r,!0),t.setUint32(4,r,!0),t.setUint16(8,1,!0);return`data:audio/wav;base64,UklGRisAAABXQVZFZm10IBAAAAABAAEA${window.btoa(String.fromCharCode(...new Uint8Array(e))).slice(0,13)}AgAZGF0YQcAAACAgICAgICAAAA=`})(),t.load(),t.play().then((()=>{Te=!0})).catch((e=>{console.warn(`Failed to play dummy sound: ${e}`)}))}),{once:!0}))}copyElement(e){if(e){for(const n of e.attributes)if(n.specified){if("title"===n.name&&"Adobe Flash Player"===n.value)continue;try{this.setAttribute(n.name,n.value)}catch(e){console.warn(`Unable to set attribute ${n.name} on Ruffle instance`)}}for(const n of Array.from(e.children))this.appendChild(n)}}static htmlDimensionToCssDimension(e){if(e){const n=e.match(Pe);if(n){let e=n[1];return n[3]||(e+="px"),e}}return null}onCallbackAvailable(e){const n=this.instance;this[e]=(...t)=>null==n?void 0:n.call_exposed_callback(e,t)}set traceObserver(e){var n;null===(n=this.instance)||void 0===n||n.set_trace_observer(e)}getPanicData(){let e="\n# Player Info\n";if(e+=`Allows script access: ${!!this.loadedConfig&&this.loadedConfig.allowScriptAccess}\n`,e+=`${this.rendererDebugInfo}\n`,e+=this.debugPlayerInfo(),e+="\n# Page Info\n",e+=`Page URL: ${document.location.href}\n`,this.swfUrl&&(e+=`SWF URL: ${this.swfUrl}\n`),e+="\n# Browser Info\n",e+=`User Agent: ${window.navigator.userAgent}\n`,e+=`Platform: ${window.navigator.platform}\n`,e+=`Has touch support: ${window.navigator.maxTouchPoints>0}\n`,e+="\n# Ruffle Info\n",e+=`Version: ${x}\n`,e+=`Name: ${S}\n`,e+=`Channel: ${z}\n`,e+=`Built: ${E}\n`,e+=`Commit: ${j}\n`,e+=`Is extension: ${d}\n`,e+="\n# Metadata\n",this.metadata)for(const[n,t]of Object.entries(this.metadata))e+=`${n}: ${t}\n`;return e}panic(e){var n;if(this.panicked)return;if(this.panicked=!0,this.hideSplashScreen(),e instanceof Error&&("AbortError"===e.name||e.message.includes("AbortError")))return;const t=null!==(n=null==e?void 0:e.ruffleIndexError)&&void 0!==n?n:0,r=Object.assign([],{stackIndex:-1,avmStackIndex:-1});if(r.push("# Error Info\n"),e instanceof Error){if(r.push(`Error name: ${e.name}\n`),r.push(`Error message: ${e.message}\n`),e.stack){const n=r.push(`Error stack:\n\`\`\`\n${e.stack}\n\`\`\`\n`)-1;if(e.avmStack){const n=r.push(`AVM2 stack:\n\`\`\`\n ${e.avmStack.trim().replace(/\t/g," ")}\n\`\`\`\n`)-1;r.avmStackIndex=n}r.stackIndex=n}}else r.push(`Error: ${e}\n`);r.push(this.getPanicData());const a=r.join(""),i=new Date(E),o=new Date;o.setMonth(o.getMonth()-6);const s=o>i;let l,u,c;if(s)l=`${Ce("update-ruffle")}`;else{let e;e=document.location.protocol.includes("extension")?this.swfUrl.href:document.location.href,e=e.split(/[?#]/,1)[0];let n=`https://github.com/ruffle-rs/ruffle/issues/new?title=${encodeURIComponent(`Error on ${e}`)}&template=error_report.md&labels=error-report&body=`,t=encodeURIComponent(a);r.stackIndex>-1&&String(n+t).length>8195&&(r[r.stackIndex]=null,r.avmStackIndex>-1&&(r[r.avmStackIndex]=null),t=encodeURIComponent(r.join(""))),n+=t,l=`${Ce("report-bug")}`}switch(t){case 2:u=Ie("error-file-protocol"),c=`\n
  • ${Ce("ruffle-demo")}
  • \n
  • ${Ce("ruffle-desktop")}
  • \n `;break;case 4:u=Ie("error-javascript-config"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 9:u=Ie("error-wasm-not-found"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 8:u=Ie("error-wasm-mime-type"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 11:u=Ie("error-swf-fetch"),c=`\n
  • ${Ce("view-error-details")}
  • \n `;break;case 12:u=Ie("error-swf-cors"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 6:u=Ie("error-wasm-cors"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 3:u=Ie("error-wasm-invalid"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 7:u=Ie("error-wasm-download"),c=`\n
  • ${Ce("view-error-details")}
  • \n `;break;case 10:u=Ie("error-wasm-disabled-on-edge"),c=`\n
  • ${Ce("more-info")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 5:u=Ie("error-javascript-conflict"),s&&(u+=Ie("error-javascript-conflict-outdated",{buildDate:E})),c=`\n
  • ${l}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 1:u=Ie("error-csp-conflict"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;default:u=Ie("error-unknown",{buildDate:E,outdated:String(s)}),c=`\n
  • ${l}
  • \n
  • ${Ce("view-error-details")}
  • \n `}this.container.innerHTML=`\n
    \n
    ${Ce("panic-title")}
    \n
    ${u}
    \n \n
    \n `;const d=this.container.querySelector("#panic-view-details");d&&(d.onclick=()=>{const e=this.container.querySelector("#panic-body");e.classList.add("details");const n=document.createElement("textarea");return n.readOnly=!0,n.value=a,e.replaceChildren(n),!1}),this.destroy()}displayRootMovieDownloadFailedMessage(){var e;if(d&&window.location.origin!==this.swfUrl.origin){const n=new URL(this.swfUrl);if(null===(e=this.loadedConfig)||void 0===e?void 0:e.parameters){const e=$e(this.loadedConfig.parameters);Object.entries(e).forEach((([e,t])=>{n.searchParams.set(e,t)}))}this.hideSplashScreen();const t=document.createElement("div");t.id="message_overlay",t.innerHTML=`
    \n ${Ie("message-cant-embed")}\n \n
    `,this.container.prepend(t)}else{const e=new Error("Failed to fetch: "+this.swfUrl);this.swfUrl.protocol.includes("http")?window.location.origin===this.swfUrl.origin?e.ruffleIndexError=11:e.ruffleIndexError=12:e.ruffleIndexError=2,this.panic(e)}}displayMessage(e){const n=document.createElement("div");n.id="message_overlay",n.innerHTML=`
    \n

    ${e}

    \n
    \n \n
    \n
    `,this.container.prepend(n),this.container.querySelector("#continue-btn").onclick=()=>{n.parentNode.removeChild(n)}}debugPlayerInfo(){return""}hideSplashScreen(){this.splashScreen.classList.add("hidden"),this.container.classList.remove("hidden")}showSplashScreen(){this.splashScreen.classList.remove("hidden"),this.container.classList.add("hidden")}setMetadata(e){this._metadata=e,this._readyState=2,this.hideSplashScreen(),this.dispatchEvent(new Event(Me.LOADED_METADATA)),this.dispatchEvent(new Event(Me.LOADED_DATA))}}function Le(e,n){switch(e||(e="sameDomain"),e.toLowerCase()){case"always":return!0;case"never":return!1;default:try{return new URL(window.location.href).origin===new URL(n,window.location.href).origin}catch(e){return!1}}}function Ne(e){return null===e||"true"===e.toLowerCase()}function Ue(e){if(e){let n="",t="";try{const r=new URL(e,Fe);n=r.pathname,t=r.hostname}catch(e){}if(n.startsWith("/v/")&&/^(?:(?:www\.|m\.)?youtube(?:-nocookie)?\.com)|(?:youtu\.be)$/i.test(t))return!0}return!1}function qe(e,n){var t,r;const a=e.getAttribute(n),i=null!==(r=null===(t=window.RufflePlayer)||void 0===t?void 0:t.config)&&void 0!==r?r:{};if(a)try{const t=new URL(a);"http:"!==t.protocol||"https:"!==window.location.protocol||"upgradeToHttps"in i&&!1===i.upgradeToHttps||(t.protocol="https:",e.setAttribute(n,t.toString()))}catch(e){}}function We(e){let n=e.parentElement;for(;null!==n;){switch(n.tagName){case"AUDIO":case"VIDEO":return!0}n=n.parentElement}return!1}Me.LOADED_METADATA="loadedmetadata",Me.LOADED_DATA="loadeddata";class Ze extends Me{constructor(){super()}connectedCallback(){var e,n,t,r,a,i,o,s,l,u,c,d,f,h,m,p,v,g,b,w;super.connectedCallback();const k=this.attributes.getNamedItem("src");if(k){const y=null!==(n=null===(e=this.attributes.getNamedItem("allowScriptAccess"))||void 0===e?void 0:e.value)&&void 0!==n?n:null,_=null!==(r=null===(t=this.attributes.getNamedItem("menu"))||void 0===t?void 0:t.value)&&void 0!==r?r:null;this.load({url:k.value,allowScriptAccess:Le(y,k.value),parameters:null!==(i=null===(a=this.attributes.getNamedItem("flashvars"))||void 0===a?void 0:a.value)&&void 0!==i?i:null,backgroundColor:null!==(s=null===(o=this.attributes.getNamedItem("bgcolor"))||void 0===o?void 0:o.value)&&void 0!==s?s:null,base:null!==(u=null===(l=this.attributes.getNamedItem("base"))||void 0===l?void 0:l.value)&&void 0!==u?u:null,menu:Ne(_),salign:null!==(d=null===(c=this.attributes.getNamedItem("salign"))||void 0===c?void 0:c.value)&&void 0!==d?d:"",quality:null!==(h=null===(f=this.attributes.getNamedItem("quality"))||void 0===f?void 0:f.value)&&void 0!==h?h:"high",scale:null!==(p=null===(m=this.attributes.getNamedItem("scale"))||void 0===m?void 0:m.value)&&void 0!==p?p:"showAll",wmode:null!==(g=null===(v=this.attributes.getNamedItem("wmode"))||void 0===v?void 0:v.value)&&void 0!==g?g:"window",allowNetworking:null!==(w=null===(b=this.attributes.getNamedItem("allowNetworking"))||void 0===b?void 0:b.value)&&void 0!==w?w:"all"})}}get src(){var e;return null===(e=this.attributes.getNamedItem("src"))||void 0===e?void 0:e.value}set src(e){if(e){const n=document.createAttribute("src");n.value=e,this.attributes.setNamedItem(n)}else this.attributes.removeNamedItem("src")}static get observedAttributes(){return["src","width","height"]}attributeChangedCallback(e,n,t){var r,a,i,o;if(super.attributeChangedCallback(e,n,t),this.isConnected&&"src"===e){const e=this.attributes.getNamedItem("src");e&&this.load({url:e.value,parameters:null!==(a=null===(r=this.attributes.getNamedItem("flashvars"))||void 0===r?void 0:r.value)&&void 0!==a?a:null,base:null!==(o=null===(i=this.attributes.getNamedItem("base"))||void 0===i?void 0:i.value)&&void 0!==o?o:null})}}static isInterdictable(e){const n=e.getAttribute("src"),t=e.getAttribute("type");return!!n&&(!We(e)&&(Ue(n)?(qe(e,"src"),!1):R(n,t)))}static fromNativeEmbedElement(e){const n=g("ruffle-embed",Ze),t=document.createElement(n);return t.copyElement(e),t}}function He(e,n,t){n=n.toLowerCase();for(const[t,r]of Object.entries(e))if(t.toLowerCase()===n)return r;return t}function Ve(e){var n,t;const r={};for(const a of e.children)if(a instanceof HTMLParamElement){const e=null===(n=a.attributes.getNamedItem("name"))||void 0===n?void 0:n.value,i=null===(t=a.attributes.getNamedItem("value"))||void 0===t?void 0:t.value;e&&i&&(r[e]=i)}return r}class Je extends Me{constructor(){super(),this.params={}}connectedCallback(){var e;super.connectedCallback(),this.params=Ve(this);let n=null;this.attributes.getNamedItem("data")?n=null===(e=this.attributes.getNamedItem("data"))||void 0===e?void 0:e.value:this.params.movie&&(n=this.params.movie);const t=He(this.params,"allowScriptAccess",null),r=He(this.params,"flashvars",this.getAttribute("flashvars")),a=He(this.params,"bgcolor",this.getAttribute("bgcolor")),i=He(this.params,"allowNetworking",this.getAttribute("allowNetworking")),o=He(this.params,"base",this.getAttribute("base")),s=He(this.params,"menu",null),l=He(this.params,"salign",""),u=He(this.params,"quality","high"),c=He(this.params,"scale","showAll"),d=He(this.params,"wmode","window");if(n){const e={url:n};e.allowScriptAccess=Le(t,n),r&&(e.parameters=r),a&&(e.backgroundColor=a),o&&(e.base=o),e.menu=Ne(s),l&&(e.salign=l),u&&(e.quality=u),c&&(e.scale=c),d&&(e.wmode=d),i&&(e.allowNetworking=i),this.load(e)}}debugPlayerInfo(){var e;let n="Player type: Object\n",t=null;return this.attributes.getNamedItem("data")?t=null===(e=this.attributes.getNamedItem("data"))||void 0===e?void 0:e.value:this.params.movie&&(t=this.params.movie),n+=`SWF URL: ${t}\n`,Object.keys(this.params).forEach((e=>{n+=`Param ${e}: ${this.params[e]}\n`})),Object.keys(this.attributes).forEach((e=>{var t;n+=`Attribute ${e}: ${null===(t=this.attributes.getNamedItem(e))||void 0===t?void 0:t.value}\n`})),n}get data(){return this.getAttribute("data")}set data(e){if(e){const n=document.createAttribute("data");n.value=e,this.attributes.setNamedItem(n)}else this.attributes.removeNamedItem("data")}static isInterdictable(e){var n,t,r,a;if(We(e))return!1;if(e.getElementsByTagName("ruffle-object").length>0||e.getElementsByTagName("ruffle-embed").length>0)return!1;const i=null===(n=e.attributes.getNamedItem("data"))||void 0===n?void 0:n.value.toLowerCase(),o=null!==(r=null===(t=e.attributes.getNamedItem("type"))||void 0===t?void 0:t.value)&&void 0!==r?r:null,s=Ve(e);let l;if(i){if(Ue(i))return qe(e,"data"),!1;l=i}else{if(!s||!s.movie)return!1;if(Ue(s.movie)){const n=e.querySelector("param[name='movie']");if(n){qe(n,"value");const t=n.getAttribute("value");t&&e.setAttribute("data",t)}return!1}l=s.movie}const u=null===(a=e.attributes.getNamedItem("classid"))||void 0===a?void 0:a.value.toLowerCase();return u==="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000".toLowerCase()?!Array.from(e.getElementsByTagName("object")).some(Je.isInterdictable)&&!Array.from(e.getElementsByTagName("embed")).some(Ze.isInterdictable):!u&&R(l,o)}static fromNativeObjectElement(e){const n=g("ruffle-object",Je),t=document.createElement(n);for(const n of Array.from(e.getElementsByTagName("embed")))Ze.isInterdictable(n)&&n.remove();for(const n of Array.from(e.getElementsByTagName("object")))Je.isInterdictable(n)&&n.remove();return t.copyElement(e),t}}class Ke{constructor(e){if(this.__mimeTypes=[],this.__namedMimeTypes={},e)for(let n=0;n>>0]}namedItem(e){return this.__namedMimeTypes[e]}get length(){return this.__mimeTypes.length}[Symbol.iterator](){return this.__mimeTypes[Symbol.iterator]()}}class Ge extends Ke{constructor(e,n,t){super(),this.name=e,this.description=n,this.filename=t}}class Ye{constructor(e){this.__plugins=[],this.__namedPlugins={};for(let n=0;n>>0]}namedItem(e){return this.__namedPlugins[e]}refresh(){}[Symbol.iterator](){return this.__plugins[Symbol.iterator]()}get length(){return this.__plugins.length}}const Xe=new Ge("Shockwave Flash","Shockwave Flash 32.0 r0","ruffle.js"),Qe=new Ge("Ruffle Extension","Ruffle Extension","ruffle.js");var en,nn;Xe.install({type:k,description:"Shockwave Flash",suffixes:"spl",enabledPlugin:Xe}),Xe.install({type:w,description:"Shockwave Flash",suffixes:"swf",enabledPlugin:Xe}),Xe.install({type:y,description:"Shockwave Flash",suffixes:"swf",enabledPlugin:Xe}),Xe.install({type:_,description:"Shockwave Flash",suffixes:"swf",enabledPlugin:Xe}),Qe.install({type:"",description:"Ruffle Detection",suffixes:"",enabledPlugin:Qe});const tn=null!==(nn=null===(en=window.RufflePlayer)||void 0===en?void 0:en.config)&&void 0!==nn?nn:{},rn=f(tn)+"ruffle.js";let an,on,sn,ln;function un(){var e,n;return(!("favorFlash"in tn)||!1!==tn.favorFlash)&&"ruffle.js"!==(null!==(n=null===(e=navigator.plugins.namedItem("Shockwave Flash"))||void 0===e?void 0:e.filename)&&void 0!==n?n:"ruffle.js")}function cn(){try{an=null!=an?an:document.getElementsByTagName("object"),on=null!=on?on:document.getElementsByTagName("embed");for(const e of Array.from(an))if(Je.isInterdictable(e)){const n=Je.fromNativeObjectElement(e);e.replaceWith(n)}for(const e of Array.from(on))if(Ze.isInterdictable(e)){const n=Ze.fromNativeEmbedElement(e);e.replaceWith(n)}}catch(e){console.error(`Serious error encountered when polyfilling native Flash elements: ${e}`)}}function dn(){sn=null!=sn?sn:document.getElementsByTagName("iframe"),ln=null!=ln?ln:document.getElementsByTagName("frame"),[sn,ln].forEach((e=>{for(const n of e){if(void 0!==n.dataset.rufflePolyfilled)continue;n.dataset.rufflePolyfilled="";const e=n.contentWindow,t=`Couldn't load Ruffle into ${n.tagName}[${n.src}]: `;try{"complete"===e.document.readyState&&fn(e,t)}catch(e){d||console.warn(t+e)}n.addEventListener("load",(()=>{fn(e,t)}),!1)}}))}async function fn(e,n){var t;let r;await new Promise((e=>{window.setTimeout((()=>{e()}),100)}));try{if(r=e.document,!r)return}catch(e){return void(d||console.warn(n+e))}if(d||void 0===r.documentElement.dataset.ruffleOptout)if(d)e.RufflePlayer||(e.RufflePlayer={}),e.RufflePlayer.config=Object.assign(Object.assign({},tn),null!==(t=e.RufflePlayer.config)&&void 0!==t?t:{});else if(!e.RufflePlayer){const n=r.createElement("script");n.setAttribute("src",rn),n.onload=()=>{e.RufflePlayer={},e.RufflePlayer.config=tn},r.head.appendChild(n)}}function hn(){un()||function(e){"install"in navigator.plugins&&navigator.plugins.install||Object.defineProperty(navigator,"plugins",{value:new Ye(navigator.plugins),writable:!1}),navigator.plugins.install(e),!(e.length>0)||"install"in navigator.mimeTypes&&navigator.mimeTypes.install||Object.defineProperty(navigator,"mimeTypes",{value:new Ke(navigator.mimeTypes),writable:!1});const n=navigator.mimeTypes;for(let t=0;te.addedNodes.length>0))&&(cn(),dn())})).observe(document,{childList:!0,subtree:!0}))}const pn={version:x,polyfill(){mn()},pluginPolyfill(){hn()},createPlayer(){const e=g("ruffle-player",Me);return document.createElement(e)}};class vn{constructor(e){this.sources={},this.config={},this.invoked=!1,this.newestName=null,this.conflict=null,null!=e&&(e instanceof vn?(this.sources=e.sources,this.config=e.config,this.invoked=e.invoked,this.conflict=e.conflict,this.newestName=e.newestName,e.superseded()):e.constructor===Object&&e.config instanceof Object?this.config=e.config:this.conflict=e),"loading"===document.readyState?document.addEventListener("readystatechange",this.init.bind(this)):window.setTimeout(this.init.bind(this),0)}get version(){return"0.1.0"}registerSource(e){this.sources[e]=pn}newestSourceName(){let n=null,t=e.fromSemver("0.0.0");for(const r in this.sources)if(Object.prototype.hasOwnProperty.call(this.sources,r)){const a=e.fromSemver(this.sources[r].version);a.hasPrecedenceOver(t)&&(n=r,t=a)}return n}init(){if(!this.invoked){if(this.invoked=!0,this.newestName=this.newestSourceName(),null===this.newestName)throw new Error("No registered Ruffle source!");!1!==(!("polyfills"in this.config)||this.config.polyfills)&&this.sources[this.newestName].polyfill()}}newest(){const e=this.newestSourceName();return null!==e?this.sources[e]:null}satisfying(t){const r=n.fromRequirementString(t);let a=null;for(const n in this.sources)if(Object.prototype.hasOwnProperty.call(this.sources,n)){const t=e.fromSemver(this.sources[n].version);r.satisfiedBy(t)&&(a=this.sources[n])}return a}localCompatible(){return void 0!==this.sources.local?this.satisfying("^"+this.sources.local.version):this.newest()}local(){return void 0!==this.sources.local?this.satisfying("="+this.sources.local.version):this.newest()}superseded(){this.invoked=!0}static negotiate(e,n){let t;if(t=e instanceof vn?e:new vn(e),void 0!==n){t.registerSource(n);!1!==(!("polyfills"in t.config)||t.config.polyfills)&&pn.pluginPolyfill()}return t}}window.RufflePlayer=vn.negotiate(window.RufflePlayer,"local")})()})(); +//# sourceMappingURL=ruffle.js.map \ No newline at end of file diff --git a/doc/ax25-2p0/index_files/seal.js b/doc/ax25-2p0/index_files/seal.js new file mode 100644 index 0000000..3108bad --- /dev/null +++ b/doc/ax25-2p0/index_files/seal.js @@ -0,0 +1,114 @@ +var _____WB$wombat$assign$function_____ = function(name) {return (self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init(name)) || self[name]; }; +if (!self.__WB_pmw) { self.__WB_pmw = function(obj) { this.__WB_source = obj; return this; } } +{ + let window = _____WB$wombat$assign$function_____("window"); + let self = _____WB$wombat$assign$function_____("self"); + let document = _____WB$wombat$assign$function_____("document"); + let location = _____WB$wombat$assign$function_____("location"); + let top = _____WB$wombat$assign$function_____("top"); + let parent = _____WB$wombat$assign$function_____("parent"); + let frames = _____WB$wombat$assign$function_____("frames"); + let opener = _____WB$wombat$assign$function_____("opener"); + +// (c) 2006. Authorize.Net is a registered trademark of Lightbridge, Inc. +var ANSVerificationURL = "http://web.archive.org/web/20190831185324/https://verify.authorize.net/anetseal/"; // String must start with "//" and end with "/" +var AuthorizeNetSeal = +{ + verification_parameters: "", + id_parameter_name: "pid", + url_parameter_name: "rurl", + seal_image_file: (ANSVerificationURL + "images/secure90x72.gif"), + seal_width: "90", + seal_height: "72", + seal_alt_text: "Authorize.Net Merchant - Click to Verify", + display_url: "http://web.archive.org/web/20190831185324/http://www.authorize.net/", + text_color: "black", + text_size: "9px", + line_spacing: "10px", + new_window_height: "430", + new_window_width: "600", + current_url: "", + display_location: true, + no_click: false, + debug: false +}; + +document.writeln( '' ); + +if( window.ANS_customer_id ) +{ + AuthorizeNetSeal.verification_parameters = '?' + AuthorizeNetSeal.id_parameter_name + '=' + escape( ANS_customer_id ); + if( window.location.href ) + { + AuthorizeNetSeal.current_url = window.location.href; + } + else if( document.URL ) + { + AuthorizeNetSeal.current_url = document.URL; + } + + if( AuthorizeNetSeal.current_url ) + { + AuthorizeNetSeal.verification_parameters += '&' + AuthorizeNetSeal.url_parameter_name + '=' + escape( AuthorizeNetSeal.current_url ); + } + + if( !AuthorizeNetSeal.no_click ) + { + document.write( '' ); + } + + document.writeln( '' + AuthorizeNetSeal.seal_alt_text + '' ); + + if( !AuthorizeNetSeal.no_click ) + { + document.writeln( '' ); + } +} + + +} +/* + FILE ARCHIVED ON 18:53:24 Aug 31, 2019 AND RETRIEVED FROM THE + INTERNET ARCHIVE ON 06:36:00 Oct 23, 2023. + JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE. + + ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C. + SECTION 108(a)(3)). +*/ +/* +playback timings (ms): + captures_list: 1475.342 + exclusion.robots: 0.121 + exclusion.robots.policy: 0.109 + cdx.remote: 0.082 + esindex: 0.01 + LoadShardBlock: 132.682 (4) + PetaboxLoader3.datanode: 100.818 (5) + load_resource: 59.75 + PetaboxLoader3.resolve: 43.32 +*/ \ No newline at end of file diff --git a/doc/ax25-2p0/index_files/secure90x72.gif b/doc/ax25-2p0/index_files/secure90x72.gif new file mode 100644 index 0000000000000000000000000000000000000000..d6fffedb731d0216cde22af73bc92836d6629139 GIT binary patch literal 2894 zcmV-U3$gTxP)pd&;_fy+=C=|)jHBhf?@I1&`02q!{F`JzI)%sX?hs;YZW{rUY<5c&jqXzsP&d}p%t zi>+U6{WAPlnO|7=Jbg1AT73osbOLs4?<)uN0MJ(!z6}43o&tSW3U3lH%MgAC&?K#k z5UvK9Ns71r+QM%wtx@`K{%r>O*1<`VeP!Uc^=l+LG}L?|I(2QN^g4x&6A*X65pW;8{nP62AO5P^eD+rL z-s?Zw|0gGK_59`M4=Q(G{rltHccf7O-sx$a$~1YBfWFB{+9Lo9EIENFpdbG2<=wla zHv;X?F5U9}O#pq9deO)@edB}b;|}yBCt(7^CHBAn`J(#p-RJJak^Ad|7k1BT2*nqD zIMD9(z0%RM;^|pEBLjHV7p0sH=me|}NE7Eu0U(K$lhhLa*(a~=JbbKYIY1I%0$u;P zQ#iDtZ%9*d1vEWFzXe*EK6VnFK-~oOYcD|pM3omkM2SQi-FZVqpaq(qCFKm1a!9@R z5dE+3omNbmC#DLnx|Z(FfldY8l(YbW>{I}UO1hyEIiyn{0k(j1p!FQn7r=6gTaw}c zvV%Q0-oEJ)>~l%Nut2QtWCZY38T`E{_~-tB5D**QI zw2<5+MA>&s=;OIqeN-b%EFGU-A7x+1?qzizfQ(j0fv?1^G6sP%z7N{-)Gz;~| zbZ4l5b&OakDtbfhGP3pFnb~(+X zp75fKDL0yJ`b8>(AHk-B!!ryy-VCHky~ibfxKJD-%6MjW+!#$1bY0#U!0_Y zc+aDpvG++}ln;%ddBCS1{eA-2fK1)>yZH5M)&IW!IuG>reS$QTsWK@|0DR&hkSUOZ zdM=<(r4FYLw#~fv2`D6AZuEoa_F*|XQQZ|s-y*0*iKbQb0f3RX!dKTfyLZCYZ#~$* zDDn9wUa4H_*j{waesKk{SpwTtG#b_;m*LVJ|XI037R$4Tm{zn?A9DqVwk>g2U zX%MboDynZ3$Kxpv{iiT5qsqljj{~i62V0HnK&wHa7d4um^_#1E3FtYQsltfAXO^P@ zm)6|>9;QmuWB4WxYWm+B&%dWfd0q~*8pg}*Jqk&tA(74;&|XIUDouc2e=($;@G`v6K9pynzNfyNYNMDd$`hNU{YF&r*se; zAE=1qPsr-SX^yHF~=^`LKnR>JU?ysJbWu6GrP85FZ7x;U*%{Ftu3 zk7+6|6n(KJMw(n>wzcfNbB?_KTGFZ}E(puPhS)9=3lxX*N05MN{dZHbZ$A-W~Zf(J;Pbo^l|_G{k``v z|1EvjB)o%Nmc|A(r*36!>PoXuaqECm2jDtDAN#)P4om6O?zzkjW@+zibIf)FlQ2Jn z(nczyl;@1V(( zqhf@2vnmIGF6^Ax<*BR^J*gp-H)qd}fPKJ)9K zK$nhO0HkXga5g7u{Xf)00Ma|R(ZKIapAQ|%WIZFN+8v>asjtBg?$CX>r{j+9={FR3 z1p(C$e(HZemxzROT#Z>AhA;67iFp4~Z0H(bcXrtRoYVo>H)+;*6}ehq!Nc&bPluVt zN6B_dfa_hVS+Mf!Dv)y0#^xD)`G8c<1`V`PF|lq3Yv0HlvOko8+uZQtB&g zabFMhtVAt}3pH?KO^u^$qkCs|Rgd@H&B9fg^JBWO^ETH4`85;_mfkDWI0rUdSE-Sg zp@)s17@#xTVe!7u;>EA%%ziIv?yl0hJJy!}ystW666mGhjn{=Wlr`Ea^_X=R`+bis z&;jtpp$ocGoN0z?R(7BCZ9#HpLA&^N0$l+AIxe`Inpm19XD{fsv0!xRdR0Tb<3CC9 sET~@713)hl*z2yZQR^J&r~dr@|6+bC^=>dUs{jB107*qoM6N<$f+MGc2mk;8 literal 0 HcmV?d00001 diff --git a/doc/ax25-2p0/index_files/tapr-80px-logo.gif b/doc/ax25-2p0/index_files/tapr-80px-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..a469e524921e317d847814c6b2a4fb995bef62a6 GIT binary patch literal 4922 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 A4gdfE literal 0 HcmV?d00001 diff --git a/doc/ax25-2p0/index_files/tapr-logo.png b/doc/ax25-2p0/index_files/tapr-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d1c606b47e59cfdef36fd17a6c5b93d97a77cba4 GIT binary patch literal 9070 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 .feature { + height: auto; +} + +.feature h1 { + font: bold 175% Arial,sans-serif; + color: #000000; + padding: 10px 0px 0px 0px; + border-bottom: 2px double #000000; +} + +.feature h2 { + font: bold 150% Arial,sans-serif; + color: #000000; + padding: 10px 0px 0px 0px; +} + +.feature h3 { + font: bold 150% Arial,sans-serif; + color: #006699; + padding: 10px 0px 0px 0px; +} + +.feature h4 { + font: bold 125% Arial,sans-serif; + color: #000000; + padding: 10px 0px 0px 0px; +} + +.feature h5 { + font: bold 110% Arial,sans-serif; + color: #000000; + padding: 10px 0px 0px 0px; +} + +.feature img { + /* float: left; */ + /* padding: 5px 5px 5px 5px; */ + margin: 5px; +} + + +/*************** story styles ******************/ + +.story { + padding: 10px 0px 0px 10px; + font-size: 90%; +} + +.story h3{ + font: bold 125% Arial,sans-serif; + color: #000000; +} + +.story p { + padding: 0px 0px 5px 0px; +} + +.story h4 { + font: bold 110% Arial,sans-serif; + color: #000000; + padding: 10px 0px 0px 0px; +} + +.story a.capsule{ + font: bold 1em Arial,sans-serif; + color: #005FA9; + display:block; + padding-bottom: 5px; + border-bottom: 2px solid #000000; +} + +.story a.capsule:hover{ + text-decoration: underline; +} + +td.storyLeft{ + padding-right: 12px; +} + +.story img{ + /* float: right; */ + /* padding: 0px 10px 0px 0px; */ + margin: 5px; +} + +td.caption { + color: #000000; + font-weight: bold; + text-decoration: underline; + font-size: 90%; +} + + +/************** siteInfo styles ****************/ + +#siteInfo{ + clear: both; + border-top: 1px solid #cccccc; + font-size: small; + color: #a3a3a3; + padding: 10px 10px 10px 10px; + margin-top: 0px; +} + +#siteInfo img{ + padding: 4px 4px 4px 0px; + vertical-align: middle; +} + + +/************ sectionLinks styles **************/ + +#sectionLinks{ + margin: 0px; + padding: 0px; + +} + +#sectionLinks h2{ + padding: 2px 0px 2px 10px; + border-bottom: 1px solid #cccccc; + color: #FF0000; + margin-left: 3px; + font-weight: bold; + font-size: 120%; +} + +#sectionLinks h3{ + padding: 10px 0px 2px 10px; + border-bottom: 1px solid #cccccc; +} + +#sectionLinks a:link, #sectionLinks a:visited { + display: block; + border-top: 1px solid #ffffff; + border-bottom: 1px solid #cccccc; + background-image: url("/web/20190831185324im_/https://tapr.org/images/bg_nav.jpg"); + font-weight: bold; + padding: 3px 0px 3px 10px; + color: #004080; +} + +#sectionLinks a:hover{ + border-top: 1px solid #cccccc; + background-color: #FFFFFF; + background-image: none; + font-weight: bold; + text-decoration: none; + color: #800000; +} + +#sectionLinks a.new:link, #sectionLinks a.new:visited { + display: block; + border-top: 1px solid #ffffff; + border-bottom: 1px solid #cccccc; + font-weight: bold; + padding: 3px 0px 3px 10px; + color: #00254A; + background: no-repeat #FF9696; +} + +#sectionLinks a.new:hover{ + border-top: 1px solid #cccccc; + background-color: #FFFFFF; + background-image: none; + font-weight: bold; + text-decoration: none; + color: #800000; +} + +#sectionLinks img { + padding-right: 5px; +} + +/************* relatedLinks styles **************/ + +.relatedLinks{ + margin: 0px; + padding: 0px 0px 10px 10px; + border-bottom: 1px solid #cccccc; +} + +.relatedLinks h3{ + padding: 10px 0px 2px 0px; +} + +.relatedLinks a{ + display: block; +} + + +/**************** advert styles *****************/ + +#advert{ + padding: 10px; +} + +#advert img{ + display: block; +} + +div.archive{ + background-color: lightgrey; +| +/********************* end **********************/ + +/* + FILE ARCHIVED ON 18:53:24 Aug 31, 2019 AND RETRIEVED FROM THE + INTERNET ARCHIVE ON 06:35:59 Oct 23, 2023. + JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE. + + ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C. + SECTION 108(a)(3)). +*/ +/* +playback timings (ms): + captures_list: 90.861 + exclusion.robots: 0.066 + exclusion.robots.policy: 0.057 + cdx.remote: 0.052 + esindex: 0.008 + LoadShardBlock: 42.583 (3) + PetaboxLoader3.datanode: 50.291 (4) + load_resource: 134.36 + PetaboxLoader3.resolve: 120.716 +*/ \ No newline at end of file diff --git a/doc/ax25-2p0/index_files/tl_curve_white.gif b/doc/ax25-2p0/index_files/tl_curve_white.gif new file mode 100644 index 0000000000000000000000000000000000000000..9cba4c7d3013d59e73aaf1dd0aacdad0a9063bb7 GIT binary patch literal 59 zcmZ?wbhEHbWMg1sn8?6z=FFM$&I$kj|7T!eQ2faPBpDcVKm{m7 I$Cwzb0p{Hg{r~^~ literal 0 HcmV?d00001 diff --git a/doc/ax25-2p0/index_files/wombat.js b/doc/ax25-2p0/index_files/wombat.js new file mode 100644 index 0000000..9f2c553 --- /dev/null +++ b/doc/ax25-2p0/index_files/wombat.js @@ -0,0 +1,21 @@ +/* +Wombat.js client-side rewriting engine for web archive replay +Copyright (C) 2014-2023 Webrecorder Software, Rhizome, and Contributors. Released under the GNU Affero General Public License. + +This file is part of wombat.js, see https://github.com/webrecorder/wombat.js for the full source +Wombat.js is part of the Webrecorder project (https://github.com/webrecorder) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + */ +(function(){"use strict";function FuncMap(){this._map=[]}function ensureNumber(maybeNumber){try{switch(typeof maybeNumber){case"number":case"bigint":return maybeNumber;}var converted=Number(maybeNumber);return isNaN(converted)?null:converted}catch(e){}return null}function addToStringTagToClass(clazz,tag){typeof self.Symbol!=="undefined"&&typeof self.Symbol.toStringTag!=="undefined"&&Object.defineProperty(clazz.prototype,self.Symbol.toStringTag,{value:tag,enumerable:false})}function autobind(clazz){for(var prop,propValue,proto=clazz.__proto__||clazz.constructor.prototype||clazz.prototype,clazzProps=Object.getOwnPropertyNames(proto),len=clazzProps.length,i=0;i(r+=String.fromCharCode(n),r),""):t?t.toString():"";try{return"__wb_post_data="+btoa(e)}catch{return"__wb_post_data="}}function w(t){function o(a){return a instanceof Uint8Array&&(a=new TextDecoder().decode(a)),a}let{method:e,headers:r,postData:n}=t;if(e==="GET")return!1;let i=(r.get("content-type")||"").split(";")[0],s="";switch(i){case"application/x-www-form-urlencoded":s=o(n);break;case"application/json":s=c(o(n));break;case"text/plain":try{s=c(o(n),!1)}catch{s=u(n)}break;case"multipart/form-data":{let a=r.get("content-type");if(!a)throw new Error("utils cannot call postToGetURL when missing content-type header");s=g(o(n),a);break}default:s=u(n);}return s!==null&&(t.url=f(t.url,s,t.method),t.method="GET",t.requestBody=s,!0)}function f(t,e,r){if(!r)return t;let n=t.indexOf("?")>0?"&":"?";return`${t}${n}__wb_method=${r}&${e}`}function p(t,e=!0){if(typeof t=="string")try{t=JSON.parse(t)}catch{t={}}let r=new URLSearchParams,n={},i=o=>r.has(o)?(o in n||(n[o]=1),o+"."+ ++n[o]+"_"):o;try{JSON.stringify(t,(o,s)=>(["object","function"].includes(typeof s)||r.set(i(o),s),s))}catch(o){if(!e)throw o}return r}function y(t,e){let r=new URLSearchParams;t instanceof Uint8Array&&(t=new TextDecoder().decode(t));try{let n=e.split("boundary=")[1],i=t.split(new RegExp("-*"+n+"-*","mi"));for(let o of i){let s=o.trim().match(/name="([^"]+)"\r\n\r\n(.*)/im);s&&r.set(s[1],s[2])}}catch{}return r}function c(t,e=!0){return p(t,e).toString()}function g(t,e){return y(t,e).toString()}function Wombat($wbwindow,wbinfo){if(!(this instanceof Wombat))return new Wombat($wbwindow,wbinfo);this.debug_rw=false,this.$wbwindow=$wbwindow,this.WBWindow=Window,this.origHost=$wbwindow.location.host,this.origHostname=$wbwindow.location.hostname,this.origProtocol=$wbwindow.location.protocol,this.HTTP_PREFIX="http://",this.HTTPS_PREFIX="https://",this.REL_PREFIX="//",this.VALID_PREFIXES=[this.HTTP_PREFIX,this.HTTPS_PREFIX,this.REL_PREFIX],this.IGNORE_PREFIXES=["#","about:","data:","blob:","mailto:","javascript:","{","*"],"ignore_prefixes"in wbinfo&&(this.IGNORE_PREFIXES=this.IGNORE_PREFIXES.concat(wbinfo.ignore_prefixes)),this.WB_CHECK_THIS_FUNC="_____WB$wombat$check$this$function_____",this.WB_ASSIGN_FUNC="_____WB$wombat$assign$function_____",this.wb_setAttribute=$wbwindow.Element.prototype.setAttribute,this.wb_getAttribute=$wbwindow.Element.prototype.getAttribute,this.wb_funToString=Function.prototype.toString,this.WBAutoFetchWorker=null,this.wbUseAFWorker=wbinfo.enable_auto_fetch&&$wbwindow.Worker!=null&&wbinfo.is_live,this.wb_rel_prefix="",this.wb_wombat_updating=false,this.message_listeners=new FuncMap,this.storage_listeners=new FuncMap,this.linkAsTypes={script:"js_",worker:"js_",style:"cs_",image:"im_",document:"if_",fetch:"mp_",font:"oe_",audio:"oe_",video:"oe_",embed:"oe_",object:"oe_",track:"oe_","":"mp_",null:"mp_",undefined:"mp_"},this.linkTagMods={linkRelToAs:{import:this.linkAsTypes,preload:this.linkAsTypes},stylesheet:"cs_",null:"mp_",undefined:"mp_","":"mp_"},this.tagToMod={A:{href:"mp_"},AREA:{href:"mp_"},AUDIO:{src:"oe_",poster:"im_"},BASE:{href:"mp_"},EMBED:{src:"oe_"},FORM:{action:"mp_"},FRAME:{src:"fr_"},IFRAME:{src:"if_"},IMAGE:{href:"im_","xlink:href":"im_"},IMG:{src:"im_",srcset:"im_"},INPUT:{src:"oe_"},INS:{cite:"mp_"},META:{content:"mp_"},OBJECT:{data:"oe_",codebase:"oe_"},Q:{cite:"mp_"},SCRIPT:{src:"js_","xlink:href":"js_"},SOURCE:{src:"oe_",srcset:"oe_"},TRACK:{src:"oe_"},VIDEO:{src:"oe_",poster:"im_"},image:{href:"im_","xlink:href":"im_"}},this.URL_PROPS=["href","hash","pathname","host","hostname","protocol","origin","search","port"],this.wb_info=wbinfo,this.wb_opts=wbinfo.wombat_opts,this.wb_replay_prefix=wbinfo.prefix,this.wb_is_proxy=this.wb_info.proxy_magic||!this.wb_replay_prefix,this.wb_info.top_host=this.wb_info.top_host||"*",this.wb_curr_host=$wbwindow.location.protocol+"//"+$wbwindow.location.host,this.wb_info.wombat_opts=this.wb_info.wombat_opts||{},this.wb_orig_scheme=this.wb_info.wombat_scheme+"://",this.wb_orig_origin=this.wb_orig_scheme+this.wb_info.wombat_host,this.wb_abs_prefix=this.wb_replay_prefix,this.wb_capture_date_part="",!this.wb_info.is_live&&this.wb_info.wombat_ts&&(this.wb_capture_date_part="/"+this.wb_info.wombat_ts+"/"),this.BAD_PREFIXES=["http:"+this.wb_replay_prefix,"https:"+this.wb_replay_prefix,"http:/"+this.wb_replay_prefix,"https:/"+this.wb_replay_prefix],this.hostnamePortRe=/^[\w-]+(\.[\w-_]+)+(:\d+)(\/|$)/,this.ipPortRe=/^\d+\.\d+\.\d+\.\d+(:\d+)?(\/|$)/,this.workerBlobRe=/__WB_pmw\(.*?\)\.(?=postMessage\()/g,this.rmCheckThisInjectRe=/_____WB\$wombat\$check\$this\$function_____\(.*?\)/g,this.STYLE_REGEX=/(url\s*\(\s*[\\"']*)([^)'"]+)([\\"']*\s*\))/gi,this.IMPORT_REGEX=/(@import\s*[\\"']*)([^)'";]+)([\\"']*\s*;?)/gi,this.IMPORT_JS_REGEX=/^(import\s*\(['"]+)([^'"]+)(["'])/i,this.no_wombatRe=/WB_wombat_/g,this.srcsetRe=/\s*(\S*\s+[\d.]+[wx]),|(?:\s*,(?:\s+|(?=https?:)))/,this.cookie_path_regex=/\bPath='?"?([^;'"\s]+)/i,this.cookie_domain_regex=/\bDomain=([^;'"\s]+)/i,this.cookie_expires_regex=/\bExpires=([^;'"]+)/gi,this.SetCookieRe=/,(?![|])/,this.IP_RX=/^(\d)+\.(\d)+\.(\d)+\.(\d)+$/,this.FullHTMLRegex=/^\s*<(?:html|head|body|!doctype html)/i,this.IsTagRegex=/^\s*=0){var fnMapping=this._map.splice(idx,1);return fnMapping[0][1]}return null},FuncMap.prototype.map=function(param){for(var i=0;i0&&afw.preserveMedia(media)})},AutoFetcher.prototype.terminate=function(){this.worker.terminate()},AutoFetcher.prototype.justFetch=function(urls){this.worker.postMessage({type:"fetch-all",values:urls})},AutoFetcher.prototype.fetchAsPage=function(url,originalUrl,title){if(url){var headers={"X-Wombat-History-Page":originalUrl};if(title){var encodedTitle=encodeURIComponent(title.trim());title&&(headers["X-Wombat-History-Title"]=encodedTitle)}var fetchData={url:url,options:{headers:headers,cache:"no-store"}};this.justFetch([fetchData])}},AutoFetcher.prototype.postMessage=function(msg,deferred){if(deferred){var afWorker=this;return void Promise.resolve().then(function(){afWorker.worker.postMessage(msg)})}this.worker.postMessage(msg)},AutoFetcher.prototype.preserveSrcset=function(srcset,mod){this.postMessage({type:"values",srcset:{value:srcset,mod:mod,presplit:true}},true)},AutoFetcher.prototype.preserveDataSrcset=function(elem){this.postMessage({type:"values",srcset:{value:elem.dataset.srcset,mod:this.rwMod(elem),presplit:false}},true)},AutoFetcher.prototype.preserveMedia=function(media){this.postMessage({type:"values",media:media},true)},AutoFetcher.prototype.getSrcset=function(elem){return this.wombat.wb_getAttribute?this.wombat.wb_getAttribute.call(elem,"srcset"):elem.getAttribute("srcset")},AutoFetcher.prototype.rwMod=function(elem){switch(elem.tagName){case"SOURCE":return elem.parentElement&&elem.parentElement.tagName==="PICTURE"?"im_":"oe_";case"IMG":return"im_";}return"oe_"},AutoFetcher.prototype.extractFromLocalDoc=function(){var afw=this;Promise.resolve().then(function(){for(var msg={type:"values",context:{docBaseURI:document.baseURI}},media=[],i=0,sheets=document.styleSheets;i=0||scriptType.indexOf("ecmascript")>=0)&&(!!(scriptType.indexOf("json")>=0)||!!(scriptType.indexOf("text/")>=0))},Wombat.prototype.skipWrapScriptTextBasedOnText=function(text){if(!text||text.indexOf(this.WB_ASSIGN_FUNC)>=0||text.indexOf("<")===0)return true;for(var override_props=["window","self","document","location","top","parent","frames","opener"],i=0;i=0)return false;return true},Wombat.prototype.nodeHasChildren=function(node){if(!node)return false;if(typeof node.hasChildNodes==="function")return node.hasChildNodes();var kids=node.children||node.childNodes;return!!kids&&kids.length>0},Wombat.prototype.rwModForElement=function(elem,attrName){if(!elem)return undefined;var mod="mp_";if(!(elem.tagName==="LINK"&&attrName==="href")){var maybeMod=this.tagToMod[elem.tagName];maybeMod!=null&&(mod=maybeMod[attrName])}else if(elem.rel){var relV=elem.rel.trim().toLowerCase(),asV=this.wb_getAttribute.call(elem,"as");if(asV&&this.linkTagMods.linkRelToAs[relV]!=null){var asMods=this.linkTagMods.linkRelToAs[relV];mod=asMods[asV.toLowerCase()]}else this.linkTagMods[relV]!=null&&(mod=this.linkTagMods[relV])}return mod},Wombat.prototype.removeWBOSRC=function(elem){elem.tagName!=="SCRIPT"||elem.__$removedWBOSRC$__||(elem.hasAttribute("__wb_orig_src")&&elem.removeAttribute("__wb_orig_src"),elem.__$removedWBOSRC$__=true)},Wombat.prototype.retrieveWBOSRC=function(elem){if(elem.tagName==="SCRIPT"&&!elem.__$removedWBOSRC$__){var maybeWBOSRC;return maybeWBOSRC=this.wb_getAttribute?this.wb_getAttribute.call(elem,"__wb_orig_src"):elem.getAttribute("__wb_orig_src"),maybeWBOSRC==null&&(elem.__$removedWBOSRC$__=true),maybeWBOSRC}return undefined},Wombat.prototype.wrapScriptTextJsProxy=function(scriptText){return"var _____WB$wombat$assign$function_____ = function(name) {return (self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init(name)) || self[name]; };\nif (!self.__WB_pmw) { self.__WB_pmw = function(obj) { this.__WB_source = obj; return this; } }\n{\nlet window = _____WB$wombat$assign$function_____(\"window\");\nlet globalThis = _____WB$wombat$assign$function_____(\"globalThis\");\nlet self = _____WB$wombat$assign$function_____(\"self\");\nlet document = _____WB$wombat$assign$function_____(\"document\");\nlet location = _____WB$wombat$assign$function_____(\"location\");\nlet top = _____WB$wombat$assign$function_____(\"top\");\nlet parent = _____WB$wombat$assign$function_____(\"parent\");\nlet frames = _____WB$wombat$assign$function_____(\"frames\");\nlet opener = _____WB$wombat$assign$function_____(\"opener\");\n{\n"+scriptText.replace(this.DotPostMessageRe,".__WB_pmw(self.window)$1")+"\n\n}}"},Wombat.prototype.watchElem=function(elem,func){if(!this.$wbwindow.MutationObserver)return false;var m=new this.$wbwindow.MutationObserver(function(records,observer){for(var r,i=0;i"},Wombat.prototype.getFinalUrl=function(useRel,mod,url){var prefix=useRel?this.wb_rel_prefix:this.wb_abs_prefix;return mod==null&&(mod=this.wb_info.mod),this.wb_info.is_live||(prefix+=this.wb_info.wombat_ts),prefix+=mod,prefix[prefix.length-1]!=="/"&&(prefix+="/"),prefix+url},Wombat.prototype.resolveRelUrl=function(url,doc){var docObj=doc||this.$wbwindow.document,parser=this.makeParser(docObj.baseURI,docObj),hash=parser.href.lastIndexOf("#"),href=hash>=0?parser.href.substring(0,hash):parser.href,lastslash=href.lastIndexOf("/");return parser.href=lastslash>=0&&lastslash!==href.length-1?href.substring(0,lastslash+1)+url:href+url,parser.href},Wombat.prototype.extractOriginalURL=function(rewrittenUrl){if(!rewrittenUrl)return"";if(this.wb_is_proxy)return rewrittenUrl;var rwURLString=rewrittenUrl.toString(),url=rwURLString;if(this.startsWithOneOf(url,this.IGNORE_PREFIXES))return url;if(url.startsWith(this.wb_info.static_prefix))return url;var start;start=this.startsWith(url,this.wb_abs_prefix)?this.wb_abs_prefix.length:this.wb_rel_prefix&&this.startsWith(url,this.wb_rel_prefix)?this.wb_rel_prefix.length:this.wb_rel_prefix?1:0;var index=url.indexOf("/http",start);return index<0&&(index=url.indexOf("///",start)),index<0&&(index=url.indexOf("/blob:",start)),index<0&&(index=url.indexOf("/about:blank",start)),index>=0?url=url.substr(index+1):(index=url.indexOf(this.wb_replay_prefix),index>=0&&(url=url.substr(index+this.wb_replay_prefix.length)),url.length>4&&url.charAt(2)==="_"&&url.charAt(3)==="/"&&(url=url.substr(4)),url!==rwURLString&&!this.startsWithOneOf(url,this.VALID_PREFIXES)&&!this.startsWith(url,"blob:")&&(url=this.wb_orig_scheme+url)),rwURLString.charAt(0)==="/"&&rwURLString.charAt(1)!=="/"&&this.startsWith(url,this.wb_orig_origin)&&(url=url.substr(this.wb_orig_origin.length)),this.startsWith(url,this.REL_PREFIX)?this.wb_info.wombat_scheme+":"+url:url},Wombat.prototype.makeParser=function(maybeRewrittenURL,doc){var originalURL=this.extractOriginalURL(maybeRewrittenURL),docElem=doc;return doc||(this.$wbwindow.location.href==="about:blank"&&this.$wbwindow.opener?docElem=this.$wbwindow.opener.document:docElem=this.$wbwindow.document),this._makeURLParser(originalURL,docElem)},Wombat.prototype._makeURLParser=function(url,docElem){try{return new this.$wbwindow.URL(url,docElem.baseURI)}catch(e){}var p=docElem.createElement("a");return p._no_rewrite=true,p.href=url,p},Wombat.prototype.defProp=function(obj,prop,setFunc,getFunc,enumerable){var existingDescriptor=Object.getOwnPropertyDescriptor(obj,prop);if(existingDescriptor&&!existingDescriptor.configurable)return false;if(!getFunc)return false;var descriptor={configurable:true,enumerable:enumerable||false,get:getFunc};setFunc&&(descriptor.set=setFunc);try{return Object.defineProperty(obj,prop,descriptor),true}catch(e){return console.warn("Failed to redefine property %s",prop,e.message),false}},Wombat.prototype.defGetterProp=function(obj,prop,getFunc,enumerable){var existingDescriptor=Object.getOwnPropertyDescriptor(obj,prop);if(existingDescriptor&&!existingDescriptor.configurable)return false;if(!getFunc)return false;try{return Object.defineProperty(obj,prop,{configurable:true,enumerable:enumerable||false,get:getFunc}),true}catch(e){return console.warn("Failed to redefine property %s",prop,e.message),false}},Wombat.prototype.getOrigGetter=function(obj,prop){var orig_getter;if(obj.__lookupGetter__&&(orig_getter=obj.__lookupGetter__(prop)),!orig_getter&&Object.getOwnPropertyDescriptor){var props=Object.getOwnPropertyDescriptor(obj,prop);props&&(orig_getter=props.get)}return orig_getter},Wombat.prototype.getOrigSetter=function(obj,prop){var orig_setter;if(obj.__lookupSetter__&&(orig_setter=obj.__lookupSetter__(prop)),!orig_setter&&Object.getOwnPropertyDescriptor){var props=Object.getOwnPropertyDescriptor(obj,prop);props&&(orig_setter=props.set)}return orig_setter},Wombat.prototype.getAllOwnProps=function(obj){for(var ownProps=[],props=Object.getOwnPropertyNames(obj),i=0;i "+final_href),actualLocation.href=final_href}}},Wombat.prototype.checkLocationChange=function(wombatLoc,isTop){var locType=typeof wombatLoc,actual_location=isTop?this.$wbwindow.__WB_replay_top.location:this.$wbwindow.location;locType==="string"?this.updateLocation(wombatLoc,actual_location.href,actual_location):locType==="object"&&this.updateLocation(wombatLoc.href,wombatLoc._orig_href,actual_location)},Wombat.prototype.checkAllLocations=function(){return!this.wb_wombat_updating&&void(this.wb_wombat_updating=true,this.checkLocationChange(this.$wbwindow.WB_wombat_location,false),this.$wbwindow.WB_wombat_location!=this.$wbwindow.__WB_replay_top.WB_wombat_location&&this.checkLocationChange(this.$wbwindow.__WB_replay_top.WB_wombat_location,true),this.wb_wombat_updating=false)},Wombat.prototype.proxyToObj=function(source){if(source)try{var proxyRealObj=source.__WBProxyRealObj__;if(proxyRealObj)return proxyRealObj}catch(e){}return source},Wombat.prototype.objToProxy=function(obj){if(obj)try{var maybeWbProxy=obj._WB_wombat_obj_proxy;if(maybeWbProxy)return maybeWbProxy}catch(e){}return obj},Wombat.prototype.defaultProxyGet=function(obj,prop,ownProps,fnCache){switch(prop){case"__WBProxyRealObj__":return obj;case"location":case"WB_wombat_location":return obj.WB_wombat_location;case"_WB_wombat_obj_proxy":return obj._WB_wombat_obj_proxy;case"__WB_pmw":case this.WB_ASSIGN_FUNC:case this.WB_CHECK_THIS_FUNC:return obj[prop];case"origin":return obj.WB_wombat_location.origin;case"constructor":return obj.constructor;}var retVal=obj[prop],type=typeof retVal;if(type==="function"&&ownProps.indexOf(prop)!==-1){switch(prop){case"requestAnimationFrame":case"cancelAnimationFrame":{if(!this.isNativeFunction(retVal))return retVal;break}case"eval":if(this.isNativeFunction(retVal))return this.wrappedEval(retVal);}var cachedFN=fnCache[prop];return cachedFN&&cachedFN.original===retVal||(cachedFN={original:retVal,boundFn:retVal.bind(obj)},fnCache[prop]=cachedFN),cachedFN.boundFn}return type==="object"&&retVal&&retVal._WB_wombat_obj_proxy?(retVal instanceof this.WBWindow&&this.initNewWindowWombat(retVal),retVal._WB_wombat_obj_proxy):retVal},Wombat.prototype.setLoc=function(loc,originalURL){var parser=this.makeParser(originalURL,loc.ownerDocument);loc._orig_href=originalURL,loc._parser=parser;var href=parser.href;loc._hash=parser.hash,loc._href=href,loc._host=parser.host,loc._hostname=parser.hostname,loc._origin=parser.origin?parser.host?parser.origin:"null":parser.protocol+"//"+parser.hostname+(parser.port?":"+parser.port:""),loc._pathname=parser.pathname,loc._port=parser.port,loc._protocol=parser.protocol,loc._search=parser.search,Object.defineProperty||(loc.href=href,loc.hash=parser.hash,loc.host=loc._host,loc.hostname=loc._hostname,loc.origin=loc._origin,loc.pathname=loc._pathname,loc.port=loc._port,loc.protocol=loc._protocol,loc.search=loc._search)},Wombat.prototype.makeGetLocProp=function(prop,origGetter){var wombat=this;return function newGetLocProp(){if(this._no_rewrite)return origGetter.call(this,prop);var curr_orig_href=origGetter.call(this,"href");return prop==="href"?wombat.extractOriginalURL(curr_orig_href):prop==="ancestorOrigins"?[]:(this._orig_href!==curr_orig_href&&wombat.setLoc(this,curr_orig_href),this["_"+prop])}},Wombat.prototype.makeSetLocProp=function(prop,origSetter,origGetter){var wombat=this;return function newSetLocProp(value){if(this._no_rewrite)return origSetter.call(this,prop,value);if(this["_"+prop]!==value){if(this["_"+prop]=value,!this._parser){var href=origGetter.call(this);this._parser=wombat.makeParser(href,this.ownerDocument)}var rel=false;if(prop==="href"&&typeof value==="string")if(value&&this._parser instanceof URL)try{value=new URL(value,this._parser).href}catch(e){console.warn("Error resolving URL",e)}else value&&(value[0]==="."||value[0]==="#"?value=wombat.resolveRelUrl(value,this.ownerDocument):value[0]==="/"&&(value.length>1&&value[1]==="/"?value=this._parser.protocol+value:(rel=true,value=WB_wombat_location.origin+value)));try{this._parser[prop]=value}catch(e){console.log("Error setting "+prop+" = "+value)}prop==="hash"?(value=this._parser[prop],origSetter.call(this,"hash",value)):(rel=rel||value===this._parser.pathname,value=wombat.rewriteUrl(this._parser.href,rel),origSetter.call(this,"href",value))}}},Wombat.prototype.styleReplacer=function(match,n1,n2,n3,offset,string){return n1+this.rewriteUrl(n2)+n3},Wombat.prototype.domConstructorErrorChecker=function(thisObj,what,args,numRequiredArgs){var errorMsg,needArgs=typeof numRequiredArgs==="number"?numRequiredArgs:1;if(thisObj instanceof this.WBWindow?errorMsg="Failed to construct '"+what+"': Please use the 'new' operator, this DOM object constructor cannot be called as a function.":args&&args.length=0)return url;if(url.indexOf(this.wb_rel_prefix)===0&&url.indexOf("http")>1){var scheme_sep=url.indexOf(":/");return scheme_sep>0&&url[scheme_sep+2]!=="/"?url.substring(0,scheme_sep+2)+"/"+url.substring(scheme_sep+2):url}return this.getFinalUrl(true,mod,this.wb_orig_origin+url)}url.charAt(0)==="."&&(url=this.resolveRelUrl(url,doc));var prefix=this.startsWithOneOf(url.toLowerCase(),this.VALID_PREFIXES);if(prefix){var orig_host=this.replayTopHost,orig_protocol=this.replayTopProtocol,prefix_host=prefix+orig_host+"/";if(this.startsWith(url,prefix_host)){if(this.startsWith(url,this.wb_replay_prefix))return url;var curr_scheme=orig_protocol+"//",path=url.substring(prefix_host.length),rebuild=false;return path.indexOf(this.wb_rel_prefix)<0&&url.indexOf("/static/")<0&&(path=this.getFinalUrl(true,mod,WB_wombat_location.origin+"/"+path),rebuild=true),prefix!==curr_scheme&&prefix!==this.REL_PREFIX&&(rebuild=true),rebuild&&(url=useRel?"":curr_scheme+orig_host,path&&path[0]!=="/"&&(url+="/"),url+=path),url}return this.getFinalUrl(useRel,mod,url)}return prefix=this.startsWithOneOf(url,this.BAD_PREFIXES),prefix?this.getFinalUrl(useRel,mod,this.extractOriginalURL(url)):url},Wombat.prototype.rewriteUrl=function(url,useRel,mod,doc){var rewritten=this.rewriteUrl_(url,useRel,mod,doc);return this.debug_rw&&(url===rewritten?console.log("NOT REWRITTEN "+url):console.log("REWRITE: "+url+" -> "+rewritten)),rewritten},Wombat.prototype.performAttributeRewrite=function(elem,name,value,absUrlOnly){switch(name){case"innerHTML":case"outerHTML":return this.rewriteHtml(value);case"filter":return this.rewriteInlineStyle(value);case"style":return this.rewriteStyle(value);case"srcset":return this.rewriteSrcset(value,elem);}if(absUrlOnly&&!this.startsWithOneOf(value,this.VALID_PREFIXES))return value;var mod=this.rwModForElement(elem,name);return this.wbUseAFWorker&&this.WBAutoFetchWorker&&this.isSavedDataSrcSrcset(elem)&&this.WBAutoFetchWorker.preserveDataSrcset(elem),this.rewriteUrl(value,false,mod,elem.ownerDocument)},Wombat.prototype.rewriteAttr=function(elem,name,absUrlOnly){var changed=false;if(!elem||!elem.getAttribute||elem._no_rewrite||elem["_"+name])return changed;var value=this.wb_getAttribute.call(elem,name);if(!value||this.startsWith(value,"javascript:"))return changed;var new_value=this.performAttributeRewrite(elem,name,value,absUrlOnly);return new_value!==value&&(this.removeWBOSRC(elem),this.wb_setAttribute.call(elem,name,new_value),changed=true),changed},Wombat.prototype.noExceptRewriteStyle=function(style){try{return this.rewriteStyle(style)}catch(e){return style}},Wombat.prototype.rewriteStyle=function(style){if(!style)return style;var value=style;return typeof style==="object"&&(value=style.toString()),typeof value==="string"?value.replace(this.STYLE_REGEX,this.styleReplacer).replace(this.IMPORT_REGEX,this.styleReplacer).replace(this.no_wombatRe,""):value},Wombat.prototype.rewriteSrcset=function(value,elem){if(!value)return"";for(var v,split=value.split(this.srcsetRe),values=[],mod=this.rwModForElement(elem,"srcset"),i=0;i=0){var JS="javascript:";new_value="javascript:window.parent._wb_wombat.initNewWindowWombat(window);"+value.substr(11)}return new_value||(new_value=this.rewriteUrl(value,false,this.rwModForElement(elem,attrName))),new_value!==value&&(this.wb_setAttribute.call(elem,attrName,new_value),true)},Wombat.prototype.rewriteScript=function(elem){if(elem.hasAttribute("src")||!elem.textContent||!this.$wbwindow.Proxy)return this.rewriteAttr(elem,"src");if(this.skipWrapScriptBasedOnType(elem.type))return false;var text=elem.textContent.trim();return!this.skipWrapScriptTextBasedOnText(text)&&(elem.textContent=this.wrapScriptTextJsProxy(text),true)},Wombat.prototype.rewriteSVGElem=function(elem){var changed=this.rewriteAttr(elem,"filter");return changed=this.rewriteAttr(elem,"style")||changed,changed=this.rewriteAttr(elem,"xlink:href")||changed,changed=this.rewriteAttr(elem,"href")||changed,changed=this.rewriteAttr(elem,"src")||changed,changed},Wombat.prototype.rewriteElem=function(elem){var changed=false;if(!elem)return changed;if(elem instanceof SVGElement)changed=this.rewriteSVGElem(elem);else switch(elem.tagName){case"META":var maybeCSP=this.wb_getAttribute.call(elem,"http-equiv");maybeCSP&&maybeCSP.toLowerCase()==="content-security-policy"&&(this.wb_setAttribute.call(elem,"http-equiv","_"+maybeCSP),changed=true);break;case"STYLE":var new_content=this.rewriteStyle(elem.textContent);elem.textContent!==new_content&&(elem.textContent=new_content,changed=true,this.wbUseAFWorker&&this.WBAutoFetchWorker&&elem.sheet!=null&&this.WBAutoFetchWorker.deferredSheetExtraction(elem.sheet));break;case"LINK":changed=this.rewriteAttr(elem,"href"),this.wbUseAFWorker&&elem.rel==="stylesheet"&&this._addEventListener(elem,"load",this.utilFns.wbSheetMediaQChecker);break;case"IMG":changed=this.rewriteAttr(elem,"src"),changed=this.rewriteAttr(elem,"srcset")||changed,changed=this.rewriteAttr(elem,"style")||changed,this.wbUseAFWorker&&this.WBAutoFetchWorker&&elem.dataset.srcset&&this.WBAutoFetchWorker.preserveDataSrcset(elem);break;case"OBJECT":if(this.wb_info.isSW&&elem.parentElement&&elem.getAttribute("type")==="application/pdf"){for(var iframe=this.$wbwindow.document.createElement("IFRAME"),i=0;i0;)for(var child,children=rewriteQ.shift(),i=0;i"+rwString+"","text/html");if(!inner_doc||!this.nodeHasChildren(inner_doc.head)||!inner_doc.head.children[0].content)return rwString;var template=inner_doc.head.children[0];if(template._no_rewrite=true,this.recurseRewriteElem(template.content)){var new_html=template.innerHTML;if(checkEndTag){var first_elem=template.content.children&&template.content.children[0];if(first_elem){var end_tag="";this.endsWith(new_html,end_tag)&&!this.endsWith(rwString.toLowerCase(),end_tag)&&(new_html=new_html.substring(0,new_html.length-end_tag.length))}else if(rwString[0]!=="<"||rwString[rwString.length-1]!==">")return this.write_buff+=rwString,undefined}return new_html}return rwString},Wombat.prototype.rewriteHtmlFull=function(string,checkEndTag){var inner_doc=new DOMParser().parseFromString(string,"text/html");if(!inner_doc)return string;for(var changed=false,i=0;i=0)inner_doc.documentElement._no_rewrite=true,new_html=this.reconstructDocType(inner_doc.doctype)+inner_doc.documentElement.outerHTML;else{inner_doc.head._no_rewrite=true,inner_doc.body._no_rewrite=true;var headHasKids=this.nodeHasChildren(inner_doc.head),bodyHasKids=this.nodeHasChildren(inner_doc.body);if(new_html=(headHasKids?inner_doc.head.outerHTML:"")+(bodyHasKids?inner_doc.body.outerHTML:""),checkEndTag)if(inner_doc.all.length>3){var end_tag="";this.endsWith(new_html,end_tag)&&!this.endsWith(string.toLowerCase(),end_tag)&&(new_html=new_html.substring(0,new_html.length-end_tag.length))}else if(string[0]!=="<"||string[string.length-1]!==">")return void(this.write_buff+=string);new_html=this.reconstructDocType(inner_doc.doctype)+new_html}return new_html}return string},Wombat.prototype.rewriteInlineStyle=function(orig){var decoded;try{decoded=decodeURIComponent(orig)}catch(e){decoded=orig}if(decoded!==orig){var parts=this.rewriteStyle(decoded).split(",",2);return parts[0]+","+encodeURIComponent(parts[1])}return this.rewriteStyle(orig)},Wombat.prototype.rewriteCookie=function(cookie){var wombat=this,rwCookie=cookie.replace(this.wb_abs_prefix,"").replace(this.wb_rel_prefix,"");return rwCookie=rwCookie.replace(this.cookie_domain_regex,function(m,m1){var message={domain:m1,cookie:rwCookie,wb_type:"cookie"};return wombat.sendTopMessage(message,true),wombat.$wbwindow.location.hostname.indexOf(".")>=0&&!wombat.IP_RX.test(wombat.$wbwindow.location.hostname)?"Domain=."+wombat.$wbwindow.location.hostname:""}).replace(this.cookie_path_regex,function(m,m1){var rewritten=wombat.rewriteUrl(m1);return rewritten.indexOf(wombat.wb_curr_host)===0&&(rewritten=rewritten.substring(wombat.wb_curr_host.length)),"Path="+rewritten}),wombat.$wbwindow.location.protocol!=="https:"&&(rwCookie=rwCookie.replace("secure","")),rwCookie.replace(",|",",")},Wombat.prototype.rewriteWorker=function(workerUrl){if(!workerUrl)return workerUrl;workerUrl=workerUrl.toString();var isBlob=workerUrl.indexOf("blob:")===0,isJS=workerUrl.indexOf("javascript:")===0;if(!isBlob&&!isJS){if(!this.startsWithOneOf(workerUrl,this.VALID_PREFIXES)&&!this.startsWith(workerUrl,"/")&&!this.startsWithOneOf(workerUrl,this.BAD_PREFIXES)){var rurl=this.resolveRelUrl(workerUrl,this.$wbwindow.document);return this.rewriteUrl(rurl,false,"wkr_",this.$wbwindow.document)}return this.rewriteUrl(workerUrl,false,"wkr_",this.$wbwindow.document)}var workerCode=isJS?workerUrl.replace("javascript:",""):null;if(isBlob){var x=new XMLHttpRequest;this.utilFns.XHRopen.call(x,"GET",workerUrl,false),this.utilFns.XHRsend.call(x),workerCode=x.responseText.replace(this.workerBlobRe,"").replace(this.rmCheckThisInjectRe,"this")}if(this.wb_info.static_prefix||this.wb_info.ww_rw_script){var originalURL=this.$wbwindow.document.baseURI,ww_rw=this.wb_info.ww_rw_script||this.wb_info.static_prefix+"wombatWorkers.js",rw="(function() { self.importScripts('"+ww_rw+"'); new WBWombat({'prefix': '"+this.wb_abs_prefix+"', 'prefixMod': '"+this.wb_abs_prefix+"wkrf_/', 'originalURL': '"+originalURL+"'}); })();";workerCode=rw+workerCode}var blob=new Blob([workerCode],{type:"application/javascript"});return URL.createObjectURL(blob)},Wombat.prototype.rewriteTextNodeFn=function(fnThis,originalFn,argsObj){var args,deproxiedThis=this.proxyToObj(fnThis);if(argsObj.length>0&&deproxiedThis.parentElement&&deproxiedThis.parentElement.tagName==="STYLE"){args=new Array(argsObj.length);var dataIndex=argsObj.length-1;dataIndex===2?(args[0]=argsObj[0],args[1]=argsObj[1]):dataIndex===1&&(args[0]=argsObj[0]),args[dataIndex]=this.rewriteStyle(argsObj[dataIndex])}else args=argsObj;return originalFn.__WB_orig_apply?originalFn.__WB_orig_apply(deproxiedThis,args):originalFn.apply(deproxiedThis,args)},Wombat.prototype.rewriteDocWriteWriteln=function(fnThis,originalFn,argsObj){var string,thisObj=this.proxyToObj(fnThis),argLen=argsObj.length;if(argLen===0)return originalFn.call(thisObj);string=argLen===1?argsObj[0]:Array.prototype.join.call(argsObj,"");var new_buff=this.rewriteHtml(string,true),res=originalFn.call(thisObj,new_buff);return this.initNewWindowWombat(thisObj.defaultView),res},Wombat.prototype.rewriteChildNodeFn=function(fnThis,originalFn,argsObj){var thisObj=this.proxyToObj(fnThis);if(argsObj.length===0)return originalFn.call(thisObj);var newArgs=this.rewriteElementsInArguments(argsObj);return originalFn.__WB_orig_apply?originalFn.__WB_orig_apply(thisObj,newArgs):originalFn.apply(thisObj,newArgs)},Wombat.prototype.rewriteInsertAdjHTMLOrElemArgs=function(fnThis,originalFn,position,textOrElem,rwHTML){var fnThisObj=this.proxyToObj(fnThis);return fnThisObj._no_rewrite?originalFn.call(fnThisObj,position,textOrElem):rwHTML?originalFn.call(fnThisObj,position,this.rewriteHtml(textOrElem)):(this.rewriteElemComplete(textOrElem),originalFn.call(fnThisObj,position,textOrElem))},Wombat.prototype.rewriteSetTimeoutInterval=function(fnThis,originalFn,argsObj){var rw=this.isString(argsObj[0]),args=rw?new Array(argsObj.length):argsObj;if(rw){args[0]=this.$wbwindow.Proxy?this.wrapScriptTextJsProxy(argsObj[0]):argsObj[0].replace(/\blocation\b/g,"WB_wombat_$&");for(var i=1;i0&&cssStyleValueOverride(this.$wbwindow.CSSStyleValue,"parse"),this.$wbwindow.CSSStyleValue.parseAll&&this.$wbwindow.CSSStyleValue.parseAll.toString().indexOf("[native code]")>0&&cssStyleValueOverride(this.$wbwindow.CSSStyleValue,"parseAll")}if(this.$wbwindow.CSSKeywordValue&&this.$wbwindow.CSSKeywordValue.prototype){var oCSSKV=this.$wbwindow.CSSKeywordValue;this.$wbwindow.CSSKeywordValue=function(CSSKeywordValue_){return function CSSKeywordValue(cssValue){return wombat.domConstructorErrorChecker(this,"CSSKeywordValue",arguments),new CSSKeywordValue_(wombat.rewriteStyle(cssValue))}}(this.$wbwindow.CSSKeywordValue),this.$wbwindow.CSSKeywordValue.prototype=oCSSKV.prototype,Object.defineProperty(this.$wbwindow.CSSKeywordValue.prototype,"constructor",{value:this.$wbwindow.CSSKeywordValue}),addToStringTagToClass(this.$wbwindow.CSSKeywordValue,"CSSKeywordValue")}if(this.$wbwindow.StylePropertyMap&&this.$wbwindow.StylePropertyMap.prototype){var originalSet=this.$wbwindow.StylePropertyMap.prototype.set;this.$wbwindow.StylePropertyMap.prototype.set=function set(){if(arguments.length<=1)return originalSet.__WB_orig_apply?originalSet.__WB_orig_apply(this,arguments):originalSet.apply(this,arguments);var newArgs=new Array(arguments.length);newArgs[0]=arguments[0];for(var i=1;i")&&(array[0]=wombat.rewriteHtml(array[0]),options.type="text/html"),new Blob_(array,options)}}(this.$wbwindow.Blob),this.$wbwindow.Blob.prototype=orig_blob.prototype}},Wombat.prototype.initWSOverride=function(){this.$wbwindow.WebSocket&&this.$wbwindow.WebSocket.prototype&&(this.$wbwindow.WebSocket=function(WebSocket_){function WebSocket(url,protocols){this.addEventListener=function(){},this.removeEventListener=function(){},this.close=function(){},this.send=function(data){console.log("ws send",data)},this.protocol=protocols&&protocols.length?protocols[0]:"",this.url=url,this.readyState=0}return WebSocket.CONNECTING=0,WebSocket.OPEN=1,WebSocket.CLOSING=2,WebSocket.CLOSED=3,WebSocket}(this.$wbwindow.WebSocket),Object.defineProperty(this.$wbwindow.WebSocket.prototype,"constructor",{value:this.$wbwindow.WebSocket}),addToStringTagToClass(this.$wbwindow.WebSocket,"WebSocket"))},Wombat.prototype.initDocTitleOverride=function(){var orig_get_title=this.getOrigGetter(this.$wbwindow.document,"title"),orig_set_title=this.getOrigSetter(this.$wbwindow.document,"title"),wombat=this,set_title=function title(value){var res=orig_set_title.call(this,value),message={wb_type:"title",title:value};return wombat.sendTopMessage(message),res};this.defProp(this.$wbwindow.document,"title",set_title,orig_get_title)},Wombat.prototype.initFontFaceOverride=function(){if(this.$wbwindow.FontFace){var wombat=this,origFontFace=this.$wbwindow.FontFace;this.$wbwindow.FontFace=function(FontFace_){return function FontFace(family,source,descriptors){wombat.domConstructorErrorChecker(this,"FontFace",arguments,2);var rwSource=source;return source!=null&&(typeof source==="string"?rwSource=wombat.rewriteInlineStyle(source):rwSource=wombat.rewriteInlineStyle(source.toString())),new FontFace_(family,rwSource,descriptors)}}(this.$wbwindow.FontFace),this.$wbwindow.FontFace.prototype=origFontFace.prototype,Object.defineProperty(this.$wbwindow.FontFace.prototype,"constructor",{value:this.$wbwindow.FontFace}),addToStringTagToClass(this.$wbwindow.FontFace,"FontFace")}},Wombat.prototype.initFixedRatio=function(value){try{this.$wbwindow.devicePixelRatio=value}catch(e){}if(Object.defineProperty)try{Object.defineProperty(this.$wbwindow,"devicePixelRatio",{value:value,writable:false})}catch(e){}},Wombat.prototype.initPaths=function(wbinfo){wbinfo.wombat_opts=wbinfo.wombat_opts||{},Object.assign(this.wb_info,wbinfo),this.wb_opts=wbinfo.wombat_opts,this.wb_replay_prefix=wbinfo.prefix,this.wb_is_proxy=wbinfo.proxy_magic||!this.wb_replay_prefix,this.wb_info.top_host=this.wb_info.top_host||"*",this.wb_curr_host=this.$wbwindow.location.protocol+"//"+this.$wbwindow.location.host,this.wb_info.wombat_opts=this.wb_info.wombat_opts||{},this.wb_orig_scheme=wbinfo.wombat_scheme+"://",this.wb_orig_origin=this.wb_orig_scheme+wbinfo.wombat_host,this.wb_abs_prefix=this.wb_replay_prefix,this.wb_capture_date_part=!wbinfo.is_live&&wbinfo.wombat_ts?"/"+wbinfo.wombat_ts+"/":"",this.initBadPrefixes(this.wb_replay_prefix),this.initCookiePreset()},Wombat.prototype.initSeededRandom=function(seed){this.$wbwindow.Math.seed=parseInt(seed);var wombat=this;this.$wbwindow.Math.random=function random(){return wombat.$wbwindow.Math.seed=(wombat.$wbwindow.Math.seed*9301+49297)%233280,wombat.$wbwindow.Math.seed/233280}},Wombat.prototype.initHistoryOverrides=function(){this.overrideHistoryFunc("pushState"),this.overrideHistoryFunc("replaceState");var wombat=this;this.$wbwindow.addEventListener("popstate",function(event){wombat.sendHistoryUpdate(wombat.$wbwindow.WB_wombat_location.href,wombat.$wbwindow.document.title)})},Wombat.prototype.initCookiePreset=function(){if(this.wb_info.presetCookie)for(var splitCookies=this.wb_info.presetCookie.split(";"),i=0;i2&&!this.__WB_xhr_open_arguments[2]&&navigator.userAgent.indexOf("Firefox")===-1&&(this.__WB_xhr_open_arguments[2]=true,console.warn("wombat.js: Sync XHR not supported in SW-based replay in this browser, converted to async")),this._no_rewrite||(this.__WB_xhr_open_arguments[1]=wombat.rewriteUrl(this.__WB_xhr_open_arguments[1])),origOpen.apply(this,this.__WB_xhr_open_arguments),!wombat.startsWith(this.__WB_xhr_open_arguments[1],"data:")){for(const[name,value]of this.__WB_xhr_headers.entries())origSetRequestHeader.call(this,name,value);origSetRequestHeader.call(this,"X-Pywb-Requested-With","XMLHttpRequest")}return origSend.call(this,value)}}else if(this.$wbwindow.XMLHttpRequest.prototype.open){var origXMLHttpOpen=this.$wbwindow.XMLHttpRequest.prototype.open;this.utilFns.XHRopen=origXMLHttpOpen,this.utilFns.XHRsend=this.$wbwindow.XMLHttpRequest.prototype.send,this.$wbwindow.XMLHttpRequest.prototype.open=function open(method,url,async,user,password){var rwURL=this._no_rewrite?url:wombat.rewriteUrl(url),openAsync=true;async==null||async||(openAsync=false),origXMLHttpOpen.call(this,method,rwURL,openAsync,user,password),wombat.startsWith(rwURL,"data:")||this.setRequestHeader("X-Pywb-Requested-With","XMLHttpRequest")}}if(this.$wbwindow.fetch){var orig_fetch=this.$wbwindow.fetch;this.$wbwindow.fetch=function fetch(input,init_opts){var rwInput=input,inputType=typeof input;if(inputType==="string")rwInput=wombat.rewriteUrl(input);else if(inputType==="object"&&input.url){var new_url=wombat.rewriteUrl(input.url);new_url!==input.url&&(rwInput=new Request(new_url,init_opts))}else inputType==="object"&&input.href&&(rwInput=wombat.rewriteUrl(input.href));if(init_opts||(init_opts={}),init_opts.credentials===undefined)try{init_opts.credentials="include"}catch(e){}return orig_fetch.call(wombat.proxyToObj(this),rwInput,init_opts)}}if(this.$wbwindow.Request&&this.$wbwindow.Request.prototype){var orig_request=this.$wbwindow.Request;this.$wbwindow.Request=function(Request_){return function Request(input,init_opts){wombat.domConstructorErrorChecker(this,"Request",arguments);var newInitOpts=init_opts||{},newInput=input,inputType=typeof input;switch(inputType){case"string":newInput=wombat.rewriteUrl(input);break;case"object":if(newInput=input,input.url){var new_url=wombat.rewriteUrl(input.url);new_url!==input.url&&(newInput=new Request_(new_url,input))}else input.href&&(newInput=wombat.rewriteUrl(input.toString(),true));}return newInitOpts.credentials="include",new Request_(newInput,newInitOpts)}}(this.$wbwindow.Request),this.$wbwindow.Request.prototype=orig_request.prototype,Object.defineProperty(this.$wbwindow.Request.prototype,"constructor",{value:this.$wbwindow.Request})}if(this.$wbwindow.Response&&this.$wbwindow.Response.prototype){var originalRedirect=this.$wbwindow.Response.prototype.redirect;this.$wbwindow.Response.prototype.redirect=function redirect(url,status){var rwURL=wombat.rewriteUrl(url,true,null,wombat.$wbwindow.document);return originalRedirect.call(this,rwURL,status)}}if(this.$wbwindow.EventSource&&this.$wbwindow.EventSource.prototype){var origEventSource=this.$wbwindow.EventSource;this.$wbwindow.EventSource=function(EventSource_){return function EventSource(url,configuration){wombat.domConstructorErrorChecker(this,"EventSource",arguments);var rwURL=url;return url!=null&&(rwURL=wombat.rewriteUrl(url)),new EventSource_(rwURL,configuration)}}(this.$wbwindow.EventSource),this.$wbwindow.EventSource.prototype=origEventSource.prototype,Object.defineProperty(this.$wbwindow.EventSource.prototype,"constructor",{value:this.$wbwindow.EventSource}),addToStringTagToClass(this.$wbwindow.EventSource,"EventSource")}},Wombat.prototype.initElementGetSetAttributeOverride=function(){if(!this.wb_opts.skip_setAttribute&&this.$wbwindow.Element&&this.$wbwindow.Element.prototype){var wombat=this,ElementProto=this.$wbwindow.Element.prototype;if(ElementProto.setAttribute){var orig_setAttribute=ElementProto.setAttribute;ElementProto._orig_setAttribute=orig_setAttribute,ElementProto.setAttribute=function setAttribute(name,value){var rwValue=value;if(name&&typeof rwValue==="string"){var lowername=name.toLowerCase();if(this.tagName==="LINK"&&lowername==="href"&&rwValue.indexOf("data:text/css")===0)rwValue=wombat.rewriteInlineStyle(value);else if(lowername==="style")rwValue=wombat.rewriteStyle(value);else if(lowername==="srcset"||lowername==="imagesrcset"&&this.tagName==="LINK")rwValue=wombat.rewriteSrcset(value,this);else{var shouldRW=wombat.shouldRewriteAttr(this.tagName,lowername);shouldRW&&(wombat.removeWBOSRC(this),!this._no_rewrite&&(rwValue=wombat.rewriteUrl(value,false,wombat.rwModForElement(this,lowername))))}}return orig_setAttribute.call(this,name,rwValue)}}if(ElementProto.getAttribute){var orig_getAttribute=ElementProto.getAttribute;this.wb_getAttribute=orig_getAttribute,ElementProto.getAttribute=function getAttribute(name){var result=orig_getAttribute.call(this,name);if(result===null)return result;var lowerName=name;if(name&&(lowerName=name.toLowerCase()),wombat.shouldRewriteAttr(this.tagName,lowerName)){var maybeWBOSRC=wombat.retrieveWBOSRC(this);return maybeWBOSRC?maybeWBOSRC:wombat.extractOriginalURL(result)}return wombat.startsWith(lowerName,"data-")&&wombat.startsWithOneOf(result,wombat.wb_prefixes)?wombat.extractOriginalURL(result):result}}}},Wombat.prototype.initSvgImageOverrides=function(){if(this.$wbwindow.SVGImageElement){var svgImgProto=this.$wbwindow.SVGImageElement.prototype,orig_getAttr=svgImgProto.getAttribute,orig_getAttrNS=svgImgProto.getAttributeNS,orig_setAttr=svgImgProto.setAttribute,orig_setAttrNS=svgImgProto.setAttributeNS,wombat=this;svgImgProto.getAttribute=function getAttribute(name){var value=orig_getAttr.call(this,name);return name.indexOf("xlink:href")>=0||name==="href"?wombat.extractOriginalURL(value):value},svgImgProto.getAttributeNS=function getAttributeNS(ns,name){var value=orig_getAttrNS.call(this,ns,name);return name.indexOf("xlink:href")>=0||name==="href"?wombat.extractOriginalURL(value):value},svgImgProto.setAttribute=function setAttribute(name,value){var rwValue=value;return(name.indexOf("xlink:href")>=0||name==="href")&&(rwValue=wombat.rewriteUrl(value)),orig_setAttr.call(this,name,rwValue)},svgImgProto.setAttributeNS=function setAttributeNS(ns,name,value){var rwValue=value;return(name.indexOf("xlink:href")>=0||name==="href")&&(rwValue=wombat.rewriteUrl(value)),orig_setAttrNS.call(this,ns,name,rwValue)}}},Wombat.prototype.initCreateElementNSFix=function(){if(this.$wbwindow.document.createElementNS&&this.$wbwindow.Document.prototype.createElementNS){var orig_createElementNS=this.$wbwindow.document.createElementNS,wombat=this,createElementNS=function createElementNS(namespaceURI,qualifiedName){return orig_createElementNS.call(wombat.proxyToObj(this),wombat.extractOriginalURL(namespaceURI),qualifiedName)};this.$wbwindow.Document.prototype.createElementNS=createElementNS,this.$wbwindow.document.createElementNS=createElementNS}},Wombat.prototype.initInsertAdjacentElementHTMLOverrides=function(){var Element=this.$wbwindow.Element;if(Element&&Element.prototype){var elementProto=Element.prototype,rewriteFn=this.rewriteInsertAdjHTMLOrElemArgs;if(elementProto.insertAdjacentHTML){var origInsertAdjacentHTML=elementProto.insertAdjacentHTML;elementProto.insertAdjacentHTML=function insertAdjacentHTML(position,text){return rewriteFn(this,origInsertAdjacentHTML,position,text,true)}}if(elementProto.insertAdjacentElement){var origIAdjElem=elementProto.insertAdjacentElement;elementProto.insertAdjacentElement=function insertAdjacentElement(position,element){return rewriteFn(this,origIAdjElem,position,element,false)}}}},Wombat.prototype.initDomOverride=function(){var Node=this.$wbwindow.Node;if(Node&&Node.prototype){var rewriteFn=this.rewriteNodeFuncArgs;if(Node.prototype.appendChild){var originalAppendChild=Node.prototype.appendChild;Node.prototype.appendChild=function appendChild(newNode,oldNode){return rewriteFn(this,originalAppendChild,newNode,oldNode)}}if(Node.prototype.insertBefore){var originalInsertBefore=Node.prototype.insertBefore;Node.prototype.insertBefore=function insertBefore(newNode,oldNode){return rewriteFn(this,originalInsertBefore,newNode,oldNode)}}if(Node.prototype.replaceChild){var originalReplaceChild=Node.prototype.replaceChild;Node.prototype.replaceChild=function replaceChild(newNode,oldNode){return rewriteFn(this,originalReplaceChild,newNode,oldNode)}}this.overridePropToProxy(Node.prototype,"ownerDocument"),this.overridePropToProxy(this.$wbwindow.HTMLHtmlElement.prototype,"parentNode"),this.overridePropToProxy(this.$wbwindow.Event.prototype,"target")}this.$wbwindow.Element&&this.$wbwindow.Element.prototype&&(this.overrideParentNodeAppendPrepend(this.$wbwindow.Element),this.overrideChildNodeInterface(this.$wbwindow.Element,false)),this.$wbwindow.DocumentFragment&&this.$wbwindow.DocumentFragment.prototype&&this.overrideParentNodeAppendPrepend(this.$wbwindow.DocumentFragment)},Wombat.prototype.initDocOverrides=function($document){if(Object.defineProperty){this.overrideReferrer($document),this.defGetterProp($document,"origin",function origin(){return this.WB_wombat_location.origin}),this.defGetterProp(this.$wbwindow,"origin",function origin(){return this.WB_wombat_location.origin});var wombat=this,domain_setter=function domain(val){var loc=this.WB_wombat_location;loc&&wombat.endsWith(loc.hostname,val)&&(this.__wb_domain=val)},domain_getter=function domain(){return this.__wb_domain||this.WB_wombat_location.hostname};this.defProp($document,"domain",domain_setter,domain_getter)}},Wombat.prototype.initDocWriteOpenCloseOverride=function(){if(this.$wbwindow.DOMParser){var DocumentProto=this.$wbwindow.Document.prototype,$wbDocument=this.$wbwindow.document,docWriteWritelnRWFn=this.rewriteDocWriteWriteln,orig_doc_write=$wbDocument.write,new_write=function write(){return docWriteWritelnRWFn(this,orig_doc_write,arguments)};$wbDocument.write=new_write,DocumentProto.write=new_write;var orig_doc_writeln=$wbDocument.writeln,new_writeln=function writeln(){return docWriteWritelnRWFn(this,orig_doc_writeln,arguments)};$wbDocument.writeln=new_writeln,DocumentProto.writeln=new_writeln;var wombat=this,orig_doc_open=$wbDocument.open,new_open=function open(){var res,thisObj=wombat.proxyToObj(this);if(arguments.length===3){var rwUrl=wombat.rewriteUrl(arguments[0],false,"mp_");res=orig_doc_open.call(thisObj,rwUrl,arguments[1],arguments[2]),wombat.initNewWindowWombat(res,arguments[0])}else res=orig_doc_open.call(thisObj),wombat.initNewWindowWombat(thisObj.defaultView);return res};$wbDocument.open=new_open,DocumentProto.open=new_open;var originalClose=$wbDocument.close,newClose=function close(){var thisObj=wombat.proxyToObj(this);return wombat.initNewWindowWombat(thisObj.defaultView),originalClose.__WB_orig_apply?originalClose.__WB_orig_apply(thisObj,arguments):originalClose.apply(thisObj,arguments)};$wbDocument.close=newClose,DocumentProto.close=newClose;var oBodyGetter=this.getOrigGetter(DocumentProto,"body"),oBodySetter=this.getOrigSetter(DocumentProto,"body");oBodyGetter&&oBodySetter&&this.defProp(DocumentProto,"body",function body(newBody){return newBody&&(newBody instanceof HTMLBodyElement||newBody instanceof HTMLFrameSetElement)&&wombat.rewriteElemComplete(newBody),oBodySetter.call(wombat.proxyToObj(this),newBody)},oBodyGetter)}},Wombat.prototype.initIframeWombat=function(iframe){var win;win=iframe._get_contentWindow?iframe._get_contentWindow.call(iframe):iframe.contentWindow;try{if(!win||win===this.$wbwindow||win._skip_wombat||win._wb_wombat)return}catch(e){return}var src=iframe.src;this.initNewWindowWombat(win,src)},Wombat.prototype.initNewWindowWombat=function(win,src){var fullWombat=false;if(win&&!win._wb_wombat){if((!src||src===""||this.startsWithOneOf(src,["about:blank","javascript:"]))&&(fullWombat=true),!fullWombat&&this.wb_info.isSW){var origURL=this.extractOriginalURL(src);(origURL==="about:blank"||origURL.startsWith("srcdoc:")||origURL.startsWith("blob:"))&&(fullWombat=true)}if(fullWombat){var newInfo={};Object.assign(newInfo,this.wb_info);var wombat=new Wombat(win,newInfo);win._wb_wombat=wombat.wombatInit()}else this.initProtoPmOrigin(win),this.initPostMessageOverride(win),this.initMessageEventOverride(win),this.initCheckThisFunc(win),this.initImportWrapperFunc(win)}},Wombat.prototype.initTimeoutIntervalOverrides=function(){var rewriteFn=this.rewriteSetTimeoutInterval;if(this.$wbwindow.setTimeout&&!this.$wbwindow.setTimeout.__$wbpatched$__){var originalSetTimeout=this.$wbwindow.setTimeout;this.$wbwindow.setTimeout=function setTimeout(){return rewriteFn(this,originalSetTimeout,arguments)},this.$wbwindow.setTimeout.__$wbpatched$__=true}if(this.$wbwindow.setInterval&&!this.$wbwindow.setInterval.__$wbpatched$__){var originalSetInterval=this.$wbwindow.setInterval;this.$wbwindow.setInterval=function setInterval(){return rewriteFn(this,originalSetInterval,arguments)},this.$wbwindow.setInterval.__$wbpatched$__=true}},Wombat.prototype.initWorkerOverrides=function(){var wombat=this;if(this.$wbwindow.Worker&&!this.$wbwindow.Worker._wb_worker_overriden){var orig_worker=this.$wbwindow.Worker;this.$wbwindow.Worker=function(Worker_){return function Worker(url,options){return wombat.domConstructorErrorChecker(this,"Worker",arguments),new Worker_(wombat.rewriteWorker(url),options)}}(orig_worker),this.$wbwindow.Worker.prototype=orig_worker.prototype,Object.defineProperty(this.$wbwindow.Worker.prototype,"constructor",{value:this.$wbwindow.Worker}),this.$wbwindow.Worker._wb_worker_overriden=true}if(this.$wbwindow.SharedWorker&&!this.$wbwindow.SharedWorker.__wb_sharedWorker_overriden){var oSharedWorker=this.$wbwindow.SharedWorker;this.$wbwindow.SharedWorker=function(SharedWorker_){return function SharedWorker(url,options){return wombat.domConstructorErrorChecker(this,"SharedWorker",arguments),new SharedWorker_(wombat.rewriteWorker(url),options)}}(oSharedWorker),this.$wbwindow.SharedWorker.prototype=oSharedWorker.prototype,Object.defineProperty(this.$wbwindow.SharedWorker.prototype,"constructor",{value:this.$wbwindow.SharedWorker}),this.$wbwindow.SharedWorker.__wb_sharedWorker_overriden=true}if(this.$wbwindow.ServiceWorkerContainer&&this.$wbwindow.ServiceWorkerContainer.prototype&&this.$wbwindow.ServiceWorkerContainer.prototype.register){var orig_register=this.$wbwindow.ServiceWorkerContainer.prototype.register;this.$wbwindow.ServiceWorkerContainer.prototype.register=function register(scriptURL,options){var newScriptURL=new URL(scriptURL,wombat.$wbwindow.document.baseURI).href,mod=wombat.getPageUnderModifier();return options&&options.scope?options.scope=wombat.rewriteUrl(options.scope,false,mod):options={scope:wombat.rewriteUrl("/",false,mod)},orig_register.call(this,wombat.rewriteUrl(newScriptURL,false,"sw_"),options)}}if(this.$wbwindow.Worklet&&this.$wbwindow.Worklet.prototype&&this.$wbwindow.Worklet.prototype.addModule&&!this.$wbwindow.Worklet.__wb_workerlet_overriden){var oAddModule=this.$wbwindow.Worklet.prototype.addModule;this.$wbwindow.Worklet.prototype.addModule=function addModule(moduleURL,options){var rwModuleURL=wombat.rewriteUrl(moduleURL,false,"js_");return oAddModule.call(this,rwModuleURL,options)},this.$wbwindow.Worklet.__wb_workerlet_overriden=true}},Wombat.prototype.initLocOverride=function(loc,oSetter,oGetter){if(Object.defineProperty)for(var prop,i=0;i=0&&props.splice(foundInx,1);return props}})}catch(e){console.log(e)}},Wombat.prototype.initHashChange=function(){if(this.$wbwindow.__WB_top_frame){var wombat=this,receive_hash_change=function receive_hash_change(event){if(event.data&&event.data.from_top){var message=event.data.message;message.wb_type&&(message.wb_type!=="outer_hashchange"||wombat.$wbwindow.location.hash==message.hash||(wombat.$wbwindow.location.hash=message.hash))}},send_hash_change=function send_hash_change(){var message={wb_type:"hashchange",hash:wombat.$wbwindow.location.hash};wombat.sendTopMessage(message)};this.$wbwindow.addEventListener("message",receive_hash_change),this.$wbwindow.addEventListener("hashchange",send_hash_change)}},Wombat.prototype.initPostMessageOverride=function($wbwindow){if($wbwindow.postMessage&&!$wbwindow.__orig_postMessage){var orig=$wbwindow.postMessage,wombat=this;$wbwindow.__orig_postMessage=orig;var postmessage_rewritten=function postMessage(message,targetOrigin,transfer,from_top){var from,src_id,this_obj=wombat.proxyToObj(this);if(this_obj||(this_obj=$wbwindow,this_obj.__WB_source=$wbwindow),this_obj.__WB_source&&this_obj.__WB_source.WB_wombat_location){var source=this_obj.__WB_source;if(from=source.WB_wombat_location.origin,this_obj.__WB_win_id||(this_obj.__WB_win_id={},this_obj.__WB_counter=0),!source.__WB_id){var id=this_obj.__WB_counter;source.__WB_id=id+source.WB_wombat_location.href,this_obj.__WB_counter+=1}this_obj.__WB_win_id[source.__WB_id]=source,src_id=source.__WB_id,this_obj.__WB_source=undefined}else from=window.WB_wombat_location.origin;var to_origin=targetOrigin;to_origin===this_obj.location.origin&&(to_origin=from);var new_message={from:from,to_origin:to_origin,src_id:src_id,message:message,from_top:from_top};if(targetOrigin!=="*"){if(this_obj.location.origin==="null"||this_obj.location.origin==="")return;targetOrigin=this_obj.location.origin}return orig.call(this_obj,new_message,targetOrigin,transfer)};$wbwindow.postMessage=postmessage_rewritten,$wbwindow.Window.prototype.postMessage=postmessage_rewritten;var eventTarget=null;eventTarget=$wbwindow.EventTarget&&$wbwindow.EventTarget.prototype?$wbwindow.EventTarget.prototype:$wbwindow;var _oAddEventListener=eventTarget.addEventListener;eventTarget.addEventListener=function addEventListener(type,listener,useCapture){var rwListener,obj=wombat.proxyToObj(this);if(type==="message"?rwListener=wombat.message_listeners.add_or_get(listener,function(){return wrapEventListener(listener,obj,wombat)}):type==="storage"?wombat.storage_listeners.add_or_get(listener,function(){return wrapSameOriginEventListener(listener,obj)}):rwListener=listener,rwListener)return _oAddEventListener.call(obj,type,rwListener,useCapture)};var _oRemoveEventListener=eventTarget.removeEventListener;eventTarget.removeEventListener=function removeEventListener(type,listener,useCapture){var rwListener,obj=wombat.proxyToObj(this);if(type==="message"?rwListener=wombat.message_listeners.remove(listener):type==="storage"?wombat.storage_listeners.remove(listener):rwListener=listener,rwListener)return _oRemoveEventListener.call(obj,type,rwListener,useCapture)};var override_on_prop=function(onevent,wrapperFN){var orig_setter=wombat.getOrigSetter($wbwindow,onevent),setter=function(value){this["__orig_"+onevent]=value;var obj=wombat.proxyToObj(this),listener=value?wrapperFN(value,obj,wombat):value;return orig_setter.call(obj,listener)},getter=function(){return this["__orig_"+onevent]};wombat.defProp($wbwindow,onevent,setter,getter)};override_on_prop("onmessage",wrapEventListener),override_on_prop("onstorage",wrapSameOriginEventListener)}},Wombat.prototype.initMessageEventOverride=function($wbwindow){!$wbwindow.MessageEvent||$wbwindow.MessageEvent.prototype.__extended||(this.addEventOverride("target"),this.addEventOverride("srcElement"),this.addEventOverride("currentTarget"),this.addEventOverride("eventPhase"),this.addEventOverride("path"),this.overridePropToProxy($wbwindow.MessageEvent.prototype,"source"),$wbwindow.MessageEvent.prototype.__extended=true)},Wombat.prototype.initUIEventsOverrides=function(){this.overrideAnUIEvent("UIEvent"),this.overrideAnUIEvent("MouseEvent"),this.overrideAnUIEvent("TouchEvent"),this.overrideAnUIEvent("FocusEvent"),this.overrideAnUIEvent("KeyboardEvent"),this.overrideAnUIEvent("WheelEvent"),this.overrideAnUIEvent("InputEvent"),this.overrideAnUIEvent("CompositionEvent")},Wombat.prototype.initOpenOverride=function(){var orig=this.$wbwindow.open;this.$wbwindow.Window.prototype.open&&(orig=this.$wbwindow.Window.prototype.open);var wombat=this,open_rewritten=function open(strUrl,strWindowName,strWindowFeatures){strWindowName&&(strWindowName=wombat.rewriteAttrTarget(strWindowName));var rwStrUrl=wombat.rewriteUrl(strUrl,false),res=orig.call(wombat.proxyToObj(this),rwStrUrl,strWindowName,strWindowFeatures);return wombat.initNewWindowWombat(res,strUrl),res};this.$wbwindow.open=open_rewritten,this.$wbwindow.Window.prototype.open&&(this.$wbwindow.Window.prototype.open=open_rewritten);for(var i=0;i Date: Mon, 23 Oct 2023 17:24:46 +1000 Subject: [PATCH 156/207] doc: Also commit AX.25 2.2 specs --- doc/ax25-2p2/ax25-2p2.pdf | Bin 0 -> 950410 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/ax25-2p2/ax25-2p2.pdf diff --git a/doc/ax25-2p2/ax25-2p2.pdf b/doc/ax25-2p2/ax25-2p2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e31e0e1efa87e6a0f386eb99aca63984101e3f86 GIT binary patch literal 950410 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�a<0&$z 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

    2-u3x(I$Ia+&kS5nZ|NN(?z!O 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�K0fFY9^wEVMLzeES>0m$ztt;ajPAn}8wE8br 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

    k&iwXkU<@&~N|waJvU` zm@%Dw_2WP4;aCdB|Dq9LV*KC86m~9_|967?|I?rQ|9RDxh@DJq2JNSOSn8y1sv#Ov z_YCi59oV6_RzOm43uyv5{mwj|_oo4`N+1F;ha}Vg2?M!1Va)X`qhDfhc~~MW)NvPd6b^oZ4rrM}8d6K$ zx0h@)A96?1C;LYGnSy)?F@UVkXr{3ahSEby+WEBsTJCQflKI;33n8T+)-UjY^WMLL zLDo`g+cUy>qAbO1bR5LUEl?Eh zz#EegXV98J+;z#;69}r}98$nGawDIwcB8Oe*eWk>o(N2&1WOf9DKwFp7{rXb7-nO2 zvKbk;GkkSjyDvs{DnnxW79HK~zG5p#2!*3&9pvt$4(+|S<~E(W$U5|wDNs*D%Y0$` zvlMuWiw%)0{$^5TgH)Lj;l+*K|L`(OU`F8o}K{IHl4#}cC{ z{c|HBtV?nOdBZrUdIB|f@BpWdo)ps+7SwT)Xv^p$Vi(9_ z*=yL?vL%JZyLCyB8@mZq72_AJ4_Ve8MY@0GTxJ@!?tgSIg_S;`0-5Ja0S&#qBP~=| z3i9Y*9N2`pPaOxIr!D#sa!@`)z9mfKT~Ju3&N@mL8TXQ>%K|_B%5j{%1gzT6B7CRxL$@N7QM?Gg0pCW5#4ma-YK$8y1&W)C!qsWka z;}CLRnFaVz(-Ted!*N-(RfBV;3uWAxKZ%tKUx}nvE)|vi%;ng&kIs;`O`U{sjFqsu zO8hQXzIifQ$tH)7thEbdEL;2v6Yg)OF#BLIVh~A14TVG8kf`A4>#LrFWmIwSyY}1& zo7woh;VJ#_VQ212{(|dRI35f`UxmOq`L<9b7s; z3J+^La-^BJsCP0qCTtl!+uHToJDJ1=H~m#;q>*;DPO%9h3~k4nh9HI6Nb2|Ebt^V; zL#YW!sX@% zpbf6=5%n34{V|2628>w!io5R=71NIAifh#5N7d2DpGy2ulgxp3Kzln2OQaS1kHymR zfHT&@ho{F*lBeMiHU`vv#n!AQ5Zi&V$9sqW#5G9JKPxfpmS>H|#oqf>cm}!E!i{Ay z*3}kd-Zsq%n4A%sNXWaG8Jv&*UBZ)c33u`G?;IM|+jDQR&T;Gm@6sZ0iFI3Tu2y%Z zUo0UUErKuRAPMckE+S#!Rs+2O6N=UI3ZLH^LOz7DrT}`X6!^((BW>sexLGH*Tfwm` zOIYduJa3+btWC3V0?$*gkeWaL9nORvT=bO0?g)O!O4lYu3bFUHIg3&m{xUXq;#j2Q zx!Jm|Bw<1v8YXnp8j(|*b(>>G^6Yh(qA1JR^ZQ#EQ zYE1tdTExWqe+IQ-4Vi?acH~~%^Pm0_{8%pHzpUgLyEi+9O(9YiTMI6J!HGOBf^7@0Y(TdUfVNf`72Q^87xhTm;>{pPgpDmz3{M{ciUcWh)&68JUi2;G3OB)IPou0}D#>u%37gpi3C5dbuH=b)t zrIO3(^`dIQda59`kmZbCN3jo9mXEd_pUei9qhw(cE?TMGjT0a6lR7l6IHoyH(rrXi zL?X{Uf*^%j*1P*Sk|CV!Uj##9m{yQkwGM9&(@ZvUS^tBdidWMXS4pApZEM&9X>+_G zJ!bTmk6}rw$%8G`@TIMc;448A&ViAlItjg~go;tz;`)n`u^dUFg;P*cW~%M1r4Slt zfQhX7?|RU}W_V$D0@)WZEb#&#IEivRNDOXSEqz({L+x{g+iwVoa{m?}jftmCcB3u< zfd*Eg-2^|q$tTY}%B7Q%hB1N^o2Du$qwbve5VLyuYKQvT8{BFuS@90tr#g4X2omZ3 ziyq}3j+F3wCOydH!*glK%%eaomr^K~AsCA!3iDP0#}azPAFWeILQpHANuwm9C%j{` zl31035!U)2> z|91!qjKxc~VGvk6s9cRlEvE71^h6$e(?SvJJ#pq|HR4yLR~lppC0LbeZFL=pnBCIN zvz!ILkp6f`lg04wnuo)puu5SBbmi`CbhDH`kdE>vvNpRMm|Gehi^S1P`u>?rx*Yp1 z!vkN~UXjh{yc%0!eX5;aM?3in_R4(Lz}m^iB_ueL9SVVJeD}~$=QH}}?D-d(^I@cr zMDm81;GQn1rj8zk@urd%4Fsidb>3<&D=i`qC3vgEWHZqwqAht?{s?21M%oD{20Q!@ z*-+~c);7B-a143&yVaSBF-RD_K;BOAMzvb@9=wfo;|O8xY{b`2eyLWg_@eI0?AerP z+lq8M_#KF#@6{^x__TyFh-ofMD8G<^$!;3DwsliL$jPfHFtM?#-k+M2zfur!B?-;7 zJKaI6sV1^^6pM|1DnLWk_`Ir`AvoPBcCdgz{8YcOr%oor=Kj-@4(=kH4^DFlxSQ_h zL1}~L3Q&CpMi+q?4?#44;>ql51d52)rL7kD-d{ZdoQ_`Ui?4my?Yz)d?!^6|;cMTX z`w;6Yv*q*($^tGbjz3L{?5lHkinZuoi8^W_Lkb`wFhvWQ3<5O{?MS1noIv#B$iX#q8&SQ>P*53*aJ>q~?}ZIXs7O|Nsz`SVa4>h-7N zx{;2fiaX9_J;^tz+Ya9lR)*ABo*YE|nbv`aSp4nybQUm|9lK_g!YT@6jMAsd#>9bT zqYIetUz)n&o}QD3Vw@o^@9Osf;ng3N%wq*jTsTL(rHV!wtLnLp$Kf3dkrLeN9dia( zr%+Er=m*PK+h;0k%GrJ^Xos=#cC{6oN1nwvrHl0+3r9GqChocm1;vd~Q4WoP>kxl6 zGNrH+9jkC4W?V_M62NJ2;eDKI|z`#Uy8p7|CZ?>e%>=5A-KDZ4WJ`s zjDkPeRMP3^(ptiV$9&?mNA`k^h;x`(l=l3cpqse2!BcUs^6mKjb-#QJpAP%l6vAW( zbfGk`I)7K%x@Xf80uKd=W)In3Aaa4bhi0ob5V4kx438_XwcZ1M=G}qXceKcwh!X!)pZXV}pKJFs7~%_(-7XSk$`%g5a8tVVY{(Jz(Ddt*?HH;0(3J1b}jK`$=fgrtO6X*x&pu?vB8CYDT`3*pJ;cJ z*f3rDHbeLj+;i0N3am8z?Y^ho=;-xMJ~``8w&9QxtGp#iox=VG045~9A-WJu5@5G6LqITJig|J`m4fgz1kh!1hZC;6FF`&5Pt_d3mxFYK-NqGg#75@TcFm#^JR4I&pdfD;24vCo3$gCwNPN8pm1JbAUyjnBgOD+!F!+xX61>K z=P-t4P7_e`JQ4r zIQc{Uz2r$6GPi|`ohPf=k*tBq)E!rw19&{iVR6b@K)0nzi~{zD>woCG=Pb*8XdsgB zDNxtqs*3pak@BNLrr*RHx5_F~f4tYB02pQ(Z@)GUK}^(p4UH0asYAo{vZAs*%jvKzcnfU6UM~M#qodm=iCe(ha+*6us!fsfj=e9ST5tI zYU>mfsH9EUaNVterbu)ctQ(E(<&t5TJnt`DD>LPuDbreDQl?Qg+&g-I|5p8ChfP`d zCn?tQ{d`+6ojW-^8hvN*YucczIn_w%Ksf4ccwRpRyqb8ubPeb|lTGNa>!C$8n!b!o zKBjx6Nq9}Vdkx$o7?E8D??f!bRIZC)QM_FWfDREI z$jsftPwhjPAQtnUYEi9IB!yF~R-TidY(tGo`H^^XnEckT*oiISAyDUno3$Hy*+r~R zySSeaLPNz~4d+WTRu31?lV@z;$+v6w^(CFnMJ2M5pWpc<4}Gpxm*2w%pVA~Zd=5(O z@HA@QP?a9>Lqr5~hS>clNX>_dTXJg5pC^42eN5lq5YjjA*Kg9fK~}y7ih<-ICUnRo zHDYrG3heM|kEg#9>L)e4(Uco`nvono(g=Tp!Xmn7snhRFfo0ruqnK&CR+?kWKtv1C zwA7BB@RcBxdZ3dgSkX-z7)=T`>YyH&sI-WPle!N(7-Lpgws5_p`7w%XmZ&H4FH*G4 zKlGu$AnfptGi{Ph*p>Mgp06JwQ9=>%*%Bk{#`gfcX<5bf?IjvN806^wG%sFC4JDMx zyrY9@j-Plj(r6K+A))v@$KV&k-jA~5JwX^-WOO+Jf}+3wN-1I}%Q7Xyz`u5Q>G7*H z0+4RxD?{|0=-I@e5W`L1bf^z4Iv6;SbHy>BTlH2Yyvz^POt)4do&$C3)s#X#%M`ii zf+Q26O8B11N6ClXGO6k2TMsEABr6;CW;S7ct%}br*|hY9t_s|gBVP+N<9Ok#t5E5P z1PU9Ck*~fAo1!wU0ySz$H$#US#tpz@2x(BMPzog%G;dc2ES#-Xy>+wx+ZL&3fh7?3rm;kmNe<%T4<)kF>a|O zolaJ}0Va}rML3x?&G^>Dqvnkg$V&B0b5MfXTZuUDe;CQmw_6aWUC)j5 z4hp1T&y{xcAOjz=F4@u5Fo?OHCH3V@Y_-x4;!)>{i}GOpe*zw{A)B zMTh#xWG1 zxAeldhx-DyFHt8-Ej{VWgZkWb-|)yKYmiJbxc1P2w`|K|lb9vmVytD3JU6JHfuoQ*UtkvuguCbm!eD7GPHK7k;^*Gs> z`qqYVN?Z%lU5FS>L4IQqI?dnuH_aa>y{I;$PpyL%_B#}R zypa9{X39pZmAlF@yTYF`L&T9R2yTgtAu`dXd7jwzQ*jlQNqQhv9f| zczsqO)yGHr1DVI6j+F6BQvinqexs8(K4!BF3}?yZHM8!bQ~@u)ROuR{)KEq9&01rr zlgMGc>{B&MDWR*Do~P_azcLyYC$=vZuJm>_m6eZ6ehS2q>R1_FS(j8kDzO@>Q5W;5 z-}R!>7G>;hkk9OhJmc0t?HIzA8~61+;8a3;iL+qLNdo?9#)`Lgxntf_-B85|QKUrA zV!1B;x*nr#a7248K zK8^>@J_|wU^XpzZtE6Z^>3mEl^NyK{MtAX$nnzcng_J|LZw{euy=Zd&QD-VxAU8gs zAqvNGoKs-@fDm2*-;)>GwYX65__QFUMj;uDMJKl(W`D)8tw)I@XnqOT-7FY$Gn#i{ zI-)mTIq0n)KHYns5Vi@EYFkc(B~~VrWY86ejfyg5eNsmc?uP!X+|egwgq>VAry7B< z7(joqL#eP7s;DYIKagHF0@~!!Jey|o4f4ygpU3y!s_MuTnC+esd*}47HE?x;VZg|a zWSX&==SrVMkMV~4bt6U29b4Kz6Qb?HTOM~Sd}fRFx&elQdEL1i$Bfa+IA_UPIe$`d zKwch!!#CG3on>*89v|dors2`6SMv9v2GZG6m3H13=Z|g?YM@Bo>(M224cFM_8oRiYx7JwO%D+O^1_F2Zg_#m$V@{G|#iGlIk`u_=WK zNv$$g!TWwn%0UWYC7X#x$Q>h3PGt^~L2BS7*p07Rf3D^N*V(>LIAy&3opupWrO*7+ zVWqj;78XVB1Z=JPS(hLjm`9IOyw|Yhp9{35aX{G!j(H}BEZd=LaFy32TQMomYyDUT}ml9k$B+d5L>OMZnvPj5@*Lf5R&MFFgM0as*wCE$ZTFz{?eXo%@t_0hh zj?4x(gAcFSf+%Fk__R~97Jkj&3p7-GT8Ad1`z-jHf9=o`Jf381tn&2ae5FmB<+jS{ zW+v2a65yqTV{-GRLH1>`2kv(25&j+ihCtlMrPK{(a@27q=%hLP*u$*&kvDR3AbHsRPiuY$>-p( z`f633?xkuBzz-3^>4rg5qf%1xG>iNRTILllE8pmY0D+FrnFpVZu)CdI$ujzR*W)a| zi6aW)V|JC5awA$gZp$*JAJW72TQ7sb{Q{otQz}Xu$isXYm|NE-XVp|Lxe(QE|1z%! z(i28K|K9u))en}Ty|6T0;iw+Hg|xWQn;YvcV&J^{d+=$WO4{H&aSN;hFu7l|Pl>J( zj%GP%kZI;w&LAEpDg?+`7C(p-?qFw(AfjrxZ@PXOd^vVGA-AFe&Y5t@I-vJ{^;Rz~p)?v~n*CV_@ zRS@{gug+S$yeka)z0%SA+UJ&2(;KpDF%0@0C+~)Ut2f|A6lYD{o^}z!>`1YerT>}s zdHCsB`0!gq3W&$?dmzI;%1NFV3OQdyyO{^}OnO}mro-j#^PpQnd$q=5rYi-{IQoou z+foaXlr)x61|L}BSxm4v%I{RqFrbiKkP(EjVUADy91f@l3U~s10qU@mbz^1DyK82W zuCr@=8qb(F2=*bqK1i$FH#AL_E+RK{V3-rr{jb~8!?>q#Rv@EW zY+>vx%O6l-1H_U1tzEv@aE!s*)16xyY03!FFd7ULqWd7hTK?}6Kz+i`PCB(IXI z(X%oUx)+i!uq20i$9cptygrqx$^6qpiHFSrXabDr*T47hvXGqZ9L^9xA`wEIz=ffD ziN8SGfCpF;ST>RQkt={Oqbv;BGYgawer-uR3V$=-z%@f$Jv~BWMad8OWqCqa`@AT` zNuXY4TtG;TnDmvP6?5%));iv65E5<_xr6XJmKIj3%qRNWMK>#%Hmu06dx?Qn;%A+C zYl!UFV$iyBXk`~A_wPe6;;6$=liBD^s(r&%RmVlH0)yWOD2 z&rx1`qOhxXvKgLiE*El`cnvPGiE&r@g_*MrPf;2M7SS%oEe0D zAg2kNQ3)DuIb3MQZ2_;GHhSrgf9Caf)a@9dNwPx2E{6iUu0(foXCKQEq0m;yl^ zGZQk@YFFBW+x~55q{kP6XJR8D-YcoeXX2!zB(@7rHy1Ej1k+n#FUZ#>N1GSCU`fEIF_7PO#ZcYKkzjHXk-vXL^k zl!1+j;HhWft0VdkL$6>a(HZ*)7?&K4*?&6DMId|h9%<3sps|*=+Ih4sDho=j`e0D7 zR6|ic+*6v2g@`TF&p|GVkA|O$YVjYH1f`+!sC?7+nTe_N%c<1N=bX1IR4H*{87?G~ z`GyuSr`0#~ZUasZuO)*87%_P?OfGQ9fW*yf*&8R|(fCw5A{OygbrhNK?DAA2JFJ)* zgq;NdE({6tNWJ_M)I*Kc*A>ckEf>4$lh>)#OcASR!QUGK0r++NhM@(6&!aXV@2;o_nvdw^<`Qddy-PSze`$F%l)9u<{y=A`oPmS4CtI z#PSlG^94s)rKt>6j2c=_wcO~-yc`I4oyEHFyN3biT#vJ-lWnkh{n4_kun493(>cK= zI>oW>(tyN7K%kDDkRDUi`T6fAM-QkN^S_;+{~L10#Qc9Vc-8k@iY1Z!&+5;|4yr}N ze+9pYFWYXb96?Ljt(;z3gP@v$hQMYHe_hpE2*%Z@>%A4r1}qr_7Z!!wR|lh7IXw*h-3XUgQe?OJNAvyd+;_)CwI__`W@S zEBT~5#WMEart6GKohN8;nJPKhLch<2-f;9ea|?q0vUwNuF+6kyj+si79C0P=M$ZT! zy4w2h_rob$hjshPn2=s0#8;zNRWdB(lUO-$e#(05lYXMQpnLGZFgfmTisJWM4w+<~ zpT)m2NOvM1ka@ps6q;Wod=SvVeYP>bqeY$VN2G*<26X4XlRgFy0W4C*dB;_SAq z%8Sj&6cKAd7{xg^Fjwspw2da#WJv~84XIGEXgww_+Pu@`J+1yhY(CreJC8<0^l{b`XoQDh%6 z@NL>HB0jNqqtyV$_N+D;(qWC1+jr8+XX4$|FiLmyZ9r@h*Te{R%06_l+|;+g`GiDV z_)+~WTO16=v%)xws*wAh&Q^iJLCGhQTy`PO%i0kv8g!RjE(v{bi@hf^eN0q|R?>I+vAT*eby;iAIH){rp74 zpj?;hFi4_h+Co*KPwMEGSPGl%`1OW=eg;HN%DOn6bJY7sVdaAag-|vY$rG8&R6&?1 zE7}O&%jIS$8kOhVJQ9X!@0el~u{ou)Mc!}zPrB94K($1NNr#S6ieC@m(57Ahu3LVJc(nEx+S)tsT{kRyo}ewXz5Pkvcr3lqZRdH0#rY_-HncWqq; z&_?EMwFntFDUtgA%4Y!`9hP^0DUL?d0%bYloAWxGW<+FzJ)aAQyxIBa50RkKiyFW>sfT} zH}8as<{9|3yVJ-Oi2c!Vkj6coe$@6W%O@qtCUvoyHpaIn*p^0-AIuHYm}-&52j86f zA@nKfLGjX$lnHsY7ue)bIdAV#rMEbZW;)kjqKye?o|&{s6}tIeW7%m~if3|J-Z|4T zRNWC6bUu$E?=8+MhYtlT@D(@fI6i@n!u5?F;Z>|id_>E#;a@@Ya@DAIDfoP1c4 zEmIhq(!FE!AN0XmN+SD|u{|yQ0e*TogL*;(k(Ltx1KgNRU`~<~)G=#Nb_>~LTKSwx z%@C5mk9?K3j0@DW>m?kKGnUS%8gKGQH{Eh@S=14N5XC?`33?9uDxR7SqjBmD3OWOZ zhLPoZJi?x5@|}AnXPsz1Ft%u|f0|mi<`{GYS3LpOT;O5Q03P4J%6B#*Z|Vetxo6Wb z22LIM$IKHM8_9y^?bebgx)dXA`T<;M=_LW^Fxb7xkgQQIUQDK;>4I=Uolami-XXT^QyJxxmu|ua6EgJ{5R<__-|@dD z@ELrDB_a4SuPB+*orM>M3>c5q?!ocKgrSbCVSUM0>;{i2el;RyhLNdK~4NS-!G4&!mNPN8^ur zj}L|WUn*lRnaC)3VSMFV@{+bnZ^kTRRczPu5)<7*<;+)8x46kX09@|pN9yxqe{u9* z3-C$?Y)BEuwT&ll)Ju%2E-IWXZ!3ECb<|_E60Ixr{GfrnSDDw-F$+|~*Emgckx{g(FdqR%UtbI-3MS<2i_ZRSY7 zrCu0cU9?CFNQ3{PhcP?-+4y#8?T%~8A+z7XXtMO;+54W8kQEswl}F*trR;$&a^UZ-A(jCqOsmHRjm;R5oLZRXTiit0p6Me&Hq55M<-E`_qUtEm45x?-Eg81DSc z`N0uP%!HSVr|G~BoU2rOx zg#L-d)G`*6HkO9kAVjBv`KfDhq!Qr&l=Zx6tZ-0ymm9&p`j|)$u|)brvF8 zdiS70i~!2;DIFH5gc`V~H1 z?5tc+*5>LG$p9xUgxy={R+4eQHA zYU=Wi$j--#tbqOQrLat5%Dq7>Jn1qL&whndnN7_7{GG;~p#rmx!H+3HQORAjB>m$^ z=!7H+2>Th)Net--C2s&}uhJ$M_!(i&wKc!oFZxz3Cy zw@^BVP!5xXEUN<7Ith&^F+4}-CIaQ8h&_GEmVub9_bGa4yhl&KUd5%$w)CxAZX4ut z|7X_rJWR|+W~ZQXOQ7YH1)uUM0m`VR>Gqzn_fike-dXAT3y9gaN1zIC{iEF^rEsb* zPR_U%0S(@F3DaATW0Zy2>_LBq!g`INrLLn^q{A0CS<4<*n!R7d{`CSA3&q1GdnP*%bQB zQYe+@LwT*@Cj2+}ma~PQxDC1jS%{$o3S1+@NKnAF49fnSoo{oOF>2am&~}|jsO3wF z<&zms6%55J1hiXXX!4KxZx@~r_*cDD7J>!6hyRbUckB^0+O~Dewr$(4nuS@mZQHhO z+qP}nwr%^)eNJw2cCtRKWc-4jjM4l1_V%)WlI!CXQPd;W`H<{l@+7XXmOz-{iUG^mIES%W0vbUOJ&9E`Cf1m@a zZg=%G(zw_?z-O&}CZgh!5k11H!B8=#`2!35k7;~T&ku1M#BI)Do~2o2{U~`B8p2Wn z>`Z8n?d_a1hFn>l7fdza)im;LoO?g%^lEf88IRBpU8VzukA2OpGgpyao4zfkM=!eH zfg6P%KNvuImB-aM8onfIDek71-VOhnrjvt~nFs#(hVnn1RQ?N2eL>8lkoBr{8@+T3 zx5dXu))mx-mdfIkWZZ2#slQZw37r|Vdapj72*08?90V%icyY>S^B5;=o|k|>Th~37 ziPod9z{PV0p~czLemAn7kG`K8orO(5hyQug-GLkOwv!abzFQP8OAbRiG7_VY~DLH#{_1c~+~TVHchUERu=G#u}^~kQ?xA$5?wh@^H4lixomjA2$V^Qjd!ssf>^P ziFBic^OWIMc!!5uYsz^`3RD|33~^Xo5#)W(l;{cok3=)FR!;TObzTJq^0h9PJ_yxk z56oh+`<#4udWqo^*XK^3ANsBPkL_p(M_~O=E%U#UlrXY0u>QYn$NvHpwXV2Qyw7It`uSp{$cdQ z^?ti|A8B!U(#QP`mt7gAfZLr}bLda#JRgjxu9R=el3wwRP8eoMr`@}B=JN>0^UWob ze2G-5y<(gUYQD-$AF34N&OT2--KZOl;GV~Li8iersNZ#?dp?SNo=swW!L&79R3BKu z9Q>u(oA)0M2am5?-gas?a2@}465z;D+U77hR=H2Wj1S)suU5la7ExIk?-nDU%BhH^ zquspk)|2S8BBu##4RYev$M_mvu&0C?1t^H$#1)wuPO6tBPcd&shbr7*2w$gBiNUx< z+_2_}K7DfykPd@GNB~m6k_y15U}FzJ6;ZVU$0VRVLfXal(|$nqYJ$@kq{_K2LSn=9 zg;`&Fj>97xf-yUR4>LiF^9r$-qnoIKNBEUMqd+)q99io^l0tvtJ2ut^PH(bvBbJCq zMmq<5D=`Qg3U&MWf{u0w;a~X^RS*}vUQ*GLX&^YwO)#>O3`Tw&zaVKB6(d(3WcHo{Mvqv;fpMNC=LazEG~am48FyJdfBOSS#ok>9 z;sIN#=DFa`FKl?k1c95`wc^ zP7*R$#J_OQj_;2zkTt68$OQXQIPP@!7x*-2h0&6YSShL2MeI0wftc7)AIZz@aiF=p zx^V-%?YD)5!+Aw#F3lZhJr8onn>Xul8MK(4LHT{AvuWA95?-DR`n~wYJ}v*kfq2yD zYYK>1T0Xe$1q3&wUEsf1DEVoM6)EAgd7;b%wqs9gNK_M(6>DPGaz5SuG*%4#LH=d3On!4=n2Y z1Wt!PmCmagM7ke4unE@R&kLQs3IQ>me#h0?t^1TrR8U}C3|6rVEwj6gH%?pZx3Syx zlWiaj9Bec*E#qnB(_gYC*(A18Tut_SE7gx$u9w5V_Lq}RLY-g@{M1E6g&84SsrT4S zZrh2(uhHu{p#B33Xh*^1aADyt+-g8|p-XxKh zCf)#>LCT7dR;wvoW2DivH2)MaOjdEh&ae>9jN$kNKzDD#fp!ouEbq{z)67-&qwHmBFJ+u zE?96rKXdJ)aUg;Fo@Lk+S}+&)dS+5P%5-5H51-c+WB_QzUiIPm?$-+besU<_5F_JN zfTIxHTboujfUy#zSU9QdXM)WP`ivj%M;iOpK>p2!x}>U2yId^(pHcw!gFtrXPL;aQFXk{xvL{1E| zE-f89+Z$$G9>lO8E4Ux9k`wlAaZM`(+i?jLj^U{9;*J{==0SeL@-$-^tu^ zPA}E;qB_f@JMH6t_00I{R=Gi#NZ2~=IQOVA$*QZdO+fR`duAm1VSOkhdYW}WROVj4 zZA3hE3iL=nM(||rV2f_);v+3DAv3{Mv0Sec+8DL;88>rcvN$?kv(aJ?6N20eE(DlX zc271evE~~WcZ4t9U=2ZHg}L6hFlO&n^E-$VUXPZoVA@#J-?8H%lkYcxDsg|%{Bgmi zsAPXECI2NQD{|54Ar*v3pU}Nbb9#~2P)9o30rqr+81#nh$xJ6-Ytwn$-Mfa8-~;n6 zfNC9ITRMSjrsJM0lY-NinMWE znqb?MG>;h(deS~Mgqgxy`|x|eP|5<)lzf_-23baR4Ch+4+JZq`e<*tzg3vzsh2X>K z1tWA0m>kB1wjiP09z&I*&ZXC2bK7I#T!w{Y5MHXctQ^kwdu(pFv_JEIbce(!hEQYO zh-3k(+{_|b_Ew4dLNfpmglL!ir+)WeZC9BXIRBR$&d|Rk3+!>k=|?Z&KeR=lCCjgq ziUjHrgH^b0!gOr>Y-&;E@l$E&={Plsy|d?&FqubUm!Tl_0EyaHHIwHRip%Vc5%G7U z4^#|{&B27dC3btcb%HQ7yAGS(n4>t8bQZ=WKF0I7)EvZ{t zRvpt`jU=&|wCy^q+MT-z@Y7$8oAI9~`t|K!qgV9F4@w-M)s3MSf{&Bye+q?jXjY<_ z1WX(Ugqdi=5CRVTT$V^LeKmNlTT>Ubn~q>hvyhm&y$FLJAizL~*>aWV{40Amz$Qk5!N-aZx4W->D3`D@d~9-dd`PiZ?>Pp?3OZswqUD zz9$rIIMP+WGq5Y_yg4C2N;MLziqSGM5VVzcYfqHDY19Pmyz*i$lxl6%OdRLtBol-t z0}0+W;q8~atsXBB`v6SHh%sJ4+YAu^RZVsL#3WJ)kdSu7FS>A5Vl>pk5QbU3Occn% z3iuf=ipbTaX;GTnKJXHns2C&yg5=%v_~un}F{B7k>1g2tj_i5=D)>E7p3F*@4V0>Z z5-9^l_;t#G@?9J4N80_B5UG|MViJhH&&Q$LVUK*SO$=>;Om;$7>ZTe_#+_Hs{A2Uy{+1N3LBRrlq(W-=dlKmn&593ED!-%UG28D1ZxRJndeepyJ( zeDD@LSm6)6L~s!(Hb!~5@Jnc3$twHptXDGLJ^^Wy3WQ{=u7A7|ND-W?L0M~pT^*_4 zpDVX*O|(Y)BQSnJIIW6&6^i-YK3U)Fsf*3ab!R)Stmbp5DD+`csTKH0_eEN^OTOW{ z{*yN!jt*2v5rLvTb?l0QE>w6bWkLjzWD(=ySmoe07$lb3RK~J}zS9MOEmgrN41o5v zFes$Kv_0>Cq4pRyxH1~W(16e*z?`(imZ&FkJX>mcWCFn!Q(7NvS+$%V$#%gHA5&Wd zm|8sb5xblY8iBag^b#usr^*wn_8VcK92pfMKnD*Gz8P`vCe0Qr@aGqWUU+Mi+Mm|W z_mjhp(?0E9{wfMG)S)LQ$G)7Ux)b!~^Z9djEL^S4UlhFy!bg`U@)^B&3bsu;K=_1q zN}LF99y4tOB+)2){-VTAv!C1K4eE*FbXa7KK$tUwh=#`Uys?`m%Sdx;J=f)gM$s%e zl=!sz^n;kU3R#R5YxMpb^1VxGL4WgDidk(w(q86l)c&sbE0RI^GxATD{>)6$mbT#= z=MQcN(SW#G-o#2%HnIF5 zVefnYW5=vzbes2ghdQc?0I7s=qz%xx8 zcj7GVNxzEc)0%`_HerI0Mwn#Kit6j$I(@%I%3kwdCYc+KGB7RfRE%T-Ik|XpVB%gr zaSdJ+6>{dAz%hXs@nb#-cz(1$LtOrLt=S-$Qt=C=JUu;)`vf^9ShRlBj+Z}Lz+jhS* zzv{=1dl=+N=EGH?6!pGT#2(@YlF}e(WL`Dq>Tp6Lh-vk`99-rCJnmM!guEr0KVl@b zwfFdvq>vGFFsDb44PMJbiI9cd@Qm}Y;(7G@Of8?4*wl96!IgpbJuEi5af5I&8 zX0PY7AzNOa<KS1QX_K6%O_r7J;S0GjP1;_k3++^I&>PNlkh<27R>I^g2yxSUrD z9-9lc;Gt&ZSfVf-w)!|4bkJEID7eNUVFjZi4_P!H9vFFIoqQ}etOhS0H`8hKqL3*Z zl;YdbyrWC9ACeUe!xLCAe zfLt<|{(E#y2ptn0&ZDsKA_RQ*@|6QsAr9!k0D=+E3t^&N^HnG7KMNl`#%~=kqHPJs@vYY#+icqAGG`^_ZI|Lzn3A~6mCXvV#dJ=ZUrtU3&tsGM07%D?!4U;wjXvjzWY z?D(%l2~3O(|4Y<0^#428{~rU|z-oyH%a+`%q3vmFhp`p98qfLt|G`V6|9S{d#JsNV z!ceX!WHt3)1BC)rQ-?o3Ofk>G)TZL*?&bRNxbl6q-HnSl^U(zU?j;^~ASc;+Ghrpz z9cV|7GhK2N&T0RwK!)m^!S6pao%3)u*6MH&y=w6!s^c&hj~U*88H!q9P9{|u%a~v; zSB;GmN9U%K<(dx^f8HMNMvwM+Jn`;^;)5eeRbNte$0dsb zCq^vU0as!dJDT>fU&NQO_j5JN%cK`w>Ksi_@pg+CpaJ{M4^q{%mZbGmiCEV-VXA_J zRJ|S+{qLy~1xG1ke3@i3C&^kQTl7*czqIwi2;sJ3&Vq78-;GfXbpP`_uB;SE{_U6J zWZwze)Y48Ot@0k+&Rr1X^lBEVA8NE=I{6uTfj}*LWaxURHa=BJ0i9|_cv?Dz7qXxk zLeaU%34ckH9b&04P)VGo)?b@hHBMuYF25qj=>)udBT>f;Mz^tF2GN2Vu_HjaM9&xz zIawGTx1u#dZ8LS=Ouo06;x$7c1^#-+7DJTmzi_(PQ~oah#t31DeYwXd#E7W=txj@V zQAFTfkl#Xqg)C!*#zxP?kHkRMpxOcf4u{d6!oAKkfnmlFQ$qLvfH@lm`xIT<(^qC! zkK?ZkI6?3{qI9JU3_7E@fcip-u{{jV_#2T3*4aj^?6wM2eLdpwbUFur_w|M1wRrXF zN>RnpMi{8Ot+kga%O_vKf!&#{@ild#M3Wl~qMA0{^7rtsz&x3syMV7tEt6U(>B{NJ z0qU`BT;1eFlW5XFBBd>S5ZH%E;`vcr;HKWK=P?79?>XYT!1J}h7WSr4H7VO=voyWD z!@hJ#g)o#ukgdP*)dXZ^t@q`5%&~4&2gDPn9|2#iKGeZp`t*#3;nz=nx29SsSe1Y> zV&aTYLD#m&CWc(F=4n9h`@oxYni(~p;`2C!U=?Vexmi$L8fK7*=)F?0eF7AGak|#& z(Zo9nglXAx!3IXoO%gD^=`l-lTl{>AJ$+~H#KmCS6FIh=O2S|W|6qOdd^%nn6Hh5b zdkEr-7NdV?b;wLBm(FGkIr;wm)ah>FGj+v}yU(9o+MR`G>P~1D=kITpm1#DPwHB3O zC_Mfhn)X5&T6=4%#%@hh5$Vr5oTz*M!EhV0osPc z(y0`${ch@4bv+82@t%s+dSd*p_6*qrM)3IctYBq7#L zRcmDSUXbagdNX_tz)L7Pi>7gzM(|i7>{S^tvAKme+gB&1Z(ycv+&KnuvC@O+e2Ok9RgUsrH@=vPI%plN(6rE8-sEfRF?!^ukh3v(2yV(4iJ~(%teke# zfbH({juHvC(&ytFDUyL~q4bXgqkiN3xTFi|Knmkh!-EKl7))plt@|}3=Utx;iTwrA z#KZ>oeg&DQXpIdDTPswV@kF<2f&I&$>QraYG^LRRl|-(2|t0NQF9=12`G z{W+0TxtT~kGaf)vHiC?+NwLsAHBfy!x_b$86*}CF!-EIXSBsJSSNrCU7P1L)@g}&A z#Uv!21YV|HWF5*321tR}u?`YOW$zBurEZh6PsTYR<=45Dr_bxP5@lLhg_D{*x=RVL zf|(AIs6S~gDu7J3bEr+O+6w27#lY@6r|_5LP%TFPDrC;dNp_@mZBatImAcv3ctu*G5;RJWb$w|vE(vzfjUor5#&1j_?bPX2Ktfx^+%o>$T zX{N0*WHn@uNXmL|HabUkEGLETO~r_zicoH3Z#oFVElCKuCrV5K=aJD$a9k@#C8^S{ zaUebNLZB|Dkxr2Z0#(V$jZMK+xiqdEg65%bd1-4V13ooudR)55z9b&glZkcHjQAWBto!?~x4nT}U+Agx+X146`AK`hG`AXm~xN_4(&kuoHd(iDo|0 z9OB6eQIf;LlniQB93VUnV~euY_!N%qrk5TU_)GG8u|4%|=Mq>-ral`<7I+l9^8j)< zx|3FL<-Z34a!SnSe2XB2BhrSwuwX;sDS-3A{H64y(*4@K3V99HNfN*oK6eej4aodE zLV8bTGD=87ITuP1Vg&DxFQtd_ zmOpKle~lTu@$GDH74dDKSktSq4tMr!-W4IzIQdSGLpj|W^d(T*VED#J#Ss@2&elLti=QrC@|JP+%<^^<{Dcoz*ATEe%+u{1| zeoo88hd_OTtF5L{in*n)p+4RI&qg@{gQBs?g zqFrS+FrUUNr_us{VZD-Lj3nE?kA-M7j^MU`9y3ui$U%^f5~NUmzGx=^o+a0a5x!PI z2kZA29BOp45EDZ#2s2%@sub}!$uh0%klloWNu+fJ0O9u9#w5RI|I)QMgUF#d%(WI3 zlh}dgSY@ZF`4xf&hoWeNF9theZGtuL?J1dcIBq#~o`osm%r};RW?2i4c008pq;Is3 z&Gi*SKWE%k(57Xp534@Un8tc-7@WBP>!^&V?4(ylJf2<#j?mHBL0DKX3ni{x&d5<9 zH$^EcM(%7V%fZf*z!r-(O)mLv43ZHGwQgJrHrB%=Hb1h~KfGp0$qJ8+>or~~YxGcM z-K}dXlIvmvYAeSwR)dBPXcel!Xgd^D3_^_^Nh{lZ9rYFWr<)7@CKC>aaGzw;)$Q*2 zJ4KV82_>IMs2s>rqgDNLv(=-HaK@J$UycVwRV&Pu?<)++qG^(cEvfFI9jtdenW5Rq z&*@3iIhrl3#U3&C5cXW#EfNXW=hb|o>mQ3PP}E3;q)ki(4-k(s@nJ&<>VNXZ1Sez8 zz_-JxTkI>!1RKNhscFBcnmk#iI*iDk^e6CQL*UT>N8Bz~c{szu$KILqH4x&SUyEpc z3CW;NNAl$=eG|Bf19fY>As+Ix(ZkByM!Uvz8C9XM76~|O&NVXps!`RI1p|SImdDWP zn9e3;q#`17X%|=>1*9mXEIhdId?Hw5i=cshJMGX*CIz#FpCmZ!8u~^p<%}y2Y}WNO z6&tg1AS$XJ6}{a}s3I^OS@@88S-YQLzO1|yY)cPSF*P8!h}pNl@94Frx+D;r*jx!I ztY^2V0>+-pcCBqs4HbxCd_fO2Y}JQbeN>eHQZ9f z`*NIcAK5N5Jr1`ca~v?Ve(HF3Mnnuq4lNP7>#!)VqhTnssS{RmE2zY|NaEB%b?FaC zp^6a#epk8Kz=-jTq%Ah2)Vw{ zkM6mSg1JUzBf0XpTCw zgd#F$*0=0&H<5BVl$*=q`0LW7vGd&%1fG?2Co1FVdwBC>)<5{L?NHyL#TSIRT%>_$N#$`Pg426K-Oy1uReai%rF89rG&epHMz_oS;er_Vm%jbQV_`! zO21#8_UUEko;1OA+xc5Ru|EB27O&b_soR17d!Ze>`?Jeoe@|9d#Ch&XrWcmW<8#MdM|t9qj|R8Yv{bq#AYTO!yL0JmytVz;e8~ z4y9V-QH*7qetFys{{5`8=!R34N6o2F7QoNzT_$**Phfwj`%#Ev*jF(262pq>nh;Jf z+G`g}m=tbrIdX^m5pNZGR)S>kDkX_R`g>oy zl>E`n&67xe%UE{x_qzBwJW?IP&Tt2S7nS=(56`SFwGMCk>D2Q;KTo+NdMTZmae0tW z5LdgtcbvKg&?M8|FA5JTmaX9g%OG`l4pnsV??K)tWf8%{wYs%qxq_19QOV-e!`q+$ z2{`gSgjD(dH(%H{U?S5?*bD6GQ1x&IT#=k;3NJjP?F^RLAHjHKQmeMN3IAhCIEKTj zq07~nw_s72B>VanB!0`_Y_$a zoUDFSdJQ!4%>S0V=h(HGst}r1$vif@#f+H%{ z?tWEos7`Sp_)2M2^L1@=T`PK63T~R_?b5<&$L(ej@Utqs`-xv>x>i2Era5_RmaX!^ zW=dIT?KPVC6RSnx$Ke+{#{w72>Y_9@Q=`NU<2z6ibA++@N1p0bRxs$% z9gg<`!2o2ZoIL-hN9DiL-7qmT{=aVuO^m$cAOlM9?j!Y#ArzfugNY^nrJR5l1|57r zSP&bnda7`u(%o*RjYE6ZV7Fb5W35=YF$Yk|3yJ2M#m99Q-fj@{h05t5Fxt@nL?j`%Wzyeg;ibC863 z*<$jT?U3@g1mH5QlE3oKR0{UUC0KwY*;{pgA?~T1m4!tedWJn1j_Ik>TQL3|YE|R- zPnuh3nnQfmTh4S>+J4n24H*h|1~Jx!P*Bg4*h)IWdh>B0k?+4^>9a}A|EFU8S4&$q z4o=Shw^~#Gv$PdO^3JV26Lkk{`SfvjTG6?^?hl6~;je=LiD*DgRFN9beBQk+zsSjC zK{3uKf*B<0;|b>isk>DJMz9Xfs%=~Bzx zrTb$$l=(OudR+Z>{&jHivF>~FdxY%FXgk5fK@=*(NDX4@8q#1Mqsh&TYQHpmE7=~G9alI(tpXo<8{P)ZZ{yjM zGInZI#Re@p6zPs^RBrMBYE{^@I_wf6_oW1REP%;TQ!~)$#!~7asKx!*Y-;DuXqm>3 zE${ZPBd%=UAUIIOP4i6WBn7D&pgt%rAD2lmBl{;#eX$H(nI77K_-uR3lIUA^-ZN^d<;5y!k{u-jr^l*qEL|`Guv+H|geN~tP8lcB=LFMiGvd?r9NbaVtP+QN-~P@C`#P0#UE28A4yt zH2tJ!8&2hEPidqjhT-eu2`l7rHexN{W=PP|V04UqHd4s!0*M5yTZ?ld$lol|Q)y&3 zndSk56vfSyRhWPU9X4CAP|;{_xXVnk zFFMG9gF_Dpfq{d~jgwJL5}R%fn(`H*c|8ugLdc$>WU(07T}}w}p_Go1SvVeM3`_py zU88dd7Ta$!6C+DgcdW1?mq^Lcc3O;k>HW(M`m)4ev@?p#z#AZ_@uGLj$Q`K zw*DoO#;xpg-LfE0_R6^v75+15d9qa>BBUgzVdI$bP>GD;XVcfOY)o^_1FZzw-5EKU zPQHtTE1xp3TY~4Mm{?Kd*PWS*?p&q$i8|*I&F&jnH*s*{6=p9Y9nazMIW%ISljmV- zfh?eAz8hIB5ljXkPIpZ0;c07Oc=RI?kFS7@!EjucFTw4O;re_w(Jhd7BD(r~&e1s& zrb!9qUUUNE%_6chou#$&p#WUVNWAJXa1>jh=W3BpaCz_~TP$EAx1||2g^5oqQX-&N z{8qRfqvs>XPW+3cG%xwwq?e=o{{lMJA)V~U5B;iQv*!HR$>6-jI_>!61XDS0E=4P& z*kLE8R-E{hsZR)e*N@N7D@NT_1z&aiJ1rzdMhA}^(`y8yzr{c59F~@hf_p?7B^Rew zQB7Z)*0&0}h%;R$+5Q>$PKc^nOPo&Sw+`}fmRPk~V_YPt=vQqpS@6jaE}NUrYt~|8 z=btD|MCcEL8Ls6yeS%uQN)xY31&&^__f-OymN#eJRH&jn~ zSc%^AS93x%;?=Nlrz>kw4S8sZ)(Zu+AV)$=R}68ALgQjMpS@g=P_Pg4~9nE9wBee^wR{_@uJp0i&QR{fd12*PcE@D$mca_lk zqTx^SK{=|3p$+C1Gf+-m(`o|2Mp*gt713137?U4QY^2T0bUSOizzt}|Wf~V>Y0>&P zm~Mziy8ch&xVL96qJE#WNOQ&;&r76qhl|fwr-1Sc51$ugZ$$SuTcSgYZrOraWi3D{ z9yz|)p!mX?;LES_^6V=pzr@5A1LX$h%>}tF9*$y%4v2-?c9@@2R%i{GI>>3!6iPxp zTeexDoz#yUH3>6rvAkcrylx&UOlcD+6*Ik~vxC$Q8D!U*Af72^t%xK5p2m~bw)h&O zXtDgV36F0IoMGZ;y_|7wHZw5&6)PK)tP<|!D>|{5OEc~MpmFg3q07W zNrA~eRH^kYWll;W`JP|{Lq)nL3*wLXv!s~hDtF}55$~Zk7UF`4#XH3w(N2woI>(^2 zR547Qj>7>9kUbe_s#L1aOK?Q>gxy|&`vF}>_zrb8wrZiSEQzolhLYGO7~RZ`$zc!EBi?)E&HsY?IL==Q($5?IYYE_ zWmR8Q5uAik)p+IOUDD7$1+IpLc9RRL*q~`Br z4)KnkS^2ztz3iLHKIjw+=gKCvBsL`s9zdVZT2;}HEX%5T^AJG?<{D>JdP>-Y*&WEL zxq$n6@1Aty|54S4AYv1|VR|F<2H^v$H0n_3Yi*j@(gOivus3*AmXBzMx~A4kBv5_+ zxsLQg!5oY$n5gbSz<%~5WU8(V4HR2e_mP_FWLtLNNC1IfM0zmUR5S~zqOckoii*=x zANOz|sAV&=DrxVj?H0b4%v;70W9Hu7-toOKfgm<08*BzHE8xRNKl(6e7%BWCJY)WV zos7AwFhL)>hTnqt zjV3-cG6(UXQw%8{MdPOw`!E*(C2Tk)Rk*Hfem8ah0NGgWG=B$!HFp#HSUz2{W{R9) z$`Ia<>#D+~&=(E|7sp)!Ji_FrLX&dNk47Rg^lYWag_7nmc_URun<7}sm=1->ISdq^ zb*`}gqT$uGQ#Gq}sXwCAwG}k5asloEs-?^Y})Hx+ZmZWe_V6d9E7Zoh!Hu-3A zQN{IFN_b-{vvnZFof_t}+Rk(b>blFg^rU*N8jsypSl+ai_mp<|x)6@Xn9zidBSzol zgTPS5!)%JFkn!^O>R%!d_9&wFZf%+4 z+Q^)>yHef|x{$jITn=sjpj0N!mmrhf842l#0tKg&gf@>u?d|*+F;*_Ou@xh@p(F=} z=2Yv|Jfc!ammC+0htc6hz0Q;&JOrD_KYX0#+$zplrC`hBCb&}{Su+G;S8qKFsBOI+ z9&yO9I2xv@_t*V-v9tHG@HjgqVC$?bzUsR_ftR>S@okg9CowQoz z{>6X?7#5?tI9|Ta-$Gt~LmvLZ?+~*k9YU)6%6SOXBkNjc&_bFJZ2|HEcwqx-G;Kpn z!~2j36TrwugrP%$^8OmVgMkrMrUgVZzutTp(wC`O#ZpeI8h};LrX?2_aJ_3ugCFL zp}hWN6sd_&UVc4}0RCd&ov=^mDg>oAS3a_=*HT0*w+c!yX)yx1It;{|HU6e*Zpqeg)pBV!X} zRntKn_FXIV6{>xysKME%gc^seK@3#AMzxcw@Rd&WF=LD!HGYllrY9{OyDVqJ4rYj#f`=0dadba!VvKqS^fxcas1&9U=i|6K{u|M&piNP z{;e4;4WZYc*M}zGUEgm%0KySig*5df4hv9+ggU%`+0{e#N_a^(9<*+CHA_oPAPR(vt}9cw(iZqT%oyPu*fjom{k@!r`2D+7{G#uoE^2uh1j{F%iv zo>9*2oBRycVbOpE%!yPHt`(w5i+@Eb?GZ2JX9Tp**ER#3z&;iqJ$|$N@u22#r`b@? zWMTue@54P6?Kg3X}7(4z%se>a?`&PV;qeMT6tL zEJ5&>W$RGs<*e{Z3;pM^2;?+zsP*Z?oi4DZ?q+o zogndnkAim=@&I&c`Xm5jjrTo5cW-VVkY*LMu#Bx4D=X--2^ia${N9hcI(jvuVKIWWQQ=rLjq63qr#9I#goir z90lEEwc(}jUmQB4Ad(UN+h+I!G`jus!rXdfrtKfMXeYF$&@{%_LNG9a!T``B@N2aq z&|}GSYG71x`iS>1K6yn#({quRL5*$vA+{jKmTCT)bPW%c9!%~at+VaqR6E(z1J-#= z3cf<*2)x4;@MA&a&Mu%J(-gC-r+uy$bU;*Bkg6hDLya>e^}(tfT9VMg>5UW1Gpp!| z92JZfzE|)w2hC}&k81`JJx6!JUg_u@ zIZ}XwoIDfQRuWEXa!JBHtla7Mc6Jq~fs$m71f`JamfH}%TUjkx^XoFHZy%5e zY;$L4sRnjThg8@+da&|Yu=bn_UZn7ee5*kfO+T=?snd2^o%da+z&_?-!j#Y1qPRvD znOH4Q6oH%?amz%U$w+-qS$Aa0sTzxb<}WH}8TSi0Kxg~UV%gC%wyYOicA6z6v@I_Y zd|K77VjPpY1RkCR>jn95wLPnLj8Fb$gB@>AIob+3(jw&4USTFV?U3M@BsuMrN^2xB zCI3IiRBT2k@UmDD7Qd`3-*phLuHM44eRXD-c&ffc_uaz-d0XCOS^ zt~fmZzMCYJ1h)ib>4s^f@wgaT9-+aQ(Ot1No#w%-5UAqlsA0g*ix8}C6fucWn_pw2 zqI5aO2L;tGCX0NnxttsZG(w6|URnd7a}{Hs2RSEu)41^@!dm9^6Hyw!3iD$?P*T|2 zxEKI?QO-x5cZcpRK9k2kna|%_zgs}Q%h$}WiCG7`cqc*^Nk2e>=#pg+B)n~M9TeFTD z_TqCvH<2%mOdIuNu(8XQa}}YVoEeJ@I_+cM-|+y0P&<%qZsOUScC#%l9AoSGJf)lV zvkdu)1;8dk3}4P7C}MG4q3fnF*ZL;uKnfwj4>Z3S#DJQXU681q~@1 zhoy<^;sK0_`f`5$5R(xZ$j&F3QPd$ILN(<-588r%ktScpJ3qzk#CI-mnF*-FbXuK7 zrSc9Z^on*&eq0><^XtNl54|#x?*yw*)X0=mS%+QD(%_uneSH}*;2{dW17;K%I>82N z-19Akh--^TBFSJtrX1EbjGaL%VQ%3^a|0%+gV@t1)Wkxxv*gsO@TT0ddR*1Bgdjks zJdUyaF{MHPlKy zdfa;xe^A$z<9i$yJ5e1w^*easkwSf^sWeul1rukYQ~AD1G#;C-CiAFw=fA^Oo&_OT z?}W+w+&0lBn`P-MKr5?+huE;iW53krW2MO^eaOp*?cu?jpGNU8Xjj{G8B3AFkDTKU zgThl>s4=oz#drhVj!Hl+U|-20${egTZq$sMt{F2)Wv!-dSf8FW;ZB)$AY+-FyW`daX>KR`XEOKR0ob3B0%7fY}w(4){wuVFE#Q z)+n>NCMwV$NV0g>i{Ua3z)%nmaY5W=sOF>VkIV)tlg5C~i4rwL|89@civRoa*lDxu z{Ksd*SM9cAR+mkeZTs+N@%!Z5{PW(s)%$`Cf3^3D4c>dTd%I^hrc-Q2_J+Xr)<+C{ zt_D7K_Rz_zq|IzhUo?Ai6DCrxG5GV3=3YVl+4_8uo`oumJp_3=;S{SJ&3V=MerI9m zsk8xyBuNG`BFMxNI_Q8Rq^!%3BU~!`VHhIt};61)4@1+DIAXl zkaFNTkqJ(lhB*pU`mQ*SgfH})pGmFtPxK1ZDOH5_zZ9TaGEyfHP+%!S25DABB?n7a zbWiwR)E`zaP1`a7>C}KtXjY*4u*t=p?I+ka{rwmaTA>3L@4)Q67XM11O}}5WwG4luQ1f*6x$W2+-&%I6RJvfVkxDu{th2}ZNq6R^@RN|a=T|T{Y3o|Q zyTu2N|An zboUKi!%W3jR-M=CKI1;+ip(?nty_w&>wJ4fBNDEcU^9F`cv+2T!wc}<@v@n|FywS7 zcJ#8`+Oi`?H8fjsDCsRcN6iNI?@5#N%d^w$7I+jxNPVyT9Q9!G(+n7@Y_srKHKNOx z5CZ^sgf^HqF~ZCC8XV`OlW>f|g(DUVpI-K^SA zL#2LGhs}UKyX5|`GIh57-8s*RByBi&H*x&Z2d#xkSRwj0^F*}vvS;Sz*8OSMyPE|%DeDl{^s;q3_-98dZ3mhw zz{ATQ4{d0nyJsG`9ry@b{7pdP96-vCiJ`vu_P2^Q0#Z8Xe%*QbA>%(?}dvfrapAu^iOd zt(=VHr6{)^sI7o~N*OjRd-~;l%K~fq^!sfG-M={|8DUtD!HvO3DCE^;_$(U=mBR5v zzUmQ09TEdEcn&;kS``%M(howS?+xbL6~`{bFHr37Klnr!8pBKqR|y3kRr5g)8zOMT zq5YnUK4%VnOB7@&a*e>^#wHy03k4hIg2EhApH5eBdAvVW3U5o^(WFB?-7eyk#F+)B zN`pJYE-Wdr#Dw4ifL4zx5Indchy_FpV3?pc^(>`%_;q}UnX9W5Nl{>FKi)XjY925} zCBZuH{vmu=V0ZB-pt^$7HrS)U(TXvZ8EOz6yZmoh$(rdU`Bgp>$C0#>DyB3bwN;@r-oLw$@MH;Lhih{ zch6K@#~qs6@IS1!HE=ban~%Lgu(+UI#Y7T@Ct+m<){|{#Gh(E_2qk86i!AGKYb?j&z*BoZ7$ixeVxEYFG- zM!uD3&TGbEX}dai+~W8v7_${{PB$_0_~in4GQ|wKn3_`#L#RwpwAgbj(S~(#{4)l@ zO>-`2F!mvJxdSD2kg3RL8~2`Wg!3?8vU4%^>*7O;&h4!lw%5+5^n`bE-+N~QI^BtI zGu$)o3drKFK(`%%0DK~09LWwysA}@zp&;a;HMnqj*mU`8`9cpq_6|k+LU&Iar z!kE1gLe`jc`&f+F{o%IR$e(Jrd32p9*$0CR*7GK*s1CP|oxo{ri_~=K$Wt*39`k#g zD;_H6oQxkn@n~8u1cZx(Je#@JS^A%7tWn>8+(8jD(#e0?oc(v)0UPW8!u$U}#=a>? zlwb{St+Tdm+qP}nw#~D)_pEK(wr$&HHmRg4N!^EA)lW0?Fg;y8-TnO^F4eT0uv##D zcJuK7KJ4cC+7Js_Id$+q1C@d++m@r}~06HQk z-gvW|y6x_x;%{RUU)CafI!z8uQ@cjrW(C5URp!7qJp*>VL9yK7~U_{peP1 z#)b?PE?p0wp0hF=rhFS#smGp7rD#mRxYEB|whH^gHdnnmXex`b6l1ln1CNuO* z%+_;GdInKEG#!S?k%;wzRY24C_?DLw9OZ(Y7VjDZ>?SfhuG;L)K;IhDlq4WMw5n}W zhkiz{I^;SW8T1ma+K$bJ^2zy9F0kr1jQE(ZcNu4RjnfviWLlpWspM20dL<0AqJbx* zHDzcj#FSYKy}uOD-_SISXfKl)hjENDb^*4zuJ1H~?CQqRyAco7vTnf%M_-8fr+^pn z=#zF&a+GMX(+wb_mBMOn@js^vOC*DrwoUDBZyYXo8d1^MnJ}jenFKKS?-l_fOqOp# z)XsVE6^2QNy%j$!0e8~ToX{^>q)+K3mf&=DCcVE-8$_t#2ITF_kz=ivu2f7>NS(67 zf&!jlLI7`KVFWrv447T|K~7=1QwwG*a33uJe9&xVlBE+p3x@Ze$$~m~B8Np3>ciMZ z8i!>e(F4!X`3Tjx{0(4feL&N^uRpD)3DFUipOBD0fF2?s5fb&8tDX|!6E)>+v_sJO z$cHu|ZJAe4H*Y+{G0xvWFqW$gNSXkyEig!pwqu!cIi8ZenSmWk>mG|AoZ06+%qS6) z{wlx-WX6GrWG%jXF`uvnx5EErtl^XkBg%Fw7Swi-34vE4tllpah1FPuR$r8kcF*J? zOUibQFEbWX!8q^M423c+12lS80jDl6Hl90EE-_Vj*bHas<13S8tB?*&t5THadr7uF zI|^2-*DrY^u>Wzh!;{$kcb0vV&w>m$ke5NswB@Rb2Njg(=?&&bAg0nvSg)A$Di_Mz;Kqh# zwzDtDCA=5!1pioERw4t&UL{-U?vWEsca?*)?g&p(rhjStJ;*1ThP&$9u^6ul0Zoj_ zkfs&Bs&ln6oJfcpm?%Iv%lm!}rNC)zbRav1qlxC&*Xxd{Zs8mq0pSPLRazq{iF|O9A z#S|wK-u2k=F z6?3XFJoO}@g&J{$Sor+wsF=aPV7;Zf)%6rWv@twus{8FEZx|KKIBoBk zj5EsmXy==EdZLW1JTH-277Q2Mvj{p6= z9p0C$q|)_F(CHP2u`7Sv9*Ot@hVyQsbbLJmc-(d<5fKBgKjc2q@?Y`8z*}adWrzVA z7sx`u^oN0f_#aP@tX=M)Y(PP75ju3xs2z2GzCuW_ql4SCx3DIG8pL{XE7)688M8TI zo77nf+|BzKg%2RHal$vqPrDBU)d>VbE^Jw=H#p81wI12?v<;ou|1i+1 zsU3Q9>7f|oeeq4tMoyY)UGo)zH~aWkc2UVDG)=39WH0>shPEU9E!G=>vXyey6Q^{V zcNZ9ohPBs_!8s_ntGDSC#8mCzrPoe5*FQ#{?_--=w;T0r(3wgF^xK9}W25xZNriVd z0fkZ!sp64eJwkKdiu1J-URmN_+2S9=n|Ik*anZVHuX5JOkzOH^lm@xDE^`smX({~P zIB&nY_@9l$C_Aq@^T2rvxA@{1T_+VR2?Ro+2cT}@Psd^4y}d#lc$)n4r+0x;GG!BR zvh5V+9;aeUQus!KfnkpAW(%M0r#;6PM|R%yX^qQt?o258aoHm)u2b8Z$P;MlZHheW zim}>uCDM}gAOAaLEe_GAcoAH=m)Kx@#|n|sI&S8cd;7j56|f>;p|);HvQq?#YWM7WxnZ|x`9whi4^a(wX=OIyU?xroeGya3 zTXOMCRm5txzPa5Sg!w)I%Mx1Hk4uUgIu~2M-CKUu)Es8kj~Rbp0utMSM4d{TaY{dx;ycMaau=)3^~ONm#0Bb0#EGd7Qv=&aZiA^06#9 zK;)h!;g2zr$Ce9s$NzS8j_;Pa`<)?3DLO-4qt0_q{PXs}-g8S8Sg%>qG}IK{>pcnV z4c&$0GDxqU8=rQmvp;d76gmW`D{v?kKz_3!m+Azj_f-#=)_j%+o+HuVI4K!gSUFW` z0CzTQ+SXD&?JiE5v9`g6`t#WL+4Vj%$J0 zQ{or{T|bwE(W#$v8G>#Zb*2n=iryWB`Z+{o$)pz{f?^E_|A8Zc%LPtiju;<8WLVzx z;0sb0n>TzFbL~A7EZvlgHOD}d!M#xEL%$Gw`rCNCuwJ%2JlU#TzYMu@kJDda zKP3zY6p(qtZbU2sSXW(vm;i^Zo5wt9ZeYAuH)WSih%Z1mRV1#-MyFArI)k-_tlRH$ z!x(hk7eBk;oUEmZ>-BKVTV#>t!^&9jr8~gr*^K3GyiPCL_?VrO?WC`Fsiu99uZ-o% z;*&s>N?X#6;V;$pvBG3tc9H=iD1-++^qhSze0@6Ed{4~-eQjGXG3@?sjRxYv2-F3L z6B*zKgpKp$tr738pIx!e(}#VV;vCP&4`a+Pr5x6Vm_Y=F^*X`m@(=8`bn(|PHqFlS z<{1MEYgMjZMa`$?h0mU9DahqA%Fe0l8^xDQp3oS$457NlzUg`a)Oj4D{?jh?ztX2L zGc*2wyVT#BSQ~=RP0jAXYo5h6U92m2n>FG{1gLS4NG&|`Ro(Kucnb3`!Rti1VW}nF zRI^|i9Rg^8b*xzZu&i9EWLuW8?=;p}uYSevm3;Mz)r%*dZ(;mUiL|07P-7tJWUg+E zv7>%(=`d9qqdK_nWK8CHVJQ=@t^Shkp@Z&n%39UB<#OOS*;%b(%d%OsR;~iZt0C+9 z?+45WeYOvU>m}Yo=G*yF4;fz)s!4s?A6n$h$~FrCQ_k`o9YtDVHnP*$B96MKkVK%mFR#t0j?3>=sV{F`7 zq3NddFqEtWujl7d{SpwP#x}9qBvMD?`p~Jsnl5 zE14*HQK0uCZy>FI#pr_K07v)w6(D93{+LZj+mjJ}z*Rr06|cfkfBZFY^qt-Gm~pIW zjjr2_a(QQ43FX4N9gJPuxjP}I0;d+Ym{?gRwHL0<0lmfsT+5zIS$FhNjYwmq?rpcm zLvc@vq5TbPjQq+fo-+6+>oaIyYEN_FZDGNuYdU?WZ+u~7m$#OO(=wkE-Pj0pGD}x@ z;_i!P;f6UcX>S6MS2y}K8?^qC`%EOX)j4Q>-F5Dd(Eui)HFIqv)8>Rbs^&TtZb8{C;|@jSSi8~|b10e+cI>V7 zVctWx89eT7F&B@hJKCSBUrf&O6TftM z`}XVqzHgrC|EP8hu}&7snrD-K7wE-QnG%b(F0-m#*~?5SN=Yw>8!;*M!2gi&CnarN zT*DA{fQ7kHF{BQ%TDA$Zs$aYje2-o`i5P4wg{sgoPNx++$4*7^$a3?v_x<;iY!kq6 zf}iAoKi3JOgQn_qL*hr9VRGp6LyoA#fsYxQ1CdkA<*4-QbuSd`avjfI~m!4Q^Ii+j$Dv63GtN;Xc!dn23+-9JzcO@_9#Lq6LEM{OL zT#YsY7M!Lo*UM~b%jzzX!j4tY!n6w9ORD6Z>7_wrq+%zi<&;0XRcc_(xmP@e1zC~n zu5-Q4s=2454Od7cMuMst8CLGjtB}Gj(53=h1oo+5VbHY-_DelgIR0zvc2il-xF2uo zepb`;5-mppJ0bw#DV60eLVa=RhnVm=RXdWRva(F}F4KoCtnZ874Q!Yzw-JuT`KE47 znhkdzfMy)YOtlq0FCiytYsv#oorkDK*i+PXD#JbLCfjI^)0eczu@Jp9vX^kpOx8ho zYy)I+XGW>FR(C2pm9bXRKiGiub;|6@8U2THDa&hM*%mL3D2$-4mP-d^E^*8M2!qg* zN!?K4hq9rTs8@e6=3!g*aL12qm1U{Oe>E}zt#&up%o~HUj|;qpLXSKh;fD5MaWqzt z)$7K8Rx zzpwf-d1{v(X3qc>BVfXyLrBGxKNuQHP(PYYh(kQPHtx9mG#?kN5DaR7C{;^tuw64e ziH1UCy#@{>8+{@c9q{L!n$I$%JsMrgw~-K1DbhnU}(e zERk00hM1Cpi?Ag(rc2RcZw(A5AsKdM+leF$LaVU+TKK-Ps50iO{}uV}_%DU!rX7UC zyF0~PdiP%RF?}JMIJGI)6)Fr$a-7KV&Eo5%u6mRrr{aVwL^(Q44x8=Zy%5SUIt#u& zlT@B;{2!eZCKmYHTcfnZC$?g7nmeSgFElMuTl2lYYO-LTN?zl9RHjQ~$1Z^rYua|d z906|Rt*y{0m`JNB?)oHdyt`a|Oz^6(1;Ee&hcDiaL_Z9S9R~4=0Hwk1k^AQwkatv1zI#6UL>Q*cq>C%D#~&b9LiX}P>NmgwPa|gK(m|lz z&Kf%Iz!qXFQ&`>=eS>c~I3;0`vBJapB=%rkq0=+?E z(n6NZO=Goz0S5E&F_|Lqv*PN1862NgCq~vPCfnAUf~^!r+Xyyvj!D!1OLl z)^Oc(!0-|NxWa`XGX|Er;QlbaQ^q zC)1eG^dHZ^F?iUxi!Fzv;ENSRcL8i(_9j&N#E9JV7W@;%O$D+28Y`DeM2$@Sh@__w zK}=Vf34Nml@<-g!_wUUE*n;DW{vpngLT9m=SkD$rI;hqyk{DQAxEeOc9d>6ycD(qC zH<5EhODZ6!+`b05!j>E0Q~ldg@EdbIajC?mbi(D$_EbEa56f4+PgkAy&tXtFG7DhtyY}f z1Or+~LX?A)mxn|s+k>B-+`X%LScg+7e;tnJX)a{ANVc9O+ijAaCpZh7-89lP0ly8jBx;KRsikU47E+F60SW#el%M%Y~RCX{`+7M zO%v_rTiM~BSp>Mrym2gF(_z|@ZGvU1GGy;q3iEGSv`5ZH9+^rncRzj6p<}#@lvrG^ z*Qp}w8(x>f=QON0m!Xi31SEi_P3sE4P4-jzIDZni!)0T~mcd3Q!&_AfyIpy&WjwNI zFh$X@2af_UK+85if&Kj_1rZ?*oZKh@W)w+@WDkWdsc__o<%-9>*U>I1t&E%V-lY;u zDznq;8((8nJ5y?tCt+4xv0gs#>FY$v$-135wUOE3!*8^1?RptbjW)0sZehJs7vxD3 z26f##2HARcMLsYLy#9t)1ccvKrMfrI1|a5G%7^>l1;!S|a_X|<<14C~$=8q8N5i4I zo6v`j>fM!cAinuP=si*-w2ws*+5zFziT$4f(hO`GmB2md0 z6+=ut;!1(as&{-j`X-_@Usi^w0QWL_uBRTGp6-27-3>JY^jJE_!gq6H{M#$EjknB( zhY~n7#GV-{mSvqq%*%yqVrf&Yo6E+v+tW94+Hcl`A07FNbml(9sQ;YO%1eQqS);0c zoIW2W3u`&_ZlS~*MQ_(Ox6azO_$gr$I+uQ561f!bYPyPbJvNyi(`x1t=(i|5v{mu| z*|*v@!?@BL$`RX8JNew>br9r{a7e z+B~LSct!Hhs^44z;|60WO73*}pXAiq zpeT`laGF~!GB3z7zI_A#5dyLp~xu+X>SU**-?pI}Ev3vvGf_wdOAszl?o@*_v6JFQ0?6 zDF~A$>u>!85Z+jL{DpqqeducP7Wiqg6y z%ukSR3XOqX^loo74$@i2`zX{%kjFRA+^W}B+s9nX? z)~YsWd$W_N=K*iTUYpTsR^*KJod7);ybU+3Z*IwrqFJ8t`ZiZK1WX z-F(OxCXY@AO3QgRbJ#xYMFCHQ_?wA@(s z^L=w`sa3OZO6FGE+cvl)qqG#XKO?TJsH!gi#ade4Tk75GrDYv$QUTOhSkH(vtGu6I zcSI*P2Ug&(krAz21FX6_n_bEbU+b6a)Nk26E9-~0doept6@P`(k^$1I-#+j8IUR3rbbTy-d!U8Gingngn)@*4ff49XxpMOW*@w1JH} zI8`QJGj z`jil2Rq&etxKK zK;TxC?KY6f%AC~Ov8M%lXG<;LBV#;L820PD2*<8cUtuye$Zw&gF$emkYCTD(RL-4Wg35~`jZlNXL+=011Y8U`y}iK z3?Q1tM(;o1G0gv!;+c(u`Tx<|4XWMPVYeWB`TBwKt%C|Xy=LNN6SBx7i)iA-fr%hM z{JkXS(1<9+zw+B*BX+ZUN%2}wz9vLpj&LG0V~>3}o>JS0M*NH&oXK><CEBFRGG96cO*c73>EyZIq$Lh|c)yKiv!b8-2)#qVzOllq?8I@ns;lJ#!y zK)bP{!>jS@K-+hoV0C)LK&(6(mp$IRmYw7s0AQRl;ujl5naMDLgNHg|8OJr0V zlqTFZVw+faCrAW|jm1Ff(5^ggq|sbkB6rLDypP<}=}cDU39&(qpNnZrwTMqtzY5Xx zUQ>a&2`XXKufHXdNw-`BOSnv&ZE6NP{oGAEoJnixc>Xl8^ux?)0gGB?%)fUF`)P^D z&_29+pS)KRR>+~(t>zA{ZdG;HrQ#mYT&d+zpAcvKSGRm|azRqu2ubmexY5J}rnprp zB%7iG?7Tanc(ilq1LDY03$Bu*IM4Nfm34g~JwI$h06JY0l$6+=kK_q(k znf|`<@8gOrKDij^PUsfcfJkuu5#YVS2=oS+_R#tmi47d(4;uRY-b?Dr(KX8Kr$7Rj zW|<>$fARyK{=^ZAMsQf~9}s3G#{ zFqv}pIUsNjI?H*hXo!_p6I@BtVjgH@IOqBFeu)odrj-(JWEAYziq%IZALQ_s36Cd!i zluEH7&{nS%^pb&ILRo9v4Wb06{z=&yLlveZqEuDIBAd01ZE(@#h5)T$gyLQ30?WfI zNxRgK=jK}z_!>gvl;^QEI-vxsXoAU^i6m?4E>G)Xmy4mb_<>YjLMQZvt@xu%N0$|) z8L+JB&k9pkmSLJICI>g@V=Wanl5OlOD%(s7++`XZC{gRUpT@L5A}2Ei1j(lzF=eXaig_OSXnO!wA@!VxO0oJG5KuYnBNL|6%8SxxvPteivi__gk* zvMCy(6H3CtR?+fU!1S;MmN*B->wc(Xihy{^IK4vvQpA){x7#sm#(=b zlWj+buXF3W@y$~!(JkX5h3u`ve2c>kEzv5(s(4GMognnab##3zF{k#Ksnoi=uP`OG z31&FC{|+hrp1O`(%A}~r*`+{F&;)r-Y!<#MB5gku^xsli--JU}0CyIqF0eCnu7oJc zMZF9mx_(e{84r7wAUbk)K}zOP>y=S7W6>I_GogVz1l$^|xi50vB5|32XtXM3 zEhYpp;J?O}%W@H5bx@Ouup}L!i8qW`L7?otqWZrW=hc!VK>zev}{$)FKL%qsY{ku zjf;l-Qw$U=5NHih(Q3lVPRMDe=D>AZ|FL#f#sTS+ zVxX~wNofW}{tF@LlP&H?M2l0mx_6KzbtXi+J!=iKxG98Hrtz2_L)~jkX*)^fX=M8Z zG=lq{H!+<>wEc$)NS5xy?@N1^=zrLcR`bg^jBY<&pSaz}{RBJ9IJjv)%LDKqoR;{L zuuk&?5@J*^K2x)2GIgmWG!?>BcDH~U6U{%k$Xc-Z-j8AB7wB)#zYrw>?3B!s7-=aa zKdtJHVls;qsxST5t2-{Q^N<*qnuF0NYRZ#MqFlg_yhG(C#3o8S(iNu1bB{t2WnjUx z@^m;d{GI8;1kK!Gg$}O3rlXyG)uooZkTF_;5uyeXCgrt06vf1D6(uN?_+EfCv4Gm^ zIn+rZeOW&1cK7xfT6f%69diF0^G%&4!~7tQew0z#oVRn@*f3sd@Q_B3!J@L}x!!G3 zyy#8LnsL5(Onlz7@OM#|8~4y3z`@d06|dx7#K)Nv6qN*zN-p zNeuLzmhL|?SB@i%zXhaJV&cHK?=`YpQ~9G#Z(wiGr^pHeeX^n(i-H4NRz%n}dN=|gGJZi1Z%ub5JDimAKXDoN z1bhbCuGY;c@5IXmI8%p|s68JUQCT>#UKuryVm$Nm(sWAwdQ-duGmGW+ ztbptSf%9ABN^UHU&X1EWwo|d-_>SP0ZukfrtM;?jEHtuQiPK!16bP{+FFB&K<@MIp zj-NDC_3tCRCaN`WbMeb{M#YwOye8I+UH6HBW6=x4Hm^OC$1==y%^PwppGk>n;CoKU zm2x+;x;uQ?FJ-g*aLD=akMWH&$^!t$!Q!a$VHuLNZvV_&I5!fk^bp@>6chYATrPkK zo$Lk;8l$<68ka(iKXE;Zhuxv6X5F;>xPe!X0~`?7DX0XJKaWxH;HROjW_9uCyZB+! zqv5h87Q3ykIV~{okM`t~jRYlL4Gnl&pY2--2(7@$ElNLA4jTA4p(({eO2Y?Rz`kn)T%PN@!eC`77jb|Vg8gE5W{h^2NAzS^|JxZQ<#@z;*c8l906azLL4Lh}a{w`^p5USR9}Dh!zFQQ$(PrM70xRfy%? z(l^BtRe8p?ndtO)hM~{P^)kmEWMgn}PO~FnJc7WSv`BGQ@*Q`J4_@XXHc3`h#_T8% zM8YXRDT}9-_b2}DcSAtMEw*sq{#h3vI$_M51>QG#0lD^gUoq5_3hOvHDC{#90V7zV zKZvG87?_kn;NTZ_Ki+?ohuthmWzmI*O01rJ7p5za<~GoBIzWp??j@|ZAJzIz*_oa$ z`Eo%?CBb1I4Bh@w!AOS(ClPrp%L|hSUshp|(vYoKsU(cRCu9YnVqmP5G9Hho7f$*s z(ZH;__XGV8U? zbLB@TY^=R55|noZ1n9FdT_^E(uE_O|wF~lRNIXe@djwIAm$dkRhO}JMCN_|A{|iJ*lw?q*c<}$hRPZC56$Z;ADrGlH$B{9k1Gcw z`K_|`pXX_SW~iS9Vte5yL2}s$WC6hTq1WP@%zLHs$ShVwiyG5mhIY9hd6bG%nqb^| z)*I^JOf86(TKMXW(843GKYfnLE2L*?O4PyWS`Z$ag0CSVw`+pTBMPKOz8X5F9=-vo zxyTxgTWiR5?{>6;PqczG+QF2xf~ijuXbO%=+CmcwP6!Nb5Xm+HIUNvP_6VFE5IQU1 zwEdIP!F@vxs{5c*_`=gglL)M^*npQLnBz=fchE8*G0ccPWchZEq@dKt0^_w5Ic7G9VR@p(s00it z=*{7im==zAylomRu>INeu*9f$DoLHjf_#z$_ob*qFuPjqKz2V5_l+N4_RvQC2DkU8 z1I&TcG3~;oRpbElMX}XNt8OoxGDDG7P0id7+-T&ne35_dHbg!=0OJj~onFYR0Ka_a z5z#485xw0l?i0X#*I9reFPOa@{LvBH!?}>Vh3i|1g!wZhHHNZh5SHj2TP~})BGa-( zw9|KjIU(#1!+Y;7)_E^?q4jVvsnCAc;Q}92m2u;C9 z&k@~LQsmOMDLp0Mzh?8>uBX3?wlB!}eY_g1LJ!DU4-Bk~?A@I`EKEEb{5(E6 z#dKfh@Y(3;A&|ogkya#3kGJ^2vDI(D*6ir&HS6KE#Qv#3@Fx{X!qldzs)$$A;)w&% zUrWWcyl`pmm!(LoR-eq7M{B}}+!*aV^qRhRn!f)teIHS@NolqQYPOcSDSAMyutQXB zT;6N!YvH>sta=Ppu_qU&;*O+|-l=+01qNSi50MT?h7q!Qtz}$}V4W;Yc1ExR^ZkpmA7J7GEY3JcQ%D)I*{Oy{KX5Z)7c;|LFD%rY{Ar0wVjD zC7ffwoP{5x2ucve3g3x;r}FsD@{mkj<*>6&aym6%KTd!X%*s+hQg|gFzaWR4V>!Q- z)=@!WUdn|2R{^8)R7roFOMiaf?=L-Tg?KZaOc5Xt6~qXV&|kb>6a$OK zR7Cr}-d>x(a>^gzP+9>)j-cV~Kb^f-4il&IlI5gfbMsQKks?PgyES2^;1@DS;4oxX zhRv{KRs@!)saAl}E^gO&ov^18ldN$A&w<;bX|O2m5kUk&U$eo3QANX1Ww2L*490&; z>qjzIx?IR11+tR_bkxBy@)>t#>xhXl(RC7+S0ftA2)4juY>W*Wnh89>sz2$a6M&jz z4biKXFFdJ=y*D<2kcg1Wj>GEW6`e(6l;Hrrq}1wGSV7LDiJ|l~zFR>opdV9x;YJ_G z1D_pp1b3E7$Y~TaO35iPL7uaslw3}Ms2ZvHf>`18pc7oeOUmt{V-oKaajk5(kX?N) zt742o)fO3I#-8vmC6g#n2~OfzWhvD}U&Tj*BnLICVUXCTEuT#VCC*!ZAyL9^*(4#e zy?SDD@}_(uAur@U_|{COSX&`SxSk^+*-V`Jw19eE$t{3OLJnURD(8dEQVs*(lu#W? zR+V8*4D!s-F?cM8=+xX;v37hf%=)i*n`}mFTllFOVB0>Y?8YdBXBh6RKM9FlL0|SZ z>(Bs}d}2G%S-Rf$YJuxZ+rLU<<^a$jHHta8WdZ@Oad^54AQJx#t{w34^fxLkf6rpB zIRen!6=$9vZiXbb+yH3tn*J5M>N9}M9{R#eT`y%{<{Hz~KNAygM@OG~5m~am7*2v0 zMr)Thrf6;rksWO*U*3xIw7uZ?ma~qelFPKV%)J_aZwZilDFDakh+8snE#s+muEUO{ zJRjxj;9qiCh!k3mm#HmD+(^jjB5>&;UD&eH&etC{CT<516b`^I7R`gI^NWR*Rai(f zVbxS*7)URG?c3bSEX%jK<5(qr{4HG1D{l@BGVHRCY>f z9W%Qltj#*(+0L*ii~ZRcMRo?UXH~WVzP1mGrQm+8?Pa%fyV63p#_n{CqpwY(vnMd2 z2ivL@m~@QH+J|9XLhsk9gQL^`V0mQMT5y(P?LO8A0s7VrMwmG@#<7>DvRe{3ERrCO z%s4vN4bKVjT2wn6ZVAJ}$a@Nv5{j*iE*i(eO`0hsin2J8B?pZmES~y5Z79p`x3aTC z7vo!-%*7EE*OPM&4bBxiXh4&Qe1Z~dwseOGJ)vMzzU7j`UXEjtBR43VD)7rALp`xD zvM4qoqBSH7y2P|Na$$l}`C&u0tk#$J;T00YP^Lj&^Z^Hpu7EB_`(G^;no`p>WwUIK z8=a_Iu|xteXCYad2C-bGNH&@1*>@4+R&qe}PBaK&1tCwqGp^tvpo}R7?DHRCOc@e_ zoH2}NqI1M!(s(@6ro?;}2n~7k;$EH*2txy{9PAuJG>e=1x7RXGK0Ew-P)NlbS(ibY zmr!81NVy6gIhpE*GLX{;h%HWZVi=m%Yl%49<5KP$(S-Lvx>x&oE>g3bD@#kKCc6)d zBuV>teylLG)w2DO<#o@Y+Sson9emdXl{+kd{>u3^9DUp-&|t|XNID~ge1eRE_~(m< zN|*GEJu@aJ)Nb#rK>7Tm>O{rMupE|fjWn5any7$Re*w@RAzUBa_hQeOppn9C*Ffv~ zab!6a-W#pq$Sn(V-(`YdN#B50^oI%W!`bay@BZ!Wt4HNqBRCSnmkvXq_{zq^RQ(8` zqzcRKo*~8)lSof;ECD!xE!R2H+KGxP>5hoFeB*Rw- zo#=B5C^t8u>jIalNefIYHt`&37`$2<@N?RJhs54qy6T(*6>hl2Vb`MjZgA0K`!b1s zhkP#osCPRGAM|Bx4?*bS@&2guXVJ-%TP4Uj*Nvf_=`OM~<9v?MZVgy}@zn;D!C2+# zb(sXGDm`7EpCkIi)5cYHgW=5dIi!hks)`YwpTSGo5_cS=pY6TMUVkNK6pBGX|7o|K zb*|7g<*)xi?@5zSSX&%`j5?HQ0I;bfp@h3MAjcHC37t%lWz|T!rdxbOSSDBTb>TSa zrPF}9g_cq>sFCaqb(Cq zzyK-8S>pSO70}%DF8hnJGJ|toiH3D6D~%I;(t@DVh;}BbFHXzLCG|gvsC44tD5QfF zBq2m8JW`K4t9!z!e~rDOwk~8uh`mBhZ&gv^&$-)C!w^XG1QpWH(3`tH@y03pF>toA ztO~^X3TPwNLHGE~8q5(r=~PMRL=6#>;c2TK49X1BmxknI6!&Y@#fj)@0*M3Gpi4dg zCZZ7liOJxA{NP@vAq(B2w_-9QmgOnNKpe1D5|BwyTmixQ(hne^tE}K_*nO>+3mRt-9i))8I|os7|1dw_qS)%2>x!~jk};`aoJoz=~A4t4=g4}s!T zvcF8}@-+)Jm>_k>ViwBu9Y=^4CLw za04Vt+~*ln z8t5d0m}s|(h08rdN?bJ=k!>4+)!PT+u0ALo>{WFVkfM~95ISp-i@#!AQS7r0a`L}p zN)_HHw-_vUn2Y5MdX!&4EHLsi%>4Vp&J>&!WEw|YaIg&J8w(JfS3O@DV=@GRuvL2GuP{6<7 z1izpRF!_vlj`c!byrBrPF>Zc9H&>!Str z7dS6%j?F-z?+ULAIcX&|RD4iINX(<8R1=9}%r3#-lbbh>ls|=X>(%$vskzf;;JN}u z*gE!Z_wn{+&l2;jI)-rDjGw7zjF@F!C0aY*%QS-%00lw>_PQ@W#{h$d!n6MiT_9IS z>Y?b~k$~6ezkR4$$3yqL{uh2JJa8{ugt`!>ychqtq$_DLQ%ld9%Q+v-=Scq6)$Fe0 zLS$;iEF*40b;>Y{w1J2p?8c~cB?wF9*kfH_FfTSjyt1%Z71dO%tt2k=qe6G72Mh#+ zS(7OQpbK3bfpOp1iUwksw}mLxM&X6nc^0UpVoW;%X>6=9 zLB%y8%7c>OtJ&;>Tg0<@hLyZqMc`c7-IGO)b=lor>YRt3i$Na59>7~?UWHLJjvO4g zO+eD;7MmV8R3MXvb1X1CNw~{y6g5n6zQH0kZQMQ0P4k$U?EB5zi*sdCX{-}y9X4Ie zMxKr28$lo7$-6fBKh@O#E5l!=-#-7}9=5-}UylFB_v;?8Ap2vI2!|j7M@&^(c_@PF zNRU4+5ELF6fOL81WYVPWg#?Yil9H?lv9+OLVWEVsq@yRpgyZf9{VlEQZNNqr_jxT7 z<72EReanpc>R9-fq3V(`$-0(2U1P>)rmdJGWj%9Uk5}_60n(1QQ;WqLn=-81yczr0 zVp&kz7;vrAN&Lm}Hm#4tjvit<|IzW%!X19RZCQGK#jdgKiK5zN9WtL7H{Z56GWPjx zR>3T_1p=i=*7U}z9llhAooqGLs!82*@s?yDI(DpcSEcsEmB9UbsTlH>f$exZ@NMA4 zR*i)-_Uk=j)@I8#`Nv3NK}%yGs91u%7K|o|lt@LHMsy(!Db_@_eY+n`1N+A2M-rO&f!Av?YXFQ?U{jF%VQ6#^>TK zZAUW~qp5L;EVbIFnAvb(o62C{k0He_J^biyv`AtRVvhJ%(*aP39&$o6S=p$FKShb) zV=J;$wZAzw#OnxMIBsxnoj}+pq5YP9up`N42xOsE4C!Bi_=v1Bl)yz}Ojx{Z$;X(~ zydtsk7B1ciS+dfA$=_8dTA3nT`^5b5^yf09%|dcELwaDb*0bPGCw2JGjQri#1zjWe@hHc{jgEmYX=>&r3og!bd~H0 zNwh68o6Tp_yw^}!#SzaZRs)d;aCpE(>ZDWTN49i@AO=~r*Urm_vHC$JE|YPV?B|1o zD$Ug}`(|wgm--4{#&acL!qzk-;3PKNm5bY9l=~BENX!!F?Xfu~e!JJ!M&}X6vpq4@ zUX^xYTA_D9iUJqeum$oWtc>QAC-3O3VJXH9W3`*+m4<%}XO$NAW$4M;P0_5HW)Fb(=exER|I_O}j=FF0wBe}D9+eiW%}(v)1ap5qa(#S zudM=bngliPo{1QbrL;|u-4azb3*Gpb2>6~E>bAs3^+BIN-@*FXOKRDNVaFSFwD(>b zifD4?f8g=yAt%cX6T`EKkQ&eSy6urO)MYW$vNJ|ANV_(}+ADktHNXZMLWxy@L1W!= z?}Z8U{VMu$F{jwp^OM{Y`40jwe`{@2H&zsMaY7kF;fQ&zA4xwOjyu^8K<|`~W1KDc z8&G3V@Z@;ap#jDooSwWR=lElZi)|Ws?$RwN753J2v2D(mh}^39?eMW>&6WWphxD;7 z*j(`=?)?wtrnYe-Q8%aR8Jtx6_A)u#&fZaWLJIsT=Zz}W2o?Br|AvNq)xxd--Twrh z7V?x}JtZ{4d5IhHAPu}?jMf!uc8uU=VHkg`S}*~d@q~+lTa_x5!_EAGaZSU3k{?)@ zm|zN$iKAs=tW8lJfZbmQ|9d*jXsJH?ofCX?sS%)6TA-smp?u!Tud{+MbvI`utcaoO zS#_8H3PeK73|C$<|8A<=JRz;zt$G)x2~HcZ_m?j77e;tbAg4R)--j=gi`%FOqD6e;7xO;u+V=;2*6#X4YOSIal% zEY`+?>-htwhnx~iOAj(Sd$yO5NkR-N>Wg9*jS}Bse0A!+Tt;gtSGTJjCW_uzsvLMS zlMjWdzN2AuG`7~EFoww9mC?NAXpflone}@jc=c+i*RWWHmiwKReEC2iSIiVeG-{#~ zQK=!S-Rp~o9>>1S zTsdm+36u_1h&V#2;dG8TYBa8~!HYRN_yLaJSTUhwG^b=TuGsfs(T<7CjQvyFOlyS? z30*P7wn*>O78mma-oCp(jt8kZ64(?<3G2FUqGkL&`MqCWY`!?`&bNg|kJmF-5Z6cL zr^ny(qv1irhZkU z<4bnn?6u#nSN7Ge65%fh&L+hIVt-QfK-?wIwKd~Gmcwy#DI|voQwA^J1ffebUs&Y0 zbj)#4>lKuF0_BZbffm9pmmXZj2F{LcCm9YLmOl+ry1AYDvhCB38lKk{c9*A{wrk<@ z55p+g_wG+knN*t#9heXy{)~goV%z;etN(LF%D|svM*l?5H%St)0k=-A$3Lq_sQ4D} z7rNP~ekhH{&OQ5mLpwkfPOVk?w+uyG&kWu_v<3D3u2HUq92`t-GK4`eOw~_$bg)pJ z6XtY#vR?QfmGJ(&I4HzSqja-5Gvv_5FsPf;At`Ou&Wn{C*!bdZqey2(WxI0yA75-N zk@QTZdhA@>gMsLNlrLN-lL6lsbdWrf?M@&S^wE$Rk2vrllF{<85?%sYS*7R zH#=}K5~Pog8=HIveNI3V_wYjZt(h_t(UC3%4GAjpL(3*z+FxF|gq1uRg3})z`dbPj z`+w$;<;OfG@_9!0wpDzw!hcV{mLBfm$Gn7>30Du_9-LybU`Ni*iK;G%s-h8B=))=C zVbIIbRso8-W`?wQ@kCYFL{(HoRbE2NdALPQ_(e_VMNQa6C(DSgTceJmaHu6>g#z;A zoq&ZRnU#<|w@S%3WtFU#%}uv-#K{~Irts*MdOl*p6UH_pJy^acF{oMxcgD0Fi4tmO zSymigTVdJk<&@MUmkS=_$dFmN5l`Dur(`iKMf8$mdim<@Q+%i(iU~ofazVr)M6PU@ ztq31w&*JL&!vOAI3&<5ZXd`cb&)b)>H!W@YkJs8j+UDH~Y6bRIHee1V7F0#v4!vQP zK@6icl4~_CjZ9fgkoBAO<5TC(aA!+Qu^O=N!7VgbA~bv*wZtNk8ERQUTXYaG6p)h? z5hr^5O?c~U^&r|rL5Lh(35!k8`%uRe>i&+B#{_DGUol*}K!Z%CP*N5q=$J;R;}Es& zcBq{!4`S{>{WX<1hdyhceqK2=?A+w#$4heuH4}7fzOOny-8Dsnsaxv3xi)!01eP zWC#xm!8m{${p{2meeLSHVG}MTYK`hT<6A~e;ujilhl8E|25TX=>^A_tJkcURaN9}e zla+Tb=#zzttW%*<8X`$V!7AmLi2?aQIJDVuBQjvfNSlP>xfEH3ytWO01#2_F2{J_r z>DFr8+TWHAW0QDD2qMCTVkH1|#nF-Q?zLU@U^p+SS101j`R+2>>oAT~mRxM@695G? z@e}^iG+~PaQFO;wKHo%v7)T5sepF>nF%JvYssts0E8G};^5R?KCo@s>iDGVwl0@-v zK9e0U09Q*~-bl%NZqmIEK|{EcsEY6iyA1n?aTqu%S&wZcO%{+`PewbSSPu53b`H*(x{wD#UPz!*Gp(hslO1i}7zN6f7l>{&24R zv}U>m#yGIK=qH!xYb@5a%3057Abz!{^4}z_1(1g)MC`(=wc6mU9K=X?Rk&YB4tZpH zWXO?#0q8^rDH5MNi6gCFi6IgvlsAX%#@%wh(*m^&WW)9zVbZ#>P6+Dca1OjPSZ2cS z9+A7WXFdEan+MnLokPLJi-(j|cIpRRQJW3lB=y(j;~i7hf4m*?{s7m^3arQm#@+(w zp_R^GxY!StyX|%-kkPqNpU0AFB22zjJ>fUe=I^mcUMDNE4R_8t^fC;}{;S?TLd{*X z;Or-fbeR0|N!oMETc?JmTLMd$44U#q=6S-y<954?MYa9vw)w zDba`4q_KZ`C3e)@-zC0W9DBP-|K=F#hAj$cNja)Oyf0-%Rp$-4WO2Im?=Jh&XyGoj z41JA)&f5=Vb2%J<8H+_;9FF`=V{(Liz9FATr@0&+OiDsZ<>y=xQmXO>AAoB9u*=1>oASy`Fuli37uvj5wONH4L9m ze+Qky7S(cPipbj5P%|FKM=?@Z(;F2=tLs+q`>waTJ^SGNnA8T}*Z6!|!V*muK|JB| zbm)IvoYY78{}aEF<-g*eu&}eT{r`8zY4vqG?SK4}KK+IG2Oyi*ULG#vcysT&VK8I^ z1}Lz|G%V#L+KI!7fAV327E$<2ac=~j9df(Ny4>H)3-I}=}-EaM0IIqQ&hyJ>Bp;( zm_LJU?Mr6|NoZB+;6F`0pPz3+WzAvl&7J9rQ4hWzJ|3;^`S0Hk?O&&&A5%MY^l16% zX9?2S)epXbcO=?=%yxXwdxA4*e40#dx||23Fsg*O8>_H;v_vb3E#G8RNyhmjv*n`O zbCVRs?`07KvVcB=%25?`Q}_7Rz;J7fp|kR*p(m^}=fY$+qPeQN~0(r(ve0O#?e+ z1VTs7hT1B4H7UYT{r&=JD{~7{?J_Otm%PCbam^8+`Q!9*5!3GN-m%7>RIB)ATb|cs zkB`jXHEG^{hE0u5SzxKe<_EW~KVj-J;~-XHro8@GUbpF@ViCg)#>-*SDw8zT%spWs zJ2FfT8wk8?B*g@fIMv3?qOP;_4|1A^f?LpaK1VE#C0kQzx5Y2aRZPbc$R}bh=jgNfVfF08SQkW8+c@0u3B@ za(M+0OMP7OKZ;SK&o8XJXr-lw0p63zTz|g7262q7GC0WstE#VOU9G%Wyn>!9+vpxu zyn35mHBm+50gt}7P6`~EvS-=pc&OKAd!aL8X6>qua_V@U&g*Hp#@ra3IE=9D=KtiX z;x{@+%v%%hmCubXb>n|H2@tMzWT&ws(^oqsFSoAnusWU3b+<|aad;h?$JC2X-XG(z z(<`S-@I1>AX1W(UeN4%4*E^lS>(&8sLJH?PjfUZu&t4$sg1aPEPgo+IcVjFKDw$65 zrWf4QBy1Aa3XsbBM7-1upDWvNhf?(x^uqYwrAcX8QDjI&*;}eV$yMwy>L>`|&rt|VKYflZ_ldejHf6`ycO^k?KZ1=)sw^B0~+No8e zTth%H0UY4C$F3o+Xb|%KMNDS`lFa8Csv+@vvF}`xGVQr_ZEO;H1L=aNOZ2Y#dXR&sV?aF*ByR%9iW&JQ$40I3j6kV`K>{!f}oY z6vQ`!-Lmx3w2nlg&32ib!Pu{?=Vl5~N_Y202{xu=f;u%S8{?H8C8(P>CJS^*&4T*t z6bZVkS`0D%jXnKX29snGP2!uH$rD`Yn(F2Gjy9mLGVEg*xy@jH7#IbOaad6?Lf!=x zl9b~&kgGQT?imgMCt;b!&0`3zmYH5(0KNYRLR8C`npYl@fwFil;t-nvC%(#{OqO3AVxOz-csN1R#{#&;z+D zL!2D!u%IYCim!Z(mWrMafn_Z*-P>25J`N@0u9^p@1}XZUcRPrzOmD1Q?6HtY9UM`! zSEMG&x>ZyK+$3>jTs;FAiMO|=@I_G$=&>1K4h+jPmI0!PBCujOESk1jf{lrSE=&C}Lo)EmT51_U^6j$)+<__CuuSauP*g8Yt4iKhk=dGWA3Cz9irz z`1j}J?aGpjBP&E;#v&#EHc~}pxi6L>eMpUei!_Qj|7m}EGlVA_19)=&>BF4o{}E`6 zuRp<{V8rg2>mvdwa-j%i{tzss7s0~i0L8!(_4C`#^>pm)(el&F;is{$)0_6c-kF9Z zWZe@*BM77kg03li{b(@l_x>L-J_p3mLdND*VK=Ecec z-yjxRQP7#0ze_EbiuZ6gdx*uZdc>UKg=^UcF`hpKJnkX+OCn^ct^m29W-}4q5%DkZ zp!0e_i_e+OTi_(1G9dh+-7Bc@eEowp0#jaCs>~cNnS~uI;xTa+1WBYyb zd`G&N-5Ww$GW&LXss5;bV0V9fdGKurCE&%1ETNIr1mNck1Z-2QsRH9GNy2~do$yed z{T43VB1=G=dds6zvFke0^r(O(I(Glpg_quV3^;-o8XG}h%Ai^(Q&7M z<J*@WW=Qt`ame4KbXc;bT=yiQt8%@s5UyV!=QPMzujhiX}rq2~dU*jyU$<(61f7 z9vU0LL#kNY7iyJWDtPm0@6qF<`RVS+SHbR~6q5;w*s0*uXY?{maane*HdAGdn~HKO zT5qf=AjYS&08bTFi7ZGL9wd zJ?*Azv_swsjW>R*m4zA@l4ql%DH6^vH{?JIRn90rH}n^Aef7j_TJ`|0LQC7E58a*v zud>byOXa{L+NaC{lXO+VQTg+_Do+YUmcYI|$j85>6WbQ*-K=9FPQn-;y*}Xz(GH>g z;N<1z!>1Kr3VWU4H7v>bjhMl0!qNT=a!`eJXc{CTN&MjsbtqhD<*8WJ+mLGaSB%B# zlr~P_IXg%=Q>(S2(^t`vS0|lRFpqAEjklsxc65@qeu~YuuaIVnnBz|oizd`rRcmNJ zej5{#2>!R7Aj?gPS(ozSf#6hfo}K)QcN_Un%Qa=4y?mpcyjg;$H}+3WnDsJPp`(B! ze1@2=v|Q#)qr`~{D=D@lN~gG4IVbk{vdI0>^Kjw1r4<*}eeO`+SIMj(6n^+C=HZ7T z6j{{hOEaQIJ8)=ujHb_|3`@LRq)VYNCv@MV-oL5qErGk6`V4{uhJRr4Zf94-e*T8U zV*@15-wCUdeEcgjWZ}%kf3T(Gv|ix#w?{6jyitGs)+f^YWz0$0LyoNj(%m|f>OPj| zBuX`yrbR8I2ySMVI7Gs^@Z@Xpv1ri`U=DmQ7}jgW*~p%olx~S19wQzQ*sT|Dlm{Uu z@QzEJ^)J)a=5#MxwP$gcmjl1QJiu))N zo>ZzILPqwlLm%@6L}p%d{Kk!hB|f+OrBHD8$9UE@sd;&J)@R)|lrP*WC2I3nZd(TL zybEW+-3;FVX-SR^`~;`zVuB&F6%m%{rQm^)Ruw`RSp*}rX5HC_C!c*lcJ^pUn9w10 z9W}{%J)qA)CjIRtbbgg1RZTV+!N59kx%BZAy;3V+3DlSf%l_-PsGTJ+E@+R`jxGwQ ziz47H6!T6M@CF$LZ1v?!2J!hr3Ne_8%c}%;94bjo&!qIprCkKe26L%RN_wSKmi;<@CJMUGZ8Zn zzO(OeeifOBHeTQMi{NdkKHndUZRk#tXN<7T$b;8!_gbD@$IychuAHF!&(~D?SNb)B z&+?|_E|}d^n3!xF+g!Yr0y_1zex4t{L9U#Oqb}1C;XvPjPOH@@dgY1s!0z&bgKT%h9au=lB!n*Rh(BDQRXry>9Qtvn<-rxaC)z_s;!)=tKTZ2JCJd` znZ$mF4^Pj>?f^6l09yS=DuXda`xEs`m_D>~7z}I}OlToRyJIQEF2y7r1&lqYMRTIE zDR?*pXCXSL1(*c#J@I&orXVqbF`C&wWZz;ZP8@g^)b1OSM8cSHe-UNYzo$(0lCyqo znkHTU3RTmLFvVappRpN3+{WzLGe+gHA*S+HEAkp!1jvFWF$e2&Jvn?#k>McLpS4pS z)WUG2#M!*gbTF{=ZP%wAJKb?4NZMV+1rcLooZv$SID~~drKR<^g;0jGC@t5EQ2_b= zIyO;T9OqTKDWY{evxfdV^ii-utv`r>)ul8aWaTm{(xFP35fXG?3oKSH|FWw$MaArV z35kFLHdKKQFewLiHE_H0a4X#UEoYgyIg7pw${}>EWUpFPOgpWDXZPV5&aJ6(7BEiA zv{EF*n=0X2fg4a({(yZ^g5Ih5n^A``wS(rzi9qC;@9^YOzcXN6R| zfh{g#tbZH7eH_@;fz~PGS4fZuHBv5VVinwI^Ot;#`k+$*1gAV$nO|E=@ith zEqL+%Ih%w5d`|L-$101|ii(ybRP;=CJ=n~C$+#iqvDTZgdQHW>)>5Z#i5ZJzMPrV;H0=1{xIU{gg{XU*FHw-~7-7485}Q9rk18 z=}YaW?d@NU-aBlL;)hlOahSc;(h%|M)*{O}A`+fy>kZS30v%O$82NG?Bf^e$gS9b@ z;yYgES?4K*D^WZ15$KR?1*$t#;##Sl4=qmK#V?az%nxR;MnuHcBaQ|`F6F?Q;hJg#eEDO_FK2Tx(3swNz&VXPFVL9u2%WvWxq4Nf5mk6&I>P}^ zqFKhI;EbYZr&n5pHbOVIM2XA0!e*g2v^oQm2b-j7qcAA^`v>F(LAp6oGn_JHc7@<7 z7C};=HsR?ZI*`R@gtGt=vU^#Ue_vft+#oVfHE6=%aiC={0HZhbcL0V($KIMHk$h)M z%~Jf*<@a%Pl1yW2cB%6Ji%!Rw6Ui$G#z@w%Y}i@t7KFr46NtXumn)mX)FMd{(?xVn zSSHun7T_4_Fd%vGm+S@u0Z18Jg}|Vk8PHdvPjPGkcCDK{Wn5uFfkOb>7_S({71xUh z_CWJ@LSu?QZPu_Ux2Cled}$cTwPpJ^pai9GfgozGt{fLsa^F?lJNE(+36^Djs$pGM zcd;};;qtt@D?)Vp4GEN73Bw)~s(8{yIupO*iS~3DTr;_<#!?*9)45jbJxMz&YKH%o zux36Qo;xQh~7isk7Oukz&R-ul&&xQ}I z-1e7?N9dVNxmIvKdpdHyT<(v?(Qv+u6o})d3JVsKJ|_8zl&ws<-F?F4;zWLQ!$S9b!=K~m4v1=Mvo@I^6P%u zKAsG{B*J4@->bJ?36T$1&#YWGe9Lb2ezk1Yqy?|>F@24AweEa&?L=++b#5>Dzh1`K z`?h8L&evReo`u45!RSF!XTBb)a8v|ng1V#X5QFDSHmxT7P}i9&@#H_}Vi17uQ$UZs zdMFOAzRhxj|Ja^78o7>C9QNt%qSUn|i!I1*xN}Wxm}`nsL#6Bp-MhV5y@0~=+s7PU z;x8oLO@=XGv-6td&r0RUlJByRmx_QnHnv33<9;DOr5K`f6Z}@t?cZ&>1q}L&6At;F z8Vz?EzF@24X0$FurToh<^Q3FERzF=@GLGqK}iu3Q;uno?Ry%x$Bl`briwIg zydn`aQ8_iBoEjqp#tkS@sMs0y4tbQ2*ndhMaJe&G^k`BXk|5%_&Q7D)hu(c#W%S7# z`Xe#X9*nsKmm{1e%7NKEzvDpVP(CQeS*8%$b!%cXNM?$WAog{?nRD6mfRz<_^f}4o z)O-0mn~6X0Q{^7@N6z@CHfpSUmMYm8vA4`aCEh35>=!_+NQ0@qvUto26r*R_Vx4&a zGoPI(j}EMhuxbJp9wCy;a_v&g?u7X9ptYg(%1NrGs@?*NV>P@fW<63b`ghE+fA9`*SaD0qVt4H$FneuUtRQ|Q`?XkrY8lZpbG>3^m6gKL*hLof(Fl&7bP$lRwXD zS1(i7-wOxn>&>E}N+5fB;W2p4jPfmwR$-dCj;Yr)#tCxoVoL!Dj8JI73W``%RM23_ zf(^7(6Q~R3%NDWlfu%QOl@(s!;fn;^5b5KL{^J(hqRDuzwol1r8Wig;g#M@S!g#Hf ziovt!UM@^WjSGeEW%VXpWQK$ADRXQ4CHs2eV_>6nmSKv)J*&7!E|ct8PkWHqkpo^3S%0{lV9VlhLnSw@-DCtH7>}$CV z)LFzGU`eZ^o8C_{q#e7bU%xJ88i7cQ_acN_L)JKw#=Lw&_U=Z3Ux-#^4(3;bUUi2X z=s|NBDLKy20_ZF`I7<3?4J^|!+iO+lWK&&*o$Yvsy%IZMo$G>NhuMV!C zsYA?UVR_NPO#*=%Q3_{_JbGyJTUs9!{MtY!tg#>z_iG0a7_^fP5QCJRCXM#d0KJn_ z+#EnQMl*(wcr%n_j=}!aFAeK2S1G0xy_-L9?}d_~199m-r;6y*EiA-Vt~T%|uCs4b zpW0^_YMfuFa$*}|jCaqNn?!f;q9<&EgGS*K{vD6@fFK~rh8T;bcB7g_M(6D{^8`Z> z3>PylO|p)W*wFL>z_HnvD-`y%!ghBr5&qD6n&Fw32Cy{ zX3vg{-USU#52^Nf_RIrodQKEzVCpm!7~b&y0lz^b=A>oCD=}Sf@=F1%KZqcfnu#_& zH~HBei#yw(vBklxvj1P$kp916gA+%Yr(@=F`$+0NQ!zBZZZb5!sS!QTEn~R=N*;O; zcN{vx?fl;jT(i()PZ~#o>kvm)>Lgqf^YnBg;dFvwTV!7gBl8+{4nu?hK6=tBR?v~! z?eFo+aBmS~GeclnBp3B)BzDg^JlRE&1>*SH2$!gV7exjDEjdIW_ZPOvO=BI2Iw}79 z`}df(WX6`JRPTzs91WdEqFT;B>jXtr=9p|i7YbbZP{*~(%)r5eg8^TvJYsJgDrQlV6YZWZ7PTLrTs!RHjJipxEd8-?KsQRaUlAar!@3_PbXZ-qh$k@(R@l)aO>P(vcHgs>-xp&XZ zJmZN8qD!Uk9#n!qwp~^mNx)z`&w~NL)8knl@}W`MidvuxoXQNGC}eS}oI@60e>8be zoQZl*Wd-gU=+pkBA2>alId|*bG+(%RH~)&L+l`p1qXBR+dWk()3OV`ucJ^Lj6J~>r z=1Ak}E6JJ5;+f+1tyXJ1wy%6j_I&!^RNJ!09Y&mZ_8H-q0&!pcJ!Vuwm4pV7K!PKN z3JNG(eIS!oB1>=hdgW8yURq-2?&e07%xwEAxIS=EcVDSgE+40j!q&E72p$+RK3IEsGp_1QZcAp<kG#&9k%rgM;kzxt&w1j0=X8;-|7Y# zG@0}|BOme<47o(#B+V|Rfofc2h5(>zyTJWU00R@~3o&gLkGKc{xtMA6Fi@;JpC{Hh z+EUih9fi=~UHnkGg*#HdY-9pd{P<={>F0^v0l)gJF=pP!jc^>WWECayVB(7jrE{<^ujw!=zTcfvlVzawys;sc4T z3SF3vfI2!>?vFpseKq{sdi;O(*Q-1$XE)Z^X2PFGPG>I4Paa?Scl!s*Iy=AU`6B1) z?AkAmKoZqZ6)vHcNt)kh${u?8KKqBR9 z&+6ECEti%-T$5oR-%91@UFA}Os0bv$+jxh($r5xFR%^}zz4^!EaOrk zOf^ydc3KY=L9ctii61IfXwwTXuB>n?z)p4J6hoOK&PQ?FO@_cYRQa_R!eKusnG5Af z0e74yDZfJu=O>SC1U7V@1Hmp8Oi(&NAO#x6CLx@@v}aZl=+&sqC2n!xse2*(T9O;O zr48xg@su21&Kw8c*Qc8#G!{fa@zy0S5xS35B96h@2!4DeDM*>G1c~LEOnDcO!@>#f zCI|CNLT>ULwd3O9wUETAXuBl&J!N^l8r^pysg8^Fqq(F@de7DHEN=%qTDT^x8@=qWvkB#za9dfh#z{Uy=QdA%4=>srT^!pmAzlfP0#wD1_F;x&Q_RIf&T_b4YVUaU;_rTQnNh&6gN|_x zRcB&8%9*rJo{bbL;JJUU_gQ`KZJi~>74KQ-ve<+Kb!ciDMhGjd))HWaw9L|ag-EDM z_$rz7W8!wEIkV)v9HSb1v~qgB0|t9Eb8;qs2x(@{-BFDcj?X!TPPIj^GQI^-2ha!H zmXs{y=+Wow+RXoKo|gX<+g1?TfHrdkHQG4lFU3cjZ(cS}Wk)}|b$68^=;yv5`8gRT zs>)CqfcJo3Rhi;E2X=|Ss@1~qDoi=as0@$rvk7C!CBSYVxxGm}fHi6gs8~2LYn0<$ zDWIJw4ne{^rdGn_DPj$V9_a;F<@}vdLQ||C8tQ&hzHut4Q-@mFB4IjLk)$SRNgme0 z%V39Uv?#Uc!23G0X$LeKfX%nL9iTt#xAl@oywrL7|(`p}Ge4eT^@?3M5Pw1!H&47AP@sTH{mOQI0 zx(^&BQkX%Xcr{J^;njD65KXSL_gF%fXiQ{t@0-P#8qiNGww=m#P8aT*^1=D|;Q@8; z8pUL?Qa-zv_&p??*0lkYxP^5lZjU*!Jd36tw*E{?tY?iQ%6`02Oweup zJsm6J9ZOANhybt9HIzynCd-(yhK&YO<JW+qgv{}@jl$YhjO8`;R2T2G? z#MDy-F)C=O5eTPm@x^C!#*5>x)63_T zXRp7`k3-iNZmH}O32flzkokJK^>%iv>G@*6wS?NCt}i^qHn}RBL>-N2tE*9YXEAOU z^>IEy>#e~2RHZNJA^CmI3Ygzk4DT+B_tZgzg6eV#Mn@2Cpv#4m(#rX{F!;+B{_9Er<)}QZI;3H|k_j%AjtlaO9B@rX4OsbMV$9#` z6>|{ZW?lW7I(xB*G$`m?mY|o5^113qTW6|JOk*5Vf)bkL01weI%4(xgbmfs4wn&A> zF?Ha#@k>U=@sndwvj2z4aqgW?j14_X&u~6iUJ;4HwlQ4XQ;sqr|Fs@5y@>i$QGa|Qzn}v zV0E!33(@>djn!aRv7rR=3g9Bi$8(Z860DVc<-KCQ$^?bIP?!aHL=(jW=Z6^Z(OV3~X!m}B(d zJ+mNqB+VM@)6hk(7?HVTderwXPr9E8_cvo33q^TRtqA@6csTs<@om%9*M*k~NHP`B zNwdNSh8sN;h0r;xP&A(+)7FWi;wQRZoH)pHDo_M4Vf635sEd8!_iYlv@EU70wJfGp z>Iyq8os~~)Lf#;aZ2q{%i6_u5H`iu?V&BkjrBB&y3IIg?MfnW`ALn`6bkWYr1QtZo z#eSiJpsuGLi+=Z}A4%X#OI^0WC3s;h6p9HyB&5@c?GR=%PFth#-j)YA9Ab^&oAP;* z#X~5&!GFs_rUJeGft;~B{uE-7uxP?;GUp?b(NDoa7bjv7)3-8WyhBXo06$i;5*wY` z!k}?W4%k44O~=;uS;ZWlGD+J!Mykw)^E5%D3K=Y+)uXf?@WB5Ndh5pt0w96^gVBr{ z?&jxhINF3^kzC2DfUq5TqYM{~>L+JhC4m!SPI0TH_xLouy-v@!)oAJM&7eb$jF3Pk z8oYbkj%W^5RB%|UFrwmx7Mj##(dsts8SIfr8k6U1k}SIGgS1am(`Y@D*+*?A?pJ^l z_vB_ZVrCgPLrRalcVtP!EG8GFh_V!U9#lvUxJ?u(eR3FHshTtTwIlfgSB-$z6)vXh zj_3e+vC4A{g>c;;Jy1H%Bx&+o!ZnsNZqWGN#UzTszH;c%lq?J*BeS9s-#1yu-SA$v z&(OL=(#EwonaX1B+vupX(d zXhff@Nz(70`<#wcBpLtL_F4{`66$Scfj1W2ZMNKaiJxr3Jf`0qfWR2hyK%H}%h3{E zh?gHtaC)*uLXMnqgz8ZEsgHm#bH4nt`-i7U72Ha%GM9*Xp;2$5X$U>tX&}?Q{)dQ@ zp3w8_%f1X1ZZyiNzUbjqXhB7yMu|D%%?59>s|hR1h!I+3N}Z^FI$^x$@$*NEys8AH zx`T6922Dygl+yxm&Md6;6uxh{izJ^yHuX1?j<(5R-%hsd(+wrFENvsQD`weWHbA6u z+2m|Ez=mbDm^6 zD0)!6w8*YW!t$C?rkz%eMlF}lCj`(?=jxZQ@%irfd-pumE&@bB4l>Um%s002-RNua zYV3W60Z^&r;2YrXx*n{wHEb=Co*^|}$hnh=EN}TiH30HbYAE?n?WzBYOvcF0@W0wq zHR`tU8)Asxal`%@@owP%6bLKwI)vA)?kl9?%Poaf=-z?X>)j^qz9F9VDh!M{yqRhW365KqP{beMmy1!dLqCFVtmj9BD&{gFuO0o zFlYzL19X|>U~C}dwYadnW@-gsRceUEJPiXIU*lnzw#T$m;X6M%U;9fEG5)WGYhqiE z;hg^GHIG>8nB%#l48FJ{y#xO5bGtE9D!zeg4^j4$@_v8I5y7=^UIh>&(mN#isoilg zOo(JglFsggb_k;PS*`e5_SyvG!Dw@guwo<#k1j-6F~P&xnV`>M*TSlXZbQT{P>n01 zC~4)!W>)y%UTFihE2eK~{5D8pBN zZ!wkqbYQrmsv?bXLKq;s(&H>rH412N!*+xA9+8*c@tT5)&N{&;`SsBTiGJURoOHqq zL3S9UpyyynKbCuRb(-9AK;=A7<0z*pH(+?AaP@|;)~e0sk1BVcO}iEv4o0JKnsmE2 z(nMWv_Y=OK)B$UW}#bP z4K~5M(Z6@wbh;e6>liOnc)Lx}qdS#1(3RFeZ88P9b%G>1Ta0KuHXP;R&5$qvaJkme zD+b6v2JJS?Gfr{*QF9(vqO<3eX@<3T+-#hsxN33c8RY9MkeP&ZN2*W+?=D zbl&&<1fzKak0Hj4EQ60ED|#TK7vjYB_kL`B*&m0Iuid-xy#2q9CN&T_S1HF>a`2j; zxa+{8Z>3&62;%yCl$>Z?b|^Xt!`tSQ9u(;sVxY3vEBLNdY0kvZKo>6oZvv@nO(6wCY z5S3p2)~pbEXn5Pdb=f)10EvPAQXKNqqvD3_7)=dez}S#EST55Eh1=InwQLffvJ+gd z0_6+jAtAw^`&~>Kwt>E>$oX_0HweWTgVJZcxw|vv@%q;H6f~r7J!ATD1-$0{d1uONCVuAb)M*S~F|OU5%G{2408lPM@YIe`=? zrGSSmPfT(dTo)55tayyT(e|}louy({Dx-r-bNc)<=Ik&YGp#efVYSFEARV7>kw5ga z%RqWDVv5CVCxE~g&r0jv!KoBxl>=9sK}+ZRd4FfzpEBB5oUmYr)_1x=6~X?~gd|k1<*eKLam40qP;PURNj^D z$CYXpQr=CfyFDwMU8m^N%Q6;s;1O6jPgmghOJ2|elX=-CixM4Ot7giboK{aUg%BW>N(E4*igp@xisC^iyp#$2Gr9*f*+on`&Yu+F4X1i*n`fw--Madwc#0-^)l z!}*Q;cn6#th2d&94a1pIHj112m}N>hYD#$eHOA0vK2&7Kw(gg&Z&Q2ZHpUXgY_(Sn z&1)Y8d@sg^{CP#g&RQD=Ds`>zveO5Vwdc0;6XW2Im!*#@CB;d>@3>I8)@JO6SpH3v zGXx!mZ#dO9!lDQdbJ(ScLsQegDj0<;UQFqb?}r{HTxqX3jDyx!tlR@ zMmg%T&YNO@-_Bw`z~W8DOeCaHN=`~ft&W#6xY3!CIIdjQPNcT7PO00bc*lP*G@?FQ zP~=A;B}oE&f$;HH>zT>HKBD(_JnuJYLQ38AOcwZ`NVGW?UTmQzfDTwN5s0dB-8t@> z{Bu1JhxdW>%|jp8bA4bA!0JSO5t4aMwT5CJiVCS?r%j<@D-Dzwhj75zY5Gnf>@<=APG5;2Hu0@H{BzMVv`B+1@$z;CnFdA1Q4anPkbEo%#YCAB}Fz# z4p#Z?Kx5{Cli#j-BX5H^fI6z+)M3+{8IY%=hP2C~Aj^6<96LwE2M*XW2p|jrn($Ag z!{P{t;EDvF6cO#A^K2571RZP&O>pW3aAxpmQ~xw0gjUvp2$vx7UH$q(w>xSz=$BDx#M>Sxg#~{TTwa#?khm$yAbc3%4iP^PU1w{Mz@j#uhn!nH$=?!Z|NC90& z8Y6`ZJsy&=0LpyC&kQQ(^NvT41fbs>6h*S^rFVu2G2Lb6I~1-sszZOVE7mj9%jk%P zof|63M{pqbXIFlSBS`r|0&d>RXm<_s{QR$G|2iI!`eIn7n(E!KU`9gG65`@Rbat7k%_RIN0d9USNeOGCWbhm38v~4;vToA0V@PxLT zX&Fz!nq}Y#r)5LDn{33iqBlWw3Y5{F0iqIPwj?dpnl$SS+&r&GDq*AQ(f;&^k+s9y z!bf}uSi29tx#JM<&kicPIz<+%;rKe_m>y^qt?N!$BELZp%y{EkS<;9ajw~F-odWz5 z1*O+trlqH|&_W4iGL-46T_^frngb!rY@tT_ScL9_9*Xpc0oqf(fpi9;%~E*Mzg|IL z8t;n6bsb8PifRHq8$WhUn$mJRE~L#R@Qmh=>eihv}9%`qz^zR zyl)y96$rIJ<3Z_dU=5cq3zmeBJ({opTagj{R=t?AZc_aPg$IY`4U1|_=IGOHHTCHp z@=DOIfg?&FLV6Rr`LeS(FGMrfimI%hJIPkN!S5 z=IC|8y#y~)Fq*WB?viDINjAw}auiy=ytUnytF60l^2LR*Rh!%Y{f0VT(9^V7f9*Dv zSGxC`Q6)EJYrf9OSw6McHHJ9-n};guqF4)d2K%;n7sgkK-<3-=3eR+qP}nwr#tr%j&Xi+qP}nwr%tFiMS8vMC^whv3@~D&dil# zjxj%0^;(5QgL#Ssjy?TV(n?#yI$w#PfJdlrYdJTr5;nD%K;)5T04}+muHjDo(l~Q3 z=|x(4!GWSYtP?p1j#pF|^}Q%Qwv3xjr4~dU~dd(!Y)eqJ~nffn(Y3gKOLGIPeyyme+|h ze4O?CxZ*=BQobc3%sOe?!%yW0SHp+*Ox^LU4<#2ipNF=1CifqQSMpg<+=pvVGh|Zm zHhf-P8D`{Svt^_HgOZXy9ayJ9^}nJv(13=)wyp(nSWC|d7iBI^hjTTnWZvl?nXm5~xD=c*|CR44ex&$=J0_%9X zkx2P^zXBlJu*c&wTwUEvUEORhLHB)QYJtVfk9EW(`hnprz}#W(rGguD9rhV!%(+8B=FX4J6GXPK6%_W3)D`T&9W%5Mbwz&gaIDO&UM5#byzwBBT6_jYIB|L)9VNm}&#`V5cvpmfly# zPmi+*VTl~7r41%HpzDSsZ7j)s$R)P}DyAs7)kkjvKVscXjPxm?`I3fliAPFP!q}C5 z1?sMvqY;W;qpdV!LMn2J=WfQ;(fvROS>ybTDOO#sd8=8wK~S0atC>?41*f8yY-Ftk z2ECuQj}Xf`d-i1y94bl5f?>N~ik0Z%WF9Y7xxKf|e`OMVY|FS_evNht#{rBcQ&C`7 z1;V|Ny^!7Dzx^SzC9T4tf^Ejt&_8~a7)^Pnw|p|UpX$=NX$nb6MgD4$$&vk7hzWkL ze44bcT`4~)l@Hd<&;6)jPS?MURZxO^A!0Mys>&@yj{DJ90qB(O`3r&pAfy-#`JeXo zf0cx0V){Q|Dw-Hs=^+Lb(bsoWxAcHSNv3c66jc=j^uWYmhOi+v+WN$BvhuxN_UZ?B z9C!4ccJ>H+8e)d{>%n;AD3i*eO-Dsw#9vBrRUtMTePK?a%MJy6krbH+rqK#=@{Jw_ z&HZsbvL;<~qai|+f)c*>nOw#76TiS4vc$Wpzn78f&uv)4xekMvsS4Xh$!kZ` zH&(3~MP(X|Y#fSPn+|ZpHyK!46K-S}_F37B``|u(=5;eU>xy^HDCMwbKD9UlNW0Id&Z@=x$O>R6=sRFV@5r9Abx;%Zr zd)r22SRdkSx#oX%d{L&K?Ox0XeF>)ztjHc~3+hW0d!lPJx2AraUS9O88>V+xTXQp8 zCy%XKt@&(bi@7^Hd$o6LGb%f6A74%me4cGLz1lQ<^tRdg^>u1)XMQ{1Hm8QvK!GbG zC)d*-Pb2l$3uW+53s5UGX~rw9PTtUas_jf66vEW35^L^KpPyT=+u_VBPvkFrtF1Cm zW_NpR)O=a=`Mh_lQ_(Zc{@OjQTeWNbwrmK^-PpJ|ymzl}ADYqb9*+)RUBavsK1Zw7 z!_Kp?GhcgMIEeH}z42CucVNv|7Z_SuI`jBnReeEbPkb*u^=OhE!=v-Eh zKrVBw)BNFhc>6;Q(zHWF<*!o?P)cX*Jf9XKL`;K{Yld&mzyJ0*(E(rWR-ri1Dpa}C zbzfZ|u%H>V;m%lab?R&)RzE3%A*<{%QcKYaxi z6C(FjlsFSN#-=1ZeS7Id#!nA7K+IZ0l`iyJ!i?GY-oZ>cJo%*? zr=W!r?xqGSlwO9Oo8deik?+LaYB5TBfSdP;eD|ikx4YTA{mw>-o9_7Ks$0-(3mSJAF_Ncp)N~uDoKN)A!a;dwmf zyy(hAb7bY9S=pr51{%V|_^JXuwXtv7P0wqQ*Ck%Go;V_sw2({-K(zm@y-G{fk3m?A z9WjrJM-WwK>qQWOei zey;27C2QdEKpWm?ws{-#a_E4!IwUI+!+{cQkJFSWV9%S{63`G-(+srP2v|EtDL!UR zO0e90pvtFpZ@E$WiXHkr638l&T1qpRKZU~FZR{Mh7Ibr#n46 zykxy5rSkbt<@Ni4I-Aj0m`$^T2$;ml@bQ&RXGhzYoc>qaSU+W_E*#j-5-bTRkL2MU z*fy9XE=hKSrm>V?m;fVbXINIEpX3DwOKH&_&d_L5=_e>{_i;lU7AF%1yhK0DA&wa0 z;$)x19PF6cyDAFR#hrZY(8T#YNwlMYz@5aYq8Xbt4R1JW8(@OJ=R`@C6qXx|^9HlC z@la7Q0F#j*qXGJ4?e0qF^Y_OM8*qq;;S#AF!bMnZZCq!kmFwl@W~8iO?O@CUq7ANT zKOM?+iTEO*>?M>do%Ppby|RQv@ph$=IBUoUe7(VGgxO+YcqRrmv}^tXkDe=EYBqGD zX{x}GQ}-Y{TYuX+i`#3i)Us#`ARW_N9r*2}t2c0!ulBXole^e*Iu8VIbPe?$wPo17 z1fek<^9eeiit52W9_L3>AJ7}4tbNwG{5rvx*yRG9CW{A)d);^(I5zShD$5buHN&EL zXUjgjJqLAqq4^r z#mbzZV?2ZciHq%cvZpWo=Ueb4PW%}&;($n;0-+^N%o#Jw{pUI;+(wRMr=p2Yh=EcN zS&@0z*`eusoanT(s1t@b`#>_KRKt?<8b$lcWY#J^$}?Hvn$#M0j+Pv)-cZx5Lc+GZi)51qa57p_iY%w?87F1- z!$X_ZGRKMgccSzYqRVc^RqQOcj`0e6yQ;(2inAg-eE$ahER>G19?5Wo z{7wyinRh(M;gA5Ygy#JAk)r;_A5}caIjZhx#(PBCs(zVEy(e%S^}2KY0FUyJ3W?~z zBM~^LiTb%GS3tx`O`-zHDBZY*XyBKwC$)**7f=_S1|+A;uC}fQT7u9JI$LCw5)aX0Wg*#? z?w|gG%?gc%I8YnK(4(aj%V>uv z_Nv)$fytYOhns3sXaX~hMM?Q&Lu)R_HRi`I?WI~=8Ndlz{Li(eqMZF=mE(rn=#@mJ z=~DZ0%6>xh2AWc5V#dfM%-b6lTuN&$?gW@StWGok`MolfQOBjo&?|hE;>BBsTLe|D zs_hDE{W4Zh+Zs(p&BVb3h&9I*UrJC{c*lIF~sm!!%|FuIf1db zJh)_f9I$+jIb=#jofjq%6rU*y7>rP`|C)-x;y{wEp%YaI6ktKbwJY%Bfns|%Gr;9m zS}r>3PA&a7QR^HbpnK5JX&KOGz09wBbS+GXMq~BV%I8N-0Yp;T96?~W`x=Kox#F^Y zNZ1?^+LQ&(eI(VD=cP2Ad@=6G|Q*FaxVy#g=b<+PCR2oN;DmghbY=(K&34-nSJ+x<*@W39q-t){Biq$2kz2 zZ0#EfF4|MImWca1j4nmY^r@KmCp6XLV}?*l{E>|QYW9@Qd&+ct2wD;9G!PTv$46up zGAU>lna(8_If{>q(vrp#vgEo7Rlxc^vy>E;m%$omHg6(>%_oG~I0#y!@=+Hfzc6K_Cm@de_Z0wTMDACYvi1h<9Bp6Q#Y8o4N8clbpLB5KoWU@}Pm~ zZJbC|R<8H0r!ka+}{b71EV zIK`&KT!8L2b+r;k6bOi|roJtGT;dI59urdCkiO<6C@4JsMS;W~plI+`Y~Wg8fHx9k z8>y6mig@3c1lV7?qD-x8XpH%CLJp%2_f;HrspLbLvtc5coJfEQBId@mvD}UxGA`qm z7&}bHiboG;n(eYO(^TDPq@~_4D1guf?R51-!YxU>bc31G)ghUlD4hpMdCjjo%5tqj zGAK=mD5M<767i2A$zZhlAAmBt1&s9P4QHRTE zNv^IX3c$;AUdMwC^V_%Nf^tPAGQt?ezyc-^0MitKV{RD0aL!2mq^XHa+!Z}J`^r}L z5L;vhyGzD_*4u}l|MOx0+JlCS8rr{|F2d5=ss5+lOJjEj1vFWFFl^#kE$i$o1+*cu zQYNa+w==J#TdRiyzW#)>@dS?5+SOp?N3SBzUNo)6xW0jb{qqy0DK@HOE%6Z~wd9y$jpWW_{2(?VI8F<3}iXHtkzUr~VN z^elv2iQYJ{R7G-htad-VYYrES3>jU5FQbZvP&}b&4`&bR^av03Q3xNAK%bKM6C>9$ zdZ3e*&s&1X#naJ;X|C)YkBBrov}dPMb3tPeQLosTMD>l0H|+O#-ucx;%3032Qd-jj zTC;BJx|=2pR<~R@et`TqwJ>T%Qb#q7(fn zl-0fp(l#G5q`7hlM5(6;vd%F9e+9(j;!$`u@M}n*7E(u55*dcOGoLJ|Iz!>q`vQ_Q zPwR>22nz9y23R(R1oMukwq?eic7Y0&xFybiwem`RPdg5fPQFW;wvz07u`%zwKUdk`$FC2_3;&KX0IwQf0z@O+DrR0E|I z?(erNI~K;2y@c+ViE51sQUR+4K&iZ2?vE>5JkYP=!Qjz)9vXF5-2`R*LkUv9-O zb;^H4ZI){E#>w&U*wRLRvJEyZt+&T3uZNSP_g?Jlp;>fw{GrETBM&EHAI)4UU&rot zJ)PR2&C6zYAI@ExKdT3+?+1g9#Cp@S^fmIn+20c670w4LY)X;qK=R8*P`?A~Gkua+ zHTzhLT3rK~iTHFo+comCJem==Pq)#wL2o}VhgR>UZ*M=7)5AtRNSV)H7bg!!jhZ#0 zAI?5MqFXk5Xt3FT*)qK{I5s6rgZqW6P*ahOH%8DSHR=IlUS;p8^aCyvCBnRxJ#urX#(ZS{9+S_p3YfS^g}? zmgc_~hWzz~-ucY=ZacjaZh&s;A4OPq98a!zFe;!>eZv4Uz*@9j@spXNoR`4NMhPX556)TiO0noD;92 zEbPGt@NOqCi{|EfqP6cZiT>_2fo1Ur@Z-o`1cU7epE2AS$}!AL$75g{pjhL8*){YC z%))W{ifbBAjg(~}*7Ua65RfH&wO1{K3(sDmT{VNrX=k=&QRuK{9ZbbdVLRK5{p{g? z+6L9_g~I{&ymnr;fveEK(jK9BQmFg9?%6acI*|>F=c4)%%b;qxaCmb=cm7t#1^1np z2UK?3|SI?*Z@yzaqx_n}|eXg)uK;#{%^BZXcUipTfhk~IkuzP zd3irmN088>-V-rT@hTqnH$%6o6c z=mm`+btQZqXklrCz$_PVcMZdK2;~=?`cEQ1$Lt_@Y9cZv+vWT6^zpD+qfYM3j{vzJ z9*3fAB)31y-lLaegWk++?;g52v!^T;{rf{w%+^DrDJ!LMVL}``7;PJ*pJa-lnB&KA zLR@~)Sw#`WF+ns33jOAlNDqV=J>d;Kv;!0{~Ut z?R1cGU{*fVu|IxKHLZY+CBc)QRZ`D~SPE8(CP1v3*N!}=uo#^YlcE6Ch?HsOf|}#E z&v^bGAtG20+ksc;pSt0IRT(q@JeVQcVbU6m8^gF3hrM3q@WgL4q*u z856ei82!qD+99&!brUL2Tq*Kv-3t);Ksmok=%n1&B4jWisBorWx%C3*>^>q=l4we= z3R=kiQuJkB68vMf9>_7+eM84yuFvf1VK@YD-}EnB}S)nRSzoa z6k}kfe~i8PqWVFfYS&AQ17-P1=S%SNbobDd@YrRhhPQZlMFxnlZO}0vz(j;I5}DSU zNQ`djp*3z08TTwdMPWcAL zAE$hPlI9Bn%VE*95EVZVAY<4UXJSP`bLe~#G&E(E#v=J$*37#x8qJlKy$U(RO_%YK~FCuUeQa{=WS|0d}GF8zq zQ|Wm_Fa}UyPN_^Q=@mnL^QM;u-OwS|=)`fh&BPwys*NXzqED}8q}9yVVw2V*paWAy z!K93D2XDaS?}OgSOg%M&wWdWKLm*gv1w#h3WYb|ZcS&duF)BYc)sWJN*YA*+FPz|H zvDhY%Bn1d6RR(`-zxPx?Cb-jXftr0uX#xf;B;3+DB$Bt>wG#y6 zAvIrK)8j6098mV46<-=-OJgh%ffqt&VwCEt%nP5|E-egbR>ri!6 z8eWAL3D8Yss2p`G0%VlH0KOyK13&0m%Seth8ciI6OWH=h>xq<_9j{_emo_GeW@(`a zgX;w11%XYK4y`S1sZ~!-{$C~v-GuQ^WScMgDv@Xlk>h#A)WbK!cCv=N zopVJjf7@a2>oVj*8wrXdNVDOFNO1SLj{(SCYW9)vbhxih=VGa%uyql{CRDJ50}1kZ z0j&QZ^@9t5ho(V`;5=$P`wTRfL6Fi0h*saO#CA->kz9*I`;@;s8l(SfFw9LSK=Wky zBJ*R-_OdK<_jqf<)`YQU!1+O|sAr|fk{sE`gFi}$CoE+L}ki~(JW33 z{8}o_S?&k~I+vB(RKXCJ6+vApYJ@NcM*d$~VOt$zX z95rn+C-%a_kAIe_xm2P)n?!+tRS|Vw@WH{451tm9m0gMOQIe|kCiyq)qFMUdcV8V5 z3!s!v4&-`AUOC!Qx|{Fo!;QV+lTBaV=N1t_{nYy72L@E%xt+-ul0ZA|-MKC;aEBd8 zM=nF&)i>ztzk5Bb9s+))(Zlx8#{qt==FcA0vQ}0f#W3y0#DC(fIyt$wt}^0Ve?B+Q zn)`4Y=;@p@t!XDVVG0_^)%NcQW};oma)EmxP^WHs7%DfZ*uTLZJHDRis z4yz0~me{P17Uw$F)s^n$9NuJKB0}`g^?cCOS7x4QegHz%4~Ila%oxEN3ER&@=)e$m zj}9%*uBt21=c}Q3!^Q~->lPW~K4L|2FJNwdjEAvP&%Qh9Hz=%)y^>}UUfL{+^<~1Q z{gGH-Fs)`BMj0KN&Cb8;b3;}uz2nKl+M>blm^Qbi!(oE_I~L%9gL5s=!cOB(PFB4! zpcvCJNH|RvaN>V>gZO%qzT5~I|6)&*lC4n8^;6Wwy)+^_mSpiGvUES%T892;u>@61g@)DK_&iQ1Wweyt?&m zC33yJ+KMbT(rmb_$h8N$bV5$NUy#9RNdQLuF;l`d)MA>#_L*0c>}{W5hhN|?<6NX@ zh>XAH^S5ZQkiWV~h%#s>R&mx$T_$S^_Zmt3b!}q0Bl2U|YI0+9F(h0C4B~zd`o)hO z)5#8r==dV-r-^3ik7(n@Adrb-g7pn2bVxhlo-d|#;}^lmH0tuvmd!voLIGKM%ku*@ zt#PT5Av9OtG^&l2-CBGlQf3!v5|Q;9E&Y+gN2Y_*at`8YoNcqXE+D*PDmENv^5Xo% zw{KPYh9Qy)bylb(4zqtil)YlWFswc-*f4vh9UGi~gSPF`aZ8LAE(Ns28y8|nR*Hnz zwBp^}a$9@BcO|F@Pkhr_00hR0;DaJIL?ZeaB8UiX=tF$N8Cbn}izcbx!d^xc=5kHb zHtRBjBV@1idn+K-!y>C^9LyIodn0NX4$3sfsS0$D(86-0R3h?-H)1pbNaQbQ4I zMxw-WjNyn49Yy3Qoqh`Eym2jxGUXYE0%D9XaCQfIH~q|MZ86kv~=i?35`nY!!kd1z%+rYKr_NCBeWecRK}}4zwo*2 zvwtmkF#$2C+$ z5leM0MaGdiE%91qE<7a?yju%7J2C}kqr=pph-CH}8%FG&?3-A72jz$%O~r1MN{v1$ zIMo>5-V(VqB5EZm{Ucp!VQD^Xrg=yRz>Bw@Iqb2YvDHwd$9d#MSm6BEH)R^z-+vh| z0qof222ngf0DL7A<0W9#LV&iUZKQCSWoSz81k*AKg(aYz5bhy)s;bkbQJDq9xFIS8 zZV0rKnw0u);XnJl)bKfkW4OynnqbQlgtcCiLg)~HDreTkBs5nQJ9-+_nzG`<$wklv z1pb-bp=~(YL<9o>pnuHv5uJZF5V_OwS?+68OwCn0jV-0%i-vo9c6+{WLiKwVtMXur z2k8?IPVV==w{Lvn5yMj4jJdrtjQ5I45fFER6FwfF5{jq@&y|Do0!tmd$WbltX1^#bv$Q+caW)k zf8o+yJeTIUK+4~m`v2;S!On?2726h$Y!8N#H&4irbmZH2xdL06Hl-oLBZIO*W)KYL z@*4yrYq4{zlULJzL@qF7nm_O2+ol{MP%<=J@_T-BLf$bVn=cZ9H^xAMF~^_kNm%Bu zRIz|bXa-8=ng~+T@icl$=*(9c0L#OEsop9odLWL;FBILGJlRE)lR!i8kLk`jrUvhI zx@CSJp6olg`SJ#m^=&+27*kLxt-ktvJANLw7$Y4QC$Fom`S8$U!a_Oik+gb7TyV~7 zI0vQ?%qjEZtYB@hqEPb=*g1OSEl8SBp&7z_tJp?Hgw9Ip+&3m8##NA+El65pgNx&C zxYo&7ZIfKY7Uz;sp)Cr^GcMCPr!+@A2izFVPvwXgJEN*&fl77oF){SGpm4+GUsCAx z{0lp)2(ZLF(Vx&I)+ zONhC_33dwVx#!e&2u{|>SDLPxYQ}>BVI!3$TAH>_tAS0f^{365F8~2nk>n4&eM+YkLd|2 z0ss89!d#xSW*WGFrPmd&C(ZW!jq3%z3zz}k=^kwg_PDKl_GT?FP67=5R*jlMaov}qS>yN>>AXBDU0!sRFUmn4Ynuw$7q zGEm=PBI8Bz9w)TlRA_pkGW_tv5o17;A#Jtq?h~Nu6j1&@$f9ijmED|)ndSe;qFw5n zcG%;H-nq4B;+PPl-n~3l#$@Z$V0Sf;Ch-Is3@D^dvJp&@q?f(3=g{V!c1tj(;30eE zhHkRC7ZqHkJ=!QqZ@y7pR69TKz@+e_2iF1L7rQh!&?!E}|B#5X=Pa665xzTHKT3|G zK3hCKM6H#}AMPZ#@PE>J)^lGfzf-nu;g4Kfd8%{akG>tBExn2O>2KLS*?+aZ6eQFg zsrOwR)VC~N&F;0|h$8i~<`WznG~R0+nFjw!9wOs7`zeOzJbk%r@Ux-p13OjY`0u^a zx=0T<@0OiE+5b>>j$2hJ+KyZ___3YS=S?171f$4u$|1M2=)n?zT~l>w6yQ^azE??#zTX}Ff)88{QKx_Wvxv4QsyQb)EC{%Ap!(a%z9EbyqWV4(la-BT02Rv zW;ipux3V1Ud$)K$?vL)mV{ITLs>IeC_ij3sC{@k}T?Bvq*EG87)+2WI&K5MpSpEf$ zZ3Yo-ISP<-O6cRgD62ia$3sfbJQ!GnfRNBB*_!#yq-0H91*dgGj8h{!?|z%Gt%Q(5 zk~cmdT6x#i;Yq+kr#kGqFzIO-;0%o94!9l9GCUnQ;B7B6a3WaVl?uIdMTXjk$33{T ze58TmL@H!`_G{)S56oUFo}k^5y~@XXz-}W-pAO%3HFINAGh*^2FiN<}|H9I>9qg!{ zany!BXM_mD*7uA(zrRk%U5AKk>z=WRA=d~3RVm9YcFI18l7D_|H~>~tsWX|*LSLX$ zGEr$WVqoo7*N@$g(=5Y9y_!ff@16gSD)dAKG8S_&t9%3bD^eh3>y732vx?_<_lg*_pCwa)1Sbs_PnPj&6T)PSWw>kgcS(6&28JNH$AT5&A80nFAsL0BZ|%> zpIdNQdtyn@$I*2`uWYj&pj@-TF;XC9#T1rbq5N%=Gfj>U5J_pMyZl5Ui4eDrCX=ZsFpCyJ2gx-G!a-%aKS;ra7?Zxj@Ee)lDKI2?!IL!S2}$-m^u7s z*lYd_f6R?O)2nySgGh)^+H<5wmaV5)u$WD)5f;~`#K>tVv#m`}sgw_aMQ)diLzyKG zY;mqvC^WJ6ck*iR>(h7YJKf>eWJF33&24zv@cZ4vn+MjwL4x%tW5NaTsI$}w%RWrv z852(aMVO;xP=H;{qE;*uz_@)o;rsmXV{aCEwgT+Id2LUR?N<|@TLZ)@-N~1pgHAoQ zjzBvZ3TnuB-apT5V^0>0v}#Ye4g&6Us)QPmS3QAH^BVt_JybVf{z?vGATFAwg*?AU z&eyT%-#WLo@HcNHVsGbU7yQL=p;ZuEE^gj9JR#y@qZ(UK0CRmsdjAUVC`whwMQEhcB3!Xsw(5Bl1b;HAP6U zPL$ZPO6svm_GA!es?jW?ZMT}s=&K-hcS3!;knGY?EmIV>ER%Swle}Yj4Mvp8jwFko z#2I2ZHDk@brlI2qO&meQTq%+CO2l6yK_3z%)jT>UunsZ1WrL1bB>PDMPpdveB>3d~ za`b*P#2%A`I1Bv+g7Rk&36Wmn^)CyWW$8B%4;n-)|6ViH$A_B?J7RIJM~QK3jogG2 z>_vzi%RK8SX5fhphApU#0lmUGjrz=eO3uGyrr?W+&7?sdWG5E`O4_F(hf;JN$s!h% znA2%6aUmG{N_~BF+oeCbH6x*9vQkl4d}qQJab&be7DCqJd~^j$uS%F}&#)Pm6jehs zg;#ARd_Dgzn^!;>1DPz?!Ag1!i%{BWm)#0dooh<#7AxYb ztPuEP#w_wbW$WD5(GLZpfz<~k@{CotNHw>zXjpumpdo8jGh0DbmjhRsjb1t!t_3GK zA#foVwdx3^t-6lL!1=p2e_$Hq%6LCPacPknKa5}sU6F7odUOF=NEYumZ1e{Rt5NNw}{uN|*E$ zPR*2rDX0of(s5*2{WLOt%aO_c-A8>{kzKJTo!LNr*P+~vIb|4JaO2gG99uRSldohi zEMvG=Tfp9DAoPWGM4F*dp`JnIPfWdikDKkJqF_#iCo;041ab^=3xLW+e8ZpmasE1W(36_-(-1OW;5p-D>z?s->ck^WV2Y#o5^FjQ z-Gxk0k^LoD?spKyD?RB^?pc$ARS6%g&K~J8^!)Uq@{?i$?wVDRTFiGDR@`)z*@eN) zz?LWYXbN6sOia+R zt<#)Dko_PQ2?mi?iNv4SiDl6b8=(h}j^8Af-|>53AsT5B3pRa+oVL z3ZGj%(f?q-mjzndd>mA-yoYQ*c(8?)&UhgV&)6_PW(gm%Xz$N)+AEhj1vJiRPBd^- zKb6g35%po-MPIs@vVtpzMs#22Vi3E0nD-hou>8C+k<2#6#tH-VGGuo!2*cFI0RuF1 z#8tXX8yro6l#9f?1JV~zq4klAFcG3U_u<8|4z+`4{D~uyU4#n3;%Nvh=t0s7MM5Z8 zAf^kE?EUEuq61c-j;ovFA;)R7gJ7QqL}fWY^cGF+I|GOOGBSlX;yk{ia#F9jLBe=X z5kMZiy;~+t(jkWDHt#pk>Z39W-RX>O2iB(zM0b7VfZ3LuTiR$)K53PF{ zGz$d3wY@8S)&oT3^vL^c)e#HDXdJRj+NnvRuZV82EI1;IV7f(<-0g{OPeSzH5&Rlv+bn{&D_&6Sm&%#Ww*osoM#TkEU;@*cZL!X-eNx{=) z&}gcg+n+7kg;^>yhKy~249jQZ;aI4%%Mw924%^&Zk<_;Aj+U3|cKW8?fp#8G&2s5N zZ(o8qW3pLcury5QGVH}+)|m({u_p?Y9MV03>(moCObDfclfuvFAFeNKmC1V|-Sh)2 zc_W&sWS!niuCZ){J|nQl0nv8nZS*Ih&fASz0&uJ(glAES?V3yt2-`o?D>7&W{e{0R zBD(BALg+Astdz)iGenUpQ)S9Xm+p%<)wp)t+vSwbAq!M%SN{TJh`1yTct$$(grt8D zv~XO<|4NG?ypK8t!%w5UyURP4oUGYqZ8o8AezE;Tx%~G0wdeoqbNd+xzsph+)<(HF zfjs5d-E`sIYU;W_k9%xZhxfsF&QAT;IaHC08nudgI z!Q4EFwL`+G&{YQ1PmDA9PN#xUlg!&a?H+ZvPMBWXM5eLdj4Czymh0uL*=^9#x-4zY z@r?7anA|}HR6i_F9<@pkK)at0blHgM?i+9w-!-`z{}-A802avi@ju;f|5ZSYk%RI7 zND5W|B!x{e1aI^_-!<`uI+gg2^|M4|>gr7tSt8W#G*Q8LYD;TJ^=;DY3jJN?MiNqe zq|bbaIFX|potY_4X0OJJXprylA=}%_KdHp^F5R{2^SMtZ&F7-ioAGIKB0TtdQ$rP3 z+aRgEaEImYwNKS;Q%vVsZw7-FXlWBBB?#EY!=~wK4J`-48QT+5UQ$k`1N>^cuVmt8VvM?Jjwvf@Sw!pW8A@uI)yGxrAF0xKu&`JmPIz3_BG z7C8`BxzshaC=1s&!_PAfC`2$~1D54D>su>Sk~iNQ~$cKI0?CTW5hGPsiuGvq$fDU){^3MF6)3}qy@L9jDZ7Um6^#u&ae(33aNAMn$3 z8piRvC1d_!y6ACH*5%Av6G4RsTZb7%l+|pVjtUfPz5Gp>{5XHz#Rnyt~orf&!P% zoYRqR@1SRDnS0?eNN3kTXGsUBEJ^MncKuB+j~*EGmO@lrAlqj*8BT9pA;o-3?+;6^ z(JORD75{P^PeLniJFrjS1M(aV>3HB5D0Og0=JBVT-Cu2{LllDs227g-3cY3|IL=f! z0dOvD;kF!fM}zTlSGP=tgQ{R%O|_QPY;lnTs_0kpTq_y8XOJ2o3}zEZ93Be$f{xPo z`Y?2G>DFRkEl{@ddIzPzXZ>j@ zcazz>VX{+DP>Lv^(^4snGMhV#`hCUTPw#HTu(f zS~XV;kowW@nYpRG!H1E*g5G*TYHe6qf@&5g1$ROWIMI8DdN{0Qif3Kc%~HuoN_QDi zVkTBBSbrd2T-5j!< zI8(s)1bOlV5<08)5WK0hDZO*AD04n)PXh=>APnQuYlSq>) zQ52iWN_=c6>RfK%=2e%kG}uRtWg&8oFqKloAqUT!u6QzNstvw>!y)}8S}E1ZJCTDd z?w*giPyQ885C8QAML~E!FM7&r4)f@vWcy|tfdNWcU@BZydO577Lflt zJ)+B^LD|$kO0tf00%FTznJH^@TehDzcn8;rVtX2t!bESOo-cyZ5BcY>a-MvPd7q{! zw+waW6JjH(rXsMhbm^R-TFLvqBC^ZAeLg!6n2Rff`D5Sf=0Th8H$Z}sM+bV7sc4hw zcXa6wE?V}zO+>MT38_BCUq*U8(Ld{-YkwOHxE(svaV0at!6z5oSLmewjHY1%sfd1I z4SXn5TW&3O#u&w_)zUSB!;Uv8R8y*%yOG6NZ4!=Kjcb;v{#Zpm%-&GKMUWY4 zSY-Vw+uuT^P1v)uh7M()=&;Ir(tPAsR^Rq9VMLLSX-MdkHG9&(j&P{0$S%8)MMmbT zATFsZS$WUX%*vUg12{LB%5ben1v?PZ*d`=a?i^L_9IlI3k+K^>`chHW$Q+2|Xp=|1 zRmP`;hU$_+Eed$5M3%gmv3q7XZnR6};}mnoQ15)2k#7jB!cy~Q8x^;)PA;mRvH6ez z>W%v#og*AF>f$W|}GV*t{Y5d&@X*~ww zBG5bDtOq%2dLMMSuxw{#?OeeXF66Jp1vlrYGLT<+9-p?l(kRk#K2rU=>w=gpG<#)h z>d4CG@~1r$)DU+Qe&xcu^~*;DLCA&q9J!rVBU!CLcR2z}M#x0DmMcK?3^!&0YY5}F zj1&suvJ}3=30~@v;buLq%@IbUOFZAY=D5j%IqW&hJqAQFDDYNz#MCZuMDUK1d`0GyHIBup`U@j%Y})IvnYE?(pUoSau$zx_Gu@NGky!}9v%ebRY#(SEG?U1)yW2*kiD zKA=4mhH`$HbB{VL|q+eHgtYhaz_Zix?le#$JYS7H>KfG?w1!K+;U zT!$@|$JdS}+I^5NXT%NYqLj^ zpDx({Dpt(Mz{2>yt;|_!|NjA*^vi(1y1d_r9!0p@2F$7=ISiWXOskS?TpNg8cYcEu z^Yd>a8tfXFqh)YVANP*OpOfpZZRA3+9Cx+qSpQFQUdV(L?HaB<7yr+f zJn;UGVJ|yS-)8sK6FKzMz&2L%!9Q%TdwkP+1{k73lxb3v*Y!H5iah}&sSG-yS~jFI zgJ+@^?l6Zetv?VxjF{U*#{YH&oFK?kA#gvOj-J8Z+@ozRIN$B2ZUCcszY+>2ehYQB zo!UWX9K-)(>>YwciI%qAwszaLZQHhOb9e8yZQHhO+qP|+f1kLM`$e3=iO5+^Dpu9X ztX28u^Dd-<;`W=TnF2j38sBFA0hLQdagvqmJ%3uAJ$G#Wqm*Sjb8AA??61o926jKV z4=W{YOxkU=I_ebVP_qvls9}wd<*L)a;%w6*w9`or(oHh7fIF{l%qjC+t5rOQD4GYm zK?S4*-U_`$5tnRGyj-SekxgTZzhnKktVN^zMYU$e+E+qx-v!|(H;hk3%(_}?H4||K z!XfMltmO{pIgdR2+u<&sJ3@6s9ll>)s8v(Ng4(vc&t#G@k|1s`{rg0Wc?&lW(pwN+ z4hR_)3|KGDsXKtoPd_oXhEUW$d4t8~YXe@nL2mtDgGk-`Wm+>yy2*ng0m}T?%98d=+5EFYOD8vlC`e`^ zM$76KaCy&SWPXF9LaP}Z*ifE9t$qw4BE^+u{U#rErB--JK1XrTRdv)n-{>C~+nM2n zosnDeT4710TL#oHh2Dc#Z|=;jpj#rBNDLU7l{*dnCkY8$vyoFUhLT0sogcnO(?v^b z$t-lAh}9ErDaE$p-3xs(jZD$fZl=bO`XvOpn{C0%j}=VDE-pLq5!_6DxtnQ&jE56E zOn(HuZrR~JKqlNPA$c=-$oF6AzZOyP`YOjzs-P9xZEHbiAWoNiYY#zTFX1rr#K)070`N<9Y0S`21FSCiY^HTDUYhI?Ez>dwvq z>FCe+Nt+UijBq(M>P%mpOtgfg66~+?o6HK$I_NWF9Kbc_+ZMN$CW+<3dR0EPKY0eyT8yt?!v33<;L7v}s zggw3GYEX~rMBk$&Qv>Qh?MXu?;RdCL+9YX|%Gt)8ox@#fm4`WJE{ zbnNu7W4iXasmVa?$Bj31lQoLYosLiq5#<>7{ew;nI!|0x&L(E|S=b_CwMp*TwSO_5r3T;Nue;^U&Sfg2z|T4)-w zVM!?V`PgY5F+fk=z8=dWL?3~W)_ZX}M21->y{aR!z~AlsZG<_@#&MRTbhT0U4@TD##+G8Wu`mC*fo6KQ zmp@|2np^@5!yusNVSHjEZH;r6J6Zz(4A8|WUiROX=zsbr%f!k`|Nk$kgKBHh8>1-y z@xUv<$(^X|I)Vt=E)|E89wfyt@J2Xk9MrxGqq*Q zPK=1%8+^r|p;b&-wx=h2ETwDH*|Hriqw_nse#h4r%8WRWoqxyU90*tiV63XMZWylJl{5YQ(Q*Gf1=!whc$Qlq}!S5 z9UtW!rB7ZTlelg6LSR!`f3F3$RZSUF29NY6~l3h;?L zD$UizxQ8~{L6Y?*+_swZxPs#j&t^u1XRq_uBpY=?(C$QsycR&7b!2+Sn6Yo>7)UJ? z4XYATqwmvr_2V3{XC6yZH-5+EJSR@k&6=+-n08uBi*ENQJ+OHI!)FkaF&M~uH*-G^ zr6>)kIL5(0qzFHAK{0(7dPMB4UDZzci>&vArzHuA@=)!Il zS`VsHjWsJ|SyUxOh2`Y1Rn>8`4=3CDlfLq8j$8N`d)B94_4-IJY%YZG`mnjTFaV~~ z|L>m{nu~fqpeB=UTY+2bypWk5bTRlk%!x9}*0zJ7`)O`?nqNQHFphNI69vjwT$>sc zWg?OHGfE{MDjjxr+}&xNPG!`11!Zgaz#b6C3_XLJ_0l}cd15|vgEJX0IhbTsXj3tYhY(SP#Bs4&PE;WyoqPKy=B1qbikrXd}qbTF4iQ4qdp`|j4_|T z@hr*E??)Tqb}UF%5GH(;+l#irlJ#t7*k=^95wm$JiuGHbzVW(j7#xRcJC`$2z zdP$%Dvm}l5!8TCgsmPI=b%6gI$^^|h?))7 zaV^K_-YH=@OC}Q)db_ZK0B5oTwBY;(G@Hp~bsXkGv*E}|4Blzz;VY0AXEa`y2^O6& z`CHZk_%w_ENJI06VKngZKZ(d50O_w-cVXY&x~6h1+Xwt6?IbUT)xQGKC-;H&r0lK+|EsJ?H>( zJJjjVfPhz}AZh-X*|34f3?513Av#cS_)yx1MG0Edp?U*T)pA~oso?P11w_^wZ%VG# zaqy^X1sbVtczOb*RN2GWYGGD2aC+d<)xXG4-JqYI%a0du~^r9dy390!yh0hQXN)G})C4JW892F*l@I&gFIlSn6!uB$zK zu1ok+(eGqE3H~R-=EliC1XYntUrYAJA@5k3k%xYLb{Wd3Wr8&guha_#=cB79b zt?Lf|zIg2Bnj229;%qN~juvr7WRJ+AxQt%wO3ar*Z4vbTUOv`8c;9KkDVQsNu>Y)O z8_Nw+TS(P?fb`+1Eaqt^gx-tM)i-U@4QUnMY2n(Wswtk^+KlP-8$2=#Y2!Z;>fZw1 z&tUTe!%q9ZLfCHL0z~>d`8Z|v-r-eb>UZ5iZ?(}Z$=>!m9kMJE=0c1clRQJ-gO#M4VC+UQ4~&HKn{k?RX~130bd>B?i*`*=?tk^Q8K_s_4|{L88f&m#I9|1 zdUCvZF$aTP+J|$uXfa0POM06=csj;WAG)yw=Ue)%=mvhYkGrd z1mAuQ73pxcf>4~aZ1ANCO%a%#br3hi&JR;pg<%-)Tj8MhN@ICC*`?v^%CuP%b`S{~ zB4|J1&0`ozdu+(!pD8yDg5t^EC%ZRWcQ?j}c8ZwDBI1>@`79rCet7DyAsNzKj*lBx z_Tb_s%3L<>C8WJUECKRIZ#OBlW>G2s%x&Cr5g=v2Yd(jXYySt)t^IKZJk(&nR=Yn^ zEHSoSJB;gnO7YG6&Xv8JVSk*HZiXHK`Z3=9kO5j-f1v6r;4%rAyPR6IwkG}{*n^K; zJW-b5r7~Ayc`$B>nN4_|!MIAXxOQFw1rKWO6;sJ`1^`RGxwQEp&qD_eF!=))mOH6y zHjIF;RL=PLz|qRzw2u}UbGJP~7JGvZu#vBVXEi6Es~XKp4%KS)0OdMmQ#Zq6PAl#` z6K8q)XLo;$&G5F%iVFTace%=mkrELJ+mi=%z0R_lH|w{wNxPmQH%QEd&UJleT0D0z zQieVjU#m)vc6zkj$zPo?&G=-}G&z$H>GIzmWhGNOD#WH4s=ED2nz0?lKMUKr?uS4Z zQDWDD@3hKmN^WTiNA5xpkF9FuYkzmTP+-6gH%+{R?xp4K9j={9XMl&B`%)e&9RE8_&5Tc*6G>Z~5VllALf zHH>5XG8Y{h+Vpm4$Axp9rm>1iR&TfXmp+ceFBRDj1|e1KxK!*Uv?%YxU((i;J+qGU zhFeIoRXzbFW*Q1_R9o{gVU*oe`_|G>t~F7v!6-Ma^fiJ;lGu`*V&qd?Hr=TT|G)it zka#|v`|@U(x>kKnTcIj!-=lP$W}h!oPL#r(q|6nl=j&SM5BIPFoI)&T@rj^Tcb;*# z@e_H#%^dRgd>0~Cs*)mo>`%Mkl}k{6lelVev$g{Wf!ML5n2o{u;aRx|+W1h)R7*LB zJYeoj;Q=HqN7E^zVnECZhrw^Wi$UBjdTUl0UO*BB+xC#yG+ZRt93DaUeFT5aNE&Ao0b zpr1bq9;(hxQSA)5BHC;MuMFu<{$fBIr4_y1#^iJ4uH*NBLf?Z@LX2nzkkiB(Z0@EocHLw#rHgENCK z^Usz^(#?T)b1%2&QEB~94jMLKX?sRdp2Oo#V~qYIv@MGMDjj!qx@LXSOE*vNCrGhz zat@GiXBP?hs8(#n&HXI<2bz6uGrihDsFN}s$1`x*IVXQD)RulKo=tDpAm-Z&Wj7b9 zpz*#p;+&n(7(etBipL2b{W^0LrvaP9mnAcwIK-SQdR%@P+py1@2?`F!5d${Ar~_Mg zZWyX2lht`xHql&M{vBS07CED~5I^MC09jB%IpHLxg>8`d`4%0LqoA-x1R+*QTAxWn z=TkD7yJ0}0MAC{F$yM}U3iwkLxdNeyeI9kxANC-?NWimTnA8kOLKL-()doo*J zjw*PMDM&F(<>~9-7uizn2PT(8k$76!eda~Hj7F<;Hc`9{zgSFMTp!cUF4=_0&^mvU zJ~vG*m#5S%#1mmKD7V}D@sh<}>ut2PAj3?6p*Gh3@TY{ILbN977h1Yj8>fd~~&4yBNSGMe|9_)Gcuk0`pyEIW<4cAwe3dlFIh-t?&%`h>|c(U0g?)r##+GX?lR1Ln_ zJUGer)S7%(=e<`TPD9}s-y3?bWrF+|e`^&~EspP>ylotxBrN0<{+h9$pLenAP$##v z?Ei9h4&|zEs?k=^_P3s**thea6L_gj{E4yN-#xM4si^MzNq zOPY03OVL>~&top&DAI67V{1(dB$_}aCr0?g1Kz}wDdhLGT8nerB|`7J^pPVJ7<+n` zJk7gO`A5vJ6GuYMbCB64PhA{`e86q9KNjRlhU^;KOMno0LAa4ke(D_*62^o)yz%wg z^WL+>#Rr~%#H02;@v6;>!9uf=-vlKKl+;7d+0Z4ys2hqvf@?G1;O;8VU=YFdlA;v0 zxPM{O6y$qWCnx^2TUSa;nrS)M4^oiXgd52&xm4>IlbI$WXx6Mz^}q_Wc(ZlNP?e3{ zZB$)!xh?-h1~6R+YD6oL$op*2DJs?eQrtW<;)n0X^*2lcz9^h8{^)+(cPKu&=xPlH z?wd>NM6+qkQWj3-p0i40E!J*KoPQFf39^nG!)yoT8*Y6oTWm?j`{O!VO`&u-8;zoL zfVF|;Y1#b+U3v4p@E_dCFg|mzT^4CDU1k^)j4oWUs=w6?VbM5Gmm(^fD+RNl-U|qg znBp-G6RIB^NDwIi=5&=cR#5wlrFIXbHua}oqKUeBAGs7nZ2{K+3THeAg_0IOvm%LD zfShTtYTj(vAs41Ae)48zPgs7mgn7B0fH>$XlyJV9Z-nim4G0zC>Z=Y(%s*G=e~~PZ zVCt_9YepHYwUMd&BRD9=EX6n1vV3{(1JmI-ShoGRhSGMg^c~98<-&|Qh2A?Z9~k9@ zECx$qolE;~=LTG{@Lzb4j4QT~GdwFK6jt6$e{eL3b!!WHBGO;LHNM!wVaWxh#;+7Y zQwCLs`l8q9Z97hfczl?HZF3jhI#!G-#}I2a^vy!S2HN#O@2aceK=Q2XCM;I@+OB5> z7;`*>e6K-mrAmPls27wFa=bO7a{~LNB&Dp_17n*x zl6azJ(-gMszp~FtZm3sV#Y1!bR74HTVG$s%(m_AvdTDUyg zb#F)bMlTC??~15&z)e$N+YZ2%9bXSEJ>yoCrQik?fz9Gx`w#?l%ziaMP#fl5oJ9CT z)~GOvD~4DQos+e*u!GfyN-%w4=0T4aw`NDGnT7(d+guf;eb2%6o%HL_u^yX($z*Bj007gPw%JPDTLjdvogXC((Tk|Hc01TONDQS5uj~W+*oSe=W%qg zQF?eJ$}%y=bpRL4!*6`R18o_O*S0>x+YeA!89yEy*l$FAK4aO-B{{}ih>NbzTX`$x zd<>jA8jIS8jqvjc%Oat%Z59)YDpo=_69k^VU`pu0gcd%B>a&uoI3ouvac~m-L$cA^ zQ{c3)Xv)bvi55XCNL(zuqa}-a@@NT|rh(gPP{AcVZtHpz#555QvLU9jNu`7XKZ#I4 zr~2eQ>KhDyHK)Y+mv4o@C(aj&az{gm=blf6(vLjowJz+*)|bL#4xlTZTRu0R%csYB z2y?%tBIT{se407jHuk{A&sO6$5VK#NS(p#OJDSQ3%ZRXcQ{i_Z#>)OJ{iYtRKtLCO zkvWe+H-T70k7*Hg23wXq89HV3hK+bKwQy2%*R!F!dfuV1Cy05AQm&^22^X#j1iwpn zN1mVn^Y`l|p8#3hzPuU%*Bo!5ilDCzKS}A_Uk;osy*~3_OOw_1r7(|KGMpr#-a-{B z46a{Gi&blskYS7J6MDuE<*!596lB(4S6!YQYbbf3yqz$6p+^>;e5c_E&O-;&;@I_| znR@%VMUu0~Es>VzwIg>Bk};76Y#r1HQ&#nW*?_4APL1V&c9S-6J+*+E`$nAfvW*3f zFaK|fY)=(zhmW)8VD6+sYu zA6m+4yC2xcr5b0khZGV>*07kGEpgs2=X!5eH(oBCyvMm} z1(bLppKNLmOyBMn?w_lP5W#_w@SzCc`~iXk{I=rCNx%bOEiz&%r!_7!+lxfFTyF3o~D-Y_TR5pj%23LQfBzsn7^Fd?&?3KN8jRQ zw_CSw@N896Dms>YQizf`#0%!S028Og*$35>nv9j#0Lt^yOtuE~_|^XfvwCwdPq#+` zX;-Ll;sFT(`f>oM#4FN_=#i#%%0k_fNpVfY=jBLS5~eYqH`JYi%AVZ5G(d90R@3ZL zKEaSTAv6156;2@6Xf-=pq%ee>JAQ9jw%57*eedU5bU*Kz@0D%Fa^ws(SV>i52^XFP zHM*9lZqRl@mAvgg@|@5+%Px}HvJ0orkx{Q2@hAXw=GB+BpQSCciPqAQ5onn;OJ5g- z#e~i)Lz}I?-nXDui`M1l8dyP|86nOkaU!@z)pG+{uhhUx_l5QS+ajq0G;A5B0|e$9 zjzLzS*j>*vfe+XN*(`0QrrK|Gt_y`?xn4V8;N{86t+}_b%zFncV z$zBbJDkDQCN|lL4v?YF{au9V&BS_+vZd(ca>85~Q5*W`bTBe+BbhvMzXdxR#d}hPm zB_Hc-QN=7*KutF0XXbvp8pTiqmUE)s=A^XEv3-GJi8wVsZaIbKkVVnUs~miC0qlva zUc__FgSHBY7>;{*X5fF3gdd0EC(hY8gNl9ac6#|h%7#fBonwk|ASs;9^&kLp=kv*q z#Ww_S$(NnY?elJHuugn%=PR+5c??7|D@Qz_rq4US*1Jm-aBl%7fv_vXsdbq(=UdsY zt8zJL#5dFIBS#Kbf%v~M;Sm|P93d$g3On%_XU`XY{>QC!< zS)owW?Gl@wY^eK=d%8M`L~I#+QiLF~8YIa09Ir~CIdyWkxDt`hG^ZV7fkAAZU72RJ zKlUGm%_@S~Ii-_OrcLWwy!W#C+^r6{kez7c(t({kwMJ%}#li~&QRKneYd0BpP_U`J z1DvJzAEL}b;qZd6eE^a#cWhNOO+?){1LUQiEy+!VHS0SwOYqvdSu z=h#F8LU$B~G`c7RB7?#)MA1lvc!RTzyBAD-8=_O+6=+je{HNOB!zB2jH+0>xz33+) z-J0>Dq1f(|2H#Lm3_X$uMCxY%x!tew&8$uA<@`*dag>V+u1|DMyEqf!Ud75Ubnbjr zUXUAzswG&yy>QU3eh0h|P-O(+@pl5cJIeEZFAX7|3(_24RUC%Rl!{c(-9!fdQlwf9m5PvL%Vh7Q|Q54ySYKmlDEoPaJ`8wIj zj8%HYxecIA4atef;?ohJ0t-A%7>ix#CQ(D?IqU5A>(W1kv4Cl_K|q==5NqZbnLkjOLz18YID-BI-d2qTNs{w@H#}tr4CkGxsW)cmIh`Lr;<_ zkl;xi_~duaz#>^B)J~H3V@jDz;|39wZbt4KFq=OHL*t4}%NzbX8rz7R2r0dOC1FxT zCgn}j^#3G6tEPN9j)hvh)~$aW2Mr{5gM3p!!+nvEiAm=}$M7{usn=%tQ4ikY07Gbc zp|v)%Ki!T@rf9#$%N>x01FH^;geX8Ec+j=xxAvJ{GiDqofs z)5u8Qi_^``@RH8{L7CTRf;TS;7*<0Bq1ZG>R;Jd&BWujl=Fsd-&(~5*9`mBXt1n?X z-Kr=Od1-EdRjrJvD?`zgwb$5ONAjHez5p}Y?wX`_^eneC55>dh}dBe{8;q zUeOgC1ZW1`M#Z}SZ2s<(V*eeWX)_P>;Hi=;g9Wx2w?Fsow@1w%O>20#a5@cZaI?YJ z`vXLl4xfKlc&hcr zqNiCER)C(zGo33UoyY%%%pQU+n(o;|J0UFxNi~IC5Qjrh{q(+Nk{#{Dh5~w03XmbK zVk#E-73cyUA^FF(PUZ^(0r9KW5brAWB`^>0PsAf_o8+ z{_vO2X=NH5&XH!&Tu&RePU<`6{P||4hw&bZy;)dilWp1#W|X~;&v&3fG9Od|!B3P# zLOyW1xQpu_1$?~b93J!U*sMGdaY3NEP;p2}d3W4wqVJ#T)3H*}bUwaT7ba&a%aBLZ}@N@+;PlgZ5O`h_u7$ZxYmE=vUi; zP=$HCnI+Xd$ee`zdW$T9@_Sh%o(r?&^FtkzmFb8tO!3oYVGfE;c7%QxV#B2NYGx-! zsh4+LcS{*huA}R!a_68c0zNR07P27D$>pYw)d(Eu`TX}(bMy0fbHfCL04JIoU`GVx zvNa;N^=&U`&ZVj#dOm+HHnGc%o?N;#F@FIb(caLTt+am2k?&-p(lL2auqr!2tkzap zA5YRnlR@aA0iMEV+E+#9uO$Q0Vi|rq1>{-W3Z7)0PQ9)6RvUH*EAe-vUOL=EJK}F- zYuWU&VAFKg{FTQM?Y4l>E~opkW}C#oaa9>&&QjubVJrk!N&_|bUZ8$-Aa%!rX)Eg`(d21b z)G-x9#(98T7;#&f>cvo?tK`=JBGKDSK#B7eY`UO1>3&_&=kqt0P7kLMQFwr0Itj8k& z{xaMet*MtPNoL1R#mepuO7)U(U?e)Hw8H`x?LPpA^4J6Js@M&)d%!gi``^YQ?6iP9 zgX6e#K?-c85O7t}Ku@SB0yBaq__jVeBC8tziPi^{Jp>W>r09q6(GTS^2$ziT-W{z7 zI&)kEQy???cnDJpM)DkUBiUC3)ozV$B3sZ1os643mCPFv!w!?~JMQCy#GBiuhbsFj zq=dF;sBDq}ZpQU>)u7misveFbILMXYMTJh*y(5i8d%PazBP%n3Qcki>FRSqs!RF!j zLhxHHe~Jj)QL9PHYmg(ZQZy&2J^W}{rElnmV^=pUsM_1bSgm%Le+oak!!tGhF3a7? zcYH#?v-UtR`1FyXM8hqPV66%Q>P71S*;p5DkM zza#HzGBH>q2wgWdsRfR^uJ0Wm=0yHB_<)(A!ExIDpdu8JW@>$svb&$}o#oZn7O-X% z?kN*TBI}gTk78b(FXzg=K7ZR8r1=djC0+n`d(_*N1|iV0`ELoNJ`E5s^u!Kw0`>yT zJzwr2$%0q%@1BYPdWb#a_INaXAZKC92j1fEhVD*O7;P&rt{lm`xg!|A_d{(vue#zQ zH%8#U43TTHu5}nWG+(t2N(JwRzWbF|+{^%?Yiiy+<+~cbxrK0aQDRHcQas&?9oeS| z5YQ4lc~f}-cf9!y#l~I?OPGbWKp(tdM`Pk|U5f*K3`!f~?b+m7bI3lWg>)odk??zr zKQ$1)Ga>o@?570MA+8-jvh{QfFzsVWw6>v-Lr}CtnIy&5A)=1!MU8aP8*56zqYiNyj~Yc7mSc2&?#AB zA((6uz+`$rI2mpmRc#^!gNnuY6VkkO;{a4x41W?MrDi8*t#Z@w1asgqpfGm@*Um92 zMmPj{ImvyN*iWl=svY`6-Vz1(FlcWKC>_*U-il?G(` zM3h7jmFzeh$ahh;i4mZAjC0#2D&0t4tI!13p$f@*jdfZ|?zxr0of?I>94}@BERJZL zA9$Z%JwDhoUx4`mb_nh!SAyTQrvHmM-9=qquDtk_BfF^>@aYCUi=*UsBweF8$5Vbz z$nkY*+$8Nms-yyEnnZAemxi7|r;=21k1RYjh&f<4<`bjVCGebG)+xy7!;+|SG$mur zXDQPXz9D^Gzv^NrjxLrgBw;Y)y{_CyUMUrcb&3}S{?zT^=JHru%_#~qjVb!Q3zJm9 zz|al}*-d6^=cXHF_E2U+Ce<+UiCL*tX!{9sUi{#q)~SLKhcG3b)1*d*zZ!g;t#sJ9 za77nIKH#c8pQ_BuaKvFXzAjRKbarfP`J}3N9y?IoRU~TQkbucM zLz>5uqrLrt%FxPR{#x>8p792Ib#1CeL=)|MGs`;) z6Ed0z$TR2>(UZe1%*7-|C5aS~+@qJVl#L3@s0kJgU$==OGO!r#D5snhtw;0|T;vfX zlZZsQBAVLrUhvxug>dfA+dYT0g6dUR&WOcwB8Ya3E1kPd*Qi5i6hB#+z*yj^2_VnX zv~N(qP|D4aW6WAZ!zPk@6!*iLc5CX~qLIoFIEiyL0Em!s#q8Yyg59(JWu5cdxcqE5 z|7`oSz}*%Q)(^Gr3r37y-hsx+#NCt=)}zLB!~0LlZJ9~G#79aP3FL~h52s48iSSCZ zJ1adp$v@=}xxb45_l^#p<*OqrB2@lalw}G%$VaoRX@=**jT1jaRn57?DA7{^#jzpU z^p=RR1YLbbZ}k^_628uUfopNm$}_PsCjG0CH^mkv$SG_P)O z0(f`00#K`Pe4^SNicZV+tu*oW9v)LH@5t&hlL@{7LC~HIzF}Tm6#SlF=3st2Z}_xp zjlopRjA%A=i7vS#kyPx#l|)YF=`JtWsB&+21y(jYrg72!u&Pz06IXF|k&rYE`QHX1 zAc3$Rc76mX8b@Q*e5_vcS&W}6h50z3ZU7D0hUhHNnYi!WBU96S@6UBg&ioq)cz?eK zyKZ&MDuD+!cqF&0Sz+p7>YQ2>5#QrDp-afmyu{J-!AnJpo%ibp z76H>H#Y`c)`F{P>$j3`ZNC$A7c;GaGcqj-YD0B4`g9J75=x`SSCv$=G@yB_pA)`2m zp^XTQLO2UCq90-`tKdwPNXVhZpg57$qmNPq6)i94zo7GoSeq#D^bx`iD$vsVW{n4R ziF3dsfBb@tk3J9B^gcaFZfV>7x?CN)`q1w1{WL-s6AX6Eg!k0;e%Y|rp}#WUXEPBj zZ8bH?R*HTT2>5V{4A^n=C6{G3=XH}Fwn~*Dn5EIA`2CIm{x)0Km-yg=g3xp z>G9mf`2Bwc+^s$iL!V&2TbS$%&w#+MWv~E7_<^u@&0PVKa_~UKPVuhDl_5N5QLuHM zA``%^Fcx32lCBI0{Vu}a9n2TSauM<;h*5KaqISL?pY0Tbmcu9U;_b%m!IQT)y|=wO z`S`^!d{6InTY%v`lremXX{n1I#6-ExVkYY+bMV^vlHI)@a*Nj%{nnJr`+L&mmM6D4 zhjTcyu2G)*og|CEpd8z!OSCQ5m}GJLEm)#ThZ0~z7OBSP90V|g<=<<`$yug&H**2k zSsXBY-k*>x#;%>Qr6~T4Ea0sct}w8o@LXT}oDFa6G{e<%A19v$$W7JHbo_#)HCZ@A zYLZUZ6qH#IL-PoZ0DXHHqE3KV^x#1INj`o0tuNBoma{FIn;O=Zc@3Jqg65XNYY~CE z+1JDI=@$0N3aWk;fE#1Z>Be|CHf9Yrt2s6Up3N`~Ypj=3%_Yz+X-~-LEzm99?!y+F zL*Bt7zMn~!+zn^X?wlvk1vK_Z4mkKaJ>2V5m;EMmUh z2qA6APPb{czVj{L9HRpNMnN@vfd9JMS5mOn@&k$)rx>z~=0RX-o{qxy!LCcmtqO>t zkTYO#8g zyc_@6cX54umoApiErTIu@B<->=#M5Aq}Cfs*rtEJXRnQTiKh@2$w$h+3S8UcJ+L!5 z9%q=8fc(tuE4}P5agp%q{-aBN7fYs=DLetAf-z_+3Ly0`NAIyTQSSw3oA%S zSXxSYeZFsdo}sO#lcinWf81BZ&g$vW_3A?2>E?TDY)7@*@nP?9{k}QkH zYd0g{*89%XTRa*HiVq*W57Z;6>*edbid>YCzh@lI@7lTS5F%7^s?{RWf7a5k|D8LY8+m?l~7Jx*p@LNb6Y1(n~Iz{tEgd>@Ra}heb~9M zuToMdr};_AE}33%m`H3A^lzSm#y^Tt7!f(D(Qojm++)S7?3Qk3bScDF?puFD+~=V z1N`#VSyv<`#z9f=ye3DQBK9KzM&|@uf^wf}S`tR~R4+zBXCv)2h6)Ofv~dBdCT4xY zKZc6dT)k{rJR;DM1BMLXb-w0Shw^F+=fD)w+E&7KjhWz2%D91s1`Aj9-dI1v zFhC(ji*&-_mr#d|a49A#2`fAty1YQZoaMEW2sAM;whKTyEjf9~(-8}H0g8TACj3gs zW_)VM1ES7{6X40g$aI?5_4Uqjg-fse0Dwq6AvC7%GoVSM8UrV2fCvFmlzRr4>Vhfl z4MH0S1rl~GdcU#i(5Pb3Yh3Q}O_vNqN(-72W|GX4l+bgC{)7 z-4{9tQgm&mb3AJL(>Sjmb08^2a5gPV1_{IPCepqJ_YtuB(1n>EpY;#i%@6A$FNC*;7eRkAsr$pk9;dfbOv zEac!_a~dI{`#;+%HMYROPnB~U@w(UEh0gyQYy^jW{<3nb?AwmtaKOA2+Y6vsbu98c z(SK2OsC}Xid_lib95ae)=1`_+8QnF0)V3v3YQ?LbP_yw&&bLGy#yZ{WQU(b9?WaZi z0RM5a;TWh+0L-i-AjVjP9TQ?4CPzhx+=iJ5JQ32h46N7VMW+}W_y+Mmx8DnNM6IoS zV7mt906nw@Iz8+y*D9Zm=U!%mn3S^DU?APnL{YNs_4y!**pH|)>$l&)8yNW-0VRw$ zV}QNP0SzDVW7T8r+j=0WN6T~2(i{;ehvWli<>At~)|L%d5G~8J3rVHf>w|zjhgFfn zr0MjzuunprHkDnzvv=g5`Ez57tL17?S8>t-K8Okka2kN7M4!r z-Ri|Bk2lq(MKkS(eBy9YwPP>K7n3@iGvZ^gXDbZL$YAL1o1b45q z%ecazQ_b3WNTo!d8rRhoNbuUIt)u{xB_C)D6Q~O<_TKpu?Rsb;XkFh|IITq=WyPL3 z9J+@7fDuAx69fFU@w>yM>S%!ckcf${S#qTxu>2e#0){2roqVY$boD2m&q)@f&*4xmaze!J`f!_zhLgGN|rSWA=DV` z)V8FinY5x%pdd3naShZJb0&^E>|Xe67*W0dt$S8``O!S)ykeid_mT}c963-`!u`I9 zxm=*u7AVCMg;LBDbr5I2!hvVxMO2+dlj8$)Axm(Vy}D+Hn!S)CizY_{{gGhC1ouwe z`!pNp=l1GuqK7Uw*LdXUd}3v3=?O#-mmifg zA6X=eCvPtDUy`w0ExsH^U7`@Lp<>)D2>&b30LgEHQ@4)Qe2E>`!txe%#k>WcJ@^GN zoO2dsVjYmBvcuskq81?-G_tDAL;9*$Y0GxTS0}GgI)thj^VJzRG@3E_BFaY&MpWXP zMTn9qzb+GBPzp@(e^Ds2;^kAKGv15^?M}BB*_3Hv+SnA^T0{m-mNe2h7zXzIeEvcl zzb*c0^qt+Ka5F~My}ir4VH8yut{5p}d;c8WUw&S`U-DJpcF~{$8iO*UV9S`i5Qx!W zr;%{LcY?9celT(z029FY$30NqiFb3reU`U5ZgnKIQ0jGs&`qTQd(ZvaHuvJJx~1HH z1{M{=yguoBLQXqYrlswo?J~6GI<^^&Km%b}68Lrnp(oR{%yj_s0MJKp{$RiMWfu3+ zLlqr!jLL@c&X50sS`fh$(L8ZUJqZd*YG3r%5{pjhP?8b+`ooh~1C+@00Hq9maKKiCvrGwrRN`!^VrD5^3KWjBsNzh6P*^ zui8ryuYg<#=<8(5XyOJ@*0DXD{y?#1Brq^lHZoutG|7xMWG8j;czbz1Uc1J}VA|ek z-NzMxEJ|EJG06pY3Fh9D!o>ee%6)M^dni@8Ttb0}fVB`f_C6E9ya?!N97oZyPKe1zEdf zBzfic_4Gjaw=77-SR#Khk#=iR-o(^+FanLvby^u+UW5VOe+__iAWgI0?`3t{{_iVe zgGn`jMO-i^XUO>H#|`N~3i;62)cxkuf!gnQFb3QR6=>B>XjPmLMm+JzD%e>pbv%`f z3s_&dA+JsS8a`bd#3BA8@SrSqjlJ*IN1(&^Sc!;`tP{Z~n|93g8V&+{g#-J%bp;tPp0u9_GAT z7=nCX4qIFO5g>`R_r@lUvj@~4!oAqexJb!lT~Uliu9<>pLhs0~z5V_+prwg?R{02m zHyVeajB&d>U@NO~d8S7EKZ2UeJCSwwV^Zfg`dkIT7dN3l;MK*7+=)B!X0k#u;bgLp zrmvCj|Av*0P%XZ${~u%L*qlkUuF!K>FjTux2R%b za$H;t6^6{03BZrA!9z9+@F;JOI1vj84Q%~@|#YY1(2!G`>eT63k#Q}ez@x~*O3E|EZ zsR~+J7wO#+ui-E-&fJF-KH$>?!G~tFb$a1P`> z@-y&X!SyPLz~$NUDrW%~e{=$&=0eNXj+Nh1W^SebBIKVTd9HhuoD{5k(7u*jXe>JZ z%E`I*Y_~ci!Voa+8hNKvUhU%&9+1X2?KMMojO0t&i?TtLb#dRL+W;J@x`(?$!Mmt$ zO#c}zaQX$kxp(2^qNA7m+W}!;K~qkbMk)EX zta*aH4Y~hNtt2?{fX5+?GR)O&*KhvMER}8fmljo(&tg zjUlLDz48iw>X@DCG$I@q@o-O?2B%@EFfcj|8^K2M;*^qXQ`;6X^k~kQDgquCi$WHQ zh8K%S7mKomwcj!qi&W(?7*L!}vdR}pDvVXfq~bM{lRNr*8Hj6K)%-=zDsC{Qs3Sju z-LOY@;xMK7gRH+-H*xLK1fcOW%IljW=aB&ZN`}Twm-R*QNC)7mxc}_`Gvb~bEw~H0 zNC0>8q7niZ?|Zr6{d5uA!Sn(PV;}!U{-AK^E9<1NS8q4x94uvk7}8g-?or!C!+`iD zhcVGvw>cY1S9!I^Xe)*LcPU6wNNImcgNxBeC|1N(r~kcMcBr|?1oHm**@0K1B=WAy z*&DvNFirCTR!W;q>sbanLm8CUQi|e~4nX|y<6rYngB%Eh&2X)|tOt{Y_%0t!sjtvL zcGxx*#w9}?31Zko8a5OHh$-gdA;x-rYrjve@cKwI32;b|680x%pexdzo*tjezQd9B zyoZ9d1)+xlweCWzk64gbgRahj`nNkY;i=KkG>$AHJ2BVMYb@t3dlWG#iF=eL9P*Kn zKryOF_!K6I2$)wU-i*nS-zM<5h*PxHDL|8%bn37-i65c0DW#q)N=X&C zmSl`|&bOMTFe^?rR#g}&EY(*KN4Wft=VF7L+CI-Z_?9N?bAl&IKSe`{yql|*rza%2pk-p z^Ff_^0hhvr6Cs=#0JRH9r%<22;X{|Bd~jxph);A?o)C{4h$ZfFP7`#Wzh3kY^TI9o zaPe&E`Y!rUhS*-?*9WoEtHKO#>h$wo`gV7Gw|4dH__X6B1q z6u-lFRV2RA&$aflQrB{N@SV|YF))va0l0fZwUHw+LwjkJB<12rK76M?rH(a#uWh-p zD)l&y62nQ3Yp~Us^8Wk4)Gat_YcVMaMYND?UjaxEpA^Y!OHT{0*L$k3@83uyyIljP zQ8C@9_o9x#&%b)D}A8kr_l9O!RTA#TN3~)IX37)LgF2G7TkoDyZjw&4{t} zT61fQ6X?z?^T>9!4N|VS`h@dcjM_o zP@vAWRmEnZgVt>9@j%PbCpjvVw0)kN&uR2RocUl$N2tJR=k`;-kcU9=kk$>@D)~6(Xz!tk{fXy!okrF=;^!G%7aRxy*0fA`%f{FdJ*M{&% zUq2fTCboO;H#;#?{Jeb{KSW534=ES)8hrmrWd-fbFZK(G{5o#tlh1<5Rso$5a8&Tc zuvf~XjV^4sB(Fb_D^bws`>VAySt25Vrg<5P&8DD?sn)#O@D0>6@mJ&{L$9q~E2{GC zswNw<^;atuPPoD94($sDGeMm@@~W}<*AQeo!~ep( z^Ftt?C^6~mj%4g^9-y`JnQGM@XS44{rZnB8HQLPPK9Ce3h@K|g!KMCCaWkWRXxIyy zwatXe$}t^TikM+J$pz9|z-iDqrETEhhKcB}s6|GaS$uKI(mT~#Qo^H|IIQ(fye|gQsf4xPmxZ3|P zZlZDsKZl~o=2zGc?v@yA8amS`?87rfTbO{ysE!7}lHB6&D#$vHl1g0TlX8OZJXCZ! zH_#}0nn%eR@OpMBnIU2N4uCaqe_Nmax<$hhAl6+4S+gj+jrV^}+rCkB3@xa~%^BJ=w1+qB!lV}m8&bgM^Tvb(h3%{t4Px-|g#SPzXzRDoWR z7{I%R6~iT|iqw8u!QA{pzVc#U_2p+!y-Z*yOxfoV3A!tgr^^hSMXE2Gj$ao3=fv6} z<>Dd|!&aA-w@a(%L;n*)dN7X;i6_zr9I==0q9PJw;LBpw<~n4?^X=L}TbTf?e`OR( zm4G;bsOZJz$TN~EdA%S2xzwV@p(r_iU6g--+-$UIMavV_9Ap5c0>NE)`*ES7rd!^} zZ!=%MPesdz+F!wY9C492k4Ls%nOOG$w!s%S;YnX?AA8pcVWcnYmd=*Ub1tZ28WgHm#O(Lu%&+9H-8M!Og-JwbYJCn304 z(S$x0k4&#r$wG0@7Z>-hfUcoo>{`|vo%CMJJyy;>S`w8O@H-*BD^L;@&q5@2^7wcn zdjF;diUgBa$^;b8YGaNF{X$}Xd6cy8E(8;{^yNGPIK6v(Uc?MrmloY>*NQ1NbgmO- zsv2OVMYQS>{cm$-o9=6ym;2KD=f3A9Q`dpV+V56B9~Bo`tWNZ?O2-VM?v*NKEOn*{ z<@ghSpEZnW@Usffzd_d48aT;(+My~~9RZrm;4x5Ee(6=<%6ED&K9=v>DfBNftLn-0 z92C)(h0ak?W++n9f=T==$$Z-^)J+y{WsULidryP}h4EQuNgBghR10J98)RH-5)FJn zz%r{&+794}z)e+*YktG%83DfGd~tZVc#Zv_JN;0*R9u5y?Mg!oF4lf1dn8I$-1z!B z84w#kKcj@0X8LYTrdk|x>c3qQ7ew9DvK5bvnQ!@fFt^Nj>2L**-811PW;SD#|Gvs0PN&)JB5E2TzMrLt@7pF|FN&_3jyC^V z;l4_WUl*n>8Fchx;HJ7<94bq?C%oh*6keSxXy=PkciM8`;|4FCy%2<#)8rzcQ~=md zzI@m`vKq;Cz(RrIndfqNCS+!qL!%jGP)7lX9VK z+3!+oJK%rwlQ{5m-5SqMRi1V8+@Q#g$=C7)xqr4UqA4ztDz?RUwT#T=i%25j8EFTu0L@!)(K7!VGZc+n%9VltE@@Bw8h!xDeirdL4RSk-$}~QNh%2Qj>i72O;R@dYZ|7K z4xzuX+eFGhkppnTJ zw+Kb`dSVzBB^HSQuNn?AmQBPDz={X-lF<E(c0gp4vyeA)}06NL@t!BQju?v~zN zd*}gG#(x&}+EEBYKbLB^HsNxfAwFA;NAfIrbsmXOZz<56&h|%`MJHWHr|EfluHNBz zWh#^5`k=VjfqAgK7ZX=(QfI-tnB_NZ4#f~#V7@Ex>Tw6JIbx*3I9H(YQg3@1t_S$B zAXWyf4%+tSk2hocq$pR5?&{{7PZc4^a!|3rXM zi$O3$QR&4cJUFE2A2+%?G-MeQWD4rNy+S;ERXaLABul!@Www9SKe!PbZn|MJ!O;}1 z>*3b*aAR}5_qjRn9vu1%gYfSJf7^!)0l_e@^Q$=YS@!wnD=jos(*qr)<{d)YYc#(k zxz&v*-u~`UP52&d?Y$%nZ8PTthlAhMzXF?>F>PRLXb`|nKI{pmdT{t8Pk8hNPd4Zf zK7UW8Eu0-IXr^mH941K$FK!#1qaiQp6ZRk>Elui;K)G`%kCNAhyGV&Gs`@t>&5l*DxmPq8PPa3V87Ohr{D0S_PM9R*y^!Oj<57e>ut zLf&$i~FN_Q^zJG4 z5?$OWC!%kKdvlkU^DG-4ooV>kQvw5aj&%7?W!7mHT&26IxwT8cpsDD0MxAmcwGYo5 z-TT?@>kewi@k~h$FY`=nEJkYT)Za+cLxky?>DZansj*k#`;W`drTw>j_xrDxh5ho^ zug}nhdsAQDH$$HI@o>i~CRWiA2TE4QkI)7|rMb3{MAev~z#RB)?oT)G+sVN|;rn^= ziVTMb*OiHi1z%c>Z%r+x)Rpf}HR=F2cy!Kl0=iz9qp zkwsmxI=W;OB;LOS6d3$gY&D(NyjQJf?6jHjCqsPrGI)~v&nPUzgX0@nH{+Za??s!;l z*Oe?Ma?(!g+t^hUX4NLY;d6FNG!4@QQ%F@{p6ld zT}K^h)3`Ds7F(9g71|s#qBS!k4fU~l)STttA2Ru7^&{)L5W z6jGd$Ux^oU<3sJ!3m}1%K7^{QH-S%K8Qumuk8Pc`pMoP)ci9LvG|6$5TS8n17Yz{< z$9_hH1C`@$Ew-(uP?Uv&0K`SFRVp3BS4#_;KEQB2pKF@wIuKqh&}|}3P ztw?xe#y*4vtdyl$1HrmcR>6q{L72EShGJouh5+y)m4C2}eSg)y&?Xcq za!93$(#ylrUQnG`{_P^L$yDmDNE#$!-L!IASS%RnZnA90fDn#S2voOfS{NowR0Z8_ zZDKJPQc#_VK{~fL4`D;ln2MCC%(?6+kXIrI0u{Qwt&5$D5Wv?;7V8uMH|};Z&hs zZXu(GLWCL1bif|%Nsmw0Y(K;>IaQTq)$|Mu{D|;4k3c-`B@7ZP;3GwZ3@Z8 zf*l^*rjC9iEDBrxk+I>8qOcJ&Xf(JGiJ-GAflU@jH=vw151`}k7=y9rFrG&r7k>&F z4@)y_(`}X)|0vVVwsKwWEVN#ru`D#FJH|ATqoY9+5C!{f(j_IdRDVeowM~Vp-i@u1 zjsJp4{t;AY7nWFA_7B(%_LmF~gFoYxY9MDA2e#2>eEos{1Ycb%8V?iA@yhCib<}(U zPwB3PWE}V;`flhjxCmSRdwBy_atf1MAR^g;f_Z9(-LmH#>zy3MHy|Q2h-lASx4yrOJwz##ro%b}A2^niKi&WQCf)wdt zTGH^%!TPx(O46Vbpl1;jZzD%A{5q+!;z& z$as?@xN;;Gd1#TV=iHX2s(ZBRdSOb6+Sz`~|C27OqQR0}BW_{_6=y+9RP?EDKNzN# z&t)J@^l71zKf38yQc7kw-5$|TAJI)VPA%b2hhRWYRcs`J)Dnc>K5uG;@pA%VxhrrE zdo;dIHSEC})gvVTOW$9my2aHf7mOpb4sFwh>Z(+(4PJyfaNO3Ra8+}RS!=_40bbPZ zFaTp1zMKS2Cjwy|Y=SBEGx1bcff;4w+o*#KzFV5TQ!)bPY;2hz8gtyXZ>eO=;hZctQl$I4U4pl z2Z9(FINGBhIS3jez(okmB@6nJ7_E_B-TE%hF;>fOd7PP|Nu1NJ4_@5@$6sLF9Lps@ zPBCpe;dcG79Sdf)dCswCdkyvk zu`xyU5}S;DUQ{k1Ug3b2`eGeBQk8!phS|n&y?wZ2%P^&F{1wO2%l0{q{}gq~g&F*f zOuA|Kx^9upK<}|*`+$YCLS3e7g@Y{4^iuIV*O?{iC z#aE|w}0o7x;8eqdh&K%X9iHB?w3foSrGXw7n z1U3o(J*&pZ>Gi4Y=xpDWhS%+OU!S*ju`}ayU-X^sec$fR8NpefAYZPcX%l9V$@RP8r z(O`MmFGc+~-@5m1QQgGBa>~Z+7QTKY&!`vj0AO*%a~>(gqGbhX%Hy@zng-#FC^rFU?6(+rx=c3=G6|)olshB_ zOwM_KVK=_N&|5aOGw=CLGRx*01V~+83ap<1VD(1aK*00VQ1KMB>5l(vHl*nZsBU}q zRmrOp!C*c20Ni^t#^E~#X|U1wjJqK_aFtA(*&Wy0O)92K|DpABMaJgP&gFytj~<)g zfMT)i)yi{2sK~tF>1iMsA6=8E2dAVaRb4GuPa&?sfZRk$L{z~q5MM^PUg_Ch;6yVm zOA>l0`UqoE_3g0a)h!ptFdD}&kYxA|`nDx<@4;`Tk`b2>l8 zQtv`ie;Hz22@Q{tRIx9!5nfs!3gu57sw9P%5EHV_TM%2uU3i85)mn3OT2Xmn!9j>* z)-)tsv~p*+EL!w_m1eyk4h;-MGW=R!oS{cK7Dsjdk=F*3S?Ys5wlNi3c!c-H<)G}! zM3vhD(}lt$clHvAwJ))Lc&ve?p3c+zk93O z-4EWGnVIZv4&IArAmr8i^S%APwFA{y&ajHj}wXE$VvF~0WI+bB&4K{!fPWO{6g5~@>(<`l{brbZB+5eP~FYBcND zqW}TMH^C;?;ta3a^^U4@uiXfoaHhygO>q>wrvh&Pxfo8JnW;Gh3z_KwyvdGR z5GB0!0NqV2BUc8~)3<`Kd6CYDpkHZ#(OAsLSx>`!N$zpNXgJ^^%X)v4_CC7blh-0< z)Z?eP`U1hgU_gZj#`_v-NtlSe_Y|4}uaCssm?C?eFQi(yAX@g}W^)KN$>DlRBgzWN zZ&)Il=@ln?zo-j5qXfVy@=oRGeSS$GKm8sL5emY=Ih|fatHWoni$#8rCnm@5l!~dz zRo(p;kCkkD<9Km5%AMvlE-jjEQX(5kaLWW=Jd3=JZ2ah7QZ}}*cxb+d4^Eenfq>bY z_b4=^h|2ZUt>zX+2--{@YM+WG5zxP4+rbts_0fbu{m-nMs_;A!X%FfCSSJ>x zI+d(EYVh2lrM~)6_)Ry@0Ua}T8>JlfID67YTru&-8Pf-UtG9Y&*!{6zv~yoTsQ-TZ zf2ak3xiS3da1tvOJY)hs(zgw$khAJbAWDwT{l(eLCUzobUW-}rQ=_{K|MFnEu!Ba< z462`HyCCdZ71$DDS6lBRoD;)sfXHDOVz3*E&73uC$Q+!yZ@}FS?SrOS1TWjVgDX{?vI z-NMsW_3}3w>qO+mQajSW8dZ#y2Rx?lmX?dFbLow+ijC;j)~>cT&D1|F`=1uBTi)&D zphqtYHTVKZyLbC~j^VOfao;rrR<{3GFnWttuU{A+g&6EQyKV;m$vloxp<9|9aF4Bv zuMB;R?rTL~^}qOu#vHP&&WTS^k(cedTL+b}vsh}?KTD=8!hm!oF%VB;o{CVIpKY~v zrRX0I(Fzk4EZt4{q2u-u)!y*rZsdz%=L=gzzo3Xfu0yU*`%tW3nGK+Aab_fp(wBuP zxyMQ;hy}`0Bm{l9CfbQNzA)pgK@sCvIy{_|M2dc+uhZajBP~cei#~ZCC?#y&zQ{G1iJ7yDVGVh;bqC9Yn{Oo zWS*rw-k_~@!-{V+>rK4U=cQ&Kd&{J4d%LoEgV6UW2@V_l4-U-Qg_sd#;{>9%saYiO z8mt(!C|Azhki$O5{e3STBjUW_Z`!k4@V_F0zvxjf>@LdKtNK3*`j$*gCCl-2DK%WQ!h7Bs>A*n zhrk$McJg9c(@y1T`=NQ9!j**8N>2ER1v7EpUxgm_UzZ=Z6Zas}Rd>Dmmu@s`O#iMk zuBt^zvUkai#O3HA#FJ~POlD1Co*nCZohio<*+rmZIYjep+`IrRo&9}F^(I%SFRR11HH&CB47$*Uo=0g1jm;%ESHC`;0 z8n+LZJKaeJPTIqWEls0GMz^yCLt&=3k%GyBN6!Q2F{XSMd}@}%Mb8LWBe2-5KoygD zeV{$%yNAZ^9r9TYaV^gm{}Ihw;sE+*LFCa~u#aK9rsi|S`R3z0hYW@?a=OCd+GjPV z1=IEofBH`bE+ycLpsq6Jaf-dILU}qXye!4+4)(U=s%cas*>qK1xx<>Jhc(pzzb=C~ zK$1Ipn<@%Q%hAferjw?R#Caoy(MPC}40HQ+o88%aowI_A_?mTg79m!*Havlh!Y4P} zA`(|7niH0`A zo?tR;5L9wOiFnu3FN+%#TsTDCrU5!B76qFphPAmOKH$nRlPWSd6ZDX{eZ4?=cAYH3 zxH&BW6k3W$5%!uX(E$PInR{m>=VulCp?Cu!3KHOf>pRvLEjK$HHfJUeX=rL5NrKvF z&$_Sph3Ly`RvbG(i~a(}3&TsqoeAnK+;~2E^QRZZbSA>g8LWT$B8oS(6u$ONQ#t%m zdjp?uLL?4DI5-I|yHFc1D`48ei#@?RVD6){K0DFz6lNUe@BH^@O#)s^HBkn0&pvfD z(7&_kfquwcHoG7IzZC>zCzH`II-fX{ruDav%o5(tVDYMToAq-@=TfJjau$a(v|`Nl z5pHLIC{eA+2Vm)t{P!!PZzo3-1L1D_7XiOwy_GZ!Kd+?j$VY6axmG4ev*AM35MW%! zr8;8${J=d@2$mRejFTw$yKzgQoPtidCBL0W`BIms-` zUJ)m-4tJ1IZfs(dPp#^{cA!)Q(nHVMpgs$lRs%#av1C4x#`i)2bJGfS8k1VjvL44RfP zS^=4mQ$swya$=N`F?peVt}U;0`ao&hJ_qxxK13#|5^2Qf7HFi2546I=PFfHwTi(S~ zlYjwGsIuyj(joc2aL_?S0{f-Kkx$cEtiw#>a^xJX6fsX=3$Hldx@bnA@Ptq41XjTs zK-kwJkt%i3CG}U`X29U_A-&kt9K%-H`98wos%FAD?Sx~}(fY}O8t(ztLtWyDaK$9v zvLX3m&_*G!IvOhzJyCVhtOzQNSD-1gehA_8CeHNXrcu&0#oHNb3(l*xPbAZzT}v{^ zzg;Ol)jb+vLWz7`^{$INqu8icQ6M$}mLDOBUT#Mrfn3W#NWtF%SbL%fYMMDKxkh?YEB9KHy6aYDYKkJ86a!g2-U(e5gd+?AR(wzl95!A zf%<-QYC|C^9IzOw6?6S87XvsE5A-Kive1>*lc4|=Rg=>Av(NkHmYBM$QF9d71T9|K zTNV`BwA$URvL*4ftx%y-pmoaPk@DEGVsSgpH~Y_m#9O$Er!*NAI|xiuqe@Pm(_l;+ zRkF<=oQnlBNjr^hK><#9ck11Oof&4l*UYA@^XHlw*GZ6@MHK~tm6_>>D8yi2ZO18m z&$vJ-2wS-bNr{7l8Sk|UYL*~>9J@XLt>%X~1Ae6!o7GNoX@I;%i4|I*oV3&e3G66` z@3X`|h|!GBCFnBsK*gxNBX$G(O3HRXZP&C^nos`(i`+<(Z_R<4 zPO)&&x}4CX2AdG!*BC~NMp-oa{SBTWv@iyS@{9*AlNf=gI}DSFoSS1Bkud=_CSl3x zhx2ViMe^O9_$VmKB5~t4WNBKWXg@G0PqXpuAK_dYURXTO60Q_3hmBP}F|dJ^wr}2@ zVG35xj9|+QkzX@Edy9yjGiouKUlveJ6j_iO8s`_cCBgeJJ#YEJY2YYO){&yCVkmuu z@)*!lSS5UOUfdHr+{`zUOGT~{z3e6r^DEcUk4Pu~)?k47jqbo{Z+hZq=xFB6LccA` z6-%1|Q5clkG=@$Onp}Ggsi7F(Ar4%fFBf_bdqWdF2>)iDM+PEizFtrG@pXKBUIu7C zU0)_9_9og+ZbZlA6tl}2LnDk27mQHI5`;2L88IM9c~uu_>nwn})=k(*0>zcYFFJ{; zj76N{JFpgE)*>YvF5%=5eRuF~6NtYUU__+5Mi)ke`g;4k z9KIafUcF1!t5LuKCsq928%9@Zs%w?1sBT}=-CEj}{5w-kutcbm>_{wgSUSB9IaAa~u8;3wutAur9K&&^MP~=L-d#_m5W@*by*`-YKwz8ghFn8sdGK_$a%OfTd6oOgkwgsyZwGqm z#f0UwppcDYsF}rZ?X2#Ga(S^W$xF45O;gmf7;xi7##N5s0$swKRJM1&HxLe@5y?PH z8Ewfu@%kj?hZT+y}-)YF@xh?rjm5DKg56QMv%@VJT9VSk0xArTvhV&ZwY9XBsJO z^5MzR31FOGq|bpq(m8)EVyuGd=3k5+Y;EjayM5ihXuq+eq5HtUf}z{=aCCKR)K|Y=;#UVQOdxrOR(v2@xzEdkd$8Ho zzhCYA(x$|TD~lp0vEUWcFQ^e+nF6Dbp1V1}6HTm=dAzRI)WI<5!!L)oP4$xRh|s|9|>%$W(m9rjzqokz0^q-*NC?wz*wb4NzCLf>AU)Q0)rP$D44*!rtVlmO?sW{EN-xpeHc6xl?Kjpye zQ#zqvYmwpE>|p5Li}NTnp}XY{$g9bbz2~aqsO@8FBC$3nu(pa(r%?GKm>vC!*aLeF z$q@QasZz{D2NX*YkK^wC=Ah`IUyIs;Lk9}*p#v>|>E!`a!7}+elzR9fog!LDHk8o< z5pfwy@?!>PoHBytxTH_t2|*5!{IXK&gEnAt4ws6B(gnxC=|=7#ae|X{D;u0BuS=y3 zQDkO=j4^WpPCbX!MiC>6fAdYmR+j(AAqo9$i?1I`Yt?!!{`xw#{994Iz%W#6 z9e&TcC~G8J_=LP|F%1_D0re#aRfGwoHXLDg^!25Z=nso1F*OL6zaSz4qcyL)n8Js_ zJmsvHnS78tKf%k{Z(4h-LDhA>+r#xqT^*!Umv2l{&*35egUjZ+PXCG)3)k^LsL@V= z;=D4rMudJsae{WMJ#wSSXa|QBJ6#v%3CHObG%J$DtXeDb=6LQ>4 zwg7{^$*|O-vM63bbp;hdj+)AERdk85lrDE+EXtiSd}?`@byga9{Ee}*eFH>eQwg`c z$Kw-sn7)wKb-80Sk6{bF#fICY^bJQcEE2Q1^=&_PsW&6Jxa=u90=bE377%=!qSo*# z@x;J%L|_s%{{xs{yEf7-B398wT7+)`pxRPV+W*q3VEdm;e)Np=|6i-3QFTM%;76_Q ziuOGlt6)*J6h$oQ^8oUfP+uQZf<*a3$s%sVEr3QR8g^o*#8af%s8`RHWeAi;d|~V4 z;cUW741#BtAh4tFJ*URIbt&+f;{9)S;XSbB&KqKIuc0khKqID?J*lq;SAe7Pb|3LX z;qpzJ%i3sJ24#iA54ctedTvx1E=Smf45t;DQMt{xs0X&cZX^-0s<^=)C3fq9;2>T- zw)^D$^A%*b~E=G{UeI5A4xtE(a2m%0z^79%hdBRJ-@wUz6 zQ%M6I0^CuAc-e8kz-1BAakV<@p^jjhx%%DGdpGmk|0dz}l3wNTLg8ppX{Aqm(J>R? zMT*A6p036@1m_92%9B{@ccVX*Pjl0!1?P#`iP7Ki9_(Z>AITfx@@uG%ZyLo`PQJ;_ z0AqrI>RL7Ou3><(VRBe*t5RA^+l4K$K#GQvt8v(&&i2WN>Y-S}g?7&d0Y1ogg zk?=dMOv5Ca#ixQ~*g&A{SR3Hx;_(0?eaILQZqW1S7SNg@tX*Y84etOq{J6-@{^Oyj zRL;Y8re&TioVIW#GPNVRKkthsf;a=LDa>kYqbHOCTW_K-QNx@WgYUN7!05; zh|}CZoMcoUT87#Ne#YuOD9@~DcLT;d;l;;`!-q?=wnou;Xz6=Lo)?%r^a1FOo{gsr!^@CbP2@o(4cLnnOquP@@MO*Mg| z4i$TXD%CDO(qZ`ewn&J71ieY;$zGE~5ahHs#5KMqw}I$~?;_nktwn!m*P1#cPg)K$ z@enF6qA=|nIV`tq|5G)-!ShnC!sK9jx?vL|q8T4;suS5iNZ1n&a@I7tyC2Y_BFVKC z(t@>VGnna`dwg|xj{eIjR8VD+ZB=ACoBGRGVy8TkwXKpQ`gxyJROH>EFB%fUvjYOF zKYk*&Ai)`VJ#gq#*ia>qp0$D>wYnnh9+aUj2RRL+-^#w2x5auE9hQuVHa2#Jo`4< zu@i*6NvT0Ea0Bo`n^!Of)NEwA&9Z|zcX!}&1AXfeh^nQRU`wQDe9fuini0g}*~t9_ z?-ZW0YU-O`_jx%Xea?L{5LsrhNQz7Q| zs|%oC=Ag3_?AD%v+ZgOTORLi+dGp;JaE8VL=g|fRDlE5MZJhGpQd6=jv(VQA0t@ss z|Ge|xa`u1Hfzs2nvHjonTBVA*?STla5613yuV`6VV}jT76b@&w=1FOlFh+EwK>Y9% z6Gvyp9LJijt^={~T{;joaxaFk55!pdOtuN=C;LsM_nWc)fQDHP*NN@hu%@}pYX#gE ze5<$RDk$ypgTJ-gx0%g|!xjA3W$=0d&znV?cG~SBmI!}ukYDJeoreeI0w7v1*@P~_ zd+ZJt|6?~@Jnj9rY;;ejZj(6N0qayTB`1IlzV9_;2fgQaNcTq5Ch)I155V$zvn5iH zn5J2eWe&H;c$F9%7}P?u)drX?M2x19L!=U@SJJ5#5fY4EhFHq0|DibuCk-orwG!a; zS2V$v=&7GUO^g`0#iw=;iU_MTK~uF~0C?B3qk>XH21jbdL7aGNAjnYn%|uB3nSpD=EN; zaP$QQW-na$Z2HvMNmAYL4~JgD5c>Gp+A-21eTI@ z00&M?w1l1f3dBbo4CPljX6AxeaU2>NrE*G@DcAz8Ty(~<`UrVwj9tOpF`Z1S8~f8{Do;oQT3JanS_UBMVswRnwiTi;{(+F4 z=!8*chu0t^kV(`<22PKTu!hEU1V;H3&;?a787p5CMEvA4|F9JT5qt$%*TKuXOgsFs7=S! zs^SxW?bD>EQ(p}6_kwkwof#ZXXGVy3*d)W}uxBp0MzGo9} z8Te=za&eyv{XEIw;je*IWLv=(|8!{chDyu%`}cgmw0H_NauH8fh*CcOA{qr4LEKT) zn?!85>)ZA0_0ZN}vTmB|f>b+)V`6K4A2W#^$>ej>(D~c*DRs7xph9>uK zU)MGcSIh`-XgpNnN+ae(jhCuV=o>Zh83$+kTI1ViwF6GUKA6v?Afh3HBc1eiPb0x) z;${8ao6_8dt8>boTf8{7taMVx_wTPduZifnzZz-ENm@?8ao6anFw$ZQ1j%?~#~A0{r}p>tU0$DdPE^84H;=c656e4DV} zKbk4S=yIU{7IpuVW{REt{}Uf4DQ`Heiy?ZS{CSgKXF$>B<5PGC;J8Pk68}1^QtDq$ zi38`dGzXU?GW`C0GIQ$FE1^8Xs62CgdpOI^zNj_ze{0ywd)}*Q0(;MY{@wVktlO_W zQ!Pu43Eztq9}c0v{BHiT6V4dKet@t$LQf5?c3gagB@m%jSc}5`qE2j{KEMz`P#UD1 zM5q6S;&Tm0I*5f3$1LSByvj20T^`juz1k9X#D^Jpb-D=eM#S)-cNjIsXUuvGGdava zx`slH-CD3G$dJCcSWCa0BpC;C92xy8FHGqI0~$L^fLM6FF<2IG68XiE_6eycOdzQt zhPFAZ;k?-xp$T^1tE(%-LCNp|I@Om}X1l&T3OQA0{Zf2_Y2E0Mt2j3Zv6I^8gbuD4 zoyj!1hjlkdUQympdVutRcO-SkIbCTcMNjUpO;gY4o2mQiNc0kOln(9Fvx+6F7InqD z;zjC8O@FLKhTq;aEs;>Ypg=TRUZ;1!!6K!O1T$?;Wj+sG|Cr9+2wOBkAA|JP_bkx{ zZ5*IlU#@r3wTmWgu5=0_(_!3-ZSR<|Vxh@i`ihHlWoI2|%p_W^#x?hGlplCS)m?v7 zf^^t+1`Fq}YN4lB{!7}T)|&N7F|oWm`VUPio6Sj54cOL~x!`uAm!^c;J_HSMm;yRO zzsoH*JM~cOaxbS$4uWzXopx{h*n32zJ$d=Dzb6=I?*$KW6@1NJYuj$?y)PUd)2un% zBj*|=`*_tI!9R-7Cf_c(moqDG(2-I<0d3dHOJ@>(ZmP(bb8a}wk7oE-qhDnjcI?!p zAHSmH9Eu!uZaVoH7npQ$n~s?ruwBsr&$P|rUsg>Sfwn}^NH8?)3!$L2a{t+H1==qiOu zN-iN0v0G8Ky6E;|m)pdY7un$GF-u`T^2~&*TNr_i6pkUT_kej|{b|SQUR*w6dB;#l z)9IpfH&jjnBoyrjzZW7}8_dnyv*$FFbCoSf>1sMl*a*3hL>C+_Ta@x`ABm@wh!6rW z8mX@(l!GDaMj1LmJvvj#REv~5EW~{ztLtpaJ4d&aQOm7wha2tY@^0V?KI)8Gg*wN? z?OknmPa6ug%sJ(>csAkcLbIwQN0|6rMbj0e39|F<*ZzcQ5L;O6+B)ljaQzT>74lHc#)z;($k znEIPnxd$;69da3{5LI<*wH0H=;}CzzE7zdFJ^rmQF&nk2`UQ+NBR z;+v?9vLS14aBWRe1}Bf1CQC zL5M>RBj4<4fI;^CNiY3TQI1;PpFSlqG>WPj)Ca(1)sm>W!0X+Q$KzCzroOBI5`$pL zM5wGoJc@XEp%~VtAf3;_aXEYl&CM48w?79Fuq+RZo3Uf+z~!!kU-4`Ludg zd1MI|bd{NKj`$Q0I}c?A-gxnSSiawKlY?B}R%7PDHWkb{g)#LNmt z?c{BoLWn=2R>efY{_bFmoNR6VD1MR=X5zj~dKI`jH>W*|)y$1xqH$H|H@7>J*|&5h zbj`xeiGjH}#GmPOGc*~_CUJmK^<65(VlBt9jB$1cns$sgM2BJPVQa0K#-ILeeWhgs z8ef*vu)yI|Qg}t$4HJbmk+Ork*o3vY!2_VpZT&w>Dqwc|DQez#?*(4`udd(M``TDf z3R9>vt%zl1;w$Y|c)+FS%kH0>jeptFrByan;>J7r-+D-n{i26_g1)jaADsh2$geOq`V9w; z>dL=Y2Iwcvy?Y--Wz;A8vt3P zW`5ZC1qOPGr!5<9&AIu_0P=3<=YYreV?Uo6zPKLmK(#kt4Gcq=flyRoDbY8kFcv4)be-s#r^jfy;<9ZTe&qH*m|YgyfKH9+JTT|zojQ47n7jQeXHC^f=%kZc6F*3^ia zLf)34q*>n1#^(sg6Hlm{YaI~p@h?NLHn(WV3y~c{t)3$Ec4N;Usmkf-2p>r&q7VK2 zAI#UGrJ^@ou}=rQaWW_{R32xvpe^D8psxyl9&@r`L_pm}Y^iB)+nSf#Bz- z&c!=-jvf|Dq2XnD`;Nz@Ko;Lf+>=D`V94)(X{=yNBlK0}jzZ9`L4_`pXKXX2AQ`Cm zs1&4^uTd+HO>s*(P7cX(e-F_7c2X#IYtUjBa1m2S>5cqVQ%d_1rGB48TA%EppRf|p zW;-Qp<|WTx3AeUsPP)&5?GKoBcU9QK{r4Q9>XKM=-==>ywAP#M3%pIb0XlhK_`u94 zDb%Ka#8dbz+r6Av2P(7a2X7c}mXFgbO7^fUhi#?46bU_H!ziN9973qrMxN}es< z-z@HV{HtbXmpmK&)A@2S5~SUShRof=S#TQ<)89+z#R%~y^oaT4vCc!_`0IG9$A0to z2+#bGGujuAOV> zZm~43P7LbFHF}XIr-X|O>d-cVv%-^NK+!S~NV;lu5^~*89>maFuGD}rNE2JRgLp$L=4_dpD*GNIz zDK@S<7)-uHL+JwW0ug}_c-g?EAmlF9?3Rr3+LrLSg0Jy^;;RdV<5vW^0O~P59L0&s zCZB5qpc*ZTM)?R{W8=a`^TH{>a8EpSo`JTf1^dLsJS}$-szDYRgxDCiwYWF7d+f3n zG}PslYx17Yw=EU#{Y{(w?!SPi=c?KX=Ai1|HI9T4irmSjAjQuJQQ|^puwQ4(>bZDu zi)WWspc#wX>FJYWaA{?MuC^cV;;eL7m1jfXAyncHbHr8EP4w)!B?seFkf}UcZDIz= z{8;z<9JD}`vA12IfnLh#x$>`of~~pQdoj|#)$L;}yVv7g%c>t}(EY9O9SbB=-_@+c z1Zo*yW;j%)IrC6T1g&OoN)mYhV+yI61%N~Dt0Ek8Ut0@cOCNVN+nBHACv#O<#+CB$ zUwrR;QEL$B#7&`7s_Pf3nfkt)NjC3Ht7Jhas)SUszS4fb`W|aS_aqXwKU^k2poMUlR zn02dt43iBXGE_IO6Ln^k*u4MuP$ft!h_*DBIOg{pf8&(FLI_TWrZ*mza*f1k#FnTMkpFMm|~ckw3=X^;+&*vGVlg7;91zz$G>h z&P|vNk0HchVE??+Bi-R5-+79O$v90vGk0d@&1~59+Qsh)pY+9?uZ%QDMG-JzvU;?0 zb!Jeh1kITBY6W3l>W2%5_B%6YGp=u;L!1~!FPyk}^nPaIB9|%s z5OX3oEQfq8(PvGlqrN&ZmimReSTb{n$uveiWqth5V8@9C;Lj9Li zgCsZ+(UF@{NHrKHnVOU3w?pDEn}sQ;aAH3X@%`mYPQl)?_qN-^>-osBnKyI6NR>iJ zprFs)cct)g#6HKWfgXTuj12%XGXS!lU^0fZ8dIDr(zy8ZJ?)R&Q6@Y z3Q0?d#Y-XXb%|_8#vV~Fnh;c0Anoe80AM>``|Q1TDbh3#jS5J^QIgk__R25A8nmP= zk8pnW#rn*#B5#ZE%EZ@LS&}f`Tc+@P$-m>bX<5ooBt+$hGoxGQ%Z2VX>%7CdMMU&j zmm4k?{mW;NZ|@lYzdViZb#8FvZ#wTq8n8oA4SsWYL@OGC?rZ83@7RwnRm3m53?IdR zlkt560^#@&O=Y{ELmC3)pz|9lPT%>o1QQ9X8BZW@Gqdhb%}b`b{HYzB$`w+^i3mZo~=jl)?_#PQP>UK+UWdTm3>VrxuppV zhXZUJwnA~Oh*ybs=DYr24g>i=_uT3PJ|F9Fj4WW9zX)%*yfuqHuDN~Roq686(|d3Z zf|{oq(`Y1rJ4d--lI8A0x-&-vr*D6ZuQ;<)cDZi66~i~f{NW%i;krh%5DOb|xs_*F z{s@shs`1~+1f(6s%@3rB$$*fVU5xw91oTe@PLc4UeN-`i5L4kPU=a}_>0UvPJxMR2A}kGsH-Mk+WJ8-3o_!ygT4XiGmj05Z`nPPix@(CI7gC z>=aNz*GryMwrsA+x~Z78!dKpG&Uodo!H#P+O61~rLE&^E<_@pRLGaifSK5==cG2S^ z0~G|eZn2V&>3k1u8blwy$&QpBF`2JuI{Lj??x>;BW7KTxIFnHvAKA?uY8Htn3wH%i z(z*r~_irp-Q)1u|Sd7*SxlDa;uLT$I2WfG+945I& z1a)xsa@un?3bp{obp3Vk?O}4;h`w)^D;{|GRlLF7*k$2jrMX)_Htzw-&hvtjdoDNq zL8_WVzD#&-Epj29dE%|Qp~j}I@~ee}@sL#R@@T34yu28lD=&B3QxF}$eD&_QH*IDA z7v5R;RH3B#^~Rya zf3LeYZWmpToEbyu2vw{PeL$=K!iV%0J{Q2!6=Yv{=-}6vt8D+UEk}NY9q2w=leFV z=g5RuJ%<=e^nZBQYM9^+1uVtr$mu?!)isBNEoXrxA}NF#ISt2eCQaK86CXLO4i;-& zfRU$cJ-xleGtGyc`{#)60>)kj&*Py_KX?D@_Ce*e_U{09Z(mo69Xn|Q8X79%hSM(ZPK4gvhV~f1j78@<{oI5HBqY@v z+fdz(jcXU*KQ|0~FMaeqr1raX8u3>5+sM>On2Yh5=GQ2@LDviVhgiTdc<%i!FQ#S; zE#In2lHtLFF3C79O8*gXw_`^d5PjS8vWs}E$}%;+;804u#nRvNg zAAdFu7E_fk5p#Ou(e-Nb_yWP%Q*MI>f=<6e{>gOo79-_d)5ta zZZh!D4x8gVxmI(DIp(5}@h(L3!`LmU%H>G#Bz8dQ-}8$ep&Q$aO0AIp&({-vwJoHI z(w{a3Y}bx5Q!-E84Cs9InczzR-=M5sJ$-+BX~7Z`S+7uaV+_!`!_+~FZj@GxIs>48 zN?F2C6ke(X(IrFN?z+pR{V-F@)tBIVX258OP3-fIkh|PRK3sLxrf1gx)K!-~wPdSf zp5xw7aP^cs0+qw`IoHbbLm18~!hIM|H9p*E6?L2H!SXsG>D`6OH=4JmV#8phwMcw{ zN1Q*dP9+a?8Bjtb`~ljE#6OGG9nx&Y2x!4tT|SVkK_DNVf^2*;i^Wh5b}Xuqq{(~E zH@0W2^0<=G@mw~g{&TZTosUV?356609no;>Ot=lp<`wH|n%#&2{Iu!w`kA*~) zS(S4)4C)0@8NOd=uzG@I%R^GmF4|V+RU?U&;o@E)LOZ_5(Xk^_i7%KVF)#&s7;`k= z+D3C1#Y#{@;+SYQTt4}O0Ghwl66pJt_0bz$fR+o{yFWN5T&c=x6W`9`O4uV_W6UCb zl?g}Gn>MoBRc<;z3&T@4O3k7b4Ahh+$!x1s2)+DwxN>8V@#X zjgj0NFS>e&@bq9rgk=~j&E3KgOf{QvATBH|m=s-QprM0gWu;6pzzX*c<+V7D=@kz1 zFoV>(+7(T zC>E!4HX?V@M{>8O=M8V4v^HyE(cRG8;lM zr0Q(U#?1ZJl}i_IKD{If5d4t8gm+l-ni$@ZYaT8;h=NwD7jMJo+K!DXiV-LJii4dDb(!lm`9uww>=e>z_LRY z2&@^+@#Vw=G}DO*E;|J$S1j6eXVwk^pucYja~T;=e*^R(fbeIwsDYI#?Vt~XfuJVi ze%&wGAOlx^zy9Qc;-A!*P>g$m%^w%n`fh|NkM`yyD2Kg6%;7Q;<>3KgFTv&(L!D6g z5HCO!e@T+Y>4)eX1K+7}DPf=f(`jfIq-=KQX5X8c@M(L?9?<2syDFEX4IhXAy%?yi zjxob}Wh>7{0(f+i0^S1CImODYW{hpF($Dg&UPcrOA8^8ml=^Bf(>_}DA9A*()?!Gi z@l_d>0dyyuYg5}H;f{;uQP9fn0x;;PSrP~t){z#SmCjbHfJ}k&Kb~o|jOA8wNTOUc zpT+#npU;iSi^->)pPoXsjOzlYa&X7?u!bSd7r| zsJ1+&Hvzlq&AUcV551!NQo_?RiMAp(YR_WA3a5Ru1FbXW``S?mg#B ziX?M&x2Ww6<^Bbmy=0R45Uh!g3~h%uqURJhEm33(O+D~BO5y~+kCAY6B2eQC-s#BP z!eP~@j#wA`IePO&<;@2e!${R7XP{6~34LjETFsR7c_ur%=6pa-g^vrHl(YW$BbIqd zd#p7LJUB2rOtHOvt&1p(1oZ<|6i0GX$23PGdoTjf(v`@65e}2_>ma>kai_bVt)qK&W)iYZMr1AqF{b?I6X|t%<3l3I z^w&9YZ{iLXhF%?nK!DX*H`eJvq2%?fyM6yYlBTLf@o{uD5pyI&Y%j_>{^hSOSAD4= zu6h5;(Q7Ld{0$*Js*=i^)3sK7c^GWnP!}wIF`Nd2o|!44^8KPXa$Q?zE=8@y&+9Yj ztpZ9rR-6u>EZx=zutg|xOTjRsE9G%`ZmpFiF`c#{F&zp2T!t>piEFYHF(ZT}V)yy( zL5eTwLFCmO;zf7IWTzRW{R?x41qBr3vO?+21ey5JLN0DSjubJSa_PZ#0Go8_fA-n; ztF*{q7@QmQOj23<==htBE+XR9G7rwF4tl*1MiQuha~*X0O^p{$FdIBe^05b#Yx zClp|4qp@)N;}71N4Dywvf@a))%9vzvm7%N+Z7L~A^dLWzHHm;fE3sIjHakI9bdlBW z&0B$Zl%#5)w(5y$HZsUFr>W+H@8TiU8TbK4HOljx(6y-pj9be>4vdo|mk}Q1BuJtgmb(h7`2Lb7uqUQihDzab>A1)A=mk%Le+ty&vhyN`whe_w$b0 zde{KXUU@)?EpXkvY~|I{BLO?9tVqc^z0WTTb6LxN6rkAQ+0tzhgVJ^iL7QrxoiP7M@KQqm(SWu74D~k`b!hs)K z-&04f2}~QV&RslUsWcMIV)=~{8`Q+5ely$9<)}z-z&|B<#nusG^li(t_IBxilHkeQ zexuw0Of^xO(g#@)Ckz>cjln)(ApVXZ$F4G$lX2x!_M0eh=paFE-Qss?o#hPD*lp45 z-mkoyzg6!~wW-@3MVM+?b4RV%Q=2>W-X9TNQb(v6#vt-5WD5&H(EX#IYGx|_>lzG4jeHnvVA>JjOJxcROsD%p5 z`?5NzY(`6XK5);~G6F>shn67j5ZnaJDFV&gd#>j!fWN?uyCg8ge?0%oDXS%?>vBWv zQ<(P$JYnlQowlODM@*M@Xi55QnbOBn)j7=`kgsRnmoWQpsLZ(GbPDm(kAwF5dVT9& zZhgU~C%X>g(nr1dVe9|{D$-!J2*Ek zKWLMOkLotF>eJ;tdUrCxpi`@6MqdLf{Dxr|IrSoA?uZ1u5Ya8TREGK;pl8lnuVSVhs1hPmPg(Z z*@?&pQ7Pjd~F;Uv~%qe!?Unkc9a#=_`%FM*Jh9izzcfk~VYh#Mk6h`NNhf(|H| zU&e(oz(*hhtU2R%AEV9dI5WMRnF_U#6u~O*kv%p{`4pzp|; z5>0q%)qkg=s_Fb>qU6M*ckxbSiH@v7NrHPj)v+V(hjXjf#o;S!u1ouf?4bJG6*oim zFjaQi`A{dDnfKM#f+}CyysNmoIwGl?(z=0EbHtwKh#s~LP?ND-iBXp>$U;uL(6k;Jjv{CO2oJ5E9-N6YjC+vaLmE2|vSMIRYjI~o9;Gjq22TIvUx!C8IoXVWxRcn} zt0>RS?``AaedMrtWUnVgKOua3Id{a66r1Wlt_+Pr%MTLZfbSm2W=9r4I-qV3l}uBI z!&;}V+>x%!hb*dW0o4>n_^#6nN3DlIAN^wrV92@Awjh&$T%g0u5{`GN*7l%x`J);b zpCmBzJgY_J@N~Lwp)H7%(NY^ASZ`bTJ$6f=|qY{W@5IdjMihpjDC>6`#TTr6Jh4IBe<+MkFq>MXmkUuA+Z^>y`kkQ>TC z#BG7~{G8I7@M1A`+d;byY8JvgQO?}gju;8MlF8a^o}G2mE_XJchi9CQ2ECa`HmNyv znp1~I6^9NzXKHyz#`e`sbHBUR;t)$*w&E%S+-w4$(5KV3+JB_B;%YM^8h0^-jJlcA zpiI&-3#kfpioPb05Vl6RH-F5cgtPXPgmFh107h}<9( zZd-WoUSPV1U^5F!q|?f=qAtafy)91c@_0~{d}1kmelOQ&p6(b-))*bxbLGt1l;;Ou ze^)+QAXF?N3tb*_tqt7^;a1xOs}9xgwm!D2-g&Ii6%3!5Mp zoH^+$uO!)B)xEM8-&%9|S9fW)J}OrOwRK;%n+YBQMqN%!WZ5ge4d zvIa5KcHv^eLq?x@Y@-DjL;#Ezdv&`C0uLNPDSsyK5fLw$rwuqF&3NHd`E1@^OJLmC z<&}VPCEBqVR;V&VshV_@u>M3kxBM2$(I3yX;seN-&V|BHoEjSNKz>Df^j|{njb%2` zh|H-I>}$`)9>u>>51g;UHd1sG+1i#y0JByi-iHxbuVp-i*FQ<@93Q_0IKzc*&_H$= zz_Rgk<(3;|gn)W{T~eZiY1qqO%$h|J4v_FpaVa2ybV&1YxLYSXRf-w8zmbiAs%iO3 z|F`1hzoOBxuyHg0&$;)p=0-g37`p$N=JF90&{P{Eq6Cqov(Xqw>MseDVPPRBE)-G( zYG*Fw`xoxt)t($VOJrjZWKdY@m^F@jPr0gl(-a{Kf)l%nz3)r71G@rpz1b6iOS6{_ zBr%5IEfLgnC8jMO6M~Kg-+8oC0b2IEEg3N*_Xoi}yFH257F~M`?Y17jcbYO@9=`6K z#|-8$Z!&zJ*WRvdJ`N3AZYU{S(BW!l9Vj^m`ZB*L{gqD{6~Hm0XE5ZdKdr?Jo@ty? z1i!8>u8yw55jbU^A&1y4FIW5o3+g8_-YVW~`Zl}GW3DV(^e#2G;C)ASN#5?pHjYV)J#R-g`^#TN@2=8Q`kH3MoGdpeUmrR`Ko1;0Syy zM1>&K?B$87^wbd^`;Or`js5r_n-YtN2G_M9e9BQz?;xTwfsI{@0Pa14X-PR+n19-> zTMva#iV0o?NFdE9b%h(^Cr{d3h{DN;5+|&ViteWF*tCjNi+z@u$}ww|wo`m9Es9Yc zwDWssH4I#e%mY0$7;TW5mM<0ule2yi^fBeK%#;jK8(t!w<$~x_7o&)~r3R_2MMHV* z$Kgv9Oo7tOpHbh%3aP-yB3?Ps{Uv@saEg7|&YCJA=3R(81!r0%lW@>C!hmBW!yd-N ziS6CTsR*X=OmVb`ijyL*tVIE%26L*NAqm#-QVnvONZyBO2DUjkt)QQPW%8$VBoIX@ zq^mtq_%+Fh{XlM7&t;UZadH-gM9S7M^Hpt5hcQ8d;!H!!$@y#IhP+qlFRytU>nr7JT z!fT1~$_Lr>ze948l1l^8hz-4BFp(UxXK}%4Dia-=dc{k;QXqbALcmK{H>cDG+Cbqi z-oFqpq^|=5jv)Q%qp3iCVH0*Y@ z@=j^h51*D9Qiu_V+9lvVZ6d2w?Y6)``GkAH6>4?f-62&}8WF<;-wuQ#%y{P(2jv9E zdb#14jSy8(;oilYVyHv$#1^?utAfGSA^x`EX_$p`S7FQpU_b+ec^Y)8GG-&`)vF+@ zXu`#$wgU@;XHirpxf~{b-6Bc*C`qZuMe13b>cH2a3o)BX>SnpWqCJp&-7g$NMf7!2Q0rrFu; z=g1rq*fb*JQi{=1R=StWQql5628=|DKSGo*LGP1o=2m}RM4s1Vv}CB+d`j_0T=#{)<%!JzNGIc^bkr+yrNA{_oJFbhvmK;N3=G;ZZ z$hrOLO@R+WGlGEXHzhV+d-zB1DW|%uIv|=C|1)hhoOzkjef}UJZ`*=UGTV0uWLLjb zYgw0Cfm4%5pQ|aCX5P}^J_E3CUGLF*ibJ&{91J65Ei1+yG&`3pFEwry*co zui%RFD%3!YLIO>q8Wi9Qbn?abjVFq^Q31Z;y_bv?KJU?)v z{_AF-aBhMEz$`E1iHy58ZPyoIh#sFhb63&#-qA4!#O&S`0@bln_Y*DO0jc8~HAQqK zqR>bNbjaPEkn`PkW{!^-?4OiD&nY$X68Jmi$(iipiG<= zoc;};*#grOPw39xrfbicacMHN=ltS7WJj9!ALS1Lin8u&B?xw7J0JC`m9*#sSjSV4 zmJkC8Lx_xLYV7u}G>2U7iaiVK7AhC9pIN}nJ4k(8HybpkPj~3+<&&J@Pfy$R{H+V*<;jJ(2hYV zLL|tDjGEpAvI_}eYXx3LY9s97lLxdG3&uz{fjv|hmT&Yt?wKSjOnr)-h11QhBAS)> zO=8^hl6>i^sdPRsmY5v2ltjljN+b#cmXr!}l!ywDVa*`n9v^~$&9JBkTbhKZbCwg! zJ}L!5z@;xMIk?F9CC$)rDItCy#HI?*21EZUbWgjO_O}e%B9`sAfF2XyS2#Yn!L?6* z^}f~428fukkA4k3gKq8aoLH}YE$yDO!eduBdGh2jdb1ds9+yC9pdPY^j=GYzu5bc4JrzMN9QO1~J_W7pE@Qs5s*M3?v>kK3 zZFL{s`%m4LxPH>r)}7)Jeyu5(yV@Qco;d?reHkrRbY2ZNd^H`32t#Hd%_R4iJW zsLzdQ8sxPwuTa$MaU=3qqNi-D>N#(ovT#d$0oMbBrBK&Phx6zfIm9qqdFhS_IRU27(@tXgjdu- z#Um^D^({7SB7J6IzhO&*bE{+y`V&j$@AXv+bAFwfbWWTZj2Z8lQ$LWv&>-+*zr}sQ zX2-ttNCpgZ>_oma7HjMD@@#H;Kb+6?^MSPK9n--~< z4l1Z<7j0QJx(TO`8(#~x$wJHz%x}2X0KC{Z_ZTo@de#8i z{`3qL>Gbdp@bi7N@&$V8;Jw>$yg}Xgo8gzZ4AfM)O6OEMq^A_D@CdFpU|Nm!qY~v-z7GwyND-phfkm{PuKEeQANkF6|54#1{bLbw3*{C)6XO1{vLgOH z94_Tk5jalX%?W+}KaZ`1Vj72x7P-Nm4v9}1ffBQXsDcLeMhO-i(|A07Lr(262;BSftBtj6qvSjaf*#I&dO2>4 z>b+jTn6Q1DmmqS%oEMKo0umbwK{!FdCCuq@!NsA!Y1qQ%E+4w+&=zs= zX*%JOFi7)oJ`&pXK^9m_ByH|SX9+e1y8~vCiK1!{m&g%y8kx}YTUyu*>^%3WQAisdb9fuU9Sf9BEumM<+p<@uBz2sS@es|dBj7eAI3GwN_s-3rns>r_D*>oAifu>lhxr)dlL2Y5k zk!|YG#9J}sbg53kHQ(n%&fsIk9WZJ;^r^k;wh~Qf40qPP>OvK-xcgrc8TnH{~#E#L`_x2!8}DZ)eG6pgpAh#!Ru zr?sk9heVBqL@~chV^LbdcbuKexPqm^=%WwaC0oP6YZbXwLg0ax6S%LsjVe184`=T% z6c8lJ+X-FfXS!m5c?S2qE>|B=2|jvS|S`VLD6g;)(NA5Ac*sGkCWbX ziavg@LJ4qWMc{!}AxHHB*F=giRf!bFqQkd`t6`DjAhc!Vr-|Zl#)hW?4v*X7Lb*!? zf=uV-K)X!iQlK}>&T8LBVKEc=x>xWQ*0%SbwFrfe+4fj_1kkBWpjh|z2nnUBPDo`~ zXfk4Wrp*8HqCL&o@PkRVMh3ksfOgL;snf32MgFxgg(cPbIc}*REff|v4~Ci$v>Pc- zf>fopv}7S|U!Yc*pyo*N?z7)*U$msPeHU1#V~g#wLBU$KF=pDkbbcT4!=*f6v0p0( zja@jm;U;njY~bfcymVXuNfWF((&m6VlMLQGxCmcKFl|OS2xudgp+~k@xQq2c@WEA) zM)#vlEfpp$7hFF?4Wr@MRAd2hJl-F(+SG5`yNW^$Mgq}|moU}(4B5TG!mlhSGGmPS zNcH=WkjpUnnfZEypZHGYxoO3&)j-Xi+! zRt)xat*qQj%-B7n{AbE&;~~JU{r$1FG7~7_kuSCHR%O!-V3Uy949#PtRDFd3Z_?(C zaYG6>z=1{A@3|#gvuDF9PSgg@NzE_t7xaHRok?(%ncEqCSb6tr%iOE0UwcEQSePmC zq_n(Ipq0B9YrdQIp%FvF1##_cw=4sfk2W8goEiaZ390ajDr8}}Fy8|)T4R(41=9qS zkgzj7*wfdbUYrzzl^Ymf!sx|#?N=#rJFJ(63vPJq;}eossC&L2T~<>*se2Z zUC|@ecBzXdm6qWop|iKl-r81+P?-@U-MLeKDCfG3KTuAOJ!AH+aDY zWs#h+RQ+M@VP(hWt5<4*1~5xO83TCo>8wO`p?nIX8;t1p1*>E)Eeug&M;;yxe@i?a zFw^GV31IPv6$e73Vp7Dq)cpy!2%Ne#sFO;7R*p$~I{R{Gx(@8fFT8 zJ1u+f@sYnk>`C-ia!cMrG269{h9nw|B#y|JX$M{B?4R=34|x#qL6e|ahR75goT5J9 zqhrq2hkNWj^cJS)8mbocAtt=_P}BqwNHJ?8;@Ze~Sh+F@aLxB`TKTOP!27Xte5|X! zs%g12?O*=)4wLht#yr3jCWo``-%-U$#mTrERLaE3Be{J~AP;Ui=mt~J@@*W{DC!+M zE`~q}y}h5n$Me+`pRM$)5vBqfTwoXC@6WlqOvD6^Z3pd3~6PovL&$T`c z9IFcT!d)yc)N1vTu1JC-&nS=qruO2TIGgOE$ zd>zaGye$u_ysp;SYR83gcSvMfdxOXfl;zdtgC`g5dmc^z1hwAJ&vpvrE<=P-x)qL- z%*Y#X(>|0Z4yGP<$z+@6+y7xEBt%KdF@zyBW_TSl(hxUKAo)dvNKC{mg7?FxN+hhtfo{X0qU;HF`Sns+s6X8N$NbHn zI0AXU?=nL%VT%K4qz@(m7Y$<^TLmrnd7gth5kxo?em;d!;t&kh4rci)GT@_^m^wPO zM}LdfUa63KxLBij{9xL*&$#?7I`b+Rnq&Ca&dv8OA)2KTj*W;A&vl^}d2Y-V&h{~1 zJ6dBkMh*&eteROhZc!HehMB|OL$T)ja2chJh2dTKsT>neRC94 zRh&G?$3CN|F&R~zdjPZqNeyW|Dx!WPDp~Lo9^AQ}6iz}m5=9KuER!@C1^ieF;!=Cy z?(S#1e^KwQT`B?LXpg=>qGRT1oaon@cB`CIo)_Ryh#O)mhzdSH5tY2@98Pgc2xeh$ z3Q64A9Q&=70bCb^uw{%cQ{J+9XKx7fzKA{Y@$9#yUMt%4ToJ~ zhR!iQKZic2CV;r1GMI3GY!P=|(kcw22)Rhb6aQh+dm6xShJ4r?x`-7~Y(nJ?*sTIK`n3lg++Hi4q*kdL86e<5OYr%0){=IIY zCoC71N!wyEZv=RSUgtXg2L^l_3f5nP1f)mEjwmU@1bMVZ0knz&M1{6%(O8q~p8nz3 zT>Bwyn)*34X~>#})uBeod+zH6ZftqQD{llw+X9CO^|7&KZiTqW>!6w4xkFsuHo@rX zqwRO?j<)HcvQKUi3;gz!;IF(xd6l@F;Ewb7lM!MYji8illwu(LZ?B*?OCg}iZ#ao% zIEc?rq`P3`DBvg>7ksY;NC=HXwgkn?R`%VI>tFV?BG zl<+WOnp>m7s8d_O#GxHs?l_3DTYL1@4e86l8Lhbn75s&BG}yJ1{~0ldVI!9q`$rIh zif&~Jw>I9xW@g4Hdz!H_HjWlh0mRT~W#7kh7pINygU0*LRg6rIEQ7UiG`eaOaM!9} zstmd6Jqw1VgKT-r;^c|B-o&;BVjgB>CC}hPkuTh{kAzn77aM6SkDF*e-Csw@l&*mt zqH;w7XKd(a0r>gJCH> zRGM-Ar&A`S;gAkg0cli`!tE!b6t?Eh(*?sJFI?Bs*xg2nHwK{RXlpybQK#8r+?39i z`{RoxTj{i;SGK|Ul1?zVHDnBJA0&NRoF?UXpYj!128-0jW)zArdLX*PZ?Tm49?fuA zgImdIZyP$JsHYYbuZ*{Di%7bnwFULwP_5zuM(8^ErIRkdwxO3MNS9S^vgc4JS zQ@P#vae^4K_YQ8XS^A%xfmzgw;i_EheG?F~hR1;!hQ}+Ik+Ceo_sj>y@2yY;KQan+ z|Lo8Ihp~5v5-i%bZNs*0+qP|+8MbX?*tTukHZpA6){pz&E4IVaXOxUrbNc;bVJ^XqM;n-_j|s%c|Ld1ow5_F9t(*kF=$dMZ;DO8J5g{{t)=~73(;o z@ad*15Q$xzo^_fIV}v@==LoYsc;&f8SzmhByAg`TqW7*Jk`2dk;=u~H!5athaBzr1 zN|UB`Vcs(WOu%ZESHA*o*Aubfq0S32m?GPCm5*59inG%8+>npolW@JOZsY`rz@ zxYHRJ*-i|nxLCG!t6EtH&J4vc zeoNsj)u%EVlmmMM56mTdM4ZMpu}a8+t8CL@-5c%I#PVXitFYT8|^*#41cp>o}0pOK_}I#)X~XYZK$2j3CA-npZtxI=H~C)AQm z?ZBY~bYFHKRD`(R#(Ry$&`_EcM{DBg5Bvl%t7W%6FYRn>ql20c?@T2~^+>a#$1{fm z!oor)6gg_fr&U<@sl@GOUX_CtTwyBZdo#WNkYX@BkZoGhX(b_zb0)2@g0%I&>e8b- z2ep9I3lGp9tQE1n)9|larU`i`nVSZ3&L8F96QW29KE03xn?B764g0_nEXp{36JWvr zyC&V@e_-hY*9{=X?Hc;K+UUh1iFo{SN^4k_pc}PI4KTR~uBA7TuH}xQMjjB%gheq| zECDO)K`D?xHb%4sL4~Qn-iCh+KHb@LXUtxbkNz0HT@|(ULdu8bJs&$T=FT2-+qXfC zli-2F2My}-GHtYRkc<~pmGqGQ-C6U|k4WG5fYdSlP+?CUA9?~4{D`x)(U-VQ_PXwl&1FNXwh-6& zc57<$!~Ap1<)OI4Xb44CcLpR$5B**O)a}Mf5fq~DgdARuyfEwS_IUWz#M_3fP*(E! zR-#GiEF;l~-pvD0K5jwT3Vf4{`}cra7Hx1jIWUMxF{urQ(T9U6j!93?$ZLh~wh9L@ifItT z*!Nh47H(|*)2S?|keRUegvWGi4=&_mHw^Kni%BSNBm#i7o`;`?M0*eEA}--eMISs8 zflz#%9FVJqCE{UqH42`4zcqqd;iJmdBOD(geAg>K zJPoo^lOaJt5*ecX+{t*{FKMeHlvJ=j65UVoiT6^tc7Kq9jopzbj3y-(L`JIT?WgGx z-*Rhg$v2hKCC_d9NnBZdXJ#`7tWqq?-Ciwdy2)R&?bu7&R#3Z(Cux0+V6!N1eesvL zxFF{&m(ZbTY3?PL)2=9Rh{?|~{@ZMq|ff8 zJW1%V?&8Q$sD{jzqk8=m{{iOc@r^cyh0Q;fv;%3#7Pt6>e^zf?AwS^@L?ipGU)jwZ zY{R}Fe(59TqsM}JNGdTk+DF2eJgXF&z!l9GeJUS0tnkX$hb{AyMdj;NUZYseSGTkm z_1UTY^Z3_eg$hQthG0R0r!H$_rD3EXJBW1j+0)V-U2G0O3TNbQTrp!`pXEcuX=mbR zUxm0D7P@dScqKWX`c+m;`KE~IE%d$YEc$KPk}twqOQ9HTRcPF-Nv^ z7~}88lnW@n*Hz4|EBqJc8*6m1x(ehRiV9M*CG%mF7U@doIS@jtq`KZT8yd(Yhv2t? zG1s)GPUK+2n2V}l{~?gLyiBRA94(bs z+>|2spNcocCffaYlCFMyjJGPk*>R`pY`r(DEdu6g&g^_tn;%oKBPShGJ&M{HSNuQqP1x!#?{bZ1ZV)3sbWx^{17 z{aC$oZBaa6GxvuWEpjGO^=i#8>w(&yp6k!9P9A;8yPa@l(XKiT!kS$g{iZ5XcX&K| zv~1F?++gZ4m_;rdx%L{oXzNU_bvB-4vf}w_>GX2Tuk@tJ`Q}Q@b{o@a;5n!U%Z8ec zXx89%YRmWuZbS~ zqlzPH+OEQvy>#);lNg_garYQoLGq-1~bHCQ($rf?Xrnh*y8goU}Gs||%7+gkiuw}H0CP%@iBM7Py~6E%6T zzeL#&eEPR&<@a%b-2=}C%9{w}(g!Ekw!5xE%9|oa;|_S=x0`rz))H(wiJ+h5@ZE-* z-(jNuC@{wkm`l!(3{`TdNW`LoX(Tn+fI?Ms8iW9jQdq>AEi51*nQ(V8Qms){Cml?@ zr7$jlUpJqzi(70WK3WJ85{d;Y2MWaj@zIu>Vsk7@h{vmUTWV$#J@ol{oemY4KGaP? z75Q@qoJ+{Dpp@Se3_~dGAP2HPkQRg zJ7!@lCE(5^E0xo|QvuCG7M*h=gd%mP^N(R97;`gSV`w{r932-bHcf|H&;|3z5w9pDi+AR2>|>pzp{%&$6!Bkm$BF__+n|-SOgi8zaEsNb z5%T4w)aZ(fgJbRiD$5|u9+zeS%O#i!t||A0y@)F3nOKq~dAZd;7V6$>8b|QvQCwD5 zgLD-u>eWd08#5{rXP6aZMS}$gX(XTuE(6bIhZ=@YElJBC(DnrE92d%zpGn1IxVT%A zkVnTNlzd!;L@C3P#LFp)g^a1Cp0@s?7PB0mMg)eUy*4ul6v}e66i5g|1fe(w;wgDL z&{*K1K95sR@J&V6P@o3Nl~Vu>f;4ed7f1vYkx&9tNrAQlqk;+tiG29h6(!xvQBoF@ zkEWvZ<2q80?@cuHvaab8*B5(mz_Cn9EA`Z~r|F6W|J?BdQuqlrp@<7$_1kYoe99Fm zkT8vsmr=nTNxt$x`ueKPNF{r&1NgQjf&tTOvFKSOqV`n7Lw!*_O|Xwplqp`5jEw+# z$sIw>s8P49`D$#&=U{3i-&U+WdOALDY~8!I>wYU9IShh{cT5uN;K(`FKfexz71HKS zS+TGLyhDQarY*s!RqDaKebaT$_b z{!(TqZ^0UkCmr?lC(Yv+NNrX31lN`Q%@6QCN_emBF-%Q;^%3^d>-r8&0<|84NRDb7 zurN~ODw8|O+}K!q@5mIcRFo?0wN|rlLQ&%YYYc*vbhuJ%v=AA`(?G`qAUyViN zq1heLu#GlCt7h?t;dxH*2}}LCC#WFHvjK;kqd3-$1YrqntS|w`#s)uq$}TDaH;jp9 z7h}bw+qcqxx+ppthS!Y9*lPeg9ImW9;r}gQ_up9H8!Paq*)KAWwrq%VmYCKM7CDS4 z({R3vCUEygmLgMRQM>c|Ft7Gw)N91rRtZX|j_G=KY<-QD*I?-u04ynoSR-CRK-UN4 zqu*%&tHgZhT0obNJ2NaFeB^}QI+}^*w`8p1DzSQ0wVaBlk(10ah%-zo=i1oDT2|3a zGH)!oAAD@hdTgr`F6*tWSdP`*{doeS2n--xySuQ7SHDMK zbLd1$Y#db7Tf*0HiPV55o50txN->3k%ZQ)NR$PRupxS6k8Gb&FPGywB3PA}MwRG@W z^;nYpO<;V2LqmnSabfT*=js-$xiWAr-o4U*WX!=PIf+48B2<-Ti~&a+MFTV93q!8x zrWzZ-Vf(2|^Si{`vX4ck5JyagHNBl*heN_ag`kvL;&zGHH5>!ma{@)t0z{f;OaFG2 z-w+gZv2PJn#w@lgs1BC|Qm@P3E-eXCRiMU;9mq)FA&C)_wz%Kb`5e>pCC8`I2o@Ee zyxUMVxV4Mx*x8(^H77PZD9SHQZ{f?5XQ6{ zsPoK!KOaWV-CB}-3WRad;~#h|$sjc{Q(lH%u0XHQrB|`*eid5G6zY`P^xc0H^Cl7E zD4FH2o^DORB5d6_?rrP(JY!nf{n)L?^;L|s=<(Eefj{Y zD+F^NoPh8DA-v?n>#+Y*9rj-lUW}X^|I3iLOMN3|V+_$}<+lz41N7>A-|hnoaJ!Uk zK(BS9A%J8OBrxZQ*~hd{PdGTdaf+Z-DiU!hns?6!ZxC(hkkggR{{F+H6zE677t`~8 z;6=(;C9W@fn)^9>=qs{OD#bzV95)_0esFqopzfEv{ulo^GIoF2TCHYff9pKr)4g4D zCIx=qw;ib#uQc<1QK~C*hA#!%fw?=AzV@5(&DJDwX@21R(3VBcnf1-BnfM47nlu?L zF`Xd0^QW3&aSNX~Rnwg+OOjh;0?pBzNEuq>)Z_w&2Wc_}F*Gl_C~@xP+IaUXwl8xmFJ#Ah4zWC*jOEN9R#z)1|aTrSM zVR;hy07c5Vra)j3T44iz=agGD-`!%P#k>BvnN)VrDZ$+ zAs?PPG;v---t32V6vSdPZE|J+&A{r_WvUG@h)D)BTAG1V2Xmhv*_)^%l#oAubLpvu zYe|&hX*{cJBfXYP0tKJqC;~F{X0in*lvcECDQJ2u!J<0oO}-#z++g$2=Qi}RYfKFx zR;ZZ#5m!-X5)aheP$ogumBX?gO>2}4DlA8qtN|NLk)s{>GB`IO=?|X?a;f`lnX?gC z|J0*+CXzmWwT;IID)U zgj)r3?*WUiO82*e7nAa5=_nlRQ9Y&lO zaOz8(IvY%?J9I#->OB4AOAL&qa` z+VrtJ6Iq1<{z8yN{`e3C@Y_TRjwujrfUvZIxEXoJZOGlfh%M{{=U=oAkW6XSYk6}^ z+OmBfflj6W5xUB#!=r0uZFg{5`SVn?#sy2P1l1hl%hLsZtIgH`P7+LvuhWY?Up(Qi zLOIVLeOEhXKN@u-p-U1Xs1KxsPg@qcpN~oJr{3xt8K3G~A~Q%E5Y*T#7Ys@}75zrw zH(xf#G#kC8q@y@_ef~>rif@t~G>Os;ALYr_FH_TAz3G-)?hH4to%TxWS*?&|unC%E zsyx9_W`8znj%Z>U)>Th5)H!cxv;FHtu9c6;3GeRpy((y7)|YH


    HbbzNGop!2*u z$k9{~pMWUh3)NwB#$T+{?pIaU{%61DkJ^zir*3Mu=;D|k8T29*79W_zDTlXfTG6q$5mA#!*U+tn;5-MdsEV+c=Z;R~PGqarXFmQ%%Hb=;aBGQbfJ#wxZNJ^318U7=uNL1u z8nl?YP^ZvQPH>`1qCNbGi`?xVA17Wz_PvjqKW3A|U*1ov6WAAVWk$?8f=%H%`ST&S z^$G4z{^cjHF;#Cg{_5Mgh%%9foS8xG+cbV_4R?fBV0eIl0U>Z|7HQFs2v_H9i`TSR z>md~`N;iCz;R-_2Uek_~9UjSR<}WObiaj@*^=HII&}n7#eZf-f&oF5pMU@*oovr2) zt$~{5GoF%KSQ}ldG_6&c>fBi#*$&4aGrFc9*6Yz_a^NL`(|5_#!hbv8u!cJ*%rft5 zR0*ocs=$6ID~hyqYh-D+V=eppYbl84m(j>mD&fp0c0;1!MDQ%vKRPHt=z5WtDxDzM zc5!Ed^HTwzAq{GOa_7IXt}^p%X^sQr(sT5G-oBdKaC?m_I=asxb5uFPwYX7ue24ED z9~s(j-GvU6L-0e&wXwbgbhPvPB1J28Z)YD)vA$H=`C`Q%UibQ9Ox8D1D5< z%OfQPZ9W%+7JoAZHvE&DMM~;q$S|2dfQud>Zc?UC8(UgHD}NFnrX@g++DOu}%NnEu zt0z`t9XF5)(bDZ|Z+|-@>)csDdl=sGX4{%djilk$s`lXiRu&fFZDXn7`%GyQ5_$_{ z?U(LXijL%^sVd4okSVsIEDkGDfyP|b5RA?y(bGGeT1BE?LZxS+&^txC8(@fTE&ZE{ z&dJy^4DB)u4I6>}F0?^FC0I4{*T8R^XQR!vM<4wzubfxR+H;~DuD zQP&q;F(+AH@E&|fa=UQ!eZ5m^zT}xk8gv!JYE|2@eL|ewA9+x35w*3j%i1)-Ch7rE zDkjr*((Qk$p@^mqm*~{AFS4X86!GX4Z9!1JI1=F8je=7@d?s4QDSuxX({a?^Wlfb% zV7$WCGb|N2hWJAAqdIk*aC=J{O{#a)X3VcaxNR;VrMtV}?z>Z5FF!K~MVpdED5XpE zkPJVVHDjFtlE;88FJ?#CI47OybHO2sSfunpWwZu}czOC2RnID0snJqmC{*I#!4i zg1in>$t}M_!x&RA|3h#5fuA&TP?2k;VJ>K(iRv;{D+EmUJoDRVh*)*`XoL(DpIads z(HjR%b~xY`g%xJ)a<6b)CK76HA67V=Gr$MrcA>>{K>LUnV@LRimtzO}@K;lhboSa( zxZL`~4&_z4;Xr;4E&%Tt>f8<2yvp%T(IoN%#ASWgj<_?9K;phy&0w&QTSv zV&?k@d71W4|7~nzRFa6H7ejfJQbhM}l}j%0m&y^j*_!C7_hoy50C~~1RZUly=TM)~ zXd*Y%-bn&Cgoec*8$+fo(X|Q={-T6|^ERGcC&`M^r*bD45Svz~KB;j2GaHB_?*r6Q z5@uq2EL5j@g9x?Ej=7R)(zFlmko$|wNcbfiK-@jKh7Uf^)+4^47KDx5GG@?$ZX~p^ zBNR=T*RX$(MyK%_#y6&?FIz8{t7ci19-gZ+m}~ZqC$yiiLN_hXvC1i3bKPbfdL>8+ z9OLyVI+Ltbhz2HH6|qh+YWE^4RDcy*MA=O2IIBetS6?imb@iSEssrktox-z+bM8!1;t?1jJ-}w(%$|}h3{EvcNWpQNk^<<@sBj7p(|TSM1&c|{Hv~T zNx96LhNs>VT*8*6f9#tzBxOI5trv-xcU1EXH_0j#!p%0qi8BsiNNuEq)3w-BYhDfT zWsQhLCfxMa0JwFRKKx;+UtBgB1P-?A!qZk`5*|wySq3t=OQ;M1G*UTB4r4UKhBe>D zVZaax9~D%ao*<&VS=vA$nm5(w!9^L7-K)${~hD_35g)tf&jTgIqehWCt#=d>UfZ;L>i$cOkk&1c@x~7K=r*<+ZO5ve;h)vjI3B!EKt5eOG zKGq09d^hKD6lW7#n3@_3*gOt@78!PFnfG{|%Aq(ovpY**;(Kq+o*$I@oQ-fX1Fge* z%B&%1EIdw9$uEOQ^E_U#r_e_!nixx;;wOBScuI=vvO$5?MKn^Cr=$>DV>~QuwZA8)RDose$5sU{m>53 z7UY8|)4Jtw5b*l4)4GA`-^+(Q?kx~KL2-Qi!$*hv!O=`}ky1EjL;QNE z3!b$IsU)j~G!@m=*u;BPhZP1XUzZwe)izbieSGcS%*xuUCJ) zL)EvZ!o!td8{mjKIC*d|EG0cw;)k7+$uCDA{bCiGiMyCg!M_lQIy|x}Oaa%^Q@3@8 zeJLQvu`UL9Fne|QeC}An9D4d$os6MvOL48dm}K4zd1?$LAE$7B86pPj3H1-e00)GPPV>N(C0mT27R66o}B zDn|m7dsBT1#(8X5sk}RFH1C9X91)XZ451hXoG)-Mi5w>VrB5J|M;6fZkWBC(9$J1- zor5)&har;=t13A{!U&tg=mGbV0pgkP8fhn(k%6H$0k{FC@2rAUYzJTA&G2T|Y)Il3 zs*g)oj;(2S)Cp;i-z(q)3lc8wH`?)sPJfXZOz`Er|LA75F+*x7DkZ6KAiA{wot&b7 zX!DLwUQbdh08dH6Md$ljv26cP$w*A|;NF=Xw`~93WP}t6eMuKVmD*HTDRc>_hByH# z(p9|IO)1cv{?WDm8{XQ#2o(94iLZx)S0dnR8M1DmgI=odY&ajPfeE4t0p(bhFyZHt zwqy_zwhE6vY002t3xF;Xw+Qj_1$`fOmNW<9pU6&947?7;^=l#ENRk_>XJ-8iGxR{r zmyp401GI-FC7 zAau|5X6Ze807f`U$J4SA0ESJc@$KOU~k(lR@@-6?|i&8Jt2hh>QblQ7cEkvxziPgt29iMA#^N6caZc@pj!6&ipP`Q zGH@=XXYaaot>7FROBgckJZwnKY%dszf-Ev+VcIjMnm3x^T5L3PT#f9hv|E<(2Zbk% znc0%>;6%I)_44FB`@LM9!SU8ABm~941l| zP7D4{pc2hzu;X4ZSFXJvPafQXE+OD}Y}n-BpsE8q4CMNXc`sc~;}KNRun zUZ-K-k`I$FMNxYmzj{5DFLdMs#JybXDAP#X*AC$aL(wsMvTi5?%{CNdRJFuaN&28H z^C&~~i4u|-SX)wD)L+f^Gd=y4D_4>xB}Rji#Tg-LRO3I+xhPtYyAWvT`ySe%I~QD= zH))UTZLuRFd#1MC9=4sasee9}f+d`!E!J(iHsneo^RzrioDl^^j4wFUJ1}6GK}FY8 zqc&bTIN&C}t8MD<=4B9-V(yH&jOTP3mi4eQigru(hd0jIzkoP1YZk3$RhK z#SirNiD5fV^=Wjiq!3qy5xkOokfZ4e3I9Hx+{y$Es8j9d>+D%*4q(Zf>UUQ0WV(cG zBx(;e+>SR6&>VB4PrvJOyyW&*)fnE%(FCpO86}VA(5I~8|N3l>3#-BYr{3$oA{3dK z8UI&~%aXc<9riflFQGV(Ukv(Z^97Iu!W+!hwL4~5Cp-{TFdC(XcA}*0^165S;+oTw z13awA#DGxX(zxSYRlS=_Z^|s`m0yf+I_K*xFd_3}t&#I%ymqSAtZK7ZL~=m(nk9W_ z=G*7_GxxXG`dEHP_UO?;dwDq=JkC06?7Z5#zh-Xl*@!!7jkLL|Bg99Wlpjv<*3qfH zp7~++V^5?zQ4>1)Y7jv6=<#r52aU zq|QlLi?$*9yk@MqUA;2wsI3$KL2ElZezZ2(`Q+N{OHo5NJ9t$~9c{6c<`K=D4;kh; zC&{6c#CZH^qC8d8o9QzQ2|I+P!8Bd0{|&A=YhE)>FYpDtcZhp8(1!kuDt#x(OnMuw zEFztV-OvTAzO%2&c(P=4&zViajoG-`nf8V1H_glIMp5E43^Jc}RMSpm+)nyEZ7;}M zuqQ;1;<_E*9|V*0&;m07R-_|5F*g`|vibuvbFX8t7HKsy?fDKC%~FmLj8kI!(R8W& zc($}8bP}3LP=m>S@ny+W;Y8)0TM)$7z8atBNW9wC&|D?1ykedWJsi~bXS3?1ax!G1 zdEIygw+W(&8OijT6?>0fw033Ge(+M?gkf)4IpBI?(t#|P}$s7#GDiwd*t zzYkc_s+nruVFkUw26F6*`?Q*T5r|3xE_BAS5yNAI-#t>}tSCwr{}duSDgB+y7)vpA zAdh&gl6Zm8Bu50BUXWifDQ>K2JzNedDcpByWY-RMGUN_zq0fF}DjsgI`HNEk@SV{+ zlCv+jCDExbMhnqjL_*nS#kgWe;SQh)&$X^QJUwZ2zgidtqz-Dy?PH%AnzbnSKcxjD$Cl)uG7Vxih>x&Cm!)h!%!V< zN2Z;o@QJDkb?Qj`OkYLEOlvF+40|v2UjZlpMT{u19a3zb(>Plh^h6c=%l9wmz;Opp zY0aDAIQVJ>NTx0%QXTGT8^rjHlGgy+c^hnQI0=6-}H+ z^hb5`#i_|wsT7^!DZXCJVu|s*=eY@}7>udAu{s84XH9Jt{>mD>&TaCpUqu%m5I%<3 zBwDv#8A20SaF-i}d!iG$zHrDJ-O?@Juj9PJ|~r_pXnE`-KS4^q6l*b#&wCh~W5rOyi9|a6R|r=q-Cc7mign zcK+Ue+>zELPMr>t1Wv{3ECG;;P`1M)eM<8pOLXnpUbNwm28)wCk#uISU8USv(y>Tu zazJSWW0La3P&|J{iAHZDD$|98A)E@-gV@l-^M*WJ03VPk_THE#dXIzo;4p1HlA>EWdQKrm?QqmfLp3&6%W>r8VnJ&t z@7Rw}H9d<-ttRGd(Ko1&3x-};E@jW<{@bK5D~tL*l_iLa1FX3dZa@0h2S3ufPm>wg zHk+MkLj$SV%4x>HYLCh)W<*y;&fKAuh zySX=SWe)vFHVQ@y{pT=>5ZI3JE-#AWruy9XJ^LZNrWoI&2BaeBiiFL&VX-H08or&v z%-pwR5iZ_>Lp$r5*ccobTfzuCH!b?hEFp_E(8eoN!2?F}o>73~t}#TRli?jkXmYGW zbB8`1oBH?j)QeFwbuZ~k=CBQ-t`6&kK7K?P5nyIplH+NOeWfL55*)iYiaEreg=4J4 zhuIPnIHfm&Cg!}?8k|2=hk=%89IF{g_xmQA71(Bk=)sEjl>=sypmR!nVxS8nrRt#T zWdhWP)q>?h5(494m_jy8G^B5az}cyNoaH|kSoeO^J6Oz(^5$v+h?+<-NOq^4PaPVz zk(GK64wmub91cv?RKD`h4_K#V0q?w-v4yI^XW=fUJps(uX>^j)c}<0RO-5IinHviB zCaSAcrKXz+dmfFzz^O^*;tI_ycL&QaYZtNjexIvp=C0ylm7j9*k?C!RGVz5r}+KjjkQr&(3S zDfhFgh?^Q(!XVFAlNe>y{wxB-mxlViZD(#Aw-1{#ZYwK5s^d{k46T>Opzlfcc6Onv zb{8yOzSyk=qPPz-sO2gXK`K8nz#;5cQt~>aRH2*GU4yw?2wqU&>B!rsH0_;iIvD{L z0m3%kRCepK6MM7M>&qD{6;Ak7vFa}-p{$SeI~*_$x@j`9ruZPi$JRl@zD!!pD}!^; zSy+@>ZG;BTP&_VAzBaALiIfVB?(jsj7 zQ;N>TrTqK)I9mHU;(Rks&Sum%_%6kBJDMR|78=!UXRfw1Ea(&!*sR~u%D+;!8-M!a zpzZvg-;{FNfq7zEDxn(} zPj0IvPl*2^)$D+NV#CY|-hrvEDMizt=zMSg@X!Lipb4$1wGk~o37N)}Db%WA99pZ+ zkE<+vqjFPTPV&#c*9=h2Jv=9a7a&Nu#-<&;GR7?9coXp(#5uXh@x zPeMXMI0X2gd|BQ3LxJ4RC}!Cf{kV#M`x(HS2@8?lMp(3GD3~Bpiua0YK*lzh1oD zA|p%UjAwtxq4~6;+z9(n0NIr-9UJj6a5M7X6{7m>81;a~A;`)Xq`+h17%sFaorVI4 zx#|XTQOMCXf6AosA!bhk(+g=#=!tBPDSkHuXje53r!b ztMZ!=Q6#*Un~gucULG}^n;kjwdJ=l*dnmQ@MHSIkr3Bk{t) z;!7R3nvEsP%9u3K7cAu?Vms*5;LuCK)UNgNlrLP*cnN*{xaVSYTl+M`?EV=XoQm9? zGvj-YT%@ETetDW=Qs4eFBqYZ0QD4-0a{DTUgFCEF-hjDux3Qa9ET^Ut*IL}_rwC2@ zcGHe7lg0ZfrP$c`9R@zeA<~Oyu1*d!l-S>XTBIWhiMI2JPl z!~YTxFR5!LVz(mtp49dooEwg`#fGU!wRBOw#U-Jt%1uZWn_o*9xu!O`QdU0REJg$V z@oyj7W1=c;A=$@Z>A>FFS^2th6omX{bZmS*wyUM|_^EsSGyWY-J?RFG=1QrUV6V(2 zo8lG;O`c2h)41Dks50<;?|FAIyy%yt6Q9erfqn1XU4w@=R{23Lqks9_`E;GqwXv00 z>h_y`cb3zoRc%+n+peW+=PLi*{Mo-Vc8(3Lr6iYPHQ?^UI$8f=h?cYcH!e9i}!4=D!Hxa-MVqjz9GN-v_bw|ag!PuYtY!z zz>;N-pl?n$?$i7lVvXLkbjVC`&%AdC9*Adn8XsGy;#F>-vasO5K_y5k_s?xR$PJn- z1PItrD9U0ac^%Rn3k z)<}k~gm)AD5e~!y{1H`2ZV#9>fr1jI422$_&v=uE_#G@SoEbAuq}&O(NM*V+Dy@%V zS}xlFD8l|hCM|p^#s%4-oCA1S6gTyXWQ{V-rl0|pl9m?M6oI?T>OE<@)=S6)fobtI zCPWeU$l2oI#VJlQsCFgm41Cuc84o{D5qDgL9R!rsm=HrAHh9;YSPo*K-YpN{PdlQ{ zME2D4cZPJuR9U?T}C9ueH>Lt zqaHA*P;o#hFdqXoo+uHqNCnyO* zP%3n$GT*g`x?w+%aGLlT1LgTzQL126CUMDKqrU=}h~j2`qz)t|_t|kw7$^@Ovpo(> z4ezHB`~I$ZJ>pW2*r?Fu8tGeDpi?EHULBUhf=0ZmApNuVfoj;@{Y z8rOL|P()MpL|{2|6-;yHb@d>YGa8AcxLE-BZ<&I;{25BRnvqFPudhk#tJ2OBM7|1k zWb#*ZSX+X@nFp>#mSEL?3+WdA=~VmyScHicp2N>OH!lZ4$A^xb#w6~j<5=OQ5XvT` zC`Nf$K6#!$4+?KAllMTM=M3(HC>d2=Wu6enb;V_mltQ3hYIN@~bt4wb#zV!ReaM7CaWn@xNF z68f%t|9Z@*_T}#`S>}gUg$6!Kt}gGuY`rj6!M(!EJ8yS|S9p?~WecmwWcI0u!NJya zVPy8Z%qflq~ejlXsP6bOB~+{v_e2;3*A)?a;v^wF-g%Ha}>6O z50SiGi5sR6^JFAzeT_?l5XQj*x*$=EE4l{3*dIt)$_L`pB6A1_-lkrBuy!b@R6>?_ zT>>PG&LqffpP!d?vLdwI^D9)MkNzh_{Lg3|15X87_%<782Xo_EmZ2GdsT6n7#i%3a zUjNV%g(wD{3H0R4Q5bIk)KC{4{dge5z9|xxygx&sL7mpZ&soY(4qsZF!Uzp_c{r3F z4WuqA{a1w#e7A2A$}SIPm%RtnK;lIPE@jCYzt1Qli-%kqLIJ_}AhcB47H~ec>k#y3 zMqCJ*4TEJ{y-{4%L_^;#P-2d2Q1q(}ho@9TBj+}2v~|=o#)@;r$&At!h6chA0=B?# zoG3l8&lwVLL@SeyvrTjIkw82ZE51eo!q-;QnEkbG>DFG4ag6fbxS-es%{i&Zay(L& z0+=^lN;iP?Z-WaU zcnW0@tFD{CUMB-h(&zw3P*Ms>CWyzA3GP&d8SC^u#hdDPBgs26GDyapji47<+JY#u z$Qk;Ber+#4>XkSEND(e5CWm$Mv7o73#*a(kW!P&tFNAa<;ypA?UVT1_N%w|r<)G)=ObFprR;xxb~%Yp zVU|A3X$VMdB1#Hr=TVpS?Da7W$UrJfsLFdQHl5qC@=4}_BED53snR?+LP+*uN_{{O zkq9ESWBR+Dz0dg4K(djC^>yx2_|PvF&l+rgRTKn#YD9UyRTTL?G+vv=%VHAM1&GVf z2c^?rsQ`UEpu1r^ZX01xZsryn(b6oPxvYD-q?4TpATF!o;~XYWsA*e<<~~;{w^*TV zVfh7`P+!$QHMj?N91pqwS`}g$>dz&awUUrN`Ot%H{I{RkiRxqlSIJ(jX5o6vx&)7Y z{Pxs=*z7H1hV(~vS&!6#4Zzll{ST1=db-|!LjqGm0ZugBoijU2^q`W((?%AA-`VN^ z{KJdlq`;x=V~VYYR8iQ`*X{1AU}lnBo__Xh)VpHuw9(ZmcfGh8j696+q-TefS>41;gpl00(nUaz%f z-^c{~C_=^rdAS^fKaKMVdt}bTCn#HHOj&7NKJU@*+pg(Rc@cRGaB1ASWUpkqcd9bN z2sHpeX0y`316S)dENlfJBTKVGNLO)M3A3s6qo zJ8v4KP$(}|^!tY@-dlZ|jTWN+DF~7=85oi=F?sR;t!?wUF=pzzVzvh5sR)sox(yPp zEfF|Xx9Ck1UN~*iPv!6a1YO>@fJ3FHrx%tU9OQ;H&$pruvo$t|>wc%%M71h}wuauqjzVfi5LqF)$g&F=g0cuQJ&pB8Zb7f_5* zUmnH)#+zUe{2e5!e$JMN78oUX#jEU@#4=mZpn`SM<(fGy0O}WLQG#PmE2Y3Rsve{U}A%b?9h&84yDy# zgS*U~tds-gFIa8bpJF6OHC0Z-<0Z%)8=f-vg)f zwg+m5q>vLtwCl^FfZJIcNY>5onUAe#(W*hqg`%PXn#~><m5IEKyV z@U<3ZI$yMiu_!zw8Q0!n(-d(`1er;eq=e@;*Su(IZ@LFT7<7t7T_qzGTt;038qb&* z_>&W;$Uh6m&>YY?gRw5FF1WQq`W_lZC@R0&vdFQtjikOzgm4JqoW~yk7(ksW(fB|0 z0sj?9$jrdT^8bQ_o9Z^d>NUjgYyEz^En0$*K{p^sxb{w+cpW;4MlpS5lp6I+>qaZ` zmY2Yfm+X9r^aj$k6m&{08l*%b&MeFso`l?uOw~^_sKwv}9i!C0RlX_%ib5+!ch|~S3!J_Q zLcSq4xC=@`BX@fv(WdO%TVFo>{}}s*AW?#C+wMMX+qP}nwr$&|ZQHhO+qP}vwDtSG zKluMeyupj88q_3XM^xrU?!DHEeQoV7KSZsS0?EK2kWAE3;J4^DJA&yV#Co)+fEZ!3 zp7bXl#iKg~J%;Y2U_T`ZhDy2UU=hrQQi8&H2&C5C-@uIK2@81+;cMPK)Bg^!XXDED-9A+!RG8S3c~S z#gfiNJWer=NeDhQK;xj_&_T(Kq~=-qv6=ZZ?j_Qp`7y{&j9UQ_3!`$){g@xF;0ZCv zLf}!`!XMo?3RbdY|0L~Lsk%=AOx0$Nzbwjkhrxzrz}go8btLTsf6N1fn30UzH!KCP z_Ydn;vebi9U-QT&xUxwz@W8o4u=8=rN{o0oU`FOWmpI!%jag*Bml6%~J4kO|^@ST; zT`BnBkr=~7{);qZAq{l7xB7`RpHB&NKc@-2i+O4*BNLhnW&FHne2=~eqp;_EE&P$; zFL&Av63%)e1xh}O$QRPur%|Y4+E-?>?m?A&Hj#21r7yuY(f})h))MstDk4xK+zY2; zkOSQ>iYCT)IiE*@iR;pEJrqAM!m%iO$6m}5pd}bN5I?Xd7S07Bj^?a@feH$K_5;bA zG0P`D8j29RrRv*v2({GW^bsC5=c@;RuCeD89@B3Yf2KT*0o^=_3chDyc?%258lA*+ zN!k%1t917X=e@H~vWs|DMIck?wVDHWz+S7G!n&9W} z41hPOl7aM9&-eW$#j5E zHYIDzoK?&Ck19J0=t9LPq`V>F205v?)J$FZEVnzu=<5~0;u%u}(RtPJmR7)GR={K7 zI3vO}3z>hjf%of{cPHf1#Ebi>8R6GZES}y|4=#o$d%N+M?|2}-i8FQOXip}Gr>`%3 z?j707$LYT5%TdnW3~Uwa)Bz^j(~b9}*&r zYpE=<-P#uc1eXCA#16H;2JIgApTpP8A zQQ~_?-ya?8YZ~s>yE17CR<e z!s|2hKF0cEDw6KdqGiL;#5G_D+3jh;;Z$O;fg?7Aj2gat^lVBL#3C+>hllFm189*4 zqT6T}WHgG?<(;U6U(nRdQ|y|IAjO`hXtIfjy(n=Z{N2!(9VHYfL7!!A(C-uyDqq~B z48^2N(YvjAYp&xsLF!)%pcA=)@A@q4gqOCnWR8L zT{)Z@x`}D@KCT0m;$t|AzI)qkm`3^U`f$N+KJNC!e12?`Xa}Ri{U0Bv7UEp%97(gH zIfARNL&?3x9Rp#^aPy&-fO7ne;M|K0)$7ghCE9GQAQSr+kH^1Bq2?Xu!S>cQsf-_) z0pW=c&9@)45n}cQl&LB)2WunaAV0$Bk;}8apUsYNyK90`MnN|w4Dw>sFEUr2Q zYQ?R{)7`om3l2zUK-QKF|5trZee-zg1Yb3{=G9dxD{V*eJA68?k(yU3Bc7#CCQg6(n~071W$^VT`|zE zP$qBjI}t+q93fw>U^({~s-)9a3ki3ar@)94^hhw!;}!QF2j7Xbc67vI(s{0>TpKgC z)if-dQc|Hpn)2_bVU-=2m2su8WX*wEmk9nzhjTF}mOx-j;J(ga<>9EpGKM)s@1=+U zFC#^plgbda9V0w5cmx4Yp?Cnt-ic2!416SFu${H#bES6+K+wlcwF!*E5G7~;5>OXF ztRh?e&Hy3}1fG?@1Pd86tB zU?yIW{GZB){}uj~iGlsUwGpQNA4U8wCCZ-h4jjJ7{*WyKnG0g3xu&p4e~l_DRfMzn zguwUT*{LgPv+-n+2&rOt{F|GrnJurI!>o^G<00|a!z0&!!+N_64?aBah66u1InuVG zT^c6BRD{$|A<{RZXgk`n-&}|hkR^58?YLjw_Xl{&<`3J}4pO?xD&4w?c&e#e+SF{5 zmK}lFmR60s`xTP3b$+3*)~1mk@9!6kxKEah z?tl^;A_U&soXm%Zt5$t@p=XiyJ7gX z5`8gXS3U$?{Z7ZUcu9Ndd9(BEq$>@#)UyrnxFmG`(BTN4b(JU8(K@gmS1%@$LMtKXKKl2Z% zkhhqHh-=8WoCD$?Ovy7~i!bzfT7UvdXx?)j#Un>WWOF0Z58da}4}_ZAel@#Niw<-e zAwfU~%+xB}fX-|HFpQdR1bZ^%zwAc!WDxiG)51%j)#X7jA9HKu)l@H}2PzHeiomTr1_3eqqy?aA-p)_HIiWZPOVZ7T!e<0BTEeTsYvW($ zA`k>Zw*tFBeXHq&QLD&811NG16a?NB$Fnm==f$J9IL>B>Fb1t6mZQg9heh70AT7Fs z*nraA+v>6-a8Bms6U&M^r_plDvXcOxzO_hng3U7lt@Xi8cLw?UOBm6ixkjuWIxpIU z0D_LpLpMWUU<%j2S<_v$2oTH*-DfNFo(;@_?ox-I1;7~uL8~Sd1jC_*2%wik2+|Q{ zE8v=;d5#5eRjmU;c5@6C*rgfBH+*vlScc|(5eOcAf) zTx1&_#xwv%(BS1~72m`gav>AjU*hU{;}*@`5pzQtED%Pd{8`d#Jp}15<@d(?I3ozo z6`o^pRfY^nPJ#rq1;pXv0mro%p8AG3EHY%AXKsy!j}2MGzFlVZS$4?{Sx`KL-Sp)Jv!%!oJ}mhgC;A%In}7%IOSlClX8mc+jy zzO>#cGU1;NfrsgX#YRp_6s1NIR={x@R^By5RH<(J7ZPOmc1yR|Q%k@n@=oB*lhS66 z*cE};T4G0zKlUIRrYZEs6F!>C5`dphcH#N)ANp&IMlJ^2^a@gGoH~{3Nn)2CPg`zz z)Oa=>LqA8w0cg+!pjM0HPymY0D}=!}e#>B7r96Mz#Y9Z3Y+0B$@G^Ew6TZK=H0AQl-@r8GX_Ld;aVb;q5Fw_^IM4)S?T3LPfO>?zpbGzfH}hx?Bcx(mVgWt~ z9y??ipe+wDz~D%+(SW=`pUtak4mN~=0EFNHy1QRdrvpzP%gfV4{jHry7Xk@C8dLFzwl(3p_V-{I7_qj0nsANKw9m4>WP z`<*sa{^ItaEx1?AaSfK70LzMq#b2c>T5PyI}hePTIF-LexqXU>VT63?1&pVZl3WDYgU zP;7Bb&ZadxLMFnItw_-sNSiE~?xa zLSsV6@F$EvogEnI)Yh4(%A;41DFx9t8749 z3X9t`vIy>Zxlw;6c%ql4Pk+_D%d52__*!?0xGSu#9+|dVszP3=J-r|P78p>Uk*>3M z8Oshn)FOmw%AWRKgb{K424&~GJc2a1swF_WK>0k=K$~ri>Y@m2paklYVu=G86sw%c zZyMHt81_1ud!BvIw`ypMl~NDKxtPe2pFU15^_Hppt(z#`O(PSzUJsq~OlFg})ZIDC z>l7b=&NEe4U+&Tgt^I}k57m{w$rDlf9k{^bKNBCykIzuZy3>Dni~?_B7hG5lHSf03 z?vj7G#BiG0mg9epk{WsghYL=cP0APy{ zROZ-_xJ_99Pdr;H_PBcVMkYH!#+Bo#bSbbP(H9Xj$sBudd^Lj5i$%UScBv*+&RprA&^B5vTZ#s%){Er|6@waU z>Se@<*H#}CQh)A;@d>{^ewtx2*<8+l?HcR2ma$JHCm-?HQX*rG@L3(j$Yo=GE-VrVj@m5^(T4>x0BN%`9o( z?B6TM;g2|)L_WcPrKpeaBoarAg!v-a#BL(ot;!;iCWh(vH;?^=lqCPs1&mG;F8oGg z5>7!qa@L^iIBg(F^r~V z=!TJ7k+ z2n;uJqTl=5?o@dPz7p~U+?`l&QqQIsr|8M-dymK`;CpCZ)bK7YQoIv8AEzxn)oPJ0 zI>s(8p5dxVxyRuIblno*%^j`_tNw|!qLQ3%-rsEvIfA53K+iBAP#HLaQpZ9bq-*~fRe z`Wd4OJo=5t5jS3tw0luDas6UvoMd_7W*6mTk;IuhJ$eIcdu@xPNN$TV{1}4#;&OW0 z8P-z~H;zwsLcShqV!lsT%bdY;bGf{#+BB)#bIThCsw;mFFYsvB3FW1td4$y^F5t3b zA9%}y0)MV%BzIf>d_f4#%ueqYG;vVdT2fFg|0sTE^`pcqyV=+k^(r}J757dS_eN6Y z9891XC43_fwWs?gN%(q3{9|*I^mPs3_8mUHP2KphkmHZbtR4i1P22dy=z`&nX;Kq5 z>3TN0+cCRijr++u2r9H8uz;?l&x^p%&Jg7jMiw&fz;|DJB2tL?I^LV5>iAFzuTuvB zTh%$pDJo}F=mLWM$8N(lI4ELDoCtvn{xL@+T1jbOI?|>`Z|axC#a043n=n7t00UqM zPHU~;4E(sOi{ zY*2vN^s_;H1$CWNq`?c}M*&v{=uql+1S&TY-|v5_UH(_VeP%YM|EA79q_%E*G=lQ? zwsx=ZU!UW4aZ4yZ`lbW`83T6bKJgfEzwn}wv?vs{h2ejwd#oGAZ6&v@Q;fmW%8NBx zFS0tbK8`(2DnG@ROPA4nU;9=0c&d8T7keKj-UlOEI$Iny9q7l^E;E<;Xm>uR>ngwh z-9IZmom^Nx-pT)U^Og8MxVgW%xaoMechl=`_jqZ0^Va+Oa&xhGQvd$F{SoVy`lIdJ zxM0jqDXY_~r&DC#{w-$|uNm3Ql2ZAkBC?fnV$EsKypi$!HJZ!2t0&pDrmR=)P*Tz3 z^YU8LGvn6Y(b>JN&11Wx1{YgrThWT9x4q5l)AiNm@$=kJ|K0g?ahl9RVc(#tg;a-b zh-hi5#Z7)}i|wa-qIW zHM~X;y@-UQSknET%*pAaqRrmOwn6&Nm+Du9Vf#{|?p~F(nG3hG)?+u3`1ZK5n<9Gl z=R9Wg$czeWFO0oidi(8qv|B~{DO;;HrplOvt35)AjOZb4aa1{&yzZ}cT!R~5IrycJ zuw6H|qt{G*=|oyWpp*9#B*fTCyGn8*R_F{#XSO8oc>BWE4!zHz?+6{H>w><;IwnOWOMMr^yZO4Im` z`fXbs8w;ueLii{W$+d7AI}M=BpsromZ2b6gDDJoDxuGgB&{!GpmZC0Q6+w*!$HRaK z(CjbfV3r9mo9gG`K9)Hv?mN0rJN+iY|Am8(c+841;5jIs8lY@ZigUyjIWos00ynhQ zIKFOW$BRRRlk+bS=1YzXZy*^bDq!*pBO*lSdkp;=L#J|%-UbR+4mJ~<;t(`sN32MO zW#*LB3B%ui-qd0cOJH)C*N)>fpX2YOIhC;tMl@IgxIS9Q%mVB(=%ZqPOWav~{b>pk zX1Hb(Te3}C=tR<+rdcF-gv}YgVZiunTFZjVv0hreA4d>q>L$|825CbDG$~2 z5GIsvu+aS4C4yiAq5~1ia1bb63XwpY!iEy@SQs#eBoG^iCocr9=g4W&l~Z(v3=lF3 zi7{Mi2c}9Au;AIFk66U_K&T;coLb>Qq_AMfAwT&Pe*Xpuu{@R1L?V8>*BudJT|?LY z1q?n(MRrW!I&z)wbV_9^;|%5#kf>+dP$dyGS z3XPTEbtH_Yvf{!sI>=mID&3qK^(<;PU}zBVDxE+8{?#@&VuU_$IoVm9C^~_;Ioh01 z4TwlAsyz*Hwh!eb3okzbdZ_lwK8O&lk>5!z_~zACzNDu|OS%GbLXzB0H((Ls(;@-^ zQB)2p1CQuXuc)wmkP}?)oFVb#v8&xaaJ)kWkxWFC8i0i$NK-61XafGCw7Vkx+YoG# ztt&9GgAxxBr6Zgo7cI=f4iaZ~N7pYE1aXosRzwh%fL%`snFi|=z&PpQXI~;YI#@$P z^zUCu2)F}^7as?1yH%1^jm@li2M-{(Y^Rs*NI$9N0x!-?8koyWi*p$VoCFNAF0duI z3i-0cag3nd3lO8vDqM}lX9u=#FcCyopE<5ZXlPuPu%#X^ewGkfmKbPt?8fds!4PiFAU+cIE}|(Q;;beE9R5ae(R=|WPKjTSQm7o4^e4VkR^B7T51*N#Syw{g z6u#4WUSmS+63pS8QMZ@}-X#KNOZZ|`Dak*Rrhk`s5f)=$)3FD4;&BlUfJq;0l;Gz> z()9yYW#JSCCkEFfD?Jvk(^^wRd%~G1eHrpkzopgH^Gh!Ius7rbaFX zL)Bc+0&Cluxv=#VPAUM086;u;!b;CF*mT&Ih4csz@)SxFpQ`SksRGY~iVpwXHNR~G zu2$|OM54h3B#GS9V8kXzTH#aDztJO*e&XlU(Pd}Q`57b%2H|T0%<)h2EPWuYJp5cE zUN|*@nsu8r%1Gz{GF%+F7e{|(_<=)KogC5dM+E&E3mL624a!RVrfy*J+zwAci18>g zX9mkUPHJk`vYzSc*i}YM8si2orNJ?!!9qQ2Y#-wFZ{-u%JJH7bZ?t9CdaO)L!8)CA z-y*P@B2ubwiHwnO43{cWk0EQ>@X0%Bpuy}tJ^#v0UueGsv`6(_QtwG68BzivmcCjA zF?qFJ-a*I|kIic8qH|(FJUxep*w`)*oh_&(^~(lASP1qmmnp??Vr)gbU9uEYXKm|u z>S2CUn$jSM_HNK_SEWTsIlLAM>Ov8WGeTe2V^)JDzXSIMm*Ii9(avs4ZpXk;x+LmD z1kje0;168qGZf|~>dmJDx0OXqauS@b+TFUIH$VKk{<^qe?`w_6cHe5-*g~-o`8%@h z{1^@Bp$R0}*g!F*l#~?~y8p5PqG0NUK1a?_5s5+-wm|NxPS`N!s+R`PkjpCtk+v3Vd1*vuA{z$!*$?`vxpXE1&jrDi67k{ndfL<-FkGGdY>@h)zy-=wl#TcL?`9AQn z0vD*fjE5F@xEv|}r8&Rp-P`L~J_1gX-Y4Iq%IaYY4Djv;)xQl;L}AA_5Mm10X%O)Y zP=7jp{{lL-+y(aTSH)bcqf@sOWSWTu65h>@*I`1E^aEHkGqb?#&R0`=b%B6PV;Ra^ zh6pkdtZ5>I^aHMv-!YC!l$4*jsnz@Zb6i%vlj+i@15!n}%o|)CmPeJxdW{xe$L+*T zTZXMiqDquVx8X(4myRB+_#i*dd}4}75^Fhs5=s9s%19P{i$<)wgS%*;6S>Z-XixD< zF8R<$MM;#2%%sX$-Q?Msog5eeE$Uj-$4Ovvu-@#HiHaOkLe)bcN+6NqqeY8QAgEd2 zHR(G=ca)eE2k2A|UmvH&sT~@x$fWfPlXpY@A6o^{$j!~O7i5!gL89w_1yK!R+B;!H zw+of-2u}D&CvKIKu`*uYoKn$G(-rNQNGQ&KrgDfW1^&j8Q!W(5sdp-%h*P}qaFs)5 za5C~z=Nqn7ns#e4=VqcmJ(udtr4xRc(9B&O%bJP2{hZCKskI|@cBeuS6X&&6u&7wE!|JYPK93|po(@#b_7I2yd^lA8BxOyJ)xIz z;RCBPS&_Hm+ven~3<47!NzNL}Jur8V0P{i{1Jcu7IGCOM`bIFYGQaN&?bOX6s{bsz z|0@p3FJvTVulxRJqfZ5i>BxBxaYv=W%@3)6`0COls_~7lTHNIMQt3}%{ij#p!To`V z08(Brz*qNO;SMD_S?2V*M)_Hl_lvii5eP?hSAq-R&|hH@^zesBFhM)vVhuF2-p>qdXRkaBbwz{1HMC_@ z#7=U81lUZ0Z)HxOy&VUMf(ggSu$C!VYaRXWH>_Pf!~_=wen}Y5d4uleP))Sp$9=|L z3h}=nID#bE$K04Mhb2yjq;7{NfLrbbO-mWEj*OC0jshYth0JwKtn!^U0XclMB2@ht zW_mB0g+`b3p#k)B(-7*IY;y*U#e=3&q1(Ki)awE(_@S*5p=ON;5{M&m3o^xzNFkI! zsjwV;DO)X>8G3A~m1P`D*}Ek*uoV~C9w0t1)Ia4%DlJN7xrrry^EWogeL<>B048^8 zj37#&)ur_@xz;f5Z?5X11~N?j6Wf__7GRIh#0OCtXkzQh16E394GUu)TJ9Rl84q*) zvcnqiwQqET@F}rMLt!bPQMS7n>A>m-bxUh-F20l?WTE6OCD&*RNdyN^3)}(7qumPH zJOWQXt%`t)szTJ>*=4nDb{YkttsFY<#6b+o4Bv%mFBNldw25$7V=9oIOh1g!H zoSVfr*NVO>_{f$4fuY$GQk3B~9By%`#$3muSw}WjB-1^rj3Bj98AC4C0b+%D`%o{C zy7`(M!7S1hUFPr`V~uC3(_qApcRr6MJKmafwkbpoQcX2VxSXhcIm0=juw(+jqZbLr z{WjeYfCD4Y+bhDZkwywEbqSDA8KD+lD4P@qD3?QT>u;0(vSbeN&H7QYs@f%v!Qp11 zZ39D%gE|Jgb_g5NvF6FiM$_d{AUVb+tko37ono#0LHIb@>4I?{)z)5e;ruFFv1MW8 zR#Kf}ovy{ohiMY0o_`r8hsUGBl=^3dO*+NoBHu6DAlDDQBdKx}*J>hmm z#TSS*B$`EyAR~w_U2I+^Br6n#x<)ZGD^i_%65nw3a?s>h6=2r~c>MT8f~Cy>SuZJO zfu3WYo$-MUwT4wh$wp{We!Vh+C{=~AEA*H0s@q50AW-7)q;BMBN651%XN9oagvqDW zXE$yyhSQC^Wgk9 zQmgdGRf1Crvm>M#U%C%wo1^P&=ouNJASFXdZoOvI#i{%7a>z8frm(yJA1YgexIA{H zYi@5*1xyL-+1ud7O!GDfeFM4oN zH%ku}GF&n@0W&xJ)-Tt`03~nNB&1#a)B*M)17YPo#}m*RsQHf>%nl* z%ZoFDm?fgN9lnmQ_^CAEq$p4?NcY{KbVH=O(%T6|G4al_Pt+jmX7+75S!yZ#*9jVP zxQIAM`8`2}@T##&LEvU!B~7Xz1_9el~;SIua{D@|X58H|Emx*1|o2W@N- zm1IRbw@Y^B0Tl#9-q{a87#@w<14k@se?sFegNZpgLhpY7WNqLf{-w*Yn(jrbeWZJM9p)&MjvJ1!N8qCwD6O_v4wn1j{5Ifk;V!Hn({kfvbE7f#-;OhC}K{#T8 z5Z&ts&%GnZ41tp9rXNBVs$Mc~p0+{#o+T+9aA=UQO~k7o{fhA#P^#X#-^o(C_7A7D zaA3JIZiW(*bbzC=Acb_&c7=2bt~;cK>{?2yw8RCLs6h#zLX-F==+nMK z8o=N!u(Tt{VtR*fy4SLii(O`HPF3b`LWn_#j93*TwjpO}hPkf8*1D8HIe*%=575OE zKh%7Czhw^&9n7edv}^_IonIdG2UIHgIjiI5fKjWmRH;I_3Jt0LD3vol zOUbAPxLESZLVv;vZIA+T2%Y%>stGP;No2ZAZE7WP0|Nq#a@h#pjyw=UD5Aqc*8*e6 z%2FrEm?c-6(TlnOX2^55E^I;kz9&U}M`*eEbqIOXxfJ&u)@YD}J3o$mwrnt%6IU!3 zppe@8qJ$|2iC!RX`NC4488d7Fsb9Q_okRO@)ZZU}x}!f!U>g~m<}Xr|KG66-;dnG@ zrIQXJ8^k<(@c=^Y;t4krS$quA9h%kGFjHj%7jLd4RmgoRt92X$k+4idwKAS8RmkhU zOj>~o3h@|=Kfl6MrUIi!xyZH()d=LG;h>s@^rLrSE$U!Rb&-2D&+l?UEr!83JUjBa zt=+Kf$1Kr`R7L5^R~_TVaYY(j3}lJpk_>U)=f`ag8afNuNCWoC7~!BQT3JaL9L-b8 z5e7B;715#u7@_x3=5vElg#(N0`LoY6MX)+LnDhCg9;KZkxv?dMB2Y)|^E^7$UF;>5 z(LzhxE2N{F#xc}c`77qyy zs8B`(t796-S-4q(jvwf0>OXr2kPXCwADUaQ$(otUnpNoP4BH9Ea%NwkFCEq%_YP!pM~|QY|5Co^-I%f#2F3Q3vw8-GyzY*KZ@4h zsY|L#Cn6kB16`ih5cPHh%S3^iqEWcCFE zE!k$BK9PtmSZ!#{PfL!nG8&nb0>HtY@A{|yQReD4iK0eorsSJ&(mGXFm^?YhbQ~(L z-#A8h1LBVLx1uxvX%B^kTY6z#wz(#~l|A<^j0)ZsPH37}%zLNhZLmDbX32#Gc9t@~!pBAL+hk2|}YI1Z?<$1@GUy zis#9!jpV-qi%hT2>AS3^TLeTQEfvaWQ;!i+T%j;8aMvMuv}04{$97_;iRJ13L4SoG zkvXJ8CEjN|rb{g>8ZCGnBj%6|t8||t=*1SzlR9>fbaVyb$kGVes!N4BAcam-n-eDw zpHV_Wj?lic8tpjbX~rbpEzmv}9rf37{6_K}^i`-*3k%5tAHF74Kz~T0ZqdO)y%{>z zMq_T-3bYHQYi+%UI(AJK2ATMK72vR~E^=O}Ng+BZ`ZRSPqHur9a+k$0(PshVNg|BZ zjbG)@RpSp_RkH>VGVNiwpB0 zS5i(=n}jjuqzDER1sz043F;s4&*~;K6tc;T`784`%}v63r*rcU&Ga{WIvo2Lg&H_f z6P;=1HB>YWa-Ix0rj7A6;6OSUD+mg3nxabp%LXGo^hGWFv%P_?+5vFm#x3l+4^mUm zW9-Fl?c(${MFsQZ^Ew5wv>E5n$ZDu@|3CSJi@24}51#dFxZr0N`Jd^9>vwzcTf!~O znx~j8vq>9IAelTXPQDSX0||3OMCN{+iD+v9t?Nc=9h@;fM{p3o)N$+F_)VgqVZDTP z(rq4W-YfvYJtaU-+q8i$l~E-NKDs6zUh+F-`xCRhcv1Zh^Nx#Sx5q81h=)QtT_3NH zgHSd*-EQ?DH-_FwHI4Ukr+0UD82(^OxRQvgBE=T|b0B|#WvW)m<3Wf}$*0xD28dl? z-0QsVI+7f<+nujF+L6?n(ba9wX6~w&&bta!_gk~w?p}&JXICtQiyBPY)&q6h*lAJb z%xHX_OxH#Qy{7dt(MFDI8o)|pvg{|SCseVGO(Q#p>YW42?&0{6i>)1?vobnt?deT) znJj8wy&Aa7ZK3qVvK204Uj)%!clKD%AL@$;0=h`2WAgGM1;AQM zOl9J*VOEEKGH*f!v{sPGIHm$pL-IQ}O#{LQ-ceA>aFdktnpQZJ0f-4_83%ceXHqp* z&faZ>SJB8lLd;*0pqom3g=N`I{F8@m>LK_HPRd&_k7#q!BC#>3&?*-~ep$S=wm5DI z`W*GNDO~hDym!zViAVR&<&Rm?*p1NNlzGLc40h-vOekbi9{oW#8x|5Gv_;YOrmo9; z5Zil9QG03G?zi6^nx`aH^^CN4anp?o`}(hP7d26+memM=`ej0v;LHS@T+3(wyd-rb z5cNCb*i9-dG3bu>r|$)ptA_cP%n7!gwxhsHO#WD}#NrWdWnkQ-gp#aRx#4xizwtd(7PmTJf$u(2xl1FA#jy)T0I{l5x1{Pu6o7k947)US3LtLQB_hAQe1#+# zC#0a%CbslF7tI-#?Q{2tIXMnzXbiT|18R+jcGv`lmmiSdp3+^5DyWIzEF#dG?6han zdJh-?zcRo;YF*6VnZzcC{HAZuLwEfoX^5%xrnUkMD|-yIX>4YaFzara`N0k zB`BE|_Bi!sxRLvPOHXfhsVanq@0QZhzIxH+_k1Oo0_r19oqx5fmzTwNjdIvxFDf<; z!ARk|Kj^??`krGhv-W(xou4TFare&HN(cGyi*0zWnB&Is+Jy=HdBI_z4QNy~#nG%0 z^E*>|z_T^|l*}2vD?e=1$0iLHL|u{Fw6knh?sayF()J4lP8Q;4#-H(|{BTHiiUDt{ z8}$2%J!VSnj~#wUUp~AR>O98`JCM+<%GsQr{3hp+r%JFDaci5Ba9kW>r~jNg1enHG zgFE2ol^{kz!H^H$|l$Q1VcbPaSs!44RwY;Lobug7Ay;}LCTHWIfF=hzYXn|a! zFA3oSzCx8**4Q+RBcU>5rQKdOCv@yE*SmP%`RDKC3ZUJ5ptfgVe9lfbzfq~uCvCvS zYR8aen4R|==GaA_!DWk8Jf%h4^E1b1j{aS)+$P$S1Eb2-1%{Ik25?kGiot|}6PkQm z@7aw`Zp9b%c>lxJfr!b$Jr2oD#s)0mKoqJYrJ_@P`$+DziqImoI9aN#kY%gjU6xbPbXW zT5_QOsH!;BtRfy=?^Nay8rE~s2#IE^MTdl56)x_{WM=#n(h*BL!M%8RHk=thgR(`m)#w28=>EBgk>sQ>tt;r zlUVZ5QrFMacz0ip#|d7 zeMEC=C2MMBr3pR~n{21T5cnSvp!bB^Yn`DTL_;WYXbsp&TLj$5Mgxj1bXVk!$;5^q zBL`Jtx`-p+h^s!rH_iD`c~6eC8EL#W_Rfv=@je%9_G14>2!AKUUuSe#-={>(U{Y5# z>JG|tjzQi6v2J(=o#YXc4KY`ffjh?6D$Ia1&oehGwz`QYqEuQR8o5vTM zQ2y)Vha6tSs+-%3UJvpvPq(AvZE_Ot&**UQ^|;OwO5 zeHfiq@VcFO%61xiG5Por`cVqBJ2CyWedv3W`>p2f5rS3n`gW2zDmQz}_vQKXe5zKA z)!WPSSM&}@ZAcTToE^#psm#5D?@QU6Qv+!Y%zCMpGzPw(3(BFR}M+v{y zu1n;5aqXZpqRqc9r1(5slWFznTo-N#8SR`bsQJ;b%j8K;4+l$0(3_b8rM195Jd*(@ zVgxi)J21E>)hpzt1{Bh+L52Ne$PnG`(9CNskjHpE((_(MvVV@N4GAvvB#g1kXk8ar z{VP+HVjL#x?e_lOl9f!URF*)YN)j3gYqgYjf8ln@U>l9Z50r`>f1$5=VP$4et%w8O+2KIN*dhdhm`;r zr3Emuq;>SD)Q31nATBV>@}SgRH)`g96n`dCtk^g%%j%v84M?YNQq;m6Cn>^$iTH#5 z1V)OToy8v0fivD`pcMMkFO|q)njBCwkFiH80lgZ~1CYqgh%GjRI8AX|?vL}5SN04l zDfDTfC3gU#OSuHI%w^E};YWd5!brkDq@9<*x-eMxNfTfpxU z#U6#k!9oZYb`uv#e?r&W8O?lqe;5;+LLTZA!%J-@%G#YlAj>=)p+KzF<+Vmh$pQEw z1(TU8a_=($7?YMhT*C?L4oPp~JwQSxT4SF|H||j)?q=4gg{11uYgBV*qW9PBkt{*g z!D6>ddMQ~Z`=sA8tYT!2q|oUo<`|(p&I2js$O!8q@I*lj}Q*_=!dxp`Q11 ze4^q8`B^@9DmBzMcUj_XE=p2QiOYc9LR=c`g~1r?0$`ac(j@-{klNb4WOY_fl)NI( zgU5 z+n{@spP|oimb&9GEQg`u2&`JjfeM;leNIQJpl+8;cA4F7w{>Ejg-=?v9j!Qxe>JC@ zprB4B!b^FG#Ia4s+Q?#TqV(kOQzCMn+sSdw(@>D@ODXNHTU&z3;5NmHP7xOWZGqIE z>>OiQ$-7I8SR#AEdpwVFwh9m&CR91ouA3+9{ROJ;=h?aNuM6!54cIF=TW}VHxLwY7 z^blNDB+)BDykH|qqK{XWb+2O=YQG7__kR*7Y?~!%w)V&W2g*plkX=?<=5?CagAnNqqy}_7dH+?sC>HNIlt+12+mUr9Mt~XzP8{a+(0;fAUBg^h7`hk>#ABW;WC7SNCnfDkuXe|_?dvK41EDc2NDM}U5oP%`N$?3P!q}L$|L^0&~ zM_`|i0r!}pP`4j_4aZmqjw=2i#?C1?m#ACU@rrHRwrzf~ZQI65R*V(fwr$(CZD%Ff z`Om&N=hVK~Rb5^4u4i}6G3V&F#(3Vde-^Sx$VTSJD$Y#Cz=xLn+v1mU^9GWa-EVwL zajw>YP*o0Cir@=uO_d+48I_ZiF{GV93WysjvsD< zB-0%=*_mn^Chd5i)Fk<7)J0G|NOo#ZLp=IwiK3{U)2rg=jcLTwkk;}tA#?~qL};e! zaLcr)$Iz1drAv+Nte~`j)P+5eP{{BNQ5ljJA(#7+=~$$3cuf#bh_?UUd5Tl9uW%Z# z$)ucalbdI<>~0ULfg%yt*T;zMMhqeJZ7 z+dZlH8H03;>%E1noJZsXmGx*6*D#$;QKRaSFlZR=;B$!~?KfEcBp5j`nAb5P9?IO5jI> z_u1zOFX|}Z89kpv)jXCBS7(lF2+Uif(uOCj`(n_Y2_YFi*QVg9S1^(Qwm-Z55glcC zILPF|$l=RWm!fi?(rkUdQcnrH?0$7gsY)8W7u%{&Zb_??kR=+H{^Fh zxLKmFvwg6*E)uS5H#zu7J*svprM!v*ECn>{(q;bqc1PxA~+1rb}K6Z*|tGqKN`U8R)`-IiKMMQdj*aTSs(@7(wWRoCt*jjTwBJ zfaq6OAamtdHKY4g#x~XsKKB#zu%pt!l=kmWRBXsy`2}cuI}-&AHM*gs>F_@xA<+<^t^;$((yo(L(a!z*bqE`^WqFtmP&pnn zL{SSwYI}|u54lWmi3@4f&#=9E(wEj=0F2+V^|WS$VZ^k|9H6LbS>0QAe$W z$AFR+Zz@h&shZ%4;gaw!b)`!aZ@lB1qQ}Z>Lo()6x5r36uoF1^?7YU3FsLrxwpUP1 zZ*)D=#jtdZ7D=XF$_wTD!*_{b;5r*gPD$SB_V?xSBWg#vUYGii!T|I_%5NHR&M3xd znI|{botg}sKCfsRXJ`{=XLq;l?~?$o{FYI?omyq)Mi`&a*?|(~ zvd-SL7Zf@%U%7Ka!PcB-E(-q{jwyY!hT{{D99aEJ6c-65h1Gnx2@&(zyep#$pA;6> zth+a+cv4szY{5PqMs*9>*NC2fsbVxed8zKp?6U~kF$rT9uhRl4nOo?;OBRxN1T;gg z7K@ME`<>9S8}^E@+PSAyy5PR!nkP<;*S(kjTo|EMGCW{U`3x08F%kySrLC*472agk zB-}(}a$~f#HfOmHER;pRu+f@}1nh6S*Or&H>=Defza+hksq0}>Bu4LE*h69J57pc% ztA>d~JdhZJ+VS)AK2F&Brv!qD8jN07m5!p#&`=s5gTYT-oW0Lupmu+4{<%v^1h=(r zx{>w)^WpIE)`ye*~=8(yR5oyZr7~g$XiK zH{~gxrC5WsSy3rM&&pUme6l>AKkww%>~1gKuK#RYQ#D_u5b)Hk47xI|Sa9-?CDGw< z&o%uqpzefaW;gpY0nd8j_x9@jW4wFn1C)LSEyX-)4k^m0N}Ot($Gv4>UM~I`W&9rD zCQpsrUpYZilK>_hdwUZWz0&2~lqhGl{vUTskK&*{zkj*(IGtv3Dfp* z@%}n~9%^(MIaZZsQ>S*GdYP{8`&Y0I_*4HB5K2;4Go3AWYI0e#R$HrUn)-~H+_Wb` z1~mXzs+7;9MxDER)AHkgjWP8*Ruy)H=KBFepeM7p(<;mGd!qN41uShIVycsFLOfMq;@0 zypV+ncXjm|2K)Z2j*>VnQ$dZ~^($mTK5%rXT@t$LOfo|9vs$o_Z85>4EJiLstG>mX zJcj@*Wtl(CbW*feCE32v@F6SHN&D5yCsQeoA!D9p%LedyTdXcrVRoYFQM1vEDkFmG#qMUBbfaLpZNJ)!LW?G77%Nb` zlOW~*_m)Lv;~;K$QuZr8VmLAbB(}V={fV^zZ&;?vp}aNymyB2osdq}aw9W4_&jp>{ z=?N)a0;3>%x+#Am%2x>3Z*?X`ixO=V!*37&9*@=x=&_lCxI*Y8_?IOw``^-bsU2r# zYEYqhxR8GakEq(1mt88kX9a?QC zuyDWN8xMmET*K7Z&Tq*G{lpYza{}yIO4YXRdzLBvhcyO5p&syXk|n?%MMuDSPl`2A z3S>?QdrH0LKLY{dQ@;!B34WO4u8sNz-HMc~yn+mWny0}XCnE+Eml#D!I%=6qmC-?9 zk!;mrB}xwsZ|erw;lx8c>AZbpzbOUf!Pr%T9!eJs9^c>Jx1KCYX-0_CFutzdHa0L0 z0)xX;iMr1dsC1Q*WJaRB`avS5Gxw3Fbg$2At)rWBDx{YqwnAV^#-S3uK_KicD0(#!Jmh^_xnFwFj( zC!$}7F}VgFM7uN5I$~tAg{GLLvS4;Tu_p?y(i)>LYYB-wStpxmmnm$H+ zQ1w(Ix^{ASDnoq0vQFiD+~neB8dL&hN*DX<^ZUekr4q_`nUqbkTBHfqH4avrjcKD?XB ziDi3ZFw3%QFkYr1MGTGkAe#lx3phQa8RdSlehtA&9t)F#Sw*Gel9dAdj!3j|NLEX~ z-w0cE2vk%zjnf?MSC@vZI>NV!&*nb|H+-BRP6n|-i-1MMlOf^eMU%AKwCR*z?-pnB zi8OR*vQ~q?8K^E|a@%7MW8^QoUg9^`-4cXC0@!V(dEpQ<6LH{H0Kf)tFHwT z={}Z!2XIRC%(h-iKferx8KzQRs17L3B5`}i#H5(=WY58kGlz;MgmD5FII$fY^09RHXNq@3n?tfR}Ojl)RoGo8yefjs=|?TULP6BzB+4mM6ur zbB#u98(~}uMUW%yMglR+dLa!BaCVC4Bnu^-iJ!N-?nM?B?s{8yo?DY1un0-3m)DEic_0W5w=6{eFTFB2X&Ot zq04~J{!%ejTsS8iwcmw+q^AkZkknt{R8_?ud|NKAx9TZSp=*ObU?*w2`aT(I+70PeayOx;b+Ay9$=@o4zn9`Ps+IxVGw8mD zZ{lGYT9@~`dwC;Zr*@5P)R5p7XZa^C`O$l@mFBRQTqIi%@iAU!W+|WD3%KGkpE>7S z3uf)S;1exLD~6kY%6g69am_|F*)*o>%amoO0@sF^T9MO%MYeQY#mbebd^szuE(Wm3 znS`k8L$mL*<4WBos=D!99~H;6G}tYgS63Ab`)3$cCm2@3 zq&}d3Q$Z7eouP4_1{53@lFJ&zy(rly1KftlTnqG~Hw8P{cO4i;K`RtIhI<*5D+mhY z4=I)ZKPl+{D~u!?Gc(8kt)MTeZ#dzMAo-4{Pm(+Z+&!Awsd~nfahZ-dCPGqCn3m|! zBD>5Xk0Fb3CH2gtfxFegRMYoPCosPOO~Si*y}P`eE$G*vPbc~_*I?*>z9q5rbZh%G zE-Ix;^%Lzmz~|Ex0B|dnXSLg`!;jlF-&SJOP}Rxqh=oJ zVVn_pH9BTh_qi~}k!%wW9O&8^EK1i z5iDoz$M5Op?Cb9N>F4|P@OSt)a%rEg6Cu1xhP-#0xfi;7ry4^b{i5EQg=-p_ouiJP zVrrYCqJhT7+UP-l^P?to%DtFS61=a)qfy9JN`A~te%QRea>{zOO$%UD2Zb};NhN=> zn{jd}TnkqoJd(()t=Rp-dt%_5>pc?ZaY41?eJ#?=WHU`cY)-PPx1VrZnc3o|m5%e=6k1%C@d!u?YLo&e{@a>> z8fCf#uojmpWmDu-9!A*v;^1qyz+sg$-@r6&LG-5X2jk{-&dn_dUhs-hWL!;TaI=C#ocN*Hi)G91ux4e8Pkp z>CCT$0D0r;1S#r1WIbQ7RMKR8+46QRme^!I*`3{4vd$IEq@j=LtFMQKcilFhT2P_+h_`u{Fo?52`f0M8D zv92HCgB8cKQ}FL=mWQek@8XvO{jPU@kK?QpnTL!`Krs%98>4hZPi2airR9lpm!{mK zY}WAOM7HmmjiPdi%??m5Jo~+pR;uVh2ap((iQ-KKLSwuj%3LDg)+*QyBQgMZT;dRl`EMuZ`a_hbd3um^61BcX69lF ziyv2$?8Whe=@NRGa%=kiHNa^6PXtB<0;9N3vk=StZ4*}&ed5Y$G#~avrFVcbW-!8l z6EZfo{uDounJff}_Y$;R6$j#TuNWdQ|L>b2Q0%7ubfQwf~t zExQ0YRTAoH1$Qmv)U4^~rE}sBRGH~faHd~DI?mJNhq`AUUK+y?Dv_+xQhcg5Z>Q*48_B9+8Bip4-SG$B~p`r`!KN3f>A)|UBMm>gW#F- z7~>AGQo{mLU_QuHjgKv8s14_xlS)fwHIheivyraIC680B4ljqr!4>C0o_1L(ckc;t zYb^eGQf`ftvhihUfudojQ9{r8mc~y}ZD~=6N65G7ShYW{rTLCz>vNXh~(VIPuad~-_X2a(0jW1I)@5`XFBExD3AWFK1Y z*MUnL2SMNtg+mC=@vY^-lNXXgl!G5RwO#_El7UaW`;*G@FHhrwfL$9X-I3xB4303HDL0*iLEFeYU zFFb4~Cx|jxAeh@?|JH<}NH`>9EVuFG;)-bCQ-gj;(csz)A3FKWL-XJeeqT_~eK2pr za}9cD7&mDE%qCPQEZg$tA0ee=gqUXTfG8e?t2H*!7R@3T6#vQym{LI}u*phlLf4() zqA8<`T?MrC^PK$<3*$tHqe(h$I!Ws}!t(&WIPhSJMT zP&iSCnra5{^GUWq%ZqxwIIpx*7h@@Z6{_%76Xa#Q8mw|+yS4swH zo6cb6u#)ywwN|n5E)w>+d0`&)&ZK#C8SFn1`pSZGjUu7cVAR1vf@z{};-cK77C&tt zYtW%4t}&}(9j4wIOaBVZ4-IM8-+o{j1p{`C72wwvkDILLsI>Rg#Cse9`Ge91B zl9NoOG5~vPf_0c-yZyGPrXXr1r4-L#w0f15Y~C7z^k?sVaSGqgZ)qT1RNG{mIB*m< z6OH2HHxhWjpAHM(3vdxj_&tB&vkB@|aQDM1_5}tQLlVMimv)HRm*6jw=DH`E!&QcN z!5Jhu)-I$#v3px!KI7E7M{djW)vk#>GF5}Jmy(dZ8Jjjk(g%&(^={L7@u2mISG*8_XEoL05_3z63A(`yQ2BZIug5`z4kK(22#^LNCeha~}r+U33 zAV!nZB0hH!wDP&z#&A$4uPz5M4Db`b=c{Wu&!=@1s0{uQ8ltI%k}er`>mPK+&I;v9_;wN z?hQ38k?RiFUEH^mzHS{&J$+`XlF|gioJ?qr87zM{=-YPLZfd-C^#m!1DH%WvY{Y@F zQ_hovD;2KwUR)3KGiZ6TBN@q*6_TFz+_;bK!E#|wBv;nKx#-j4#}4js@t$<-zDhjP zjslf{oiFc_kNY<0`kI|=ny($dQh%wFLy^N#<;ppF)>qOgFu*aCaz^Gn8m(2I>;N0H zPBkL%8F4R&cU(mH?W+GK?vC7Hh-)xf&GdUM~)BFYbitT%*C^shNaKm zXrzYjlI!?A^T?3UXiF}zDl<;{*bVBv`C}x zudd@K_%K}GP?6LP6oB2535bZ$rDuOwa$1{gnK~*n1l2=LINsyR5NRn6@%0is#5O=E zDkEB_9t!(Yx&?$hAb>2(dMyb!^eEWnK5*>azi4Ri&jtsR?Y)x=4d6Yxk2Dw1=T$HW z9&lgomz6rM!+TEPdch_6a270Gxoimp3-m?rAwEbGfX}0_;=N^J))wW{x?^01m}Aa4YFarOB_|=*}w9%sD&S?w}E9s1@-O$16Vx;yEVWkwm*p5Mu$@-pNFMeAfbtgyr^hMK#s_1|(rXOirH z`k!rC?P+F1@q-7Lbzf~M@+eDH^Mz0z&ks?>h6xQRV>`Dnj4?UO;kXm54gh(z(`tSf zB9;+^)#6`O4tahDHqW-9#ae#3$=u(-c$ktE815w%-A`T*Zp%ub%jKJK&5MS!F);3E ziWna0zs|irwC`$G?7tqoMj}5uwZ81@y76-uH53sXT1EQ)!Xx9##kwEZhJO4VgI1P& zFT9=cc)vXKr3>(YfXv6`l%?30DNQ&C%e6V_?}SS1xAL#fGS_5#aZf0+Nv#GR?iHXu zwYg@(bd}wGa|X?a?LzCrYO#;bM$Ht-*+R**l;m{8`UiW{$3<@WL8?D3P?fw>oj!pe zCwN&1bYo8&!0bVk!|V^GOKd-$gQKYy>OPVx2-jMk_=7LPl_&#_+)HGCDxbQKWr%#r z`NC&bY_?<1QJ`w2lL);+q3=4uLScwrLm`6*ij(>A?P#RNc8F!4G~LxdC4`$9?4I{p9K2y zA4@>LRJdR~U+^FawhXJE1$?d#&7EEL->=T5L zp|>Lcu^!L1dO?F+){od=(%Bmb<^j(l%54eBD+Gc#6LQ`lIKhHL!*$NNq@=nOZA8?+ zjhKB|qUMIw15B^K#T|Bha+e%idsyUu*e=lX=a8A$k2sbrr-Zs^_47u%5n3k;8N~do zVkn-(9z@ZdsKXM@9M|EZH{px3DB+-d@ugBzaSee#u8d5b{(b(qecfk`{vq9;`vSr{ zF8cz+J6(eUL^%YlAL0JpBwxlOG2!32eJF<6*3T-_CEWfw|6=X(Mx#aJy`uh3e3R;` zI_f~=;t$4|PK3~U%0=%yOA&w)FlFa4y`O|p0xP|Mtf(E#H^E5nE^X|;xZ6*|v?D1l zf6{e%d4A37Te-Pk$cE-r;v@eZxBfx(GOUWg{o$Mrkj%{qsl>j2J%9g!qvDFGL75=m z+sV>9leZ{q!euzO`mn;M2>t>kaKnICkDc7JB8E?CB}lwc?1B*D#W+Ul3@x|%Czmz6 zcA@`QsNY>CP%2~A%4O`P3A;t_2LnS&CH+SMFW1m-X>$3}cR{ykqPkvlmwrt_pZy?R z){&czF%UI6Q9<-CRdlq&#g9`<6GHA-Aqib5GbMM!<8L_Wy#(KxQYTGae?tZh@>MsO z^2EDn9xXt3Nxsj1f?Bc45*%5Quhdg*;r(tx0w(C?5@8XeE8Xw9o8VU^U!h;`9Powv z5dHnKTye++g|{9)gk+O}N}U3uXK1xSqO-f#*MuXK21<6-_1z!D!&?#{2#3EbI=|W? zI}zJD8PhsTb@Qo0g;X##ijjl+^^8dsSNTz}_UA-Hk*f~@g6>FI@9|=x*^(xu(uqoG z1){_7e$p_n0VAoaPD3{sZ)#%i$w(7yF>lCnfCnVxA-Yra2RTh|W56YU2J8d4N$}M4 z(mr>c^~5zaPa5jOhq?R6KS&X!0yv`iQt&U|I~%vV%+CX{4KS$f&JK^}Yy7#(O~#icDht#Gtz+CxI4YQUb+~|bM4>39tL~8eFwr3 zXTL%F{?xI7W*VuMaA1JdzVx^2**_$+~NU;Awk4uN{ z)$^{*yJX*3GIyIENl23i8nStYA&j1t;wb`}7w<(#|9~Ql-mVBxC$+DGH?^%!e`}_* zDcY$hPAQkp!5l+TVFGj&ekO`^sfs_8n|h|y zc1~z)Khf3{lX(`gr7O#i@wE3l<5~#ZP)7STR11uP1#67e?>7sKD#{nKe+l+46(RO> zO!KTH=xwu3d38n1&u&w~{h|%`W0h2}&HW-LFi6196*o_!5L4yw*S*f-p1_TI>xf9SovO zuIj)3QvIzXZYl*TDG(_Xr2Vgye!<~jM6WFLWFvF;6T~8Ao|D!tCP>EKP-y-| zaA9rQo@#ziyZ{*_DiGV1hNEMzy0u3><`|J=#smb1(tTP#Hw~3L*ytZHoZ`^hwO4=? zsHBl+9LPLSEHbCjS-(9u6*`u(Gz|e{gj<|sgHS3_JU(H({Lj$lh(iGPj{})J3yshQ z@E_C-`s$2P4=08LF;K>$YYVvE&wtg!;Xut{A6sgc0&E>PNzZ$3>UievY?YzCiMW>T zJeyh7Ld3WDYlI1)9=Z^n8CN!4aB6wiBfB~=9MG_^OKn+&T>u)VQZ}r`W%|AqL&Q1s z2@sB&_Z?J8?K)n_j4*?3a$MqAV!tGxcQ}ebE!~ZT19ONKgfl<)(yCZct-^B&rs3*C z%w*pCj+P5}6OmQ5u;er1OE1bntEeoo8XTkk0A9k3+^h77=9DjNprKRk|FP!xe?_il zV`KdP=x@6An?Llo9et+QdC(D-7h`_>tUw^X}l}>g?+GG}qbH&i>t)KN}{_!cE5FH}cV$V*o{kCvre--1`Vti0Z(!j@&?WV1~#;sIezwxecv*X*w zf8^q1*o}r$$+VNTmB~ghp=r0Rucw1E8TvQ!q2gOl=X=MeyN{{z?dWi$LV@FFE^QtU z6PDgl>Kjuxtv-?_y3D{ItiT<+Z|_DA2oGhG>z8<@@Ew0OQvRAOxw-g-TS}RHg*ks) zjQZF^2Xq6y^5|U&+WI^ijY$-&X(v3hOzPPA5I2V-LlA@o_sI@ylPgYscfNUl+?0|` z;f{3J1iy$URZ;L~7T>9XQLdJn_Ap0$jy9QZW&uMnH`q<9fu`-|%Q3|yo94(i3SX7O z5+ZY@pZ7T7ZhnL731X#OUVf3<)vvUFA1KU?GCj zn))3$A!yWs5NR<&`n15#)cnNJQYS_=CdHfDf2V#Bs-CMYAs=Zb{-Y%EWy^fGm?<@_ zRRJ#b`;}w6|JLtYs^#`ph@{cVb5!rd@7j|3ccPtB%7|1gt6jMk^Z#MKqH2nMDoV6#iY@TVKtdO;sp}ig9KhV7k}t0V3n!W(RkG@A)B-{PhlBy~@jn zckKX;)CZFNK?g~ul`(C@RywJfA5eusmTch~Rk>jotGl;K+%bH4(R?Q+MNnbGsIih^ zj~F;LDI9wr!?dy?E=baGSsbcnWj3eG*D=()LiP-HFADL}U$I83Kmrd9Xe_cGL1vIoST$-FhE-^Ds% z&PtD2ZJ|@UGAjMk!Dv;vnZI^U5VD9CK!qUWF6k&%OC~9o*53tbZ0I^Ok@I8PxH#mJAqMzWdAV9@3yBk`Ch(Mlm=$iWzC_ZRF5;yE2X+0FcEt(Fa zhW(3XdxyQZ*gQr8q5>j%h8+&lpU)i0fvv_hQ_N7+3%Ut;~+)eRNGSiJt#QIfc79^%S_i@kGrx#kQT!be1D?36H206GwtdM5OXurF) zPDb2ih0#%cGZPUFP9}AggaY?EbK-3v0U}2FAkF~-Hc;#nJ|@EFM9P%#mqkkP77GZt zKvv+kVY7TGWZQYZ3KSZ!1s2ThfXC|?4#xdyJ=nV*JQF6Y8MaHeRHN`gko;6V4MZd` zSIxeYd{h%JcM9YR-_gB`K#Bn0BDSKa!4T3S+$&|?Hzo)^2(7wmuWvn2CG;+I4>E=D zKf+!Nk@YQMz+0e*Z|t0XT)wJ06uBA>i@-10I9ZNBDl~rmnWQar3s{?bRR`*1)y=vm zZIx&eWG1mf@UI}7SPrbrOgJI~@JzA~FYNMBa0ZI(Z3E|`2ul3Ho3-l-AU=is2Bn!;HIfViN?dkq(I z6Hi*oKIE&5d?#8Tqq6D|CdoI_P@8*=(@l1xo<7xyD0dxCx#lHh_J{kADSH6@KxVHJaDJCnyX@{ojiY` zl3lRHs4fjZoo!ad!n^{g5X+N8rPv@t2*mo;WllfYFu4f*V3nbyz#rzW7*{0e2Q3vz z+Mvlof%ZG{NtmJ8wU}9jj+E_|{5gkt%`a*#IP}AW?90}K-XiA_LIk!L*oKl}`H1Mt zvo2d^9`YF+U-15bmLR~0>Jy*Gfm8G%N~)lI((h>)P!Ffi065B$=M#}^^(fwS$b1}IbtM#+PS2LnX->z2&73p@6(~ay?|kp++Gul2 z0H-8-V5e3_VqWL8hjgjuiG zti6z7wR{!h9?IJ7I`|J$Ve|LG&z)^kFWlY}$EENmmFZ4@NU;(yXOL~tzwHG6nNc5a zKb#wkp$ouaAHq495%zg7g`}}N`i=Xzo?|1cj`c3&H8a?FEszZ+4C5F6&98BgwLE*L zF*DKjc0OY|%uP4**LwVsWI1NIj&3&iXcXQkdKZh3^l=lysMZ1EF!3m#p{&^s)L3^L zQQ@*n=0?fq9v`^2M6HbC@zKt3U79!DbFe%mURb?2@YU!Ma=%+p4k1)^Tpa~1-f>>1 z#at_&QHUgC&@1|vt=J}pFs*)JGCq*@l=yZ~9Rg`YEJ}0!sJT~S=-ZWahy@AV=-QwF z0)t3G{;MTF3%xGC&z6;=AdQ|w*o>=;DS}IN8D|_0z|7x+Ad zuD{kgxQNA{L3 z(Kgc!+ZkL3tIQFJSM{L%c<2>>@XsIpV4i}!fA*)T(u+t7<2uOb9>g}?-7+gYi0d>P zIRTJqdD#O2_DaPTUl01y)|)GV6TZ;F!MV?8)d9|l$*qy(TfR^!iI{coit7SmnItQG zvRwP7-G4sVx!zZSuMHNsqS2qgKU@j43;b0b{Fa?%x^)-JU3x~IEp6Pla&^rThK9>%{5)LrOWwd-nz z_Z)_i%C~_)uA3VRUlMgAM+2^eehg!0d=MwIR@?a?H%aj}{IDUH0KYP2Mmcd8FQy(> zI^N!mX+?_-TNb7oTQeB^cH$kEtu*!(S>)+irz`~*0xW;aX;%G?vEu8fD5)5vXzn=K zdX(v+cJa!+m(R36UWhUM2&w|tbNk6rOuYr`T&q{Oht4*skfnl&XI|Xt?-~94Jqqy zYtDXB@~Lf0XFYLh|JS*iG^NS+tx`kWr2MkGGV+wNf1{a9c6SM0F8m4Q%L7s)a}Wed zZY`slHGVOV7z$9vG!$Z1>|cH=wzy`v$Dg=i_-GN}*$1G=CWCGA6uA-6zmhM7`l=`4 zXM?cU_qIcYx3+h#FM(Zz9T{brDJFH4Mec_QKjNw`{0f1HI~=|Qg@e)h@wV%QSR*^i zd$7yBVa~xQZrH(?m~nr)3X2QYbHcZ3of;k8gwxg$v0y=HyE@lIcADmJrtQf4V;Rk> zfgVB5?7`5kmTGFE^S&ZTm)DvIu zj&o$N*+1Dbu`~Pj8O_W{)O&;V87(ZMvK@0~p9r2+N6a^|Lm5O3z4X8#w1OSB@b?yj zmD7h0*aPv8nC6Z%=Ur&lkfqTxCo~KAu~3zQGZ*x1bGfiuS!MaDQ+34J{g+>R>sG=; z#!64`{AhNOrL;i)&+VI6rT{hs+LODch)eym$w012ACF4Tzc6tid>&W*OBCFjLOMku zF@;LmZAUPzi6yGNdDh}UM#jYtW!}#x5hh%qYb2BvB#&kh6>Sq4(xg%v1UOEOFYq)w znj8_kUQ}>3Sz9LG*6OXa3(eX z;~$p*{0GjRGU;9EF#S0lnbknXGrMGjp(goZVw9jKv-Wk=mHW+7iebD(NT?L)lhPcQ z%9)jdf1tfeuakB~95KM%)jHuaI0))RM&Ud9U2j}8g`qg=Hf?mL335AcFl(Fm*U~(` zMtH8^E{{{PsE=V@Va{*u`7d0+C)8{~bGk3PJy~3~G@OxmHt!T`SLE*4X4tJ8(_3v<&DD@j z{j6l#m7gESPmjkP`}P8)?)W12c_M`s94P7C zDXVV)n}%WYjCDa0C5mPhqJz-s(Rb%6#NyFer9m^vsRsA_IjD72EmpJ5bh%_%8&@bf zDj^hagXSuRACvR%o|7_5w_yTvcvpZ({ehRcj^yI+`=G7{wy

    aGS%131;$csZM@o0G!_>R;rK;JI8y+*TifMC11 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%0DD_Jk}T($vn}n< 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 k_w?>f{ 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∈18oEqs0`JUwfYL~ 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%0DA(Rk;WU0yE&uy$ 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 NEAdK7UNwO7{ts?!vAX~O literal 0 HcmV?d00001 From cd407598a70fe095083b77e77a0f5593e564d39b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 24 Oct 2023 10:45:04 +1000 Subject: [PATCH 157/207] peer: Attempt to debug N(R)/N(S) flow control handshake. The AX.25 spec does my head in trying to understand this. --- aioax25/peer.py | 74 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index acbec12..7aa1c74 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -430,6 +430,8 @@ def _on_receive(self, frame): """ Handle an incoming AX.25 frame from this peer. """ + self._log.debug("Received: %s", frame) + # Kick off the idle timer self._reset_idle_timeout() @@ -515,6 +517,7 @@ def _on_receive(self, frame): frame = AX25Frame.decode( frame, modulo128=(self._modulo == 128) ) + self._log.debug("Decoded frame: %s", frame) self.received_frame.emit(frame=frame, peer=self) if isinstance(frame, AX25InformationFrameMixin): # This is an I-frame @@ -564,8 +567,8 @@ def _on_receive_iframe(self, frame): return # "…it accepts the received I frame, - # increments its receive state variable, and acts in one of the following - # manners:…" + # increments its receive state variable, and acts in one of the + # following manners:…" self._update_state("_recv_state", delta=1) # TODO: the payload here may be a repeat of data already seen, or @@ -601,6 +604,23 @@ def _on_receive_sframe(self, frame): elif isinstance(frame, self._SREJFrameClass): self._on_receive_srej(frame) + def _on_receive_isframe_nr_ns(self, frame): + """ + Handle the N(R) / N(S) fields from an I or S frame from the peer. + """ + # "Whenever an I or S frame is correctly received, even in a busy + # condition, the N(R) of the received frame should be checked to see + # if it includes an acknowledgement of outstanding sent I frames. The + # T1 timer should be cancelled if the received frame actually + # acknowledges previously unacknowledged frames. If the T1 timer is + # cancelled and there are still some frames that have been sent that + # are not acknowledged, T1 should be started again. If the T1 timer + # runs out before an acknowledgement is received, the device should + # proceed to the retransmission procedure in 2.4.4.9." + + # Check N(R) for received frames. + self._ack_outstanding((frame.nr - 1) % self._modulo) + def _on_receive_rr(self, frame): if frame.pf: # Peer requesting our RR status @@ -608,7 +628,9 @@ def _on_receive_rr(self, frame): self._on_receive_rr_rnr_rej_query() else: # Received peer's RR status, peer no longer busy - self._log.debug("RR notification received from peer") + self._log.debug( + "RR notification received from peer N(R)=%d", frame.nr + ) # AX.25 sect 4.3.2.1: "acknowledges properly received # I frames up to and including N(R)-1" self._ack_outstanding((frame.nr - 1) % self._modulo) @@ -640,6 +662,7 @@ def _on_receive_rej(self, frame): self._ack_outstanding((frame.nr - 1) % self._modulo) # AX.25 2.2 section 6.4.7 says we set V(S) to this frame's # N(R) and begin re-transmission. + self._log.debug("Set state V(S) from frame N(R) = %d", frame.nr) self._update_state("_send_state", value=frame.nr) self._send_next_iframe() @@ -671,15 +694,19 @@ def _ack_outstanding(self, nr): Receive all frames up to N(R) """ self._log.debug("%d through to %d are received", self._send_state, nr) - while self._send_state != nr: - frame = self._pending_iframes.pop(self._send_state) + while self._send_seq != nr: + if self._log.isEnabledFor(logging.DEBUG): + self._log.debug("Pending frames: %r", self._pending_iframes) + + frame = self._pending_iframes.pop(self._send_seq) if self._log.isEnabledFor(logging.DEBUG): self._log.debug( - "Popped %s off pending queue, N(R)s pending: %s", + "Popped %s off pending queue, N(R)s pending: %r", frame, - ", ".join(sorted(self._pending_iframes.keys())), + self._pending_iframes, ) - self._update_state("_send_state", delta=1) + self._log.debug("Increment N(S) due to ACK") + self._update_state("_send_seq", delta=1) def _on_receive_test(self, frame): self._log.debug("Received TEST response: %s", frame) @@ -1346,6 +1373,7 @@ def _schedule_rr_notification(self): """ Schedule a RR notification frame to be sent. """ + self._log.debug("Waiting before sending RR notification") self._cancel_rr_notification() self._rr_notification_timeout_handle = self._loop.call_later( self._rr_delay, self._send_rr_notification @@ -1355,8 +1383,16 @@ def _send_rr_notification(self): """ Send a RR notification frame """ + # "If there are no outstanding I frames, the receiving device will + # send a RR frame with N(R) equal to V(R). The receiving DXE may wait + # a small period of time before sending the RR frame to be sure + # additional I frames are not being transmitted." + self._cancel_rr_notification() if self._state is self.AX25PeerState.CONNECTED: + self._log.debug( + "Sending RR with N(R) == V(R) == %d", self._recv_state + ) self._transmit_frame( self._RRFrameClass( destination=self.address, @@ -1413,19 +1449,39 @@ def _send_next_iframe(self): # "After the I frame is sent, the send state variable is incremented # by one." + self._log.debug("Increment send state V(S) by one") self._update_state("_send_state", delta=1) + if self._log.isEnabledFor(logging.DEBUG): + self._log.debug("Pending frames: %r", self._pending_iframes) + def _transmit_iframe(self, ns): """ Transmit the I-frame identified by the given N(S) parameter. """ + # "Whenever a DXE has an I frame to transmit, it will send the I frame + # with N(S) of the control field equal to its current send state + # variable V(S). Once the I frame is sent, the send state variable is + # incremented by one. If timer T1 is not running, it should be + # started. If timer T1 is running, it should be restarted." + + # "If it has an I frame to send, that I frame may be sent with the + # transmitted N(R) equal to its receive state variable V(R) (thus + # acknowledging the received frame)." (pid, payload) = self._pending_iframes[ns] + self._log.debug( + "Sending I-frame N(R)=%d N(S)=%d PID=0x%02x Payload=%r", + self._recv_state, + ns, + pid, + payload, + ) self._transmit_frame( self._IFrameClass( destination=self.address, source=self._station().address, repeaters=self.reply_path, - nr=self._recv_seq, + nr=self._recv_state, # N(R) == V(R) ns=ns, pf=False, pid=pid, From 07b709638d20da1058f69f8216ba58ca742b28de Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 24 Oct 2023 10:47:38 +1000 Subject: [PATCH 158/207] station: Drop RR-delay to 3 seconds Not sure why 10 was chosen, I guess a round number, but it makes debugging a little annoying. --- aioax25/station.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aioax25/station.py b/aioax25/station.py index 3e77ad1..9b7d6b9 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -55,7 +55,7 @@ def __init__( # Timer parameters ack_timeout=3.0, # Acknowledge timeout (aka T1) idle_timeout=900.0, # Idle timeout before we "forget" peers - rr_delay=10.0, # Delay between I-frame and RR + rr_delay=3.0, # Delay between I-frame and RR rr_interval=30.0, # Poll interval when peer in busy state rnr_interval=10.0, # Delay between RNRs when busy # Protocol version to use for our station @@ -64,7 +64,6 @@ def __init__( log=None, loop=None, ): - if log is None: log = logging.getLogger(self.__class__.__module__) From a93a481f0f6a0d5525fae207820a79d589657a76 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 24 Oct 2023 10:48:17 +1000 Subject: [PATCH 159/207] Apply `black` updates to coding style --- aioax25/aprs/aprs.py | 1 - aioax25/aprs/message.py | 2 -- aioax25/frame.py | 6 +++--- tests/test_frame/test_ax25frame.py | 10 ++++++++++ tests/test_frame/test_sframe.py | 9 +++------ tests/test_frame/test_uframe.py | 17 +++++++++++++++++ tests/test_kiss/test_serial.py | 1 - 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/aioax25/aprs/aprs.py b/aioax25/aprs/aprs.py index a32f489..4513cf9 100644 --- a/aioax25/aprs/aprs.py +++ b/aioax25/aprs/aprs.py @@ -78,7 +78,6 @@ def __init__( # Logger instance log=None, ): - super(APRSRouter, self).__init__() if log is None: log = logging.getLogger(self.__class__.__module__) diff --git a/aioax25/aprs/message.py b/aioax25/aprs/message.py index d85ce92..7a361e9 100644 --- a/aioax25/aprs/message.py +++ b/aioax25/aprs/message.py @@ -176,7 +176,6 @@ def _enter_state(self, state): class APRSMessageFrame(APRSFrame): - MSGID_RE = re.compile(r"{([0-9A-Za-z]+)(}[0-9A-Za-z]*)?(\r?)$") ACKREJ_RE = re.compile(r"^(ack|rej)([0-9A-Za-z]+)$") @@ -259,7 +258,6 @@ def __init__( cr=True, src_cr=None, ): - self._addressee = AX25Address.decode(addressee).normalised self._msgid = msgid self._replyack = replyack diff --git a/aioax25/frame.py b/aioax25/frame.py index 2c9a765..cedf733 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -195,7 +195,7 @@ def __str__(self): return "%s %s:\nPayload=%r" % ( self.__class__.__name__, self.header, - self.frame_payload + self.frame_payload, ) @property @@ -290,7 +290,7 @@ def __str__(self): self.__class__.__name__, self.header, self.control, - self.payload + self.payload, ) @property @@ -350,7 +350,7 @@ def __str__(self): self.__class__.__name__, self.header, self.control, - self.payload + self.payload, ) @property diff --git a/tests/test_frame/test_ax25frame.py b/tests/test_frame/test_ax25frame.py index 4b309d9..2c4b3ef 100644 --- a/tests/test_frame/test_ax25frame.py +++ b/tests/test_frame/test_ax25frame.py @@ -133,6 +133,16 @@ def test_frame_deadline_ro_if_set(): assert frame.deadline == 44556677 +def test_frame_tnc2(): + """ + Test that we can get a TNC2-compatible frame string. + """ + frame = AX25RawFrame( + destination="VK4BWI", source="VK4MSL", deadline=11223344 + ) + assert frame.tnc2 == "VK4MSL>VK4BWI" + + def test_encode_raw(): """ Test that we can encode a raw frame. diff --git a/tests/test_frame/test_sframe.py b/tests/test_frame/test_sframe.py index dbbd966..fc34865 100644 --- a/tests/test_frame/test_sframe.py +++ b/tests/test_frame/test_sframe.py @@ -142,12 +142,9 @@ def test_rr_frame_str(): destination="VK4BWI", source="VK4MSL", nr=6 ) - assert ( - str(frame) - == ( - "AX258BitReceiveReadyFrame VK4MSL>VK4BWI: N(R)=6 P/F=False " - "Code=0x00" - ) + assert str(frame) == ( + "AX258BitReceiveReadyFrame VK4MSL>VK4BWI: N(R)=6 P/F=False " + "Code=0x00" ) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index 54dd826..dee7c4e 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -678,6 +678,23 @@ def test_frmr_copy(): ) +def test_ua_str(): + """ + Test we can get a string representation of a UA frame. + """ + frame = AX25UnnumberedAcknowledgeFrame( + destination="VK4BWI", + source="VK4MSL", + cr=True, + pid=0xF0, + ) + assert str(frame) == ( + "AX25UnnumberedAcknowledgeFrame VK4MSL>VK4BWI: " + "Control=0x03 P/F=False Modifier=0x03 PID=0xf0\n" + "Payload=b'This is a test'" + ) + + def test_ui_str(): """ Test we can get a string representation of a UI frame. diff --git a/tests/test_kiss/test_serial.py b/tests/test_kiss/test_serial.py index 961e6c1..952c5ee 100644 --- a/tests/test_kiss/test_serial.py +++ b/tests/test_kiss/test_serial.py @@ -27,7 +27,6 @@ def __init__( dsrdtr, inter_byte_timeout, ): - assert port == "/dev/ttyS0" assert baudrate == 9600 assert bytesize == EIGHTBITS From e67ac91c18c7617c8daaffb66b9d2caf3563e353 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 24 Oct 2023 10:50:44 +1000 Subject: [PATCH 160/207] doc: AX.25 2.0 docs: Remove JavaScript --- doc/ax25-2p0/index.html | 25 +- doc/ax25-2p0/index_files/analytics.js | 474 -------------------- doc/ax25-2p0/index_files/bundle-playback.js | 3 - doc/ax25-2p0/index_files/ruffle.js | 3 - doc/ax25-2p0/index_files/seal.js | 114 ----- doc/ax25-2p0/index_files/wombat.js | 21 - 6 files changed, 2 insertions(+), 638 deletions(-) delete mode 100644 doc/ax25-2p0/index_files/analytics.js delete mode 100644 doc/ax25-2p0/index_files/bundle-playback.js delete mode 100644 doc/ax25-2p0/index_files/ruffle.js delete mode 100644 doc/ax25-2p0/index_files/seal.js delete mode 100644 doc/ax25-2p0/index_files/wombat.js diff --git a/doc/ax25-2p0/index.html b/doc/ax25-2p0/index.html index bc3b9a0..521205e 100644 --- a/doc/ax25-2p0/index.html +++ b/doc/ax25-2p0/index.html @@ -1,15 +1,4 @@ - - - - - - - @@ -24,17 +13,7 @@ - - -

    - - - - +
    @@ -1992,4 +1971,4 @@
    2.4.7.4 Maximum Number of I Frames Outstanding (k)
    PetaboxLoader3.datanode: 67.154 (4) load_resource: 81.49 PetaboxLoader3.resolve: 24.029 ---> \ No newline at end of file +--> diff --git a/doc/ax25-2p0/index_files/analytics.js b/doc/ax25-2p0/index_files/analytics.js deleted file mode 100644 index ac86472..0000000 --- a/doc/ax25-2p0/index_files/analytics.js +++ /dev/null @@ -1,474 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3.0 -/* eslint-disable no-var, semi, prefer-arrow-callback, prefer-template */ - -/** - * Collection of methods for sending analytics events to Archive.org's analytics server. - * - * These events are used for internal stats and sent (in anonymized form) to Google Analytics. - * - * @see analytics.md - * - * @type {Object} - */ -window.archive_analytics = (function defineArchiveAnalytics() { - // keep orignal Date object so as not to be affected by wayback's - // hijacking global Date object - var Date = window.Date; - var ARCHIVE_ANALYTICS_VERSION = 2; - var DEFAULT_SERVICE = 'ao_2'; - var NO_SAMPLING_SERVICE = 'ao_no_sampling'; // sends every event instead of a percentage - - var startTime = new Date(); - - /** - * @return {Boolean} - */ - function isPerformanceTimingApiSupported() { - return 'performance' in window && 'timing' in window.performance; - } - - /** - * Determines how many milliseconds elapsed between the browser starting to parse the DOM and - * the current time. - * - * Uses the Performance API or a fallback value if it's not available. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API - * - * @return {Number} - */ - function getLoadTime() { - var start; - - if (isPerformanceTimingApiSupported()) - start = window.performance.timing.domLoading; - else - start = startTime.getTime(); - - return new Date().getTime() - start; - } - - /** - * Determines how many milliseconds elapsed between the user navigating to the page and - * the current time. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Performance_API - * - * @return {Number|null} null if the browser doesn't support the Performance API - */ - function getNavToDoneTime() { - if (!isPerformanceTimingApiSupported()) - return null; - - return new Date().getTime() - window.performance.timing.navigationStart; - } - - /** - * Performs an arithmetic calculation on a string with a number and unit, while maintaining - * the unit. - * - * @param {String} original value to modify, with a unit - * @param {Function} doOperation accepts one Number parameter, returns a Number - * @returns {String} - */ - function computeWithUnit(original, doOperation) { - var number = parseFloat(original, 10); - var unit = original.replace(/(\d*\.\d+)|\d+/, ''); - - return doOperation(number) + unit; - } - - /** - * Computes the default font size of the browser. - * - * @returns {String|null} computed font-size with units (typically pixels), null if it cannot be computed - */ - function getDefaultFontSize() { - var fontSizeStr; - - if (!('getComputedStyle' in window)) - return null; - - var style = window.getComputedStyle(document.documentElement); - if (!style) - return null; - - fontSizeStr = style.fontSize; - - // Don't modify the value if tracking book reader. - if (document.querySelector('#BookReader')) - return fontSizeStr; - - return computeWithUnit(fontSizeStr, function reverseBootstrapFontSize(number) { - // Undo the 62.5% size applied in the Bootstrap CSS. - return number * 1.6; - }); - } - - /** - * Get the URL parameters for a given Location - * @param {Location} - * @return {Object} The URL parameters - */ - function getParams(location) { - if (!location) location = window.location; - var vars; - var i; - var pair; - var params = {}; - var query = location.search; - if (!query) return params; - vars = query.substring(1).split('&'); - for (i = 0; i < vars.length; i++) { - pair = vars[i].split('='); - params[pair[0]] = decodeURIComponent(pair[1]); - } - return params; - } - - function getMetaProp(name) { - var metaTag = document.querySelector('meta[property=' + name + ']'); - return metaTag ? metaTag.getAttribute('content') || null : null; - } - - var ArchiveAnalytics = { - /** - * @type {String|null} - */ - service: getMetaProp('service'), - mediaType: getMetaProp('mediatype'), - primaryCollection: getMetaProp('primary_collection'), - - /** - * Key-value pairs to send in pageviews (you can read this after a pageview to see what was - * sent). - * - * @type {Object} - */ - values: {}, - - /** - * Sends an analytics ping, preferably using navigator.sendBeacon() - * @param {Object} values - * @param {Function} [onload_callback] (deprecated) callback to invoke once ping to analytics server is done - * @param {Boolean} [augment_for_ao_site] (deprecated) if true, add some archive.org site-specific values - */ - send_ping: function send_ping(values, onload_callback, augment_for_ao_site) { - if (typeof window.navigator !== 'undefined' && typeof window.navigator.sendBeacon !== 'undefined') - this.send_ping_via_beacon(values); - else - this.send_ping_via_image(values); - }, - - /** - * Sends a ping via Beacon API - * NOTE: Assumes window.navigator.sendBeacon exists - * @param {Object} values Tracking parameters to pass - */ - send_ping_via_beacon: function send_ping_via_beacon(values) { - var url = this.generate_tracking_url(values || {}); - window.navigator.sendBeacon(url); - }, - - /** - * Sends a ping via Image object - * @param {Object} values Tracking parameters to pass - */ - send_ping_via_image: function send_ping_via_image(values) { - var url = this.generate_tracking_url(values || {}); - var loadtime_img = new Image(1, 1); - loadtime_img.src = url; - loadtime_img.alt = ''; - }, - - /** - * Construct complete tracking URL containing payload - * @param {Object} params Tracking parameters to pass - * @return {String} URL to use for tracking call - */ - generate_tracking_url: function generate_tracking_url(params) { - var baseUrl = '//analytics.archive.org/0.gif'; - var keys; - var outputParams = params; - var outputParamsArray = []; - - outputParams.service = outputParams.service || this.service || DEFAULT_SERVICE; - - // Build array of querystring parameters - keys = Object.keys(outputParams); - keys.forEach(function keyIteration(key) { - outputParamsArray.push(encodeURIComponent(key) + '=' + encodeURIComponent(outputParams[key])); - }); - outputParamsArray.push('version=' + ARCHIVE_ANALYTICS_VERSION); - outputParamsArray.push('count=' + (keys.length + 2)); // Include `version` and `count` in count - - return baseUrl + '?' + outputParamsArray.join('&'); - }, - - /** - * @param {int} page Page number - */ - send_scroll_fetch_event: function send_scroll_fetch_event(page) { - var additionalValues = { ev: page }; - var loadTime = getLoadTime(); - var navToDoneTime = getNavToDoneTime(); - if (loadTime) additionalValues.loadtime = loadTime; - if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime; - this.send_event('page_action', 'scroll_fetch', location.pathname, additionalValues); - }, - - send_scroll_fetch_base_event: function send_scroll_fetch_base_event() { - var additionalValues = {}; - var loadTime = getLoadTime(); - var navToDoneTime = getNavToDoneTime(); - if (loadTime) additionalValues.loadtime = loadTime; - if (navToDoneTime) additionalValues.nav_to_done_ms = navToDoneTime; - this.send_event('page_action', 'scroll_fetch_base', location.pathname, additionalValues); - }, - - /** - * @param {Object} [options] - * @param {String} [options.mediaType] - * @param {String} [options.mediaLanguage] - * @param {String} [options.page] The path portion of the page URL - */ - send_pageview: function send_pageview(options) { - var settings = options || {}; - - var defaultFontSize; - var loadTime = getLoadTime(); - var mediaType = settings.mediaType; - var primaryCollection = settings.primaryCollection; - var page = settings.page; - var navToDoneTime = getNavToDoneTime(); - - /** - * @return {String} - */ - function get_locale() { - if (navigator) { - if (navigator.language) - return navigator.language; - - else if (navigator.browserLanguage) - return navigator.browserLanguage; - - else if (navigator.systemLanguage) - return navigator.systemLanguage; - - else if (navigator.userLanguage) - return navigator.userLanguage; - } - return ''; - } - - defaultFontSize = getDefaultFontSize(); - - // Set field values - this.values.kind = 'pageview'; - this.values.timediff = (new Date().getTimezoneOffset()/60)*(-1); // *timezone* diff from UTC - this.values.locale = get_locale(); - this.values.referrer = (document.referrer == '' ? '-' : document.referrer); - - if (loadTime) - this.values.loadtime = loadTime; - - if (navToDoneTime) - this.values.nav_to_done_ms = navToDoneTime; - - if (settings.trackingId) { - this.values.ga_tid = settings.trackingId; - } - - /* START CUSTOM DIMENSIONS */ - if (defaultFontSize) - this.values.iaprop_fontSize = defaultFontSize; - - if ('devicePixelRatio' in window) - this.values.iaprop_devicePixelRatio = window.devicePixelRatio; - - if (mediaType) - this.values.iaprop_mediaType = mediaType; - - if (settings.mediaLanguage) { - this.values.iaprop_mediaLanguage = settings.mediaLanguage; - } - - if (primaryCollection) { - this.values.iaprop_primaryCollection = primaryCollection; - } - /* END CUSTOM DIMENSIONS */ - - if (page) - this.values.page = page; - - this.send_ping(this.values); - }, - - /** - * Sends a tracking "Event". - * @param {string} category - * @param {string} action - * @param {string} label - * @param {Object} additionalEventParams - */ - send_event: function send_event( - category, - action, - label, - additionalEventParams - ) { - if (!label) label = window.location.pathname; - if (!additionalEventParams) additionalEventParams = {}; - if (additionalEventParams.mediaLanguage) { - additionalEventParams.ga_cd4 = additionalEventParams.mediaLanguage; - delete additionalEventParams.mediaLanguage; - } - var eventParams = Object.assign( - { - kind: 'event', - ec: category, - ea: action, - el: label, - cache_bust: Math.random(), - }, - additionalEventParams - ); - this.send_ping(eventParams); - }, - - /** - * Sends every event instead of a small percentage. - * - * Use this sparingly as it can generate a lot of events. - * - * @param {string} category - * @param {string} action - * @param {string} label - * @param {Object} additionalEventParams - */ - send_event_no_sampling: function send_event_no_sampling( - category, - action, - label, - additionalEventParams - ) { - var extraParams = additionalEventParams || {}; - extraParams.service = NO_SAMPLING_SERVICE; - this.send_event(category, action, label, extraParams); - }, - - /** - * @param {Object} options see this.send_pageview options - */ - send_pageview_on_load: function send_pageview_on_load(options) { - var self = this; - window.addEventListener('load', function send_pageview_with_options() { - self.send_pageview(options); - }); - }, - - /** - * Handles tracking events passed in URL. - * Assumes category and action values are separated by a "|" character. - * NOTE: Uses the unsampled analytics property. Watch out for future high click links! - * @param {Location} - */ - process_url_events: function process_url_events(location) { - var eventValues; - var actionValue; - var eventValue = getParams(location).iax; - if (!eventValue) return; - eventValues = eventValue.split('|'); - actionValue = eventValues.length >= 1 ? eventValues[1] : ''; - this.send_event_no_sampling( - eventValues[0], - actionValue, - window.location.pathname - ); - }, - - /** - * Attaches handlers for event tracking. - * - * To enable click tracking for a link, add a `data-event-click-tracking` - * attribute containing the Google Analytics Event Category and Action, separated - * by a vertical pipe (|). - * e.g. `` - * - * To enable form submit tracking, add a `data-event-form-tracking` attribute - * to the `form` tag. - * e.g. `` - * - * Additional tracking options can be added via a `data-event-tracking-options` - * parameter. This parameter, if included, should be a JSON string of the parameters. - * Valid parameters are: - * - service {string}: Corresponds to the Google Analytics property data values flow into - */ - set_up_event_tracking: function set_up_event_tracking() { - var self = this; - var clickTrackingAttributeName = 'event-click-tracking'; - var formTrackingAttributeName = 'event-form-tracking'; - var trackingOptionsAttributeName = 'event-tracking-options'; - - function handleAction(event, attributeName) { - var selector = '[data-' + attributeName + ']'; - var eventTarget = event.target; - if (!eventTarget) return; - var target = eventTarget.closest(selector); - if (!target) return; - var categoryAction; - var categoryActionParts; - var options; - categoryAction = target.dataset[toCamelCase(attributeName)]; - if (!categoryAction) return; - categoryActionParts = categoryAction.split('|'); - options = target.dataset[toCamelCase(trackingOptionsAttributeName)]; - options = options ? JSON.parse(options) : {}; - self.send_event( - categoryActionParts[0], - categoryActionParts[1], - categoryActionParts[2] || window.location.pathname, - options.service ? { service: options.service } : {} - ); - } - - function toCamelCase(str) { - return str.replace(/\W+(.)/g, function (match, chr) { - return chr.toUpperCase(); - }); - }; - - document.addEventListener('click', function(e) { - handleAction(e, clickTrackingAttributeName); - }); - - document.addEventListener('submit', function(e) { - handleAction(e, formTrackingAttributeName); - }); - }, - - /** - * @returns {Object[]} - */ - get_data_packets: function get_data_packets() { - return [this.values]; - }, - - /** - * Creates a tracking image for tracking JS compatibility. - * - * @param {string} type The type value for track_js_case in query params for 0.gif - */ - create_tracking_image: function create_tracking_image(type) { - this.send_ping_via_image({ - cache_bust: Math.random(), - kind: 'track_js', - track_js_case: type, - }); - } - }; - - return ArchiveAnalytics; -}()); -// @license-end diff --git a/doc/ax25-2p0/index_files/bundle-playback.js b/doc/ax25-2p0/index_files/bundle-playback.js deleted file mode 100644 index 00c2744..0000000 --- a/doc/ax25-2p0/index_files/bundle-playback.js +++ /dev/null @@ -1,3 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 -!function(t){var e={};function n(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)n.d(o,r,function(e){return t[e]}.bind(null,r));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=9)}([function(t,e,n){"use strict";function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function r(t,e){for(var n=0;n=0}function v(t,e){var n=window["HTML".concat(t,"Element")];if(void 0!==n){var o=Object.getOwnPropertyDescriptor(n.prototype,e);void 0!==o&&Object.defineProperty(n.prototype,"_wm_".concat(e),o)}}function y(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"src",n="_wm_".concat(e);return n in t.__proto__?t[n]:t[e]}v("Image","src"),v("Media","src"),v("Embed","src"),v("IFrame","src"),v("Script","src"),v("Link","href"),v("Anchor","href")},function(t,e,n){"use strict";n.d(e,"c",(function(){return s})),n.d(e,"b",(function(){return a})),n.d(e,"a",(function(){return c}));var o=["January","February","March","April","May","June","July","August","September","October","November","December"],r=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],i={Y:function(t){return t.getUTCFullYear()},m:function(t){return t.getUTCMonth()+1},b:function(t){return r[t.getUTCMonth()]},B:function(t){return o[t.getUTCMonth()]},d:function(t){return t.getUTCDate()},H:function(t){return("0"+t.getUTCHours()).slice(-2)},M:function(t){return("0"+t.getUTCMinutes()).slice(-2)},S:function(t){return("0"+t.getUTCSeconds()).slice(-2)},"%":function(){return"%"}};function s(t){var e=function(t){return"number"==typeof t&&(t=t.toString()),[t.slice(-14,-10),t.slice(-10,-8),t.slice(-8,-6),t.slice(-6,-4),t.slice(-4,-2),t.slice(-2)]}(t);return new Date(Date.UTC(e[0],e[1]-1,e[2],e[3],e[4],e[5]))}function a(t){return r[t]}function c(t,e){return e.replace(/%./g,(function(e){var n=i[e[1]];return n?n(s(t)):e}))}},function(t,e,n){"use strict";n.d(e,"b",(function(){return a})),n.d(e,"a",(function(){return c}));var o=n(0);function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function i(t,e){for(var n=0;n=400?r.failure&&r.failure(t):r.success&&r.success(t)}),{"Content-Type":"application/json"},s.stringify({url:t,snapshot:e,tags:n||[]})),!1}var c=function(){function t(e,n,r){var i=this;!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.el=e,this.url=n,this.timestamp=r,e.onclick=this.save.bind(this),document.addEventListener("DOMContentLoaded",(function(){i.enableSaveSnapshot(Object(o.c)("logged-in-user"))}))}var e,n,r;return e=t,(n=[{key:"save",value:function(t){this.start(),a(this.url,this.timestamp,[],{failure:this.failure.bind(this),success:this.success.bind(this)})}},{key:"start",value:function(){this.hide(["wm-save-snapshot-fail","wm-save-snapshot-open","wm-save-snapshot-success"]),this.show(["wm-save-snapshot-in-progress"])}},{key:"failure",value:function(t){401==t.status?this.userNotLoggedIn(t):(this.hide(["wm-save-snapshot-in-progress","wm-save-snapshot-success"]),this.show(["wm-save-snapshot-fail","wm-save-snapshot-open"]),console.log("You have got an error."),console.log("If you think something wrong here please send it to support."),console.log('Response: "'+t.responseText+'"'),console.log('status: "'+t.status+'"'))}},{key:"success",value:function(t){this.hide(["wm-save-snapshot-fail","wm-save-snapshot-in-progress"]),this.show(["wm-save-snapshot-open","wm-save-snapshot-success"])}},{key:"enableSaveSnapshot",value:function(){var t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];t?(this.show("wm-save-snapshot-open"),this.hide("wm-sign-in")):(this.hide(["wm-save-snapshot-open","wm-save-snapshot-in-progress"]),this.show("wm-sign-in"))}},{key:"show",value:function(t){this.setDisplayStyle(t,"inline-block")}},{key:"hide",value:function(t){this.setDisplayStyle(t,"none")}},{key:"setDisplayStyle",value:function(t,e){var n=this;(Object(o.d)(t)?t:[t]).forEach((function(t){var o=n.el.getRootNode().getElementById(t);o&&(o.style.display=e)}))}}])&&i(e.prototype,n),r&&i(e,r),Object.defineProperty(e,"prototype",{writable:!1}),t}()},,,,,,,function(t,e,n){"use strict";var o;n.r(e);var r,i={createElementNS:document.createElementNS};var s=!0;function a(t){s=t}function c(t){try{a(!1),t()}finally{a(!0)}}function l(t){!function(t,e,n){if(n){var o=new Date;o.setTime(o.getTime()+24*n*60*60*1e3);var r="; expires="+o.toGMTString()}else r="";document.cookie=t+"="+e+r+"; path=/"}(t,"",-1)}var u=n(0),f=n(1),h=window.Date;function p(t,e){return(t=t.toString()).length>=e?t:"00000000".substring(0,e-t.length)+t}function d(t){for(var e=0,n=0;n3}(t)){var o=[];for(n=0;n=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,i=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw i}}}}function v(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n2&&void 0!==arguments[2]?arguments[2]:"src",i=window.location.origin,s=T(window,t),c=m(s);try{for(c.s();!(o=c.n()).done;){var l=o.value;if(!n||n(l)){var f=Object(u.b)(l,r);f&&!f.startsWith(e)&&f.startsWith(i)&&(f.startsWith("data:")||a.push(f))}}}catch(t){c.e(t)}finally{c.f()}}c("img"),c("frame"),c("iframe",(function(t){return"playback"!==t.id})),c("script"),c("link",(function(t){return"stylesheet"===t.rel}),"href");var l=a.filter((function(t,e,n){return n.indexOf(t)===e}));l.length>0?(s=0,l.map((function(t){t.match("^https?://")&&(s++,Object(u.a)("HEAD",t,(function(t){if(200==t.status){var e=t.getResponseHeader("Memento-Datetime");if(null==e)console.log("%s: no Memento-Datetime",u);else{var n=document.createElement("span"),a=function(t,e){var n=new Date(t).getTime()-e,o="";n<0?(o+="-",n=Math.abs(n)):o+="+";var r=!1;if(n<1e3)return{delta:n,text:"",highlight:r};var i=n,s=Math.floor(n/1e3/60/60/24/30/12);n-=1e3*s*60*60*24*30*12;var a=Math.floor(n/1e3/60/60/24/30);n-=1e3*a*60*60*24*30;var c=Math.floor(n/1e3/60/60/24);n-=1e3*c*60*60*24;var l=Math.floor(n/1e3/60/60);n-=1e3*l*60*60;var u=Math.floor(n/1e3/60);n-=1e3*u*60;var f=Math.floor(n/1e3),h=[];s>1?(h.push(s+" years"),r=!0):1==s&&(h.push(s+" year"),r=!0);a>1?(h.push(a+" months"),r=!0):1==a&&(h.push(a+" month"),r=!0);c>1?h.push(c+" days"):1==c&&h.push(c+" day");l>1?h.push(l+" hours"):1==l&&h.push(l+" hour");u>1?h.push(u+" minutes"):1==u&&h.push(u+" minute");f>1?h.push(f+" seconds"):1==f&&h.push(f+" second");h.length>2&&(h=h.slice(0,2));return{delta:i,text:o+h.join(" "),highlight:r}}(e,i),c=a.highlight?"color:red;":"";n.innerHTML=" "+a.text,n.title=e,n.setAttribute("style",c);var l=t.getResponseHeader("Content-Type"),u=t.responseURL.replace(window.location.origin,""),f=document.createElement("a");f.innerHTML=u.split("/").splice(3).join("/"),f._wm_href=u,f.title=l,f.onmouseover=w,f.onmouseout=S;var h=document.createElement("div");h.setAttribute("data-delta",a.delta),h.appendChild(f),h.append(n),o.appendChild(h);var p=Array.prototype.slice.call(o.childNodes,0);p.sort((function(t,e){return e.getAttribute("data-delta")-t.getAttribute("data-delta")})),o.innerHTML="";for(var d=0,m=p.length;d0)for(var n=0;n0)for(var n=0;n0?this.sc.scrollTop=r+this.sc.suggestionHeight+o-this.sc.maxHeight:r<0&&(this.sc.scrollTop=r+o)}}},{key:"blurHandler",value:function(){var t=this;try{var e=this.root.querySelector(".wb-autocomplete-suggestions:hover")}catch(t){e=null}e?this.input!==document.activeElement&&setTimeout((function(){return t.focus()}),20):(this.last_val=this.input.value,this.sc.style.display="none",setTimeout((function(){return t.sc.style.display="none"}),350))}},{key:"suggest",value:function(t){var e=this.input.value;if(this.cache[e]=t,t.length&&e.length>=this.minChars){for(var n="",o=0;o40)&&13!=n&&27!=n){var o=this.input.value;if(o.length>=this.minChars){if(o!=this.last_val){if(this.last_val=o,clearTimeout(this.timer),this.cache){if(o in this.cache)return void this.suggest(this.cache[o]);for(var r=1;r'+t.replace(n,"$1")+"
    "}},{key:"onSelect",value:function(t,e,n){}}]),t}(),L=function(){function t(e,n){_(this,t);var o=e.getRootNode();if(o.querySelector){var r="object"==M(e)?[e]:o.querySelectorAll(e);this.elems=r.map((function(t){return new j(t,n)}))}}return x(t,[{key:"destroy",value:function(){for(;this.elems.length>0;)this.elems.pop().unload()}}]),t}(),A=n(2);function R(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=function(t,e){if(!t)return;if("string"==typeof t)return N(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return N(t,e)}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var o=0,r=function(){};return{s:r,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,s=!0,a=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){a=!0,i=t},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw i}}}}function N(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n0&&i<60,i)}))}window.__wm={init:function(t){!function(){var t=document.cookie.split(";");if(t.length>40)for(var e=0;e1?e-1:0),o=1;o0;)E.appendChild(O.children[0]);if(m)for(var H=0;H'+((""+n).replace(/\B(?=(\d{3})+$)/g,",")+" ")+(n>1?"captures":"capture")+"",h=f.a(r,"%d %b %Y");s!=r&&(h+=" - "+f.a(s,"%d %b %Y")),u+='
    '+h+"
    ",e.innerHTML=u}(o),function(t,e,n,o,r,i,s){var a=o.getContext("2d");if(a){a.fillStyle="#FFF";var c=(new h).getUTCFullYear(),l=e/(c-r+1),u=d(t.years),f=u[0],p=n/u[1];if(i>=r){var m=_(i);a.fillStyle="#FFFFA5",a.fillRect(m,0,l,n)}for(var v=r;v<=c;v++){m=_(v);a.beginPath(),a.moveTo(m,0),a.lineTo(m,n),a.lineWidth=1,a.strokeStyle="#CCC",a.stroke()}s=parseInt(s)-1;for(var y=(l-1)/12,g=0;g0){var M=Math.ceil(T*p);a.fillStyle=v==i&&S==s?"#EC008C":"#000",a.fillRect(Math.round(w),Math.ceil(n-M),Math.ceil(y),Math.round(M))}w+=y}}}function _(t){return Math.ceil((t-r)*l)+.5}}(o,t,e,rt,a,M,_)}}))}else{var st=new Image;st.src="/__wb/sparkline?url="+encodeURIComponent(i)+"&width="+t+"&height="+e+"&selected_year="+M+"&selected_month="+_+(r&&"&collection="+r||""),st.alt="sparkline",st.width=t,st.height=e,st.id="sparklineImgId",st.border="0",ot.parentNode.replaceChild(st,ot)}function at(t){for(var e=[],n=t.length,o=0;o0)try{var o=document.createElement("div");o.setAttribute("style","background-color:#666;color:#fff;font-weight:bold;text-align:center"),o.textContent="NOTICE";var r=document.createElement("div");r.className="wm-capinfo-content";var i,s=R(n);try{var a=function(){var t=i.value;"string"==typeof t.notice&&c((function(){var e=document.createElement("div");e.innerHTML=t.notice,r.appendChild(e)}))};for(s.s();!(i=s.n()).done;)a()}catch(t){s.e(t)}finally{s.f()}ct.appendChild(o),c((function(){return ct.appendChild(r)})),J(!0)}catch(t){console.error("failed to build content of %o - maybe notice text is malformed: %s",ct,n)}}))}else J(!0);new A.a(X("wm-save-snapshot-open"),i,Y)},ex:function(t){t.stopPropagation(),J(!1)},ajax:u.a,sp:function(){return $}}}]); -// @license-end diff --git a/doc/ax25-2p0/index_files/ruffle.js b/doc/ax25-2p0/index_files/ruffle.js deleted file mode 100644 index 231aa13..0000000 --- a/doc/ax25-2p0/index_files/ruffle.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see ruffle.js.LICENSE.txt */ -(()=>{var e,n,t={297:(e,n,t)=>{e.exports=function e(n,t,r){function a(o,s){if(!t[o]){if(!n[o]){if(i)return i(o,!0);var l=new Error("Cannot find module '"+o+"'");throw l.code="MODULE_NOT_FOUND",l}var u=t[o]={exports:{}};n[o][0].call(u.exports,(function(e){return a(n[o][1][e]||e)}),u,u.exports,e,n,t,r)}return t[o].exports}for(var i=void 0,o=0;o>2,s=(3&n)<<4|t>>4,l=1>6:64,u=2>4,t=(15&o)<<4|(s=i.indexOf(e.charAt(u++)))>>2,r=(3&s)<<6|(l=i.indexOf(e.charAt(u++))),f[c++]=n,64!==s&&(f[c++]=t),64!==l&&(f[c++]=r);return f}},{"./support":30,"./utils":32}],2:[function(e,n,t){"use strict";var r=e("./external"),a=e("./stream/DataWorker"),i=e("./stream/Crc32Probe"),o=e("./stream/DataLengthProbe");function s(e,n,t,r,a){this.compressedSize=e,this.uncompressedSize=n,this.crc32=t,this.compression=r,this.compressedContent=a}s.prototype={getContentWorker:function(){var e=new a(r.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new o("data_length")),n=this;return e.on("end",(function(){if(this.streamInfo.data_length!==n.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")})),e},getCompressedWorker:function(){return new a(r.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},s.createWorkerFrom=function(e,n,t){return e.pipe(new i).pipe(new o("uncompressedSize")).pipe(n.compressWorker(t)).pipe(new o("compressedSize")).withStreamInfo("compression",n)},n.exports=s},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(e,n,t){"use strict";var r=e("./stream/GenericWorker");t.STORE={magic:"\0\0",compressWorker:function(){return new r("STORE compression")},uncompressWorker:function(){return new r("STORE decompression")}},t.DEFLATE=e("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(e,n,t){"use strict";var r=e("./utils"),a=function(){for(var e,n=[],t=0;t<256;t++){e=t;for(var r=0;r<8;r++)e=1&e?3988292384^e>>>1:e>>>1;n[t]=e}return n}();n.exports=function(e,n){return void 0!==e&&e.length?"string"!==r.getTypeOf(e)?function(e,n,t,r){var i=a,o=r+t;e^=-1;for(var s=r;s>>8^i[255&(e^n[s])];return-1^e}(0|n,e,e.length,0):function(e,n,t,r){var i=a,o=r+t;e^=-1;for(var s=r;s>>8^i[255&(e^n.charCodeAt(s))];return-1^e}(0|n,e,e.length,0):0}},{"./utils":32}],5:[function(e,n,t){"use strict";t.base64=!1,t.binary=!1,t.dir=!1,t.createFolders=!0,t.date=null,t.compression=null,t.compressionOptions=null,t.comment=null,t.unixPermissions=null,t.dosPermissions=null},{}],6:[function(e,n,t){"use strict";var r=null;r="undefined"!=typeof Promise?Promise:e("lie"),n.exports={Promise:r}},{lie:37}],7:[function(e,n,t){"use strict";var r="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,a=e("pako"),i=e("./utils"),o=e("./stream/GenericWorker"),s=r?"uint8array":"array";function l(e,n){o.call(this,"FlateWorker/"+e),this._pako=null,this._pakoAction=e,this._pakoOptions=n,this.meta={}}t.magic="\b\0",i.inherits(l,o),l.prototype.processChunk=function(e){this.meta=e.meta,null===this._pako&&this._createPako(),this._pako.push(i.transformTo(s,e.data),!1)},l.prototype.flush=function(){o.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},l.prototype.cleanUp=function(){o.prototype.cleanUp.call(this),this._pako=null},l.prototype._createPako=function(){this._pako=new a[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(n){e.push({data:n,meta:e.meta})}},t.compressWorker=function(e){return new l("Deflate",e)},t.uncompressWorker=function(){return new l("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(e,n,t){"use strict";function r(e,n){var t,r="";for(t=0;t>>=8;return r}function a(e,n,t,a,o,c){var d,f,h=e.file,m=e.compression,p=c!==s.utf8encode,v=i.transformTo("string",c(h.name)),g=i.transformTo("string",s.utf8encode(h.name)),b=h.comment,w=i.transformTo("string",c(b)),k=i.transformTo("string",s.utf8encode(b)),y=g.length!==h.name.length,_=k.length!==b.length,R="",x="",S="",z=h.dir,E=h.date,j={crc32:0,compressedSize:0,uncompressedSize:0};n&&!t||(j.crc32=e.crc32,j.compressedSize=e.compressedSize,j.uncompressedSize=e.uncompressedSize);var A=0;n&&(A|=8),p||!y&&!_||(A|=2048);var C=0,I=0;z&&(C|=16),"UNIX"===o?(I=798,C|=function(e,n){var t=e;return e||(t=n?16893:33204),(65535&t)<<16}(h.unixPermissions,z)):(I=20,C|=function(e){return 63&(e||0)}(h.dosPermissions)),d=E.getUTCHours(),d<<=6,d|=E.getUTCMinutes(),d<<=5,d|=E.getUTCSeconds()/2,f=E.getUTCFullYear()-1980,f<<=4,f|=E.getUTCMonth()+1,f<<=5,f|=E.getUTCDate(),y&&(x=r(1,1)+r(l(v),4)+g,R+="up"+r(x.length,2)+x),_&&(S=r(1,1)+r(l(w),4)+k,R+="uc"+r(S.length,2)+S);var O="";return O+="\n\0",O+=r(A,2),O+=m.magic,O+=r(d,2),O+=r(f,2),O+=r(j.crc32,4),O+=r(j.compressedSize,4),O+=r(j.uncompressedSize,4),O+=r(v.length,2),O+=r(R.length,2),{fileRecord:u.LOCAL_FILE_HEADER+O+v+R,dirRecord:u.CENTRAL_FILE_HEADER+r(I,2)+O+r(w.length,2)+"\0\0\0\0"+r(C,4)+r(a,4)+v+R+w}}var i=e("../utils"),o=e("../stream/GenericWorker"),s=e("../utf8"),l=e("../crc32"),u=e("../signature");function c(e,n,t,r){o.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=n,this.zipPlatform=t,this.encodeFileName=r,this.streamFiles=e,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}i.inherits(c,o),c.prototype.push=function(e){var n=e.meta.percent||0,t=this.entriesCount,r=this._sources.length;this.accumulate?this.contentBuffer.push(e):(this.bytesWritten+=e.data.length,o.prototype.push.call(this,{data:e.data,meta:{currentFile:this.currentFile,percent:t?(n+100*(t-r-1))/t:100}}))},c.prototype.openedSource=function(e){this.currentSourceOffset=this.bytesWritten,this.currentFile=e.file.name;var n=this.streamFiles&&!e.file.dir;if(n){var t=a(e,n,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:t.fileRecord,meta:{percent:0}})}else this.accumulate=!0},c.prototype.closedSource=function(e){this.accumulate=!1;var n=this.streamFiles&&!e.file.dir,t=a(e,n,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(t.dirRecord),n)this.push({data:function(e){return u.DATA_DESCRIPTOR+r(e.crc32,4)+r(e.compressedSize,4)+r(e.uncompressedSize,4)}(e),meta:{percent:100}});else for(this.push({data:t.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},c.prototype.flush=function(){for(var e=this.bytesWritten,n=0;n=this.index;n--)t=(t<<8)+this.byteAt(n);return this.index+=e,t},readString:function(e){return r.transformTo("string",this.readData(e))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var e=this.readInt(4);return new Date(Date.UTC(1980+(e>>25&127),(e>>21&15)-1,e>>16&31,e>>11&31,e>>5&63,(31&e)<<1))}},n.exports=a},{"../utils":32}],19:[function(e,n,t){"use strict";var r=e("./Uint8ArrayReader");function a(e){r.call(this,e)}e("../utils").inherits(a,r),a.prototype.readData=function(e){this.checkOffset(e);var n=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,n},n.exports=a},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(e,n,t){"use strict";var r=e("./DataReader");function a(e){r.call(this,e)}e("../utils").inherits(a,r),a.prototype.byteAt=function(e){return this.data.charCodeAt(this.zero+e)},a.prototype.lastIndexOfSignature=function(e){return this.data.lastIndexOf(e)-this.zero},a.prototype.readAndCheckSignature=function(e){return e===this.readData(4)},a.prototype.readData=function(e){this.checkOffset(e);var n=this.data.slice(this.zero+this.index,this.zero+this.index+e);return this.index+=e,n},n.exports=a},{"../utils":32,"./DataReader":18}],21:[function(e,n,t){"use strict";var r=e("./ArrayReader");function a(e){r.call(this,e)}e("../utils").inherits(a,r),a.prototype.readData=function(e){if(this.checkOffset(e),0===e)return new Uint8Array(0);var n=this.data.subarray(this.zero+this.index,this.zero+this.index+e);return this.index+=e,n},n.exports=a},{"../utils":32,"./ArrayReader":17}],22:[function(e,n,t){"use strict";var r=e("../utils"),a=e("../support"),i=e("./ArrayReader"),o=e("./StringReader"),s=e("./NodeBufferReader"),l=e("./Uint8ArrayReader");n.exports=function(e){var n=r.getTypeOf(e);return r.checkSupport(n),"string"!==n||a.uint8array?"nodebuffer"===n?new s(e):a.uint8array?new l(r.transformTo("uint8array",e)):new i(r.transformTo("array",e)):new o(e)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(e,n,t){"use strict";t.LOCAL_FILE_HEADER="PK\x03\x04",t.CENTRAL_FILE_HEADER="PK\x01\x02",t.CENTRAL_DIRECTORY_END="PK\x05\x06",t.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK\x06\x07",t.ZIP64_CENTRAL_DIRECTORY_END="PK\x06\x06",t.DATA_DESCRIPTOR="PK\x07\b"},{}],24:[function(e,n,t){"use strict";var r=e("./GenericWorker"),a=e("../utils");function i(e){r.call(this,"ConvertWorker to "+e),this.destType=e}a.inherits(i,r),i.prototype.processChunk=function(e){this.push({data:a.transformTo(this.destType,e.data),meta:e.meta})},n.exports=i},{"../utils":32,"./GenericWorker":28}],25:[function(e,n,t){"use strict";var r=e("./GenericWorker"),a=e("../crc32");function i(){r.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}e("../utils").inherits(i,r),i.prototype.processChunk=function(e){this.streamInfo.crc32=a(e.data,this.streamInfo.crc32||0),this.push(e)},n.exports=i},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(e,n,t){"use strict";var r=e("../utils"),a=e("./GenericWorker");function i(e){a.call(this,"DataLengthProbe for "+e),this.propName=e,this.withStreamInfo(e,0)}r.inherits(i,a),i.prototype.processChunk=function(e){if(e){var n=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=n+e.data.length}a.prototype.processChunk.call(this,e)},n.exports=i},{"../utils":32,"./GenericWorker":28}],27:[function(e,n,t){"use strict";var r=e("../utils"),a=e("./GenericWorker");function i(e){a.call(this,"DataWorker");var n=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,e.then((function(e){n.dataIsReady=!0,n.data=e,n.max=e&&e.length||0,n.type=r.getTypeOf(e),n.isPaused||n._tickAndRepeat()}),(function(e){n.error(e)}))}r.inherits(i,a),i.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this.data=null},i.prototype.resume=function(){return!!a.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,r.delay(this._tickAndRepeat,[],this)),!0)},i.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(r.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},i.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var e=null,n=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":e=this.data.substring(this.index,n);break;case"uint8array":e=this.data.subarray(this.index,n);break;case"array":case"nodebuffer":e=this.data.slice(this.index,n)}return this.index=n,this.push({data:e,meta:{percent:this.max?this.index/this.max*100:0}})},n.exports=i},{"../utils":32,"./GenericWorker":28}],28:[function(e,n,t){"use strict";function r(e){this.name=e||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}r.prototype={push:function(e){this.emit("data",e)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(e){this.emit("error",e)}return!0},error:function(e){return!this.isFinished&&(this.isPaused?this.generatedError=e:(this.isFinished=!0,this.emit("error",e),this.previous&&this.previous.error(e),this.cleanUp()),!0)},on:function(e,n){return this._listeners[e].push(n),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(e,n){if(this._listeners[e])for(var t=0;t "+e:e}},n.exports=r},{}],29:[function(e,n,t){"use strict";var r=e("../utils"),a=e("./ConvertWorker"),i=e("./GenericWorker"),o=e("../base64"),s=e("../support"),l=e("../external"),u=null;if(s.nodestream)try{u=e("../nodejs/NodejsStreamOutputAdapter")}catch(e){}function c(e,n){return new l.Promise((function(t,a){var i=[],s=e._internalType,l=e._outputType,u=e._mimeType;e.on("data",(function(e,t){i.push(e),n&&n(t)})).on("error",(function(e){i=[],a(e)})).on("end",(function(){try{var e=function(e,n,t){switch(e){case"blob":return r.newBlob(r.transformTo("arraybuffer",n),t);case"base64":return o.encode(n);default:return r.transformTo(e,n)}}(l,function(e,n){var t,r=0,a=null,i=0;for(t=0;t>>6:(t<65536?n[o++]=224|t>>>12:(n[o++]=240|t>>>18,n[o++]=128|t>>>12&63),n[o++]=128|t>>>6&63),n[o++]=128|63&t);return n}(e)},t.utf8decode=function(e){return a.nodebuffer?r.transformTo("nodebuffer",e).toString("utf-8"):function(e){var n,t,a,i,o=e.length,l=new Array(2*o);for(n=t=0;n>10&1023,l[t++]=56320|1023&a)}return l.length!==t&&(l.subarray?l=l.subarray(0,t):l.length=t),r.applyFromCharCode(l)}(e=r.transformTo(a.uint8array?"uint8array":"array",e))},r.inherits(u,o),u.prototype.processChunk=function(e){var n=r.transformTo(a.uint8array?"uint8array":"array",e.data);if(this.leftOver&&this.leftOver.length){if(a.uint8array){var i=n;(n=new Uint8Array(i.length+this.leftOver.length)).set(this.leftOver,0),n.set(i,this.leftOver.length)}else n=this.leftOver.concat(n);this.leftOver=null}var o=function(e,n){var t;for((n=n||e.length)>e.length&&(n=e.length),t=n-1;0<=t&&128==(192&e[t]);)t--;return t<0||0===t?n:t+s[e[t]]>n?t:n}(n),l=n;o!==n.length&&(a.uint8array?(l=n.subarray(0,o),this.leftOver=n.subarray(o,n.length)):(l=n.slice(0,o),this.leftOver=n.slice(o,n.length))),this.push({data:t.utf8decode(l),meta:e.meta})},u.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:t.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},t.Utf8DecodeWorker=u,r.inherits(c,o),c.prototype.processChunk=function(e){this.push({data:t.utf8encode(e.data),meta:e.meta})},t.Utf8EncodeWorker=c},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(e,n,t){"use strict";var r=e("./support"),a=e("./base64"),i=e("./nodejsUtils"),o=e("./external");function s(e){return e}function l(e,n){for(var t=0;t>8;this.dir=!!(16&this.externalFileAttributes),0==e&&(this.dosPermissions=63&this.externalFileAttributes),3==e&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var e=r(this.extraFields[1].value);this.uncompressedSize===a.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===a.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===a.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===a.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(e){var n,t,r,a=e.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});e.index+4>>6:(t<65536?n[o++]=224|t>>>12:(n[o++]=240|t>>>18,n[o++]=128|t>>>12&63),n[o++]=128|t>>>6&63),n[o++]=128|63&t);return n},t.buf2binstring=function(e){return l(e,e.length)},t.binstring2buf=function(e){for(var n=new r.Buf8(e.length),t=0,a=n.length;t>10&1023,u[r++]=56320|1023&a)}return l(u,r)},t.utf8border=function(e,n){var t;for((n=n||e.length)>e.length&&(n=e.length),t=n-1;0<=t&&128==(192&e[t]);)t--;return t<0||0===t?n:t+o[e[t]]>n?t:n}},{"./common":41}],43:[function(e,n,t){"use strict";n.exports=function(e,n,t,r){for(var a=65535&e|0,i=e>>>16&65535|0,o=0;0!==t;){for(t-=o=2e3>>1:e>>>1;n[t]=e}return n}();n.exports=function(e,n,t,a){var i=r,o=a+t;e^=-1;for(var s=a;s>>8^i[255&(e^n[s])];return-1^e}},{}],46:[function(e,n,t){"use strict";var r,a=e("../utils/common"),i=e("./trees"),o=e("./adler32"),s=e("./crc32"),l=e("./messages"),u=0,c=4,d=0,f=-2,h=-1,m=4,p=2,v=8,g=9,b=286,w=30,k=19,y=2*b+1,_=15,R=3,x=258,S=x+R+1,z=42,E=113,j=1,A=2,C=3,I=4;function O(e,n){return e.msg=l[n],n}function D(e){return(e<<1)-(4e.avail_out&&(t=e.avail_out),0!==t&&(a.arraySet(e.output,n.pending_buf,n.pending_out,t,e.next_out),e.next_out+=t,n.pending_out+=t,e.total_out+=t,e.avail_out-=t,n.pending-=t,0===n.pending&&(n.pending_out=0))}function T(e,n){i._tr_flush_block(e,0<=e.block_start?e.block_start:-1,e.strstart-e.block_start,n),e.block_start=e.strstart,P(e.strm)}function $(e,n){e.pending_buf[e.pending++]=n}function B(e,n){e.pending_buf[e.pending++]=n>>>8&255,e.pending_buf[e.pending++]=255&n}function M(e,n){var t,r,a=e.max_chain_length,i=e.strstart,o=e.prev_length,s=e.nice_match,l=e.strstart>e.w_size-S?e.strstart-(e.w_size-S):0,u=e.window,c=e.w_mask,d=e.prev,f=e.strstart+x,h=u[i+o-1],m=u[i+o];e.prev_length>=e.good_match&&(a>>=2),s>e.lookahead&&(s=e.lookahead);do{if(u[(t=n)+o]===m&&u[t+o-1]===h&&u[t]===u[i]&&u[++t]===u[i+1]){i+=2,t++;do{}while(u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&u[++i]===u[++t]&&il&&0!=--a);return o<=e.lookahead?o:e.lookahead}function L(e){var n,t,r,i,l,u,c,d,f,h,m=e.w_size;do{if(i=e.window_size-e.lookahead-e.strstart,e.strstart>=m+(m-S)){for(a.arraySet(e.window,e.window,m,m,0),e.match_start-=m,e.strstart-=m,e.block_start-=m,n=t=e.hash_size;r=e.head[--n],e.head[n]=m<=r?r-m:0,--t;);for(n=t=m;r=e.prev[--n],e.prev[n]=m<=r?r-m:0,--t;);i+=m}if(0===e.strm.avail_in)break;if(u=e.strm,c=e.window,d=e.strstart+e.lookahead,h=void 0,(f=i)<(h=u.avail_in)&&(h=f),t=0===h?0:(u.avail_in-=h,a.arraySet(c,u.input,u.next_in,h,d),1===u.state.wrap?u.adler=o(u.adler,c,h,d):2===u.state.wrap&&(u.adler=s(u.adler,c,h,d)),u.next_in+=h,u.total_in+=h,h),e.lookahead+=t,e.lookahead+e.insert>=R)for(l=e.strstart-e.insert,e.ins_h=e.window[l],e.ins_h=(e.ins_h<=R&&(e.ins_h=(e.ins_h<=R)if(r=i._tr_tally(e,e.strstart-e.match_start,e.match_length-R),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=R){for(e.match_length--;e.strstart++,e.ins_h=(e.ins_h<=R&&(e.ins_h=(e.ins_h<=R&&e.match_length<=e.prev_length){for(a=e.strstart+e.lookahead-R,r=i._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-R),e.lookahead-=e.prev_length-1,e.prev_length-=2;++e.strstart<=a&&(e.ins_h=(e.ins_h<e.pending_buf_size-5&&(t=e.pending_buf_size-5);;){if(e.lookahead<=1){if(L(e),0===e.lookahead&&n===u)return j;if(0===e.lookahead)break}e.strstart+=e.lookahead,e.lookahead=0;var r=e.block_start+t;if((0===e.strstart||e.strstart>=r)&&(e.lookahead=e.strstart-r,e.strstart=r,T(e,!1),0===e.strm.avail_out))return j;if(e.strstart-e.block_start>=e.w_size-S&&(T(e,!1),0===e.strm.avail_out))return j}return e.insert=0,n===c?(T(e,!0),0===e.strm.avail_out?C:I):(e.strstart>e.block_start&&(T(e,!1),e.strm.avail_out),j)})),new q(4,4,8,4,N),new q(4,5,16,8,N),new q(4,6,32,32,N),new q(4,4,16,16,U),new q(8,16,32,32,U),new q(8,16,128,128,U),new q(8,32,128,256,U),new q(32,128,258,1024,U),new q(32,258,258,4096,U)],t.deflateInit=function(e,n){return V(e,n,v,15,8,0)},t.deflateInit2=V,t.deflateReset=H,t.deflateResetKeep=Z,t.deflateSetHeader=function(e,n){return e&&e.state?2!==e.state.wrap?f:(e.state.gzhead=n,d):f},t.deflate=function(e,n){var t,a,o,l;if(!e||!e.state||5>8&255),$(a,a.gzhead.time>>16&255),$(a,a.gzhead.time>>24&255),$(a,9===a.level?2:2<=a.strategy||a.level<2?4:0),$(a,255&a.gzhead.os),a.gzhead.extra&&a.gzhead.extra.length&&($(a,255&a.gzhead.extra.length),$(a,a.gzhead.extra.length>>8&255)),a.gzhead.hcrc&&(e.adler=s(e.adler,a.pending_buf,a.pending,0)),a.gzindex=0,a.status=69):($(a,0),$(a,0),$(a,0),$(a,0),$(a,0),$(a,9===a.level?2:2<=a.strategy||a.level<2?4:0),$(a,3),a.status=E);else{var h=v+(a.w_bits-8<<4)<<8;h|=(2<=a.strategy||a.level<2?0:a.level<6?1:6===a.level?2:3)<<6,0!==a.strstart&&(h|=32),h+=31-h%31,a.status=E,B(a,h),0!==a.strstart&&(B(a,e.adler>>>16),B(a,65535&e.adler)),e.adler=1}if(69===a.status)if(a.gzhead.extra){for(o=a.pending;a.gzindex<(65535&a.gzhead.extra.length)&&(a.pending!==a.pending_buf_size||(a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),P(e),o=a.pending,a.pending!==a.pending_buf_size));)$(a,255&a.gzhead.extra[a.gzindex]),a.gzindex++;a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),a.gzindex===a.gzhead.extra.length&&(a.gzindex=0,a.status=73)}else a.status=73;if(73===a.status)if(a.gzhead.name){o=a.pending;do{if(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),P(e),o=a.pending,a.pending===a.pending_buf_size)){l=1;break}l=a.gzindexo&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),0===l&&(a.gzindex=0,a.status=91)}else a.status=91;if(91===a.status)if(a.gzhead.comment){o=a.pending;do{if(a.pending===a.pending_buf_size&&(a.gzhead.hcrc&&a.pending>o&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),P(e),o=a.pending,a.pending===a.pending_buf_size)){l=1;break}l=a.gzindexo&&(e.adler=s(e.adler,a.pending_buf,a.pending-o,o)),0===l&&(a.status=103)}else a.status=103;if(103===a.status&&(a.gzhead.hcrc?(a.pending+2>a.pending_buf_size&&P(e),a.pending+2<=a.pending_buf_size&&($(a,255&e.adler),$(a,e.adler>>8&255),e.adler=0,a.status=E)):a.status=E),0!==a.pending){if(P(e),0===e.avail_out)return a.last_flush=-1,d}else if(0===e.avail_in&&D(n)<=D(t)&&n!==c)return O(e,-5);if(666===a.status&&0!==e.avail_in)return O(e,-5);if(0!==e.avail_in||0!==a.lookahead||n!==u&&666!==a.status){var m=2===a.strategy?function(e,n){for(var t;;){if(0===e.lookahead&&(L(e),0===e.lookahead)){if(n===u)return j;break}if(e.match_length=0,t=i._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,t&&(T(e,!1),0===e.strm.avail_out))return j}return e.insert=0,n===c?(T(e,!0),0===e.strm.avail_out?C:I):e.last_lit&&(T(e,!1),0===e.strm.avail_out)?j:A}(a,n):3===a.strategy?function(e,n){for(var t,r,a,o,s=e.window;;){if(e.lookahead<=x){if(L(e),e.lookahead<=x&&n===u)return j;if(0===e.lookahead)break}if(e.match_length=0,e.lookahead>=R&&0e.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=R?(t=i._tr_tally(e,1,e.match_length-R),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(t=i._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),t&&(T(e,!1),0===e.strm.avail_out))return j}return e.insert=0,n===c?(T(e,!0),0===e.strm.avail_out?C:I):e.last_lit&&(T(e,!1),0===e.strm.avail_out)?j:A}(a,n):r[a.level].func(a,n);if(m!==C&&m!==I||(a.status=666),m===j||m===C)return 0===e.avail_out&&(a.last_flush=-1),d;if(m===A&&(1===n?i._tr_align(a):5!==n&&(i._tr_stored_block(a,0,0,!1),3===n&&(F(a.head),0===a.lookahead&&(a.strstart=0,a.block_start=0,a.insert=0))),P(e),0===e.avail_out))return a.last_flush=-1,d}return n!==c?d:a.wrap<=0?1:(2===a.wrap?($(a,255&e.adler),$(a,e.adler>>8&255),$(a,e.adler>>16&255),$(a,e.adler>>24&255),$(a,255&e.total_in),$(a,e.total_in>>8&255),$(a,e.total_in>>16&255),$(a,e.total_in>>24&255)):(B(a,e.adler>>>16),B(a,65535&e.adler)),P(e),0=t.w_size&&(0===s&&(F(t.head),t.strstart=0,t.block_start=0,t.insert=0),h=new a.Buf8(t.w_size),a.arraySet(h,n,m-t.w_size,t.w_size,0),n=h,m=t.w_size),l=e.avail_in,u=e.next_in,c=e.input,e.avail_in=m,e.next_in=0,e.input=n,L(t);t.lookahead>=R;){for(r=t.strstart,i=t.lookahead-(R-1);t.ins_h=(t.ins_h<>>=k=w>>>24,m-=k,0==(k=w>>>16&255))z[i++]=65535&w;else{if(!(16&k)){if(0==(64&k)){w=p[(65535&w)+(h&(1<>>=k,m-=k),m<15&&(h+=S[r++]<>>=k=w>>>24,m-=k,!(16&(k=w>>>16&255))){if(0==(64&k)){w=v[(65535&w)+(h&(1<>>=k,m-=k,(k=i-o)<_){if(c<(k=_-k)&&t.sane){e.msg="invalid distance too far back",t.mode=30;break e}if(x=f,(R=0)===d){if(R+=u-k,k>3,h&=(1<<(m-=y<<3))-1,e.next_in=r,e.next_out=i,e.avail_in=r>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function v(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new r.Buf16(320),this.work=new r.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function g(e){var n;return e&&e.state?(n=e.state,e.total_in=e.total_out=n.total=0,e.msg="",n.wrap&&(e.adler=1&n.wrap),n.mode=f,n.last=0,n.havedict=0,n.dmax=32768,n.head=null,n.hold=0,n.bits=0,n.lencode=n.lendyn=new r.Buf32(h),n.distcode=n.distdyn=new r.Buf32(m),n.sane=1,n.back=-1,c):d}function b(e){var n;return e&&e.state?((n=e.state).wsize=0,n.whave=0,n.wnext=0,g(e)):d}function w(e,n){var t,r;return e&&e.state?(r=e.state,n<0?(t=0,n=-n):(t=1+(n>>4),n<48&&(n&=15)),n&&(n<8||15=o.wsize?(r.arraySet(o.window,n,t-o.wsize,o.wsize,0),o.wnext=0,o.whave=o.wsize):(a<(i=o.wsize-o.wnext)&&(i=a),r.arraySet(o.window,n,t-a,i,o.wnext),(a-=i)?(r.arraySet(o.window,n,t-a,a,0),o.wnext=a,o.whave=o.wsize):(o.wnext+=i,o.wnext===o.wsize&&(o.wnext=0),o.whave>>8&255,t.check=i(t.check,L,2,0),y=k=0,t.mode=2;break}if(t.flags=0,t.head&&(t.head.done=!1),!(1&t.wrap)||(((255&k)<<8)+(k>>8))%31){e.msg="incorrect header check",t.mode=30;break}if(8!=(15&k)){e.msg="unknown compression method",t.mode=30;break}if(y-=4,P=8+(15&(k>>>=4)),0===t.wbits)t.wbits=P;else if(P>t.wbits){e.msg="invalid window size",t.mode=30;break}t.dmax=1<>8&1),512&t.flags&&(L[0]=255&k,L[1]=k>>>8&255,t.check=i(t.check,L,2,0)),y=k=0,t.mode=3;case 3:for(;y<32;){if(0===b)break e;b--,k+=h[v++]<>>8&255,L[2]=k>>>16&255,L[3]=k>>>24&255,t.check=i(t.check,L,4,0)),y=k=0,t.mode=4;case 4:for(;y<16;){if(0===b)break e;b--,k+=h[v++]<>8),512&t.flags&&(L[0]=255&k,L[1]=k>>>8&255,t.check=i(t.check,L,2,0)),y=k=0,t.mode=5;case 5:if(1024&t.flags){for(;y<16;){if(0===b)break e;b--,k+=h[v++]<>>8&255,t.check=i(t.check,L,2,0)),y=k=0}else t.head&&(t.head.extra=null);t.mode=6;case 6:if(1024&t.flags&&(b<(z=t.length)&&(z=b),z&&(t.head&&(P=t.head.extra_len-t.length,t.head.extra||(t.head.extra=new Array(t.head.extra_len)),r.arraySet(t.head.extra,h,v,z,P)),512&t.flags&&(t.check=i(t.check,h,z,v)),b-=z,v+=z,t.length-=z),t.length))break e;t.length=0,t.mode=7;case 7:if(2048&t.flags){if(0===b)break e;for(z=0;P=h[v+z++],t.head&&P&&t.length<65536&&(t.head.name+=String.fromCharCode(P)),P&&z>9&1,t.head.done=!0),e.adler=t.check=0,t.mode=12;break;case 10:for(;y<32;){if(0===b)break e;b--,k+=h[v++]<>>=7&y,y-=7&y,t.mode=27;break}for(;y<3;){if(0===b)break e;b--,k+=h[v++]<>>=1)){case 0:t.mode=14;break;case 1:if(x(t),t.mode=20,6!==n)break;k>>>=2,y-=2;break e;case 2:t.mode=17;break;case 3:e.msg="invalid block type",t.mode=30}k>>>=2,y-=2;break;case 14:for(k>>>=7&y,y-=7&y;y<32;){if(0===b)break e;b--,k+=h[v++]<>>16^65535)){e.msg="invalid stored block lengths",t.mode=30;break}if(t.length=65535&k,y=k=0,t.mode=15,6===n)break e;case 15:t.mode=16;case 16:if(z=t.length){if(b>>=5,y-=5,t.ndist=1+(31&k),k>>>=5,y-=5,t.ncode=4+(15&k),k>>>=4,y-=4,286>>=3,y-=3}for(;t.have<19;)t.lens[N[t.have++]]=0;if(t.lencode=t.lendyn,t.lenbits=7,$={bits:t.lenbits},T=s(0,t.lens,0,19,t.lencode,0,t.work,$),t.lenbits=$.bits,T){e.msg="invalid code lengths set",t.mode=30;break}t.have=0,t.mode=19;case 19:for(;t.have>>16&255,I=65535&M,!((A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>>=A,y-=A,t.lens[t.have++]=I;else{if(16===I){for(B=A+2;y>>=A,y-=A,0===t.have){e.msg="invalid bit length repeat",t.mode=30;break}P=t.lens[t.have-1],z=3+(3&k),k>>>=2,y-=2}else if(17===I){for(B=A+3;y>>=A)),k>>>=3,y-=3}else{for(B=A+7;y>>=A)),k>>>=7,y-=7}if(t.have+z>t.nlen+t.ndist){e.msg="invalid bit length repeat",t.mode=30;break}for(;z--;)t.lens[t.have++]=P}}if(30===t.mode)break;if(0===t.lens[256]){e.msg="invalid code -- missing end-of-block",t.mode=30;break}if(t.lenbits=9,$={bits:t.lenbits},T=s(l,t.lens,0,t.nlen,t.lencode,0,t.work,$),t.lenbits=$.bits,T){e.msg="invalid literal/lengths set",t.mode=30;break}if(t.distbits=6,t.distcode=t.distdyn,$={bits:t.distbits},T=s(u,t.lens,t.nlen,t.ndist,t.distcode,0,t.work,$),t.distbits=$.bits,T){e.msg="invalid distances set",t.mode=30;break}if(t.mode=20,6===n)break e;case 20:t.mode=21;case 21:if(6<=b&&258<=w){e.next_out=g,e.avail_out=w,e.next_in=v,e.avail_in=b,t.hold=k,t.bits=y,o(e,R),g=e.next_out,m=e.output,w=e.avail_out,v=e.next_in,h=e.input,b=e.avail_in,k=t.hold,y=t.bits,12===t.mode&&(t.back=-1);break}for(t.back=0;C=(M=t.lencode[k&(1<>>16&255,I=65535&M,!((A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>O)])>>>16&255,I=65535&M,!(O+(A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>>=O,y-=O,t.back+=O}if(k>>>=A,y-=A,t.back+=A,t.length=I,0===C){t.mode=26;break}if(32&C){t.back=-1,t.mode=12;break}if(64&C){e.msg="invalid literal/length code",t.mode=30;break}t.extra=15&C,t.mode=22;case 22:if(t.extra){for(B=t.extra;y>>=t.extra,y-=t.extra,t.back+=t.extra}t.was=t.length,t.mode=23;case 23:for(;C=(M=t.distcode[k&(1<>>16&255,I=65535&M,!((A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>O)])>>>16&255,I=65535&M,!(O+(A=M>>>24)<=y);){if(0===b)break e;b--,k+=h[v++]<>>=O,y-=O,t.back+=O}if(k>>>=A,y-=A,t.back+=A,64&C){e.msg="invalid distance code",t.mode=30;break}t.offset=I,t.extra=15&C,t.mode=24;case 24:if(t.extra){for(B=t.extra;y>>=t.extra,y-=t.extra,t.back+=t.extra}if(t.offset>t.dmax){e.msg="invalid distance too far back",t.mode=30;break}t.mode=25;case 25:if(0===w)break e;if(z=R-w,t.offset>z){if((z=t.offset-z)>t.whave&&t.sane){e.msg="invalid distance too far back",t.mode=30;break}E=z>t.wnext?(z-=t.wnext,t.wsize-z):t.wnext-z,z>t.length&&(z=t.length),j=t.window}else j=m,E=g-t.offset,z=t.length;for(wb?(k=$[B+d[x]],D[F+d[x]]):(k=96,0),h=1<>A)+(m-=h)]=w<<24|k<<16|y|0,0!==m;);for(h=1<>=1;if(0!==h?(O&=h-1,O+=h):O=0,x++,0==--P[R]){if(R===z)break;R=n[t+d[x]]}if(E>>7)]}function $(e,n){e.pending_buf[e.pending++]=255&n,e.pending_buf[e.pending++]=n>>>8&255}function B(e,n,t){e.bi_valid>p-t?(e.bi_buf|=n<>p-e.bi_valid,e.bi_valid+=t-p):(e.bi_buf|=n<>>=1,t<<=1,0<--n;);return t>>>1}function N(e,n,t){var r,a,i=new Array(m+1),o=0;for(r=1;r<=m;r++)i[r]=o=o+t[r-1]<<1;for(a=0;a<=n;a++){var s=e[2*a+1];0!==s&&(e[2*a]=L(i[s]++,s))}}function U(e){var n;for(n=0;n>1;1<=t;t--)Z(e,i,t);for(a=l;t=e.heap[1],e.heap[1]=e.heap[e.heap_len--],Z(e,i,1),r=e.heap[1],e.heap[--e.heap_max]=t,e.heap[--e.heap_max]=r,i[2*a]=i[2*t]+i[2*r],e.depth[a]=(e.depth[t]>=e.depth[r]?e.depth[t]:e.depth[r])+1,i[2*t+1]=i[2*r+1]=a,e.heap[1]=a++,Z(e,i,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,n){var t,r,a,i,o,s,l=n.dyn_tree,u=n.max_code,c=n.stat_desc.static_tree,d=n.stat_desc.has_stree,f=n.stat_desc.extra_bits,p=n.stat_desc.extra_base,v=n.stat_desc.max_length,g=0;for(i=0;i<=m;i++)e.bl_count[i]=0;for(l[2*e.heap[e.heap_max]+1]=0,t=e.heap_max+1;t>=7;r>>=1)if(1&t&&0!==e.dyn_ltree[2*n])return a;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return i;for(n=32;n>>3,(s=e.static_len+3+7>>>3)<=o&&(o=s)):o=s=t+5,t+4<=o&&-1!==n?Y(e,n,t,r):4===e.strategy||s===o?(B(e,2+(r?1:0),3),H(e,S,z)):(B(e,4+(r?1:0),3),function(e,n,t,r){var a;for(B(e,n-257,5),B(e,t-1,5),B(e,r-4,4),a=0;a>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&n,e.pending_buf[e.l_buf+e.last_lit]=255&t,e.last_lit++,0===n?e.dyn_ltree[2*t]++:(e.matches++,n--,e.dyn_ltree[2*(j[t]+u+1)]++,e.dyn_dtree[2*T(n)]++),e.last_lit===e.lit_bufsize-1},t._tr_align=function(e){B(e,2,3),M(e,g,S),function(e){16===e.bi_valid?($(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,n,t){"use strict";n.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,n,r){(function(e){!function(e,n){"use strict";if(!e.setImmediate){var t,r,a,i,o=1,s={},l=!1,u=e.document,c=Object.getPrototypeOf&&Object.getPrototypeOf(e);c=c&&c.setTimeout?c:e,t="[object process]"==={}.toString.call(e.process)?function(e){process.nextTick((function(){f(e)}))}:function(){if(e.postMessage&&!e.importScripts){var n=!0,t=e.onmessage;return e.onmessage=function(){n=!1},e.postMessage("","*"),e.onmessage=t,n}}()?(i="setImmediate$"+Math.random()+"$",e.addEventListener?e.addEventListener("message",h,!1):e.attachEvent("onmessage",h),function(n){e.postMessage(i+n,"*")}):e.MessageChannel?((a=new MessageChannel).port1.onmessage=function(e){f(e.data)},function(e){a.port2.postMessage(e)}):u&&"onreadystatechange"in u.createElement("script")?(r=u.documentElement,function(e){var n=u.createElement("script");n.onreadystatechange=function(){f(e),n.onreadystatechange=null,r.removeChild(n),n=null},r.appendChild(n)}):function(e){setTimeout(f,0,e)},c.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var n=new Array(arguments.length-1),r=0;r{"use strict";e.exports=t.p+"ad9ce752523ea9fa4f5d.wasm"},878:(e,n,t)=>{"use strict";e.exports=t.p+"44d967c3c705de5b1d1b.wasm"}},r={};function a(e){var n=r[e];if(void 0!==n)return n.exports;var i=r[e]={id:e,loaded:!1,exports:{}};return t[e](i,i.exports,a),i.loaded=!0,i.exports}a.m=t,a.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return a.d(n,{a:n}),n},a.d=(e,n)=>{for(var t in n)a.o(n,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce(((n,t)=>(a.f[t](e,n),n)),[])),a.u=e=>"core.ruffle."+{159:"8c80148e5adc80f63dfe",339:"92e6746d93be1e95cd77"}[e]+".js",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.hmd=e=>((e=Object.create(e)).children||(e.children=[]),Object.defineProperty(e,"exports",{enumerable:!0,set:()=>{throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+e.id)}}),e),a.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),e={},n="ruffle-selfhosted:",a.l=(t,r,i,o)=>{if(e[t])e[t].push(r);else{var s,l;if(void 0!==i)for(var u=document.getElementsByTagName("script"),c=0;c{s.onerror=s.onload=null,clearTimeout(h);var a=e[t];if(delete e[t],s.parentNode&&s.parentNode.removeChild(s),a&&a.forEach((e=>e(r))),n)return n(r)},h=setTimeout(f.bind(null,void 0,{type:"timeout",target:s}),12e4);s.onerror=f.bind(null,s.onerror),s.onload=f.bind(null,s.onload),l&&document.head.appendChild(s)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.p="",(()=>{a.b=document.baseURI||self.location.href;var e={179:0};a.f.j=(n,t)=>{var r=a.o(e,n)?e[n]:void 0;if(0!==r)if(r)t.push(r[2]);else{var i=new Promise(((t,a)=>r=e[n]=[t,a]));t.push(r[2]=i);var o=a.p+a.u(n),s=new Error;a.l(o,(t=>{if(a.o(e,n)&&(0!==(r=e[n])&&(e[n]=void 0),r)){var i=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;s.message="Loading chunk "+n+" failed.\n("+i+": "+o+")",s.name="ChunkLoadError",s.type=i,s.request=o,r[1](s)}}),"chunk-"+n,n)}};var n=(n,t)=>{var r,i,[o,s,l]=t,u=0;if(o.some((n=>0!==e[n]))){for(r in s)a.o(s,r)&&(a.m[r]=s[r]);if(l)l(a)}for(n&&n(t);u{"use strict";class e{constructor(e,n,t,r,a){this.major=e,this.minor=n,this.patch=t,this.prIdent=r,this.buildIdent=a}static fromSemver(n){const t=n.split("+"),r=t[0].split("-"),a=r[0].split("."),i=parseInt(a[0],10);let o=0,s=0,l=null,u=null;return void 0!==a[1]&&(o=parseInt(a[1],10)),void 0!==a[2]&&(s=parseInt(a[2],10)),void 0!==r[1]&&(l=r[1].split(".")),void 0!==t[1]&&(u=t[1].split(".")),new e(i,o,s,l,u)}isCompatibleWith(e){return 0!==this.major&&this.major===e.major||0===this.major&&0===e.major&&0!==this.minor&&this.minor===e.minor||0===this.major&&0===e.major&&0===this.minor&&0===e.minor&&0!==this.patch&&this.patch===e.patch}hasPrecedenceOver(e){if(this.major>e.major)return!0;if(this.majore.minor)return!0;if(this.minore.patch)return!0;if(this.patchparseInt(e.prIdent[t],10))return!0;if(parseInt(this.prIdent[t],10)e.prIdent[t])return!0;if(this.prIdent[t]e.prIdent.length}return!1}isEqual(e){return this.major===e.major&&this.minor===e.minor&&this.patch===e.patch}isStableOrCompatiblePrerelease(e){return null===e.prIdent||this.major===e.major&&this.minor===e.minor&&this.patch===e.patch}}class n{constructor(e){this.requirements=e}satisfiedBy(e){for(const n of this.requirements){let t=!0;for(const{comparator:r,version:a}of n)t=t&&a.isStableOrCompatiblePrerelease(e),""===r||"="===r?t=t&&a.isEqual(e):">"===r?t=t&&e.hasPrecedenceOver(a):">="===r?t=t&&(e.hasPrecedenceOver(a)||a.isEqual(e)):"<"===r?t=t&&a.hasPrecedenceOver(e):"<="===r?t=t&&(a.hasPrecedenceOver(e)||a.isEqual(e)):"^"===r&&(t=t&&a.isCompatibleWith(e));if(t)return!0}return!1}static fromRequirementString(t){const r=t.split(" ");let a=[];const i=[];for(const n of r)if("||"===n)a.length>0&&(i.push(a),a=[]);else if(n.length>0){const t=/[0-9]/.exec(n);if(t){const r=n.slice(0,t.index).trim(),i=e.fromSemver(n.slice(t.index).trim());a.push({comparator:r,version:i})}}return a.length>0&&i.push(a),new n(i)}}const t=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),r=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,7,1,5,0,208,112,26,11])),i=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,12,1,10,0,67,0,0,0,0,252,0,26,11])),o=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,65,0,192,26,11])),s=async()=>WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11]));function l(e){const n="function"==typeof Function.prototype.toString?Function.prototype.toString():null;return"string"==typeof n&&n.indexOf("[native code]")>=0&&Function.prototype.toString.call(e).indexOf("[native code]")>=0}function u(){"function"==typeof Array.prototype.reduce&&l(Array.prototype.reduce)||Object.defineProperty(Array.prototype,"reduce",{value(...e){if(0===e.length&&window.Prototype&&window.Prototype.Version&&window.Prototype.Version<"1.6.1")return this.length>1?this:this[0];const n=e[0];if(null===this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!=typeof n)throw new TypeError(`${n} is not a function`);const t=Object(this),r=t.length>>>0;let a,i=0;if(e.length>=2)a=e[1];else{for(;i=r)throw new TypeError("Reduce of empty array with no initial value");a=t[i++]}for(;ie[n]}),"function"!=typeof Reflect.set&&Object.defineProperty(Reflect,"set",{value(e,n,t){e[n]=t}}),"function"!=typeof Reflect.has&&Object.defineProperty(Reflect,"has",{value:(e,n)=>n in e}),"function"!=typeof Reflect.ownKeys&&Object.defineProperty(Reflect,"ownKeys",{value:e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]})}let c=null,d=!1;try{if(void 0!==document.currentScript&&null!==document.currentScript&&"src"in document.currentScript&&""!==document.currentScript.src){let e=document.currentScript.src;e.endsWith(".js")||e.endsWith("/")||(e+="/"),c=new URL(".",e),d=c.protocol.includes("extension")}}catch(e){console.warn("Unable to get currentScript URL")}function f(e){var n;let t=null!==(n=null==c?void 0:c.href)&&void 0!==n?n:"";return!d&&"publicPath"in e&&null!==e.publicPath&&void 0!==e.publicPath&&(t=e.publicPath),""===t||t.endsWith("/")||(t+="/"),t}let h=null;function m(e,n){return null===h&&(h=async function(e,n){var l;u();const c=(await Promise.all([t(),s(),i(),o(),r()])).every(Boolean);c||console.log("Some WebAssembly extensions are NOT available, falling back to the vanilla WebAssembly module"),a.p=f(e);const{default:d,Ruffle:h}=await(c?a.e(339).then(a.bind(a,339)):a.e(159).then(a.bind(a,159)));let m;const p=c?new URL(a(899),a.b):new URL(a(878),a.b),v=await fetch(p),g="function"==typeof ReadableStream;if(n&&g){const e=(null===(l=null==v?void 0:v.headers)||void 0===l?void 0:l.get("content-length"))||"";let t=0;const r=parseInt(e);m=new Response(new ReadableStream({async start(e){var a;const i=null===(a=v.body)||void 0===a?void 0:a.getReader();if(!i)throw"Response had no body";for(n(t,r);;){const{done:a,value:o}=await i.read();if(a)break;(null==o?void 0:o.byteLength)&&(t+=null==o?void 0:o.byteLength),e.enqueue(o),n(t,r)}e.close()}}),v)}else m=v;return await d(m),h}(e,n)),h}const p=document.createElement("template");p.innerHTML='\n \n \n\n
    \n
    \n
    \n \n
    \n\n \n\n \n\n \n';const v={};function g(e,n){const t=v[e];if(void 0!==t){if(t.class!==n)throw new Error("Internal naming conflict on "+e);return t.name}let r=0;if(void 0!==window.customElements)for(;r<999;){let t=e;if(r>0&&(t=t+"-"+r),void 0===window.customElements.get(t))return window.customElements.define(t,n),v[e]={class:n,name:t,internalName:e},t;r+=1}throw new Error("Failed to assign custom element "+e)}const b={allowScriptAccess:!1,parameters:{},autoplay:"auto",backgroundColor:null,letterbox:"fullscreen",unmuteOverlay:"visible",upgradeToHttps:!0,compatibilityRules:!0,favorFlash:!0,warnOnUnsupportedContent:!0,logLevel:"error",showSwfDownload:!1,contextMenu:"on",preloader:!0,splashScreen:!0,maxExecutionDuration:15,base:null,menu:!0,salign:"",forceAlign:!1,quality:"high",scale:"showAll",forceScale:!1,frameRate:null,wmode:"opaque",publicPath:null,polyfills:!0,playerVersion:null,preferredRenderer:null,openUrlMode:"allow",allowNetworking:"all"},w="application/x-shockwave-flash",k="application/futuresplash",y="application/x-shockwave-flash2-preview",_="application/vnd.adobe.flash.movie";function R(e,n){const t=function(e){let n="";try{n=new URL(e,"https://example.com").pathname}catch(e){}if(n&&n.length>=4){const e=n.slice(-4).toLowerCase();if(".swf"===e||".spl"===e)return!0}return!1}(e);return n?function(e,n){switch(e=e.toLowerCase()){case w.toLowerCase():case k.toLowerCase():case y.toLowerCase():case _.toLowerCase():return!0;default:if(n)switch(e){case"application/octet-stream":case"binary/octet-stream":return!0}}return!1}(n,t):t}const x="0.1.0",S="nightly 2023-08-02",z="nightly",E="2023-08-02T00:22:54.390Z",j="006393c5816e68b54c6720fcd8e657009cfb90d8";class A{constructor(e){this.value=e}valueOf(){return this.value}}class C extends A{constructor(e="???"){super(e)}toString(e){return`{${this.value}}`}}class I extends A{constructor(e,n={}){super(e),this.opts=n}toString(e){try{return e.memoizeIntlObject(Intl.NumberFormat,this.opts).format(this.value)}catch(n){return e.reportError(n),this.value.toString(10)}}}class O extends A{constructor(e,n={}){super(e),this.opts=n}toString(e){try{return e.memoizeIntlObject(Intl.DateTimeFormat,this.opts).format(this.value)}catch(n){return e.reportError(n),new Date(this.value).toISOString()}}}const D=100,F="\u2068",P="\u2069";function T(e,n,t){if(t===n)return!0;if(t instanceof I&&n instanceof I&&t.value===n.value)return!0;if(n instanceof I&&"string"==typeof t){if(t===e.memoizeIntlObject(Intl.PluralRules,n.opts).select(n.value))return!0}return!1}function $(e,n,t){return n[t]?N(e,n[t].value):(e.reportError(new RangeError("No default")),new C)}function B(e,n){const t=[],r=Object.create(null);for(const a of n)"narg"===a.type?r[a.name]=M(e,a.value):t.push(M(e,a));return{positional:t,named:r}}function M(e,n){switch(n.type){case"str":return n.value;case"num":return new I(n.value,{minimumFractionDigits:n.precision});case"var":return function(e,{name:n}){let t;if(e.params){if(!Object.prototype.hasOwnProperty.call(e.params,n))return new C(`$${n}`);t=e.params[n]}else{if(!e.args||!Object.prototype.hasOwnProperty.call(e.args,n))return e.reportError(new ReferenceError(`Unknown variable: $${n}`)),new C(`$${n}`);t=e.args[n]}if(t instanceof A)return t;switch(typeof t){case"string":return t;case"number":return new I(t);case"object":if(t instanceof Date)return new O(t.getTime());default:return e.reportError(new TypeError(`Variable type not supported: $${n}, ${typeof t}`)),new C(`$${n}`)}}(e,n);case"mesg":return function(e,{name:n,attr:t}){const r=e.bundle._messages.get(n);if(!r)return e.reportError(new ReferenceError(`Unknown message: ${n}`)),new C(n);if(t){const a=r.attributes[t];return a?N(e,a):(e.reportError(new ReferenceError(`Unknown attribute: ${t}`)),new C(`${n}.${t}`))}if(r.value)return N(e,r.value);return e.reportError(new ReferenceError(`No value: ${n}`)),new C(n)}(e,n);case"term":return function(e,{name:n,attr:t,args:r}){const a=`-${n}`,i=e.bundle._terms.get(a);if(!i)return e.reportError(new ReferenceError(`Unknown term: ${a}`)),new C(a);if(t){const n=i.attributes[t];if(n){e.params=B(e,r).named;const t=N(e,n);return e.params=null,t}return e.reportError(new ReferenceError(`Unknown attribute: ${t}`)),new C(`${a}.${t}`)}e.params=B(e,r).named;const o=N(e,i.value);return e.params=null,o}(e,n);case"func":return function(e,{name:n,args:t}){let r=e.bundle._functions[n];if(!r)return e.reportError(new ReferenceError(`Unknown function: ${n}()`)),new C(`${n}()`);if("function"!=typeof r)return e.reportError(new TypeError(`Function ${n}() is not callable`)),new C(`${n}()`);try{let n=B(e,t);return r(n.positional,n.named)}catch(t){return e.reportError(t),new C(`${n}()`)}}(e,n);case"select":return function(e,{selector:n,variants:t,star:r}){let a=M(e,n);if(a instanceof C)return $(e,t,r);for(const n of t){if(T(e,a,M(e,n.key)))return N(e,n.value)}return $(e,t,r)}(e,n);default:return new C}}function L(e,n){if(e.dirty.has(n))return e.reportError(new RangeError("Cyclic reference")),new C;e.dirty.add(n);const t=[],r=e.bundle._useIsolating&&n.length>1;for(const a of n)if("string"!=typeof a){if(e.placeables++,e.placeables>D)throw e.dirty.delete(n),new RangeError(`Too many placeables expanded: ${e.placeables}, max allowed is ${D}`);r&&t.push(F),t.push(M(e,a).toString(e)),r&&t.push(P)}else t.push(e.bundle._transform(a));return e.dirty.delete(n),t.join("")}function N(e,n){return"string"==typeof n?e.bundle._transform(n):L(e,n)}class U{constructor(e,n,t){this.dirty=new WeakSet,this.params=null,this.placeables=0,this.bundle=e,this.errors=n,this.args=t}reportError(e){if(!(this.errors&&e instanceof Error))throw e;this.errors.push(e)}memoizeIntlObject(e,n){let t=this.bundle._intls.get(e);t||(t={},this.bundle._intls.set(e,t));let r=JSON.stringify(n);return t[r]||(t[r]=new e(this.bundle.locales,n)),t[r]}}function q(e,n){const t=Object.create(null);for(const[r,a]of Object.entries(e))n.includes(r)&&(t[r]=a.valueOf());return t}const W=["unitDisplay","currencyDisplay","useGrouping","minimumIntegerDigits","minimumFractionDigits","maximumFractionDigits","minimumSignificantDigits","maximumSignificantDigits"];function Z(e,n){let t=e[0];if(t instanceof C)return new C(`NUMBER(${t.valueOf()})`);if(t instanceof I)return new I(t.valueOf(),{...t.opts,...q(n,W)});if(t instanceof O)return new I(t.valueOf(),{...q(n,W)});throw new TypeError("Invalid argument to NUMBER")}const H=["dateStyle","timeStyle","fractionalSecondDigits","dayPeriod","hour12","weekday","era","year","month","day","hour","minute","second","timeZoneName"];function V(e,n){let t=e[0];if(t instanceof C)return new C(`DATETIME(${t.valueOf()})`);if(t instanceof O)return new O(t.valueOf(),{...t.opts,...q(n,H)});if(t instanceof I)return new O(t.valueOf(),{...q(n,H)});throw new TypeError("Invalid argument to DATETIME")}const J=new Map;class K{constructor(e,{functions:n,useIsolating:t=!0,transform:r=(e=>e)}={}){this._terms=new Map,this._messages=new Map,this.locales=Array.isArray(e)?e:[e],this._functions={NUMBER:Z,DATETIME:V,...n},this._useIsolating=t,this._transform=r,this._intls=function(e){const n=Array.isArray(e)?e.join(" "):e;let t=J.get(n);return void 0===t&&(t=new Map,J.set(n,t)),t}(e)}hasMessage(e){return this._messages.has(e)}getMessage(e){return this._messages.get(e)}addResource(e,{allowOverrides:n=!1}={}){const t=[];for(let r=0;r\s*/y,ge=/\s*:\s*/y,be=/\s*,?\s*/y,we=/\s+/y;class ke{constructor(e){this.body=[],G.lastIndex=0;let n=0;for(;;){let t=G.exec(e);if(null===t)break;n=G.lastIndex;try{this.body.push(s(t[1]))}catch(e){if(e instanceof SyntaxError)continue;throw e}}function t(t){return t.lastIndex=n,t.test(e)}function r(t,r){if(e[n]===t)return n++,!0;if(r)throw new r(`Expected ${t}`);return!1}function a(e,r){if(t(e))return n=e.lastIndex,!0;if(r)throw new r(`Expected ${e.toString()}`);return!1}function i(t){t.lastIndex=n;let r=t.exec(e);if(null===r)throw new SyntaxError(`Expected ${t.toString()}`);return n=t.lastIndex,r}function o(e){return i(e)[1]}function s(e){let n=l(),r=function(){let e=Object.create(null);for(;t(Y);){let n=o(Y),t=l();if(null===t)throw new SyntaxError("Expected attribute value");e[n]=t}return e}();if(null===n&&0===Object.keys(r).length)throw new SyntaxError("Expected message value or attributes");return{id:e,value:n,attributes:r}}function l(){let r;if(t(re)&&(r=o(re)),"{"===e[n]||"}"===e[n])return u(r?[r]:[],1/0);let a=g();return a?r?u([r,a],a.length):(a.value=b(a.value,se),u([a],a.length)):r?b(r,le):null}function u(r=[],a){for(;;){if(t(re)){r.push(o(re));continue}if("{"===e[n]){r.push(c());continue}if("}"===e[n])throw new SyntaxError("Unbalanced closing brace");let i=g();if(!i)break;r.push(i),a=Math.min(a,i.length)}let i=r.length-1,s=r[i];"string"==typeof s&&(r[i]=b(s,le));let l=[];for(let e of r)e instanceof ye&&(e=e.value.slice(0,e.value.length-a)),e&&l.push(e);return l}function c(){a(de,SyntaxError);let e=d();if(a(fe))return e;if(a(ve)){let n=function(){let e,n=[],a=0;for(;t(X);){r("*")&&(e=a);let t=h(),i=l();if(null===i)throw new SyntaxError("Expected variant value");n[a++]={key:t,value:i}}if(0===a)return null;if(void 0===e)throw new SyntaxError("Expected default variant");return{variants:n,star:e}}();return a(fe,SyntaxError),{type:"select",selector:e,...n}}throw new SyntaxError("Unclosed placeable")}function d(){if("{"===e[n])return c();if(t(ne)){let[,t,r,o=null]=i(ne);if("$"===t)return{type:"var",name:r};if(a(pe)){let i=function(){let t=[];for(;;){switch(e[n]){case")":return n++,t;case void 0:throw new SyntaxError("Unclosed argument list")}t.push(f()),a(be)}}();if("-"===t)return{type:"term",name:r,attr:o,args:i};if(te.test(r))return{type:"func",name:r,args:i};throw new SyntaxError("Function names must be all upper-case")}return"-"===t?{type:"term",name:r,attr:o,args:[]}:{type:"mesg",name:r,attr:o}}return m()}function f(){let e=d();return"mesg"!==e.type?e:a(ge)?{type:"narg",name:e.name,value:m()}:e}function h(){let e;return a(he,SyntaxError),e=t(Q)?p():{type:"str",value:o(ee)},a(me,SyntaxError),e}function m(){if(t(Q))return p();if('"'===e[n])return function(){r('"',SyntaxError);let t="";for(;;){if(t+=o(ae),"\\"!==e[n]){if(r('"'))return{type:"str",value:t};throw new SyntaxError("Unclosed string literal")}t+=v()}}();throw new SyntaxError("Invalid expression")}function p(){let[,e,n=""]=i(Q),t=n.length;return{type:"num",value:parseFloat(e),precision:t}}function v(){if(t(ie))return o(ie);if(t(oe)){let[,e,n]=i(oe),t=parseInt(e||n,16);return t<=55295||57344<=t?String.fromCodePoint(t):"\ufffd"}throw new SyntaxError("Unknown escape sequence")}function g(){let t=n;switch(a(we),e[n]){case".":case"[":case"*":case"}":case void 0:return!1;case"{":return w(e.slice(t,n))}return" "===e[n-1]&&w(e.slice(t,n))}function b(e,n){return e.replace(n,"")}function w(e){let n=e.replace(ue,"\n"),t=ce.exec(e)[1].length;return new ye(n,t)}}}class ye{constructor(e,n){this.value=e,this.length=n}}const _e=new RegExp("^([a-z]{2,3}|\\*)(?:-([a-z]{4}|\\*))?(?:-([a-z]{2}|\\*))?(?:-(([0-9][a-z0-9]{3}|[a-z0-9]{5,8})|\\*))?$","i");class Re{constructor(e){const n=_e.exec(e.replace(/_/g,"-"));if(!n)return void(this.isWellFormed=!1);let[,t,r,a,i]=n;t&&(this.language=t.toLowerCase()),r&&(this.script=r[0].toUpperCase()+r.slice(1)),a&&(this.region=a.toUpperCase()),this.variant=i,this.isWellFormed=!0}isEqual(e){return this.language===e.language&&this.script===e.script&&this.region===e.region&&this.variant===e.variant}matches(e,n=!1,t=!1){return(this.language===e.language||n&&void 0===this.language||t&&void 0===e.language)&&(this.script===e.script||n&&void 0===this.script||t&&void 0===e.script)&&(this.region===e.region||n&&void 0===this.region||t&&void 0===e.region)&&(this.variant===e.variant||n&&void 0===this.variant||t&&void 0===e.variant)}toString(){return[this.language,this.script,this.region,this.variant].filter((e=>void 0!==e)).join("-")}clearVariants(){this.variant=void 0}clearRegion(){this.region=void 0}addLikelySubtags(){const e=function(e){if(Object.prototype.hasOwnProperty.call(xe,e))return new Re(xe[e]);const n=new Re(e);if(n.language&&Se.includes(n.language))return n.region=n.language.toUpperCase(),n;return null}(this.toString().toLowerCase());return!!e&&(this.language=e.language,this.script=e.script,this.region=e.region,this.variant=e.variant,!0)}}const xe={ar:"ar-arab-eg","az-arab":"az-arab-ir","az-ir":"az-arab-ir",be:"be-cyrl-by",da:"da-latn-dk",el:"el-grek-gr",en:"en-latn-us",fa:"fa-arab-ir",ja:"ja-jpan-jp",ko:"ko-kore-kr",pt:"pt-latn-br",sr:"sr-cyrl-rs","sr-ru":"sr-latn-ru",sv:"sv-latn-se",ta:"ta-taml-in",uk:"uk-cyrl-ua",zh:"zh-hans-cn","zh-hant":"zh-hant-tw","zh-hk":"zh-hant-hk","zh-mo":"zh-hant-mo","zh-tw":"zh-hant-tw","zh-gb":"zh-hant-gb","zh-us":"zh-hant-us"},Se=["az","bg","cs","de","es","fi","fr","hu","it","lt","lv","nl","pl","ro","ru"];function ze(e,n,{strategy:t="filtering",defaultLocale:r}={}){const a=function(e,n,t){const r=new Set,a=new Map;for(let e of n)new Re(e).isWellFormed&&a.set(e,new Re(e));e:for(const n of e){const e=n.toLowerCase(),i=new Re(e);if(void 0!==i.language){for(const n of a.keys())if(e===n.toLowerCase()){if(r.add(n),a.delete(n),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}for(const[e,n]of a.entries())if(n.matches(i,!0,!1)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}if(i.addLikelySubtags())for(const[e,n]of a.entries())if(n.matches(i,!0,!1)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}i.clearVariants();for(const[e,n]of a.entries())if(n.matches(i,!0,!0)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}if(i.clearRegion(),i.addLikelySubtags())for(const[e,n]of a.entries())if(n.matches(i,!0,!1)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}i.clearRegion();for(const[e,n]of a.entries())if(n.matches(i,!0,!0)){if(r.add(e),a.delete(e),"lookup"===t)return Array.from(r);if("filtering"===t)continue;continue e}}}return Array.from(r)}(Array.from(null!=e?e:[]).map(String),Array.from(null!=n?n:[]).map(String),t);if("lookup"===t){if(void 0===r)throw new Error("defaultLocale cannot be undefined for strategy `lookup`");0===a.length&&a.push(r)}else r&&!a.includes(r)&&a.push(r);return a}const Ee={"ar-SA":{"context_menu.ftl":"context-menu-download-swf = \u062a\u062d\u0645\u064a\u0644 .swf\ncontext-menu-copy-debug-info = \u0646\u0633\u062e \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0627\u0644\u062a\u0635\u062d\u064a\u062d\ncontext-menu-open-save-manager = \u0641\u062a\u062d \u0645\u062f\u064a\u0631 \u0627\u0644\u062d\u0641\u0638\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u062d\u0648\u0644 \u0645\u0644\u062d\u0642 \u0631\u0641\u0644 ({ $version })\n *[other] \u062d\u0648\u0644 \u0631\u0641\u0644 ({ $version })\n }\ncontext-menu-hide = \u0625\u062e\u0641\u0627\u0621 \u0647\u0630\u0647 \u0627\u0644\u0642\u0627\u0626\u0645\u0629\ncontext-menu-exit-fullscreen = \u0627\u0644\u062e\u0631\u0648\u062c \u0645\u0646 \u0648\u0636\u0639\u064a\u0629 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629\ncontext-menu-enter-fullscreen = \u062a\u0641\u0639\u064a\u0644 \u0648\u0636\u0639\u064a\u0629 \u0627\u0644\u0634\u0627\u0634\u0629 \u0627\u0644\u0643\u0627\u0645\u0644\u0629\n","messages.ftl":'message-cant-embed =\n \u0644\u0645 \u062a\u0643\u0646 \u0631\u0641\u0644 \u0642\u0627\u062f\u0631\u0629 \u0639\u0644\u0649 \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0641\u0644\u0627\u0634 \u0627\u0644\u0645\u0636\u0645\u0646\u0629 \u0641\u064a \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062d\u0629.\n \u064a\u0645\u0643\u0646\u0643 \u0645\u062d\u0627\u0648\u0644\u0629 \u0641\u062a\u062d \u0627\u0644\u0645\u0644\u0641 \u0641\u064a \u0639\u0644\u0627\u0645\u0629 \u062a\u0628\u0648\u064a\u0628 \u0645\u0646\u0641\u0635\u0644\u0629\u060c \u0644\u062a\u062c\u0627\u0648\u0632 \u0647\u0630\u0647 \u0627\u0644\u0645\u0634\u0643\u0644\u0629.\npanic-title = \u0644\u0642\u062f \u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0627 :(\nmore-info = \u0645\u0639\u0644\u0648\u0645\u0627\u062a \u0623\u0643\u062b\u0631\nrun-anyway = \u0627\u0644\u062a\u0634\u063a\u064a\u0644 \u0639\u0644\u0649 \u0623\u064a \u062d\u0627\u0644\ncontinue = \u0627\u0644\u0627\u0633\u062a\u0645\u0631\u0627\u0631\nreport-bug = \u0625\u0628\u0644\u0627\u063a \u0639\u0646 \u062e\u0644\u0644\nupdate-ruffle = \u062a\u062d\u062f\u064a\u062b \u0631\u0641\u0644\nruffle-demo = \u0648\u064a\u0628 \u0627\u0644\u062a\u062c\u0631\u064a\u0628\u064a\nruffle-desktop = \u0628\u0631\u0646\u0627\u0645\u062c \u0633\u0637\u062d \u0627\u0644\u0645\u0643\u062a\u0628\nruffle-wiki = \u0639\u0631\u0636 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a\nview-error-details = \u0639\u0631\u0636 \u062a\u0641\u0627\u0635\u064a\u0644 \u0627\u0644\u062e\u0637\u0623\nopen-in-new-tab = \u0641\u062a\u062d \u0641\u064a \u0639\u0644\u0627\u0645\u0629 \u062a\u0628\u0648\u064a\u0628 \u062c\u062f\u064a\u062f\u0629\nclick-to-unmute = \u0627\u0646\u0642\u0631 \u0644\u0625\u0644\u063a\u0627\u0621 \u0627\u0644\u0643\u062a\u0645\nerror-file-protocol =\n \u064a\u0628\u062f\u0648 \u0623\u0646\u0643 \u062a\u0642\u0648\u0645 \u0628\u062a\u0634\u063a\u064a\u0644 \u0631\u0641\u0644 \u0639\u0644\u0649 \u0628\u0631\u0648\u062a\u0648\u0643\u0648\u0644 "\u0627\u0644\u0645\u0644\u0641:".\n \u0647\u0630\u0627 \u0644\u0646 \u064a\u0639\u0645\u0644 \u0644\u0623\u0646 \u0627\u0644\u0645\u062a\u0635\u0641\u062d\u0627\u062a \u062a\u0645\u0646\u0639 \u0627\u0644\u0639\u062f\u064a\u062f \u0645\u0646 \u0627\u0644\u0645\u064a\u0632\u0627\u062a \u0645\u0646 \u0627\u0644\u0639\u0645\u0644 \u0644\u0623\u0633\u0628\u0627\u0628 \u0623\u0645\u0646\u064a\u0629.\n \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0630\u0644\u0643\u060c \u0646\u062f\u0639\u0648\u0643 \u0625\u0644\u0649 \u0625\u0639\u062f\u0627\u062f \u062e\u0627\u062f\u0645 \u0645\u062d\u0644\u064a \u0623\u0648 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0639\u0631\u0636 \u0627\u0644\u0648\u064a\u0628 \u0623\u0648 \u062a\u0637\u0628\u064a\u0642 \u0633\u0637\u062d \u0627\u0644\u0645\u0643\u062a\u0628.\nerror-javascript-config =\n \u062a\u0639\u0631\u0636 \u0631\u0641\u0644 \u0625\u0644\u0649 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0628\u0633\u0628\u0628 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u062e\u0627\u0637\u0626\u0629 \u0644\u0644\u062c\u0627\u0641\u0627 \u0633\u0643\u0631\u064a\u0628\u062a.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0646\u062d\u0646 \u0646\u062f\u0639\u0648\u0643 \u0625\u0644\u0649 \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 \u062a\u0641\u0627\u0635\u064a\u0644 \u0627\u0644\u062e\u0637\u0623 \u0644\u0645\u0639\u0631\u0641\u0629 \u0633\u0628\u0628 \u0627\u0644\u0645\u0634\u0643\u0644\u0629.\n \u064a\u0645\u0643\u0646\u0643 \u0623\u064a\u0636\u0627 \u0627\u0644\u0631\u062c\u0648\u0639 \u0625\u0644\u0649 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-not-found =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0643\u0648\u0646 \u0627\u0644\u0645\u0644\u0641 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0627\u0644\u0631\u062c\u0627\u0621 \u0627\u0644\u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646 \u0627\u0644\u0645\u0644\u0641 \u0642\u062f \u062a\u0645 \u062a\u062d\u0645\u064a\u0644\u0647 \u0628\u0634\u0643\u0644 \u0635\u062d\u064a\u062d.\n \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631\u062a \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u060c \u0642\u062f \u062a\u062d\u062a\u0627\u062c \u0625\u0644\u0649 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0625\u0639\u062f\u0627\u062f\u0627\u062a "\u0627\u0644\u0645\u0633\u0627\u0631 \u0627\u0644\u0639\u0627\u0645": \u0627\u0644\u0631\u062c\u0627\u0621 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-mime-type =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u062e\u0627\u062f\u0645 \u0627\u0644\u0648\u064a\u0628 \u0647\u0630\u0627 \u0644\u0627 \u064a\u062e\u062f\u0645 \u0645\u0644\u0641\u0627\u062a ". wasm" \u0645\u0639 \u0646\u0648\u0639 MIME \u0627\u0644\u0635\u062d\u064a\u062d.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-swf-fetch =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0644\u0641 \u0641\u0644\u0627\u0634 SWF.\n \u0627\u0644\u0633\u0628\u0628 \u0627\u0644\u0623\u0643\u062b\u0631 \u0627\u062d\u062a\u0645\u0627\u0644\u0627 \u0647\u0648 \u0623\u0646 \u0627\u0644\u0645\u0644\u0641 \u0644\u0645 \u064a\u0639\u062f \u0645\u0648\u062c\u0648\u062f\u0627\u060c \u0644\u0630\u0644\u0643 \u0644\u0627 \u064a\u0648\u062c\u062f \u0634\u064a\u0621 \u0644\u064a\u062d\u0645\u0644\u0647 \u0631\u0641\u0644.\n \u062d\u0627\u0648\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u0645\u0648\u0642\u0639 \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-swf-cors =\n \u0641\u0634\u0644 \u0627\u0644\u0631\u0648\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0644\u0641 \u0641\u0644\u0627\u0634 SWF.\n \u0645\u0646 \u0627\u0644\u0645\u062d\u062a\u0645\u0644 \u0623\u0646 \u062a\u0645 \u062d\u0638\u0631 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0645\u0646\u0627\u0644 \u0628\u0648\u0627\u0633\u0637\u0629 \u0633\u064a\u0627\u0633\u0629 CORS.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-cors =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0643\u0648\u0646 \u0645\u0644\u0641 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0645\u0646 \u0627\u0644\u0645\u062d\u062a\u0645\u0644 \u0623\u0646 \u062a\u0645 \u062d\u0638\u0631 \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0645\u0646\u0627\u0644 \u0628\u0648\u0627\u0633\u0637\u0629 \u0633\u064a\u0627\u0633\u0629 CORS.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-invalid =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u062e\u0627\u062f\u0645 \u0627\u0644\u0648\u064a\u0628 \u0647\u0630\u0627 \u0644\u0627 \u064a\u062e\u062f\u0645 \u0645\u0644\u0641\u0627\u062a ". wasm" \u0645\u0639 \u0646\u0648\u0639 MIME \u0627\u0644\u0635\u062d\u064a\u062d.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0645\u0631\u0627\u062c\u0639\u0629 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-wasm-download =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u062a\u0647\u0627 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u0647\u0630\u0627 \u064a\u0645\u0643\u0646 \u0623\u0646 \u064a\u062d\u0644 \u0646\u0641\u0633\u0647 \u0641\u064a \u0643\u062b\u064a\u0631 \u0645\u0646 \u0627\u0644\u0623\u062d\u064a\u0627\u0646\u060c \u0644\u0630\u0644\u0643 \u064a\u0645\u0643\u0646\u0643 \u0645\u062d\u0627\u0648\u0644\u0629 \u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0635\u0641\u062d\u0629.\n \u062e\u0644\u0627\u0641 \u0630\u0644\u0643\u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0627\u062a\u0635\u0627\u0644 \u0628\u0645\u062f\u064a\u0631 \u0627\u0644\u0645\u0648\u0642\u0639.\nerror-wasm-disabled-on-edge =\n \u0641\u0634\u0644 \u0631\u0641\u0644 \u0641\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0643\u0648\u0646 \u0627\u0644\u0645\u0644\u0641 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0644\u0625\u0635\u0644\u0627\u062d \u0647\u0630\u0647 \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u060c \u062d\u0627\u0648\u0644 \u0641\u062a\u062d \u0625\u0639\u062f\u0627\u062f\u0627\u062a \u0627\u0644\u0645\u062a\u0635\u0641\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u0643\u060c \u0627\u0646\u0642\u0631 \u0641\u0648\u0642 "\u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629\u060c \u0627\u0644\u0628\u062d\u062b\u060c \u0627\u0644\u062e\u062f\u0645\u0627\u062a"\u060c \u0648\u0627\u0644\u062a\u0645\u0631\u064a\u0631 \u0644\u0623\u0633\u0641\u0644\u060c \u0648\u0625\u064a\u0642\u0627\u0641 "\u062a\u0639\u0632\u064a\u0632 \u0623\u0645\u0627\u0646\u0643 \u0639\u0644\u0649 \u0627\u0644\u0648\u064a\u0628".\n \u0647\u0630\u0627 \u0633\u064a\u0633\u0645\u062d \u0644\u0644\u0645\u062a\u0635\u0641\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641\u0627\u062a ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629.\n \u0625\u0630\u0627 \u0627\u0633\u062a\u0645\u0631\u062a \u0627\u0644\u0645\u0634\u0643\u0644\u0629\u060c \u0642\u062f \u062a\u062d\u062a\u0627\u062c \u0625\u0644\u0649 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0645\u062a\u0635\u0641\u062d \u0623\u062e\u0631.\nerror-javascript-conflict =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u064a\u0628\u062f\u0648 \u0623\u0646 \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062d\u0629 \u062a\u0633\u062a\u062e\u062f\u0645 \u0643\u0648\u062f \u062c\u0627\u0641\u0627 \u0633\u0643\u0631\u064a\u0628\u062a \u0627\u0644\u0630\u064a \u064a\u062a\u0639\u0627\u0631\u0636 \u0645\u0639 \u0631\u0641\u0644.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0641\u0625\u0646\u0646\u0627 \u0646\u062f\u0639\u0648\u0643 \u0625\u0644\u0649 \u0645\u062d\u0627\u0648\u0644\u0629 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641 \u0639\u0644\u0649 \u0635\u0641\u062d\u0629 \u0641\u0627\u0631\u063a\u0629.\nerror-javascript-conflict-outdated = \u064a\u0645\u0643\u0646\u0643 \u0623\u064a\u0636\u064b\u0627 \u0645\u062d\u0627\u0648\u0644\u0629 \u062a\u062d\u0645\u064a\u0644 \u0646\u0633\u062e\u0629 \u0623\u062d\u062f\u062b \u0645\u0646 \u0631\u0641\u0644 \u0627\u0644\u062a\u064a \u0642\u062f \u062a\u062d\u0644 \u0627\u0644\u0645\u0634\u0643\u0644\u0629 (\u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u062d\u0627\u0644\u064a\u0629 \u0642\u062f\u064a\u0645\u0629: { $buildDate }).\nerror-csp-conflict =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0641\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0627\u0644\u062a\u0647\u064a\u0626\u0629.\n \u0644\u0627 \u062a\u0633\u0645\u062d \u0633\u064a\u0627\u0633\u0629 \u0623\u0645\u0627\u0646 \u0627\u0644\u0645\u062d\u062a\u0648\u0649 \u0644\u062e\u0627\u062f\u0645 \u0627\u0644\u0648\u064a\u0628 \u0647\u0630\u0627 \u0628\u062a\u0634\u063a\u064a\u0644 \u0645\u0643\u0648\u0646 ".wasm" \u0627\u0644\u0645\u0637\u0644\u0648\u0628.\n \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u064a\u0631\u062c\u0649 \u0627\u0644\u0631\u062c\u0648\u0639 \u0625\u0644\u0649 \u0631\u0641\u0644 \u0648\u064a\u0643\u064a \u0644\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062f\u0629.\nerror-unknown =\n \u0648\u0627\u062c\u0647\u062a \u0631\u0648\u0644 \u0645\u0634\u0643\u0644\u0629 \u0631\u0626\u064a\u0633\u064a\u0629 \u0623\u062b\u0646\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u0639\u0631\u0636 \u0645\u062d\u062a\u0648\u0649 \u0627\u0644\u0641\u0644\u0627\u0634 \u0647\u0630\u0627.\n { $outdated ->\n [true] \u0625\u0630\u0627 \u0643\u0646\u062a \u0645\u0633\u0624\u0648\u0644 \u0627\u0644\u062e\u0627\u062f\u0645\u060c \u0627\u0644\u0631\u062c\u0627\u0621 \u0645\u062d\u0627\u0648\u0644\u0629 \u062a\u062d\u0645\u064a\u0644 \u0625\u0635\u062f\u0627\u0631 \u0623\u062d\u062f\u062b \u0645\u0646 \u0631\u0641\u0644 (\u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u062d\u0627\u0644\u064a\u0629 \u0642\u062f\u064a\u0645\u0629: { $buildDate }).\n *[false] \u0644\u064a\u0633 \u0645\u0646 \u0627\u0644\u0645\u0641\u062a\u0631\u0636 \u0623\u0646 \u064a\u062d\u062f\u062b \u0647\u0630\u0627\u060c \u0644\u0630\u0644\u0643 \u0646\u062d\u0646 \u0646\u0642\u062f\u0631 \u062d\u0642\u064b\u0627 \u0625\u0630\u0627 \u0642\u0645\u062a \u0628\u0627\u0644\u062a\u0628\u0644\u064a\u063a \u0639\u0646 \u0627\u0644\u062e\u0637\u0623!\n }\n',"save-manager.ftl":"save-delete-prompt = \u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u062d\u0630\u0641 \u0645\u0644\u0641 \u062d\u0641\u0638 \u0627\u0644\u0644\u0639\u0628\u0629 \u0647\u0630\u0627\u061f\nsave-reload-prompt =\n \u0627\u0644\u0637\u0631\u064a\u0642\u0629 \u0627\u0644\u0648\u062d\u064a\u062f\u0629 \u0644\u0640 { $action ->\n [delete] \u062d\u0630\u0641\n *[replace] \u0627\u0633\u062a\u0628\u062f\u0627\u0644\n } \u0647\u0630\u0627 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u062d\u0641\u0638 \u062f\u0648\u0646 \u062a\u0636\u0627\u0631\u0628 \u0645\u062d\u062a\u0645\u0644 \u0647\u064a \u0625\u0639\u0627\u062f\u0629 \u062a\u062d\u0645\u064a\u0644 \u0647\u0630\u0627 \u0627\u0644\u0645\u062d\u062a\u0648\u0649. \u0647\u0644 \u062a\u0631\u063a\u0628 \u0641\u064a \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u0629 \u0639\u0644\u0649 \u0623\u064a \u062d\u0627\u0644\u061f\nsave-download = \u062a\u062d\u0645\u064a\u0644\nsave-replace = \u0627\u0633\u062a\u0628\u062f\u0627\u0644\nsave-delete = \u062d\u0630\u0641\nsave-backup-all = \u062a\u062d\u0645\u064a\u0644 \u062c\u0645\u064a\u0639 \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0627\u0644\u0645\u062d\u0641\u0648\u0638\u0629\n"},"ca-ES":{"context_menu.ftl":"context-menu-download-swf = Baixa el fitxer .swf\ncontext-menu-copy-debug-info = Copia la informaci\xf3 de depuraci\xf3\ncontext-menu-open-save-manager = Obre el gestor d'emmagatzematge\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Quant a l'extensi\xf3 de Ruffle ({ $version })\n *[other] Quant a Ruffle ({ $version })\n }\ncontext-menu-hide = Amaga aquest men\xfa\ncontext-menu-exit-fullscreen = Surt de la pantalla completa\ncontext-menu-enter-fullscreen = Pantalla completa\n","messages.ftl":"panic-title = Alguna cosa ha fallat :(\nmore-info = M\xe9s informaci\xf3\nrun-anyway = Reprodueix igualment\ncontinue = Continua\nreport-bug = Informa d'un error\nupdate-ruffle = Actualitza Ruffle\nruffle-demo = Demostraci\xf3 web\nruffle-desktop = Aplicaci\xf3 d'escriptori\nruffle-wiki = Obre la wiki de Ruffle\nview-error-details = Mostra detalls de l'error\nopen-in-new-tab = Obre en una pestanya nova\nclick-to-unmute = Feu clic per activar el so\n","save-manager.ftl":""},"cs-CZ":{"context_menu.ftl":"context-menu-download-swf = St\xe1hnout .swf\ncontext-menu-copy-debug-info = Zkop\xedrovat debug info\ncontext-menu-open-save-manager = Otev\u0159\xedt spr\xe1vce ulo\u017een\xed\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] O Ruffle roz\u0161\xed\u0159en\xed ({ $version })\n *[other] O Ruffle ({ $version })\n }\ncontext-menu-hide = Skr\xfdt menu\ncontext-menu-exit-fullscreen = Ukon\u010dit re\u017eim cel\xe9 obrazovky\ncontext-menu-enter-fullscreen = P\u0159ej\xedt do re\u017eimu cel\xe9 obrazovky\n","messages.ftl":'message-cant-embed =\n Ruffle nemohl spustit Flash vlo\u017een\xfd na t\xe9to str\xe1nce.\n M\u016f\u017eete se pokusit otev\u0159\xedt soubor na samostatn\xe9 kart\u011b, abyste se vyhnuli tomuto probl\xe9mu.\npanic-title = N\u011bco se pokazilo :(\nmore-info = Dal\u0161\xed informace\nrun-anyway = P\u0159esto spustit\ncontinue = Pokra\u010dovat\nreport-bug = Nahl\xe1sit chybu\nupdate-ruffle = Aktualizovat Ruffle\nruffle-demo = Web Demo\nruffle-desktop = Desktopov\xe1 aplikace\nruffle-wiki = Zobrazit Ruffle Wiki\nview-error-details = Zobrazit podrobnosti o chyb\u011b\nopen-in-new-tab = Otev\u0159\xedt na nov\xe9 kart\u011b\nclick-to-unmute = Kliknut\xedm zru\u0161\xedte ztlumen\xed\nerror-file-protocol =\n Zd\xe1 se, \u017ee pou\u017e\xedv\xe1te Ruffle na protokolu "file:".\n To nen\xed mo\u017en\xe9, proto\u017ee prohl\xed\u017ee\u010de blokuj\xed fungov\xe1n\xed mnoha funkc\xed z bezpe\u010dnostn\xedch d\u016fvod\u016f.\n Nam\xedsto toho v\xe1m doporu\u010dujeme nastavit lok\xe1ln\xed server nebo pou\u017e\xedt web demo \u010di desktopovou aplikaci.\nerror-javascript-config =\n Ruffle narazil na probl\xe9m v d\u016fsledku nespr\xe1vn\xe9 konfigurace JavaScriptu.\n Pokud jste spr\xe1vcem serveru, doporu\u010dujeme v\xe1m zkontrolovat podrobnosti o chyb\u011b, abyste zjistili, kter\xfd parametr je vadn\xfd.\n Pomoc m\u016f\u017eete z\xedskat tak\xe9 na wiki Ruffle.\nerror-wasm-not-found =\n Ruffle se nepoda\u0159ilo na\u010d\xedst po\u017eadovanou komponentu souboru \u201e.wasm\u201c.\n Pokud jste spr\xe1vcem serveru, zkontrolujte, zda byl soubor spr\xe1vn\u011b nahr\xe1n.\n Pokud probl\xe9m p\u0159etrv\xe1v\xe1, mo\u017en\xe1 budete muset pou\u017e\xedt nastaven\xed \u201epublicPath\u201c: pomoc naleznete na wiki Ruffle.\nerror-wasm-mime-type =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Tento webov\xfd server neposkytuje soubory \u201e.wasm\u201c se spr\xe1vn\xfdm typem MIME.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-swf-fetch =\n Ruffle se nepoda\u0159ilo na\u010d\xedst SWF soubor Flash.\n Nejpravd\u011bpodobn\u011bj\u0161\xedm d\u016fvodem je, \u017ee soubor ji\u017e neexistuje, tak\u017ee Ruffle nem\xe1 co na\u010d\xedst.\n Zkuste po\u017e\xe1dat o pomoc spr\xe1vce webu.\nerror-swf-cors =\n Ruffle se nepoda\u0159ilo na\u010d\xedst SWF soubor Flash.\n P\u0159\xedstup k na\u010d\xedt\xe1n\xed byl pravd\u011bpodobn\u011b zablokov\xe1n politikou CORS.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-wasm-cors =\n Ruffle se nepoda\u0159ilo na\u010d\xedst po\u017eadovanou komponentu souboru \u201e.wasm\u201c.\n P\u0159\xedstup k na\u010d\xedt\xe1n\xed byl pravd\u011bpodobn\u011b zablokov\xe1n politikou CORS.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-wasm-invalid =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Zd\xe1 se, \u017ee na t\xe9to str\xe1nce chyb\xed nebo jsou neplatn\xe9 soubory ke spu\u0161t\u011bn\xed Ruffle.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-wasm-download =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Probl\xe9m se m\u016f\u017ee vy\u0159e\u0161it i s\xe1m, tak\u017ee m\u016f\u017eete zkusit str\xe1nku na\u010d\xedst znovu.\n V opa\u010dn\xe9m p\u0159\xedpad\u011b kontaktujte administr\xe1tora str\xe1nky.\nerror-wasm-disabled-on-edge =\n Ruffle se nepoda\u0159ilo na\u010d\xedst po\u017eadovanou komponentu souboru \u201e.wasm\u201c.\n Chcete-li tento probl\xe9m vy\u0159e\u0161it, zkuste otev\u0159\xedt nastaven\xed prohl\xed\u017ee\u010de, klikn\u011bte na polo\u017eku \u201eOchrana osobn\xedch \xfadaj\u016f, vyhled\xe1v\xe1n\xed a slu\u017eby\u201c, p\u0159ejd\u011bte dol\u016f a vypn\u011bte mo\u017enost \u201eZvy\u0161te svou bezpe\u010dnost na webu\u201c.\n Va\u0161emu prohl\xed\u017ee\u010di to umo\u017en\xed na\u010d\xedst po\u017eadovan\xe9 soubory \u201e.wasm\u201c.\n Pokud probl\xe9m p\u0159etrv\xe1v\xe1, budete mo\u017en\xe1 muset pou\u017e\xedt jin\xfd prohl\xed\u017ee\u010d.\nerror-javascript-conflict =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Zd\xe1 se, \u017ee tato str\xe1nka pou\u017e\xedv\xe1 k\xf3d JavaScript, kter\xfd je v konfliktu s Ruffle.\n Pokud jste spr\xe1vcem serveru, doporu\u010dujeme v\xe1m zkusit na\u010d\xedst soubor na pr\xe1zdnou str\xe1nku.\nerror-javascript-conflict-outdated = M\u016f\u017eete se tak\xe9 pokusit nahr\xe1t nov\u011bj\u0161\xed verzi Ruffle, kter\xe1 m\u016f\u017ee dan\xfd probl\xe9m vy\u0159e\u0161it (aktu\xe1ln\xed build je zastaral\xfd: { $buildDate }).\nerror-csp-conflict =\n Ruffle narazil na probl\xe9m p\u0159i pokusu o inicializaci.\n Z\xe1sady zabezpe\u010den\xed obsahu tohoto webov\xe9ho serveru nepovoluj\xed spu\u0161t\u011bn\xed po\u017eadovan\xe9 komponenty \u201e.wasm\u201c.\n Pokud jste spr\xe1vcem serveru, n\xe1pov\u011bdu najdete na Ruffle wiki.\nerror-unknown =\n Ruffle narazil na probl\xe9m p\u0159i pokusu zobrazit tento Flash obsah.\n { $outdated ->\n [true] Pokud jste spr\xe1vcem serveru, zkuste nahr\xe1t nov\u011bj\u0161\xed verzi Ruffle (aktu\xe1ln\xed build je zastaral\xfd: { $buildDate }).\n *[false] Toto by se nem\u011blo st\xe1t, tak\u017ee bychom opravdu ocenili, kdybyste mohli nahl\xe1sit chybu!\n }\n',"save-manager.ftl":"save-delete-prompt = Opravdu chcete odstranit tento soubor s ulo\u017een\xfdmi pozicemi?\nsave-reload-prompt =\n Jedin\xfd zp\u016fsob, jak { $action ->\n [delete] vymazat\n *[replace] nahradit\n } tento soubor s ulo\u017een\xfdmi pozicemi bez potenci\xe1ln\xedho konfliktu je op\u011btovn\xe9 na\u010dten\xed tohoto obsahu. Chcete p\u0159esto pokra\u010dovat?\nsave-download = St\xe1hnout\nsave-replace = Nahradit\nsave-delete = Vymazat\nsave-backup-all = St\xe1hnout v\u0161echny soubory s ulo\u017een\xfdmi pozicemi\n"},"de-DE":{"context_menu.ftl":"context-menu-download-swf = .swf herunterladen\ncontext-menu-copy-debug-info = Debug-Info kopieren\ncontext-menu-open-save-manager = Dateimanager \xf6ffnen\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \xdcber Ruffle Erweiterung ({ $version })\n *[other] \xdcber Ruffle ({ $version })\n }\ncontext-menu-hide = Men\xfc ausblenden\ncontext-menu-exit-fullscreen = Vollbild verlassen\ncontext-menu-enter-fullscreen = Vollbildmodus aktivieren\n","messages.ftl":'message-cant-embed =\n Ruffle konnte den Flash in dieser Seite nicht ausf\xfchren.\n Du kannst versuchen, die Datei in einem separaten Tab zu \xf6ffnen, um dieses Problem zu umgehen.\npanic-title = Etwas ist schief gelaufen\nmore-info = Weitere Informationen\nrun-anyway = Trotzdem ausf\xfchren\ncontinue = Fortfahren\nreport-bug = Fehler melden\nupdate-ruffle = Ruffle aktuallisieren\nruffle-demo = Web-Demo\nruffle-desktop = Desktop-Anwendung\nruffle-wiki = Ruffle-Wiki anzeigen\nview-error-details = Fehlerdetails anzeigen\nopen-in-new-tab = In einem neuen Tab \xf6ffnen\nclick-to-unmute = Klicke zum Entmuten\nerror-file-protocol =\n Es scheint, dass Sie Ruffle auf dem "file:"-Protokoll ausf\xfchren.\n Dies funktioniert nicht so, als Browser viele Funktionen aus Sicherheitsgr\xfcnden blockieren.\n Stattdessen laden wir Sie ein, einen lokalen Server einzurichten oder entweder die Webdemo oder die Desktop-Anwendung zu verwenden.\nerror-javascript-config =\n Ruffle ist aufgrund einer falschen JavaScript-Konfiguration auf ein gro\xdfes Problem gesto\xdfen.\n Wenn du der Server-Administrator bist, laden wir dich ein, die Fehlerdetails zu \xfcberpr\xfcfen, um herauszufinden, welcher Parameter fehlerhaft ist.\n Sie k\xf6nnen auch das Ruffle-Wiki f\xfcr Hilfe konsultieren.\nerror-wasm-not-found =\n Ruffle konnte die erforderliche ".wasm"-Datei-Komponente nicht laden.\n Wenn Sie der Server-Administrator sind, stellen Sie bitte sicher, dass die Datei korrekt hochgeladen wurde.\n Wenn das Problem weiterhin besteht, m\xfcssen Sie unter Umst\xe4nden die "publicPath"-Einstellung verwenden: Bitte konsultieren Sie das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-mime-type =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Dieser Webserver dient nicht ". asm"-Dateien mit dem korrekten MIME-Typ.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-swf-fetch =\n Ruffle konnte die Flash-SWF-Datei nicht laden.\n Der wahrscheinlichste Grund ist, dass die Datei nicht mehr existiert, so dass Ruffle nicht geladen werden kann.\n Kontaktieren Sie den Website-Administrator f\xfcr Hilfe.\nerror-swf-cors =\n Ruffle konnte die Flash-SWF-Datei nicht laden.\n Der Zugriff auf den Abruf wurde wahrscheinlich durch die CORS-Richtlinie blockiert.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-cors =\n Ruffle konnte die Flash-SWF-Datei nicht laden.\n Der Zugriff auf den Abruf wurde wahrscheinlich durch die CORS-Richtlinie blockiert.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-invalid =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Dieser Webserver dient nicht ". asm"-Dateien mit dem korrekten MIME-Typ.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-wasm-download =\n Ruffle ist auf ein gro\xdfes Problem gesto\xdfen, w\xe4hrend er versucht hat zu initialisieren.\n Dies kann sich oft selbst beheben, so dass Sie versuchen k\xf6nnen, die Seite neu zu laden.\n Andernfalls kontaktieren Sie bitte den Website-Administrator.\nerror-wasm-disabled-on-edge =\n Ruffle konnte die erforderliche ".wasm"-Datei-Komponente nicht laden.\n Um dies zu beheben, versuche die Einstellungen deines Browsers zu \xf6ffnen, klicke auf "Privatsph\xe4re, Suche und Dienste", scrollen nach unten und schalte "Verbessere deine Sicherheit im Web" aus.\n Dies erlaubt Ihrem Browser die erforderlichen ".wasm"-Dateien zu laden.\n Wenn das Problem weiterhin besteht, m\xfcssen Sie m\xf6glicherweise einen anderen Browser verwenden.\nerror-javascript-conflict =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Es scheint, als ob diese Seite JavaScript-Code verwendet, der mit Ruffle kollidiert.\n Wenn Sie der Server-Administrator sind, laden wir Sie ein, die Datei auf einer leeren Seite zu laden.\nerror-javascript-conflict-outdated = Du kannst auch versuchen, eine neuere Version von Ruffle hochzuladen, die das Problem umgehen k\xf6nnte (aktuelle Version ist veraltet: { $buildDate }).\nerror-csp-conflict =\n Ruffle ist auf ein gro\xdfes Problem beim Initialisieren gesto\xdfen.\n Dieser Webserver dient nicht ". asm"-Dateien mit dem korrekten MIME-Typ.\n Wenn Sie der Server-Administrator sind, konsultieren Sie bitte das Ruffle-Wiki f\xfcr Hilfe.\nerror-unknown =\n Bei dem Versuch, diesen Flash-Inhalt anzuzeigen, ist Ruffle auf ein gro\xdfes Problem gesto\xdfen.\n { $outdated ->\n [true] Wenn Sie der Server-Administrator sind, Bitte versuchen Sie, eine neuere Version von Ruffle hochzuladen (aktuelle Version ist veraltet: { $buildDate }).\n *[false] Dies soll nicht passieren, deshalb w\xfcrden wir uns sehr dar\xfcber freuen, wenn Sie einen Fehler melden k\xf6nnten!\n }\n',"save-manager.ftl":"save-delete-prompt = Sind Sie sicher, dass Sie diese Speicherdatei l\xf6schen m\xf6chten?\nsave-reload-prompt =\n Der einzige Weg zu { $action ->\n [delete] l\xf6schen\n *[replace] ersetzen\n } diese Speicherdatei ohne m\xf6glichen Konflikt ist das erneute Laden dieses Inhalts. M\xf6chten Sie trotzdem fortfahren?\nsave-download = Herunterladen\nsave-replace = Ersetzen\nsave-delete = L\xf6schen\nsave-backup-all = Alle gespeicherten Dateien herunterladen\n"},"en-US":{"context_menu.ftl":"context-menu-download-swf = Download .swf\ncontext-menu-copy-debug-info = Copy debug info\ncontext-menu-open-save-manager = Open Save Manager\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] About Ruffle Extension ({$version})\n *[other] About Ruffle ({$version})\n }\ncontext-menu-hide = Hide this menu\ncontext-menu-exit-fullscreen = Exit fullscreen\ncontext-menu-enter-fullscreen = Enter fullscreen","messages.ftl":'message-cant-embed =\n Ruffle wasn\'t able to run the Flash embedded in this page.\n You can try to open the file in a separate tab, to sidestep this issue.\npanic-title = Something went wrong :(\nmore-info = More info\nrun-anyway = Run anyway\ncontinue = Continue\nreport-bug = Report Bug\nupdate-ruffle = Update Ruffle\nruffle-demo = Web Demo\nruffle-desktop = Desktop Application\nruffle-wiki = View Ruffle Wiki\nview-error-details = View Error Details\nopen-in-new-tab = Open in a new tab\nclick-to-unmute = Click to unmute\nerror-file-protocol =\n It appears you are running Ruffle on the "file:" protocol.\n This doesn\'t work as browsers block many features from working for security reasons.\n Instead, we invite you to setup a local server or either use the web demo or the desktop application.\nerror-javascript-config =\n Ruffle has encountered a major issue due to an incorrect JavaScript configuration.\n If you are the server administrator, we invite you to check the error details to find out which parameter is at fault.\n You can also consult the Ruffle wiki for help.\nerror-wasm-not-found =\n Ruffle failed to load the required ".wasm" file component.\n If you are the server administrator, please ensure the file has correctly been uploaded.\n If the issue persists, you may need to use the "publicPath" setting: please consult the Ruffle wiki for help.\nerror-wasm-mime-type =\n Ruffle has encountered a major issue whilst trying to initialize.\n This web server is not serving ".wasm" files with the correct MIME type.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-swf-fetch =\n Ruffle failed to load the Flash SWF file.\n The most likely reason is that the file no longer exists, so there is nothing for Ruffle to load.\n Try contacting the website administrator for help.\nerror-swf-cors =\n Ruffle failed to load the Flash SWF file.\n Access to fetch has likely been blocked by CORS policy.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-wasm-cors =\n Ruffle failed to load the required ".wasm" file component.\n Access to fetch has likely been blocked by CORS policy.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-wasm-invalid =\n Ruffle has encountered a major issue whilst trying to initialize.\n It seems like this page has missing or invalid files for running Ruffle.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-wasm-download =\n Ruffle has encountered a major issue whilst trying to initialize.\n This can often resolve itself, so you can try reloading the page.\n Otherwise, please contact the website administrator.\nerror-wasm-disabled-on-edge =\n Ruffle failed to load the required ".wasm" file component.\n To fix this, try opening your browser\'s settings, clicking "Privacy, search, and services", scrolling down, and turning off "Enhance your security on the web".\n This will allow your browser to load the required ".wasm" files.\n If the issue persists, you might have to use a different browser.\nerror-javascript-conflict =\n Ruffle has encountered a major issue whilst trying to initialize.\n It seems like this page uses JavaScript code that conflicts with Ruffle.\n If you are the server administrator, we invite you to try loading the file on a blank page.\nerror-javascript-conflict-outdated = You can also try to upload a more recent version of Ruffle that may circumvent the issue (current build is outdated: {$buildDate}).\nerror-csp-conflict =\n Ruffle has encountered a major issue whilst trying to initialize.\n This web server\'s Content Security Policy does not allow the required ".wasm" component to run.\n If you are the server administrator, please consult the Ruffle wiki for help.\nerror-unknown =\n Ruffle has encountered a major issue whilst trying to display this Flash content.\n {$outdated ->\n [true] If you are the server administrator, please try to upload a more recent version of Ruffle (current build is outdated: {$buildDate}).\n *[false] This isn\'t supposed to happen, so we\'d really appreciate if you could file a bug!\n }',"save-manager.ftl":"save-delete-prompt = Are you sure you want to delete this save file?\nsave-reload-prompt =\n The only way to {$action ->\n [delete] delete\n *[replace] replace\n } this save file without potential conflict is to reload this content. Do you wish to continue anyway?\nsave-download = Download\nsave-replace = Replace\nsave-delete = Delete\nsave-backup-all = Download all save files"},"es-ES":{"context_menu.ftl":"context-menu-download-swf = Descargar .swf\ncontext-menu-copy-debug-info = Copiar Informaci\xf3n de depuraci\xf3n\ncontext-menu-open-save-manager = Abrir gestor de guardado\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Sobre la extensi\xf3n de Ruffle ({ $version })\n *[other] Sobre Ruffle ({ $version })\n }\ncontext-menu-hide = Ocultar este men\xfa\ncontext-menu-exit-fullscreen = Salir de pantalla completa\ncontext-menu-enter-fullscreen = Entrar a pantalla completa\n","messages.ftl":'message-cant-embed =\n Ruffle no pudo ejecutar el Flash incrustado en esta p\xe1gina.\n Puedes intentar abrir el archivo en una pesta\xf1a aparte, para evitar este problema.\npanic-title = Algo sali\xf3 mal :(\nmore-info = M\xe1s info\nrun-anyway = Ejecutar de todos modos\ncontinue = Continuar\nreport-bug = Reportar un Error\nupdate-ruffle = Actualizar Ruffle\nruffle-demo = Demostraci\xf3n de web\nruffle-desktop = Aplicaci\xf3n de Desktop\nruffle-wiki = Ver la p\xe1gina wiki\nview-error-details = Ver los detalles del error\nopen-in-new-tab = Abrir en una pesta\xf1a nueva\nclick-to-unmute = Haz clic para dejar de silenciar\nerror-file-protocol =\n Parece que est\xe1 ejecutando Ruffle en el protocolo "archivo:".\n Esto no funciona porque los navegadores bloquean que muchas caracter\xedsticas funcionen por razones de seguridad.\n En su lugar, le invitamos a configurar un servidor local o bien usar la demostraci\xf3n web o la aplicaci\xf3n de desktop.\nerror-javascript-config =\n Ruffle ha encontrado un problema cr\xedtico debido a una configuraci\xf3n JavaScript incorrecta.\n Si usted es el administrador del servidor, le invitamos a comprobar los detalles del error para averiguar qu\xe9 par\xe1metro est\xe1 en falta.\n Tambi\xe9n puedes consultar la wiki de Ruffle para obtener ayuda.\nerror-wasm-not-found =\n Ruffle no pudo cargar el componente de archivo ".wasm" requerido.\n Si usted es el administrador del servidor, aseg\xfarese de que el archivo ha sido subido correctamente.\n Si el problema persiste, puede que necesite usar la configuraci\xf3n "publicPath": por favor consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-mime-type =\n Ruffle ha encontrado un problema cr\xedtico al intentar inicializar.\n Este servidor web no est\xe1 sirviendo archivos wasm" con el tipo MIME correcto.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-swf-fetch =\n Ruffle no pudo cargar el archivo Flash SWF.\n La raz\xf3n m\xe1s probable es que el archivo ya no existe, as\xed que no hay nada para cargar Ruffle.\n Intente ponerse en contacto con el administrador del sitio web para obtener ayuda.\nerror-swf-cors =\n Ruffle no pudo cargar el archivo Flash SWF.\n Es probable que el acceso a la b\xfasqueda haya sido bloqueado por la pol\xedtica CORS.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-cors =\n Ruffle no pudo cargar el archivo ".wasm."\n Es probable que el acceso a la b\xfasqueda o la llamada a la funci\xf3n fetch haya sido bloqueado por la pol\xedtica CORS.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-invalid =\n Ruffle ha encontrado un problema cr\xedtico al intentar inicializar.\n Este servidor web no est\xe1 sirviendo archivos wasm" con el tipo Mime correcto.\n Si usted es el administrador del servidor, consulte la wiki de Ruffle para obtener ayuda.\nerror-wasm-download =\n Ruffle ha encontrado un problema cr\xedtico mientras intentaba inicializarse.\n Esto a menudo puede resolverse por s\xed mismo, as\xed que puede intentar recargar la p\xe1gina.\n De lo contrario, p\xf3ngase en contacto con el administrador del sitio web.\nerror-wasm-disabled-on-edge =\n Ruffle no pudo cargar el componente de archivo ".wasm" requerido.\n Para solucionar esto, intenta abrir la configuraci\xf3n de tu navegador, haciendo clic en "Privacidad, b\xfasqueda y servicios", desplaz\xe1ndote y apagando "Mejore su seguridad en la web".\n Esto permitir\xe1 a su navegador cargar los archivos ".wasm" necesarios.\n Si el problema persiste, puede que tenga que utilizar un navegador diferente.\nerror-javascript-conflict =\n Ruffle ha encontrado un problema cr\xedtico mientras intentaba inicializarse.\n Parece que esta p\xe1gina utiliza c\xf3digo JavaScript que entra en conflicto con Ruffle.\n Si usted es el administrador del servidor, le invitamos a intentar cargar el archivo en una p\xe1gina en blanco.\nerror-javascript-conflict-outdated = Tambi\xe9n puedes intentar subir una versi\xf3n m\xe1s reciente de Ruffle que puede eludir el problema (la versi\xf3n actual est\xe1 desactualizada: { $buildDate }).\nerror-csp-conflict =\n Ruffle encontr\xf3 un problema al intentar inicializarse.\n La Pol\xedtica de Seguridad de Contenido de este servidor web no permite el componente requerido ".wasm". \n Si usted es el administrador del servidor, por favor consulta la wiki de Ruffle para obtener ayuda.\nerror-unknown =\n Ruffle ha encontrado un problema al tratar de mostrar el contenido Flash.\n { $outdated ->\n [true] Si usted es el administrador del servidor, intenta cargar una version m\xe1s reciente de Ruffle (la version actual esta desactualizada: { $buildDate }).\n *[false] Esto no deberia suceder! apreciariamos que reportes el error!\n }\n',"save-manager.ftl":"save-delete-prompt = \xbfEst\xe1 seguro de querer eliminar este archivo de guardado?\nsave-reload-prompt =\n La \xfanica forma de { $action ->\n [delete] eliminar\n *[replace] sobreescribir\n } este archivo de guardado sin conflictos potenciales es reiniciando el contenido. \xbfDesea continuar de todos modos?\nsave-download = Descargar\nsave-replace = Sobreescribir\nsave-delete = Borrar\nsave-backup-all = Borrar todos los archivos de guardado\n"},"fr-FR":{"context_menu.ftl":"context-menu-download-swf = T\xe9l\xe9charger en tant que .swf\ncontext-menu-copy-debug-info = Copier les infos de d\xe9bogage\ncontext-menu-open-save-manager = Ouvrir le gestionnaire de stockage\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \xc0 propos de Ruffle Extension ({ $version })\n *[other] \xc0 propos de Ruffle ({ $version })\n }\ncontext-menu-hide = Masquer ce menu\ncontext-menu-exit-fullscreen = Sortir du mode plein \xe9cran\ncontext-menu-enter-fullscreen = Afficher en plein \xe9cran\n","messages.ftl":"message-cant-embed =\n Ruffle n'a pas \xe9t\xe9 en mesure de lire le fichier Flash int\xe9gr\xe9 dans cette page.\n Vous pouvez essayer d'ouvrir le fichier dans un onglet isol\xe9, pour contourner le probl\xe8me.\npanic-title = Une erreur est survenue :(\nmore-info = Plus d'infos\nrun-anyway = Ex\xe9cuter quand m\xeame\ncontinue = Continuer\nreport-bug = Signaler le bug\nupdate-ruffle = Mettre \xe0 jour Ruffle\nruffle-demo = D\xe9mo en ligne\nruffle-desktop = Application de bureau\nruffle-wiki = Wiki de Ruffle\nview-error-details = D\xe9tails de l'erreur\nopen-in-new-tab = Ouvrir dans un nouvel onglet\nclick-to-unmute = Cliquez pour activer le son\nerror-file-protocol =\n Il semblerait que vous ex\xe9cutiez Ruffle sur le protocole \"file:\".\n Cela ne fonctionne pas car les navigateurs bloquent de nombreuses fonctionnalit\xe9s pour des raisons de s\xe9curit\xe9.\n Nous vous invitons soit \xe0 configurer un serveur local, soit \xe0 utiliser la d\xe9mo en ligne ou l'application de bureau.\nerror-javascript-config =\n Ruffle a rencontr\xe9 un probl\xe8me majeur en raison d'une configuration JavaScript incorrecte.\n Si vous \xeates l'administrateur du serveur, nous vous invitons \xe0 v\xe9rifier les d\xe9tails de l'erreur pour savoir quel est le param\xe8tre en cause.\n Vous pouvez \xe9galement consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-not-found =\n Ruffle n'a pas r\xe9ussi \xe0 charger son fichier \".wasm\".\n Si vous \xeates l'administrateur du serveur, veuillez vous assurer que ce fichier a bien \xe9t\xe9 mis en ligne.\n Si le probl\xe8me persiste, il vous faudra peut-\xeatre utiliser le param\xe8tre \"publicPath\" : veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-mime-type =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Ce serveur web ne renvoie pas le bon type MIME pour les fichiers \".wasm\".\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-swf-fetch =\n Ruffle n'a pas r\xe9ussi \xe0 charger le fichier Flash.\n La raison la plus probable est que le fichier n'existe pas ou plus.\n Vous pouvez essayer de prendre contact avec l'administrateur du site pour obtenir plus d'informations.\nerror-swf-cors =\n Ruffle n'a pas r\xe9ussi \xe0 charger le fichier Flash.\n La requ\xeate a probablement \xe9t\xe9 rejet\xe9e en raison de la configuration du CORS.\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-cors =\n Ruffle n'a pas r\xe9ussi \xe0 charger son fichier \".wasm\".\n La requ\xeate a probablement \xe9t\xe9 rejet\xe9e en raison de la configuration du CORS.\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-invalid =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Il semblerait que cette page comporte des fichiers manquants ou invalides pour ex\xe9cuter Ruffle.\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-wasm-download =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Le probl\xe8me d\xe9tect\xe9 peut souvent se r\xe9soudre de lui-m\xeame, donc vous pouvez essayer de recharger la page.\n Si le probl\xe8me persiste, veuillez prendre contact avec l'administrateur du site.\nerror-wasm-disabled-on-edge =\n Ruffle n'a pas r\xe9ussi \xe0 charger son fichier \".wasm\".\n Pour r\xe9soudre ce probl\xe8me, essayez d'ouvrir les param\xe8tres de votre navigateur et de cliquer sur \"Confidentialit\xe9, recherche et services\". Puis, vers le bas de la page, d\xe9sactivez l'option \"Am\xe9liorez votre s\xe9curit\xe9 sur le web\".\n Cela permettra \xe0 votre navigateur de charger les fichiers \".wasm\".\n Si le probl\xe8me persiste, vous devrez peut-\xeatre utiliser un autre navigateur.\nerror-javascript-conflict =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n Il semblerait que cette page contienne du code JavaScript qui entre en conflit avec Ruffle.\n Si vous \xeates l'administrateur du serveur, nous vous invitons \xe0 essayer de charger le fichier dans une page vide.\nerror-javascript-conflict-outdated = Vous pouvez \xe9galement essayer de mettre en ligne une version plus r\xe9cente de Ruffle qui pourrait avoir corrig\xe9 le probl\xe8me (la version que vous utilisez est obsol\xe8te : { $buildDate }).\nerror-csp-conflict =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant sa phase d'initialisation.\n La strat\xe9gie de s\xe9curit\xe9 du contenu (CSP) de ce serveur web n'autorise pas l'ex\xe9cution de fichiers \".wasm\".\n Si vous \xeates l'administrateur du serveur, veuillez consulter le wiki de Ruffle pour obtenir de l'aide.\nerror-unknown =\n Ruffle a rencontr\xe9 un probl\xe8me majeur durant l'ex\xe9cution de ce contenu Flash.\n { $outdated ->\n [true] Si vous \xeates l'administrateur du serveur, veuillez essayer de mettre en ligne une version plus r\xe9cente de Ruffle (la version que vous utilisez est obsol\xe8te : { $buildDate }).\n *[false] Cela n'est pas cens\xe9 se produire, donc nous vous serions reconnaissants si vous pouviez nous signaler ce bug !\n }\n","save-manager.ftl":"save-delete-prompt = Voulez-vous vraiment supprimer ce fichier de sauvegarde ?\nsave-reload-prompt =\n La seule fa\xe7on de { $action ->\n [delete] supprimer\n *[replace] remplacer\n } ce fichier de sauvegarde sans conflit potentiel est de recharger ce contenu. Souhaitez-vous quand m\xeame continuer ?\nsave-download = T\xe9l\xe9charger\nsave-replace = Remplacer\nsave-delete = Supprimer\nsave-backup-all = T\xe9l\xe9charger tous les fichiers de sauvegarde\n"},"he-IL":{"context_menu.ftl":"context-menu-download-swf = \u05d4\u05d5\u05e8\u05d3\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4swf.\ncontext-menu-copy-debug-info = \u05d4\u05e2\u05ea\u05e7\u05ea \u05e0\u05ea\u05d5\u05e0\u05d9 \u05e0\u05d9\u05e4\u05d5\u05d9 \u05e9\u05d2\u05d9\u05d0\u05d5\u05ea\ncontext-menu-open-save-manager = \u05e4\u05ea\u05d7 \u05d0\u05ea \u05de\u05e0\u05d4\u05dc \u05d4\u05e9\u05de\u05d9\u05e8\u05d5\u05ea\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u05d0\u05d5\u05d3\u05d5\u05ea \u05d4\u05ea\u05d5\u05e1\u05e3 Ruffle ({ $version })\n *[other] \u05d0\u05d5\u05d3\u05d5\u05ea Ruffle ({ $version })\n }\ncontext-menu-hide = \u05d4\u05e1\u05ea\u05e8 \u05ea\u05e4\u05e8\u05d9\u05d8 \u05d6\u05d4\ncontext-menu-exit-fullscreen = \u05d9\u05e6\u05d9\u05d0\u05d4 \u05de\u05de\u05e1\u05da \u05de\u05dc\u05d0\ncontext-menu-enter-fullscreen = \u05de\u05e1\u05da \u05de\u05dc\u05d0\n","messages.ftl":'message-cant-embed =\n Ruffle \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d4\u05e8\u05d9\u05e5 \u05d0\u05ea \u05ea\u05d5\u05db\u05df \u05d4\u05e4\u05dc\u05d0\u05e9 \u05d4\u05de\u05d5\u05d8\u05de\u05e2 \u05d1\u05d3\u05e3 \u05d6\u05d4.\n \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea \u05d4\u05e7\u05d5\u05d1\u05e5 \u05d1\u05dc\u05e9\u05d5\u05e0\u05d9\u05ea \u05e0\u05e4\u05e8\u05d3\u05ea, \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5.\npanic-title = \u05de\u05e9\u05d4\u05d5 \u05d4\u05e9\u05ea\u05d1\u05e9 :(\nmore-info = \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3\nrun-anyway = \u05d4\u05e4\u05e2\u05dc \u05d1\u05db\u05dc \u05d6\u05d0\u05ea\ncontinue = \u05d4\u05de\u05e9\u05da\nreport-bug = \u05d3\u05d5\u05d5\u05d7 \u05e2\u05dc \u05ea\u05e7\u05dc\u05d4\nupdate-ruffle = \u05e2\u05d3\u05db\u05df \u05d0\u05ea Ruffle\nruffle-demo = \u05d4\u05d3\u05d2\u05de\u05d4\nruffle-desktop = \u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d9\u05ea \u05e9\u05d5\u05dc\u05d7\u05df \u05e2\u05d1\u05d5\u05d3\u05d4\nruffle-wiki = \u05e8\u05d0\u05d4 \u05d0\u05ea Ruffle wiki\nview-error-details = \u05e8\u05d0\u05d4 \u05e4\u05e8\u05d8\u05d9 \u05e9\u05d2\u05d9\u05d0\u05d4\nopen-in-new-tab = \u05e4\u05ea\u05d7 \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d7\u05d3\u05e9\u05d4\nclick-to-unmute = \u05dc\u05d7\u05e5 \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05d1\u05d8\u05dc \u05d4\u05e9\u05ea\u05e7\u05d4\nerror-file-protocol =\n \u05e0\u05d3\u05de\u05d4 \u05e9\u05d0\u05ea\u05d4 \u05de\u05e8\u05d9\u05e5 \u05d0\u05ea Ruffle \u05ea\u05d7\u05ea \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc "file:".\n \u05d6\u05d4 \u05dc\u05d0 \u05d9\u05e2\u05d1\u05d5\u05d3 \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d3\u05e4\u05d3\u05e4\u05e0\u05d9\u05dd \u05d7\u05d5\u05e1\u05de\u05d9\u05dd \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e8\u05d1\u05d5\u05ea \u05de\u05dc\u05e2\u05d1\u05d5\u05d3 \u05e2\u05e7\u05d1 \u05e1\u05d9\u05d1\u05d5\u05ea \u05d0\u05d1\u05d8\u05d7\u05d4.\n \u05d1\u05de\u05e7\u05d5\u05dd \u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d0\u05d5\u05ea\u05da \u05dc\u05d0\u05d7\u05e1\u05df \u05d0\u05ea\u05e8 \u05d6\u05d4 \u05ea\u05d7\u05ea \u05e9\u05e8\u05ea \u05de\u05e7\u05d5\u05de\u05d9 \u05d0\u05d5 \u05d4\u05d3\u05d2\u05de\u05d4 \u05d1\u05e8\u05e9\u05ea \u05d0\u05d5 \u05d3\u05e8\u05da \u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d9\u05ea \u05e9\u05d5\u05dc\u05d7\u05df \u05d4\u05e2\u05d1\u05d5\u05d3\u05d4.\nerror-javascript-config =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05ea\u05e7\u05dc\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05e2\u05e7\u05d1 \u05d4\u05d2\u05d3\u05e8\u05ea JavaScript \u05e9\u05d2\u05d5\u05d9\u05d4.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d5 \u05de\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d0\u05d5\u05ea\u05da \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05d9\u05d6\u05d4 \u05e4\u05e8\u05de\u05d8\u05e8 \u05d4\u05d5\u05d0 \u05e9\u05d2\u05d5\u05d9.\n \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e2\u05d9\u05d9\u05df \u05d5\u05dc\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-not-found =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4"wasm." \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05d5\u05d5\u05d3\u05d0 \u05db\u05d9 \u05d4\u05e7\u05d5\u05d1\u05e5 \u05d4\u05d5\u05e2\u05dc\u05d4 \u05db\u05e9\u05d5\u05e8\u05d4.\n \u05d0\u05dd \u05d4\u05d1\u05e2\u05d9\u05d4 \u05de\u05de\u05e9\u05d9\u05db\u05d4, \u05d9\u05d9\u05ea\u05db\u05df \u05d5\u05ea\u05e6\u05d8\u05e8\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05d2\u05d3\u05e8\u05ea "publicPath": \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-mime-type =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05e9\u05e8\u05ea\u05d5 \u05e9\u05dc \u05d0\u05ea\u05e8 \u05d6\u05d4 \u05dc\u05d0 \u05de\u05e9\u05d9\u05d9\u05da \u05e7\u05d1\u05e6\u05d9 ".wasm" \u05e2\u05dd \u05e1\u05d5\u05d2 \u05d4MIME \u05d4\u05e0\u05db\u05d5\u05df.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-swf-fetch =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4\u05e4\u05dc\u05d0\u05e9/swf. .\n \u05d6\u05d4 \u05e0\u05d5\u05d1\u05e2 \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05d5\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e7\u05d9\u05d9\u05dd \u05d9\u05d5\u05ea\u05e8, \u05d0\u05d6 \u05d0\u05d9\u05df \u05dcRuffle \u05de\u05d4 \u05dc\u05d8\u05e2\u05d5\u05df.\n \u05e0\u05e1\u05d4 \u05dc\u05d9\u05e6\u05d5\u05e8 \u05e7\u05e9\u05e8 \u05e2\u05dd \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8 \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-swf-cors =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4\u05e4\u05dc\u05d0\u05e9/swf. .\n \u05d2\u05d9\u05e9\u05d4 \u05dcfetch \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05e0\u05d7\u05e1\u05de\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea CORS.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-cors =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4".wasm" \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05d2\u05d9\u05e9\u05d4 \u05dcfetch \u05db\u05db\u05dc \u05d4\u05e0\u05e8\u05d0\u05d4 \u05e0\u05d7\u05e1\u05de\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea CORS.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-invalid =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05e0\u05d3\u05de\u05d4 \u05db\u05d9 \u05d1\u05d3\u05e3 \u05d6\u05d4 \u05d7\u05e1\u05e8\u05d9\u05dd \u05d0\u05d5 \u05dc\u05d0 \u05e2\u05d5\u05d1\u05d3\u05d9\u05dd \u05db\u05e8\u05d0\u05d5\u05d9 \u05e7\u05d1\u05e6\u05d9\u05dd \u05d0\u05e9\u05e8 \u05de\u05e9\u05de\u05e9\u05d9\u05dd \u05d0\u05ea Ruffle \u05db\u05d3\u05d9 \u05dc\u05e4\u05e2\u05d5\u05dc\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-wasm-download =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05dc\u05e2\u05d9\u05ea\u05d9\u05dd \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5 \u05d9\u05db\u05d5\u05dc\u05d4 \u05dc\u05e4\u05ea\u05d5\u05e8 \u05d0\u05ea \u05e2\u05e6\u05de\u05d4, \u05d0\u05d6 \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e0\u05e1\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d3\u05e3 \u05d6\u05d4.\n \u05d0\u05dd \u05dc\u05d0, \u05d0\u05e0\u05d0 \u05e4\u05e0\u05d4 \u05dc\u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8.\nerror-wasm-disabled-on-edge =\n Ruffle \u05e0\u05db\u05e9\u05dc \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4".wasm" \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05ea\u05e7\u05df \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5, \u05e0\u05e1\u05d4 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d3\u05e4\u05d3\u05e4\u05df \u05e9\u05dc\u05da, \u05dc\u05d7\u05e5 \u05e2\u05dc "\u05d0\u05d1\u05d8\u05d7\u05d4, \u05d7\u05d9\u05e4\u05d5\u05e9 \u05d5\u05e9\u05d9\u05e8\u05d5\u05ea",\n \u05d2\u05dc\u05d5\u05dc \u05de\u05d8\u05d4, \u05d5\u05db\u05d1\u05d4 \u05d0\u05ea "\u05d4\u05d2\u05d1\u05e8 \u05d0\u05ea \u05d4\u05d0\u05d1\u05d8\u05d7\u05d4 \u05e9\u05dc\u05da \u05d1\u05e8\u05e9\u05ea".\n \u05d6\u05d4 \u05d9\u05d0\u05e4\u05e9\u05e8 \u05dc\u05d3\u05e4\u05d3\u05e4\u05df \u05e9\u05dc\u05da \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4".wasm" \u05d4\u05d3\u05e8\u05d5\u05e9.\n \u05d0\u05dd \u05d4\u05d1\u05e2\u05d9\u05d4 \u05de\u05de\u05e9\u05d9\u05db\u05d4, \u05d9\u05d9\u05ea\u05db\u05df \u05d5\u05e2\u05dc\u05d9\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d3\u05e4\u05d3\u05e4\u05df \u05d0\u05d7\u05e8.\nerror-javascript-conflict =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05e0\u05d3\u05de\u05d4 \u05db\u05d9 \u05d3\u05e3 \u05d6\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d5\u05d3 JavaScript \u05d0\u05e9\u05e8 \u05de\u05ea\u05e0\u05d2\u05e9 \u05e2\u05dd Ruffle.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d5 \u05de\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d0\u05d5\u05ea\u05da \u05dc\u05e0\u05e1\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05d0\u05ea \u05d4\u05d3\u05e3 \u05ea\u05d7\u05ea \u05e2\u05de\u05d5\u05d3 \u05e8\u05d9\u05e7.\nerror-javascript-conflict-outdated = \u05d1\u05e0\u05d5\u05e1\u05e3, \u05d0\u05ea\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05e0\u05e1\u05d5\u05ea \u05d5\u05dc\u05d4\u05e2\u05dc\u05d5\u05ea \u05d2\u05e8\u05e1\u05d0\u05d5\u05ea \u05e2\u05d3\u05db\u05e0\u05d9\u05d5\u05ea \u05e9\u05dc Ruffle \u05d0\u05e9\u05e8 \u05e2\u05dc\u05d5\u05dc\u05d9\u05dd \u05dc\u05e2\u05e7\u05d5\u05e3 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5 (\u05d2\u05e8\u05e1\u05d4 \u05d6\u05d5 \u05d4\u05d9\u05e0\u05d4 \u05de\u05d9\u05d5\u05e9\u05e0\u05ea : { $buildDate }).\nerror-csp-conflict =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05ea\u05d5\u05da \u05db\u05d3\u05d9 \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d0\u05ea\u05d7\u05dc.\n \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea \u05d0\u05d1\u05d8\u05d7\u05ea \u05d4\u05ea\u05d5\u05db\u05df \u05e9\u05dc \u05e9\u05e8\u05ea\u05d5 \u05e9\u05dc \u05d0\u05ea\u05e8 \u05d6\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d0\u05e4\u05e9\u05e8\u05ea \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4"wasm." \u05d4\u05d3\u05e8\u05d5\u05e9 \u05dc\u05e4\u05e2\u05d5\u05dc.\n \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e2\u05d9\u05d9\u05df \u05d5\u05d4\u05d5\u05e2\u05e5 \u05d1wiki \u05e9\u05dc Ruffle \u05e2\u05dc \u05de\u05e0\u05ea \u05dc\u05e7\u05d1\u05dc \u05e2\u05d6\u05e8\u05d4.\nerror-unknown =\n Ruffle \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d4 \u05d7\u05de\u05d5\u05e8\u05d4 \u05d1\u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05e6\u05d9\u05d2 \u05d0\u05ea \u05ea\u05d5\u05db\u05df \u05e4\u05dc\u05d0\u05e9 \u05d6\u05d4.\n { $outdated ->\n [true] \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e0\u05d4\u05dc \u05d4\u05d0\u05ea\u05e8, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e2\u05dc\u05d5\u05ea \u05d2\u05e8\u05e1\u05d4 \u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d9\u05d5\u05ea\u05e8 \u05e9\u05dc Ruffle (\u05d2\u05e8\u05e1\u05d4 \u05d6\u05d5 \u05d4\u05d9\u05e0\u05d4 \u05de\u05d9\u05d5\u05e9\u05e0\u05ea: { $buildDate }).\n *[false] \u05d6\u05d4 \u05dc\u05d0 \u05d0\u05de\u05d5\u05e8 \u05dc\u05e7\u05e8\u05d5\u05ea, \u05e0\u05e9\u05de\u05d7 \u05d0\u05dd \u05ea\u05d5\u05db\u05dc \u05dc\u05e9\u05ea\u05e3 \u05ea\u05e7\u05dc\u05d4 \u05d6\u05d5!\n }\n',"save-manager.ftl":"save-delete-prompt = \u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05d1\u05d8\u05d5\u05d7 \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05de\u05d7\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05e9\u05de\u05d9\u05e8\u05d4 \u05d6\u05d4?\nsave-reload-prompt =\n \u05d4\u05d3\u05e8\u05da \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4 { $action ->\n [delete] \u05dc\u05de\u05d7\u05d5\u05e7\n *[replace] \u05dc\u05d4\u05d7\u05dc\u05d9\u05e3\n } \u05d0\u05ea \u05e7\u05d5\u05d1\u05e5 \u05d4\u05e9\u05de\u05d9\u05e8\u05d4 \u05d4\u05d6\u05d4 \u05de\u05d1\u05dc\u05d9 \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d5 \u05dc\u05d4\u05ea\u05e0\u05d2\u05e9 \u05d4\u05d9\u05d0 \u05dc\u05d8\u05e2\u05d5\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05ea\u05d5\u05db\u05df \u05d6\u05d4. \u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05de\u05e9\u05d9\u05da \u05d1\u05db\u05dc \u05d6\u05d0\u05ea?\nsave-download = \u05d4\u05d5\u05e8\u05d3\u05d4\nsave-replace = \u05d4\u05d7\u05dc\u05e4\u05d4\nsave-delete = \u05de\u05d7\u05d9\u05e7\u05d4\nsave-backup-all = \u05d4\u05d5\u05e8\u05d3\u05ea \u05db\u05dc \u05e7\u05d1\u05e6\u05d9 \u05d4\u05e9\u05de\u05d9\u05e8\u05d4\n"},"hu-HU":{"context_menu.ftl":"context-menu-download-swf = .swf f\xe1jl let\xf6lt\xe9se\ncontext-menu-copy-debug-info = Hibakeres\xe9si inform\xe1ci\xf3k m\xe1sol\xe1sa\ncontext-menu-open-save-manager = Ment\xe9skezel\u0151 megnyit\xe1sa\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] A Ruffle kieg\xe9sz\xedt\u0151 ({ $version }) n\xe9vjegye\n *[other] A Ruffle ({ $version }) n\xe9vjegye\n }\ncontext-menu-hide = Ezen men\xfc elrejt\xe9se\ncontext-menu-exit-fullscreen = Kil\xe9p\xe9s a teljes k\xe9perny\u0151b\u0151l\ncontext-menu-enter-fullscreen = V\xe1lt\xe1s teljes k\xe9perny\u0151re\n","messages.ftl":'message-cant-embed =\n A Ruffle nem tudta futtatni az oldalba \xe1gyazott Flash tartalmat.\n A probl\xe9ma kiker\xfcl\xe9s\xe9hez megpr\xf3b\xe1lhatod megnyitni a f\xe1jlt egy k\xfcl\xf6n lapon.\npanic-title = Valami baj t\xf6rt\xe9nt :(\nmore-info = Tov\xe1bbi inform\xe1ci\xf3\nrun-anyway = Futtat\xe1s m\xe9gis\ncontinue = Folytat\xe1s\nreport-bug = Hiba jelent\xe9se\nupdate-ruffle = Ruffle friss\xedt\xe9se\nruffle-demo = Webes dem\xf3\nruffle-desktop = Asztali alkalmaz\xe1s\nruffle-wiki = Ruffle Wiki megnyit\xe1sa\nview-error-details = Hiba r\xe9szletei\nopen-in-new-tab = Megnyit\xe1s \xfaj lapon\nclick-to-unmute = Kattints a n\xe9m\xedt\xe1s felold\xe1s\xe1hoz\nerror-file-protocol =\n \xdagy t\u0171nik, a Ruffle-t a "file:" protokollon futtatod.\n Ez nem m\u0171k\xf6dik, mivel \xedgy a b\xf6ng\xe9sz\u0151k biztons\xe1gi okokb\xf3l sz\xe1mos funkci\xf3 m\u0171k\xf6d\xe9s\xe9t letiltj\xe1k.\n Ehelyett azt aj\xe1nljuk hogy ind\xedts egy helyi kiszolg\xe1l\xf3t, vagy haszn\xe1ld a webes dem\xf3t vagy az asztali alkalmaz\xe1st.\nerror-javascript-config =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt egy helytelen JavaScript-konfigur\xe1ci\xf3 miatt.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, ellen\u0151rizd a hiba r\xe9szleteit, hogy megtudd, melyik param\xe9ter a hib\xe1s.\n A Ruffle wikiben is tal\xe1lhatsz ehhez seg\xedts\xe9get.\nerror-wasm-not-found =\n A Ruffle nem tudta bet\xf6lteni a sz\xfcks\xe9ges ".wasm" \xf6sszetev\u0151t.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck ellen\u0151rizd, hogy a f\xe1jl megfelel\u0151en lett-e felt\xf6ltve.\n Ha a probl\xe9ma tov\xe1bbra is fenn\xe1ll, el\u0151fordulhat, hogy a "publicPath" be\xe1ll\xedt\xe1st kell haszn\xe1lnod: seg\xedts\xe9g\xe9rt keresd fel a Ruffle wikit.\nerror-wasm-mime-type =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n Ez a webszerver a ".wasm" f\xe1jlokat nem a megfelel\u0151 MIME-t\xedpussal szolg\xe1lja ki.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-swf-fetch =\n A Ruffle nem tudta bet\xf6lteni a Flash SWF f\xe1jlt.\n A legval\xf3sz\xedn\u0171bb ok az, hogy a f\xe1jl m\xe1r nem l\xe9tezik, \xedgy a Ruffle sz\xe1m\xe1ra nincs mit bet\xf6lteni.\n Pr\xf3b\xe1ld meg felvenni a kapcsolatot a webhely rendszergazd\xe1j\xe1val seg\xedts\xe9g\xe9rt.\nerror-swf-cors =\n A Ruffle nem tudta bet\xf6lteni a Flash SWF f\xe1jlt.\n A lek\xe9r\xe9shez val\xf3 hozz\xe1f\xe9r\xe9st val\xf3sz\xedn\u0171leg letiltotta a CORS-h\xe1zirend.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-wasm-cors =\n A Ruffle nem tudta bet\xf6lteni a sz\xfcks\xe9ges ".wasm" \xf6sszetev\u0151t.\n A lek\xe9r\xe9shez val\xf3 hozz\xe1f\xe9r\xe9st val\xf3sz\xedn\u0171leg letiltotta a CORS-h\xe1zirend.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-wasm-invalid =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n \xdagy t\u0171nik, hogy ezen az oldalon hi\xe1nyoznak vagy hib\xe1sak a Ruffle futtat\xe1s\xe1hoz sz\xfcks\xe9ges f\xe1jlok.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-wasm-download =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n Ez gyakran mag\xe1t\xf3l megold\xf3dik, ez\xe9rt megpr\xf3b\xe1lhatod \xfajrat\xf6lteni az oldalt.\n Ellenkez\u0151 esetben fordulj a webhely rendszergazd\xe1j\xe1hoz.\nerror-wasm-disabled-on-edge =\n A Ruffle nem tudta bet\xf6lteni a sz\xfcks\xe9ges ".wasm" \xf6sszetev\u0151t.\n A probl\xe9ma megold\xe1s\xe1hoz nyisd meg a b\xf6ng\xe9sz\u0151 be\xe1ll\xedt\xe1sait, kattints az \u201eAdatv\xe9delem, keres\xe9s \xe9s szolg\xe1ltat\xe1sok\u201d elemre, g\xf6rgess le, \xe9s kapcsold ki a \u201eFokozott biztons\xe1g a weben\u201d opci\xf3t.\n Ez lehet\u0151v\xe9 teszi a b\xf6ng\xe9sz\u0151 sz\xe1m\xe1ra, hogy bet\xf6ltse a sz\xfcks\xe9ges ".wasm" f\xe1jlokat.\n Ha a probl\xe9ma tov\xe1bbra is fenn\xe1ll, lehet, hogy m\xe1sik b\xf6ng\xe9sz\u0151t kell haszn\xe1lnod.\nerror-javascript-conflict =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n \xdagy t\u0171nik, ez az oldal olyan JavaScript-k\xf3dot haszn\xe1l, amely \xfctk\xf6zik a Ruffle-lel.\n Ha a kiszolg\xe1l\xf3 rendszergazd\xe1ja vagy, k\xe9rj\xfck, pr\xf3b\xe1ld meg a f\xe1jlt egy \xfcres oldalon bet\xf6lteni.\nerror-javascript-conflict-outdated = Megpr\xf3b\xe1lhatod tov\xe1bb\xe1 felt\xf6lteni a Ruffle egy \xfajabb verzi\xf3j\xe1t is, amely megker\xfclheti a probl\xe9m\xe1t (a jelenlegi elavult: { $buildDate }).\nerror-csp-conflict =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt az inicializ\xe1l\xe1s sor\xe1n.\n A kiszolg\xe1l\xf3 tartalombiztons\xe1gi h\xe1zirendje nem teszi lehet\u0151v\xe9 a sz\xfcks\xe9ges \u201e.wasm\u201d \xf6sszetev\u0151k futtat\xe1s\xe1t.\n Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, keresd fel a Ruffle wikit seg\xedts\xe9g\xe9rt.\nerror-unknown =\n A Ruffle komoly probl\xe9m\xe1ba \xfctk\xf6z\xf6tt, mik\xf6zben megpr\xf3b\xe1lta megjelen\xedteni ezt a Flash-tartalmat.\n { $outdated ->\n [true] Ha a szerver rendszergazd\xe1ja vagy, k\xe9rj\xfck, pr\xf3b\xe1ld meg felt\xf6lteni a Ruffle egy \xfajabb verzi\xf3j\xe1t (a jelenlegi elavult: { $buildDate }).\n *[false] Ennek nem lett volna szabad megt\xf6rt\xe9nnie, ez\xe9rt nagyon h\xe1l\xe1sak lenn\xe9nk, ha jelezn\xe9d a hib\xe1t!\n }\n',"save-manager.ftl":"save-delete-prompt = Biztosan t\xf6r\xf6lni akarod ezt a ment\xe9st?\nsave-reload-prompt =\n Ennek a ment\xe9snek az esetleges konfliktus n\xe9lk\xfcli { $action ->\n [delete] t\xf6rl\xe9s\xe9hez\n *[replace] cser\xe9j\xe9hez\n } \xfajra kell t\xf6lteni a tartalmat. M\xe9gis szeretn\xe9d folytatni?\nsave-download = Let\xf6lt\xe9s\nsave-replace = Csere\nsave-delete = T\xf6rl\xe9s\nsave-backup-all = Az \xf6sszes f\xe1jl let\xf6lt\xe9se\n"},"id-ID":{"context_menu.ftl":"context-menu-download-swf = Unduh .swf\ncontext-menu-copy-debug-info = Salin info debug\ncontext-menu-open-save-manager = Buka Manager Save\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Tentang Ekstensi Ruffle ({ $version })\n *[other] Tentang Ruffle ({ $version })\n }\ncontext-menu-hide = Sembunyikan Menu ini\ncontext-menu-exit-fullscreen = Keluar dari layar penuh\ncontext-menu-enter-fullscreen = Masuk mode layar penuh\n","messages.ftl":'message-cant-embed =\n Ruffle tidak dapat menjalankan Flash yang disematkan di halaman ini.\n Anda dapat mencoba membuka file di tab terpisah, untuk menghindari masalah ini.\npanic-title = Terjadi kesalahan :(\nmore-info = Info lebih lanjut\nrun-anyway = Jalankan\ncontinue = Lanjutkan\nreport-bug = Laporkan Bug\nupdate-ruffle = Perbarui Ruffle\nruffle-demo = Demo Web\nruffle-desktop = Aplikasi Desktop\nruffle-wiki = Kunjungi Wiki Ruffle\nview-error-details = Tunjukan Detail Error\nopen-in-new-tab = Buka di Tab Baru\nclick-to-unmute = Tekan untuk menyalakan suara\nerror-file-protocol =\n Sepertinya anda menjalankan Ruffle di protokol "file:". \n Ini tidak berfungsi karena browser memblokir fitur ini dengan alasan keamanan.\n Sebagai gantinya, kami mengajak anda untuk membuat server lokal, menggunakan demo web atau aplikasi desktop.\nerror-javascript-config =\n Ruffle mengalami masalah besar karena konfigurasi JavaScript yang salah.\n Jika Anda adalah administrator server ini, kami mengajak Anda untuk memeriksa detail kesalahan untuk mengetahui parameter mana yang salah.\n Anda juga dapat membaca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-not-found =\n Ruffle gagal memuat komponen file ".wasm" yang diperlukan.\n Jika Anda adalah administrator server ini, pastikan file telah diunggah dengan benar.\n Jika masalah terus berlanjut, Anda mungkin perlu menggunakan pengaturan "publicPath": silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-mime-type =\n Ruffle mengalami masalah ketika mencoba melakukan inisialisasi.\n Server web ini tidak melayani file ".wasm" dengan tipe MIME yang benar.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-swf-fetch =\n Ruffle gagal memuat file SWF Flash.\n Kemungkinan file tersebut sudah tidak ada, sehingga tidak dapat dimuat oleh Ruffle.\n Coba hubungi administrator situs web ini untuk mendapatkan bantuan.\nerror-swf-cors =\n Ruffle gagal memuat file SWF Flash.\n Akses untuk memuat kemungkinan telah diblokir oleh kebijakan CORS.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-cors =\n Ruffle gagal memuat komponen file ".wasm" yang diperlukan.\n Akses untuk mengambil kemungkinan telah diblokir oleh kebijakan CORS.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-invalid =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Sepertinya halaman ini memiliki file yang hilang atau tidak valid untuk menjalankan Ruffle.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-wasm-download =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Hal ini sering kali dapat teratasi dengan sendirinya, sehingga Anda dapat mencoba memuat ulang halaman.\n Jika tidak, silakan hubungi administrator situs web ini.\nerror-wasm-disabled-on-edge =\n Ruffle gagal memuat komponen file ".wasm" yang diperlukan.\n Untuk mengatasinya, coba buka pengaturan peramban Anda, klik "Privasi, pencarian, dan layanan", turun ke bawah, dan matikan "Tingkatkan keamanan Anda di web".\n Ini akan memungkinkan browser Anda memuat file ".wasm" yang diperlukan.\n Jika masalah berlanjut, Anda mungkin harus menggunakan browser yang berbeda.\nerror-javascript-conflict =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Sepertinya situs web ini menggunakan kode JavaScript yang bertentangan dengan Ruffle.\n Jika Anda adalah administrator server ini, kami mengajak Anda untuk mencoba memuat file pada halaman kosong.\nerror-javascript-conflict-outdated = Anda juga dapat mencoba mengunggah versi Ruffle yang lebih baru yang mungkin dapat mengatasi masalah ini (versi saat ini sudah kedaluwarsa: { $buildDate }).\nerror-csp-conflict =\n Ruffle mengalami masalah besar ketika mencoba melakukan inisialisasi.\n Kebijakan Keamanan Konten server web ini tidak mengizinkan komponen ".wasm" yang diperlukan untuk dijalankan.\n Jika Anda adalah administrator server ini, silakan baca wiki Ruffle untuk mendapatkan bantuan.\nerror-unknown =\n Ruffle telah mengalami masalah besar saat menampilkan konten Flash ini.\n { $outdated ->\n [true] Jika Anda administrator server ini, cobalah untuk mengganti versi Ruffle yang lebih baru (versi saat ini sudah kedaluwarsa: { $buildDate }).\n *[false] Hal ini seharusnya tidak terjadi, jadi kami sangat menghargai jika Anda dapat melaporkan bug ini!\n }\n',"save-manager.ftl":"save-delete-prompt = Anda yakin ingin menghapus berkas ini?\nsave-reload-prompt =\n Satu-satunya cara untuk { $action ->\n [delete] menghapus\n *[replace] mengganti\n } berkas penyimpanan ini tanpa potensi konflik adalah dengan memuat ulang konten ini. Apakah Anda ingin melanjutkannya?\nsave-download = Unduh\nsave-replace = Ganti\nsave-delete = Hapus\nsave-backup-all = Unduh semua berkas penyimpanan\n"},"it-IT":{"context_menu.ftl":"context-menu-download-swf = Scarica .swf\ncontext-menu-copy-debug-info = Copia informazioni di debug\ncontext-menu-open-save-manager = Apri Gestione salvataggi\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Informazioni su Ruffle Extension ({ $version })\n *[other] Informazioni su Ruffle ({ $version })\n }\ncontext-menu-hide = Nascondi questo menu\ncontext-menu-exit-fullscreen = Esci dallo schermo intero\ncontext-menu-enter-fullscreen = Entra a schermo intero\n","messages.ftl":"message-cant-embed =\n Ruffle non \xe8 stato in grado di eseguire il Flash incorporato in questa pagina.\n Puoi provare ad aprire il file in una scheda separata, per evitare questo problema.\npanic-title = Qualcosa \xe8 andato storto :(\nmore-info = Maggiori informazioni\nrun-anyway = Esegui comunque\ncontinue = Continua\nreport-bug = Segnala Un Bug\nupdate-ruffle = Aggiorna Ruffle\nruffle-demo = Demo Web\nruffle-desktop = Applicazione Desktop\nruffle-wiki = Visualizza Ruffle Wiki\nview-error-details = Visualizza Dettagli Errore\nopen-in-new-tab = Apri in una nuova scheda\nclick-to-unmute = Clicca per riattivare l'audio\nerror-file-protocol =\n Sembra che tu stia eseguendo Ruffle sul protocollo \"file:\".\n Questo non funziona come browser blocca molte funzionalit\xe0 di lavoro per motivi di sicurezza.\n Invece, ti invitiamo a configurare un server locale o a utilizzare la demo web o l'applicazione desktop.\nerror-javascript-config =\n Ruffle ha incontrato un problema importante a causa di una configurazione JavaScript non corretta.\n Se sei l'amministratore del server, ti invitiamo a controllare i dettagli dell'errore per scoprire quale parametro \xe8 in errore.\n Puoi anche consultare il wiki Ruffle per aiuto.\nerror-wasm-not-found =\n Ruffle non \xe8 riuscito a caricare il componente di file \".wasm\".\n Se sei l'amministratore del server, assicurati che il file sia stato caricato correttamente.\n Se il problema persiste, potrebbe essere necessario utilizzare l'impostazione \"publicPath\": si prega di consultare il wiki Ruffle per aiuto.\nerror-wasm-mime-type =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzazione.\n Questo server web non serve \". asm\" file con il tipo MIME corretto.\n Se sei l'amministratore del server, consulta la wiki Ruffle per aiuto.\nerror-swf-fetch =\n Ruffle non \xe8 riuscito a caricare il file Flash SWF.\n La ragione pi\xf9 probabile \xe8 che il file non esiste pi\xf9, quindi non c'\xe8 nulla che Ruffle possa caricare.\n Prova a contattare l'amministratore del sito web per aiuto.\nerror-swf-cors =\n Ruffle non \xe8 riuscito a caricare il file SWF Flash.\n L'accesso al recupero probabilmente \xe8 stato bloccato dalla politica CORS.\n Se sei l'amministratore del server, consulta la wiki Ruffle per ricevere aiuto.\nerror-wasm-cors =\n Ruffle non \xe8 riuscito a caricare il componente di file \".wasm\".\n L'accesso al recupero probabilmente \xe8 stato bloccato dalla politica CORS.\n Se sei l'amministratore del server, consulta la wiki Ruffle per ricevere aiuto.\nerror-wasm-invalid =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzazione.\n Sembra che questa pagina abbia file mancanti o non validi per l'esecuzione di Ruffle.\n Se sei l'amministratore del server, consulta la wiki Ruffle per ricevere aiuto.\nerror-wasm-download =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzazione.\n Questo pu\xf2 spesso risolversi da solo, quindi puoi provare a ricaricare la pagina.\n Altrimenti, contatta l'amministratore del sito.\nerror-wasm-disabled-on-edge =\n Ruffle non ha caricato il componente di file \".wasm\" richiesto.\n Per risolvere il problema, prova ad aprire le impostazioni del tuo browser, facendo clic su \"Privacy, search, and services\", scorrendo verso il basso e disattivando \"Migliora la tua sicurezza sul web\".\n Questo permetter\xe0 al tuo browser di caricare i file \".wasm\" richiesti.\n Se il problema persiste, potresti dover usare un browser diverso.\nerror-javascript-conflict =\n Ruffle ha riscontrato un problema importante durante il tentativo di inizializzazione.\n Sembra che questa pagina utilizzi il codice JavaScript che \xe8 in conflitto con Ruffle.\n Se sei l'amministratore del server, ti invitiamo a provare a caricare il file su una pagina vuota.\nerror-javascript-conflict-outdated = Puoi anche provare a caricare una versione pi\xf9 recente di Ruffle che potrebbe aggirare il problema (l'attuale build \xe8 obsoleta: { $buildDate }).\nerror-csp-conflict =\n Ruffle ha incontrato un problema importante durante il tentativo di inizializzare.\n La Politica di Sicurezza dei Contenuti di questo server web non consente l'impostazione richiesta\". asm\" componente da eseguire.\n Se sei l'amministratore del server, consulta la Ruffle wiki per aiuto.\nerror-unknown =\n Ruffle ha incontrato un problema importante durante il tentativo di visualizzare questo contenuto Flash.\n { $outdated ->\n [true] Se sei l'amministratore del server, prova a caricare una versione pi\xf9 recente di Ruffle (la versione attuale \xe8 obsoleta: { $buildDate }).\n *[false] Questo non dovrebbe accadere, quindi ci piacerebbe molto se si potesse inviare un bug!\n }\n","save-manager.ftl":"save-delete-prompt = Sei sicuro di voler eliminare questo file di salvataggio?\nsave-reload-prompt =\n L'unico modo per { $action ->\n [delete] delete\n *[replace] replace\n } questo salvataggio file senza potenziali conflitti \xe8 quello di ricaricare questo contenuto. Volete continuare comunque?\nsave-download = Scarica\nsave-replace = Sostituisci\nsave-delete = Elimina\nsave-backup-all = Scarica tutti i file di salvataggio\n"},"ja-JP":{"context_menu.ftl":"context-menu-download-swf = .swf\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\ncontext-menu-copy-debug-info = \u30c7\u30d0\u30c3\u30b0\u60c5\u5831\u3092\u30b3\u30d4\u30fc\ncontext-menu-open-save-manager = \u30bb\u30fc\u30d6\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u3092\u958b\u304f\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Ruffle\u62e1\u5f35\u6a5f\u80fd\u306b\u3064\u3044\u3066 ({ $version })\n *[other] Ruffle\u306b\u3064\u3044\u3066 ({ $version })\n }\ncontext-menu-hide = \u30e1\u30cb\u30e5\u30fc\u3092\u96a0\u3059\ncontext-menu-exit-fullscreen = \u30d5\u30eb\u30b9\u30af\u30ea\u30fc\u30f3\u3092\u7d42\u4e86\ncontext-menu-enter-fullscreen = \u30d5\u30eb\u30b9\u30af\u30ea\u30fc\u30f3\u306b\u3059\u308b\n","messages.ftl":'message-cant-embed =\n Ruffle\u306f\u3053\u306e\u30da\u30fc\u30b8\u306b\u57cb\u3081\u8fbc\u307e\u308c\u305f Flash \u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\n \u5225\u306e\u30bf\u30d6\u3067\u30d5\u30a1\u30a4\u30eb\u3092\u958b\u304f\u3053\u3068\u3067\u3001\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3067\u304d\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\npanic-title = \u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f :(\nmore-info = \u8a73\u7d30\u60c5\u5831\nrun-anyway = \u3068\u306b\u304b\u304f\u5b9f\u884c\u3059\u308b\ncontinue = \u7d9a\u884c\nreport-bug = \u30d0\u30b0\u3092\u5831\u544a\nupdate-ruffle = Ruffle\u3092\u66f4\u65b0\nruffle-demo = Web\u30c7\u30e2\nruffle-desktop = \u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\nruffle-wiki = Ruffle Wiki\u3092\u898b\u308b\nview-error-details = \u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u3092\u8868\u793a\nopen-in-new-tab = \u65b0\u3057\u3044\u30bf\u30d6\u3067\u958b\u304f\nclick-to-unmute = \u30af\u30ea\u30c3\u30af\u3067\u30df\u30e5\u30fc\u30c8\u3092\u89e3\u9664\nerror-file-protocol =\n Ruffle\u3092"file:"\u30d7\u30ed\u30c8\u30b3\u30eb\u3067\u4f7f\u7528\u3057\u3066\u3044\u308b\u3088\u3046\u3067\u3059\u3002\n \u30d6\u30e9\u30a6\u30b6\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u4e0a\u306e\u7406\u7531\u304b\u3089\u6b86\u3069\u306e\u6a5f\u80fd\u3092\u5236\u9650\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002\n \u30ed\u30fc\u30ab\u30eb\u30b5\u30fc\u30d0\u30fc\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u304b\u3001\u30a6\u30a7\u30d6\u30c7\u30e2\u307e\u305f\u306f\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u3092\u3054\u5229\u7528\u304f\u3060\u3055\u3044\u3002\nerror-javascript-config =\n JavaScript\u306e\u8a2d\u5b9a\u304c\u6b63\u3057\u304f\u306a\u3044\u305f\u3081\u3001Ruffle\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u30b5\u30fc\u30d0\u30fc\u7ba1\u7406\u8005\u306e\u65b9\u306f\u3001\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u304b\u3089\u3001\u3069\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u306b\u554f\u984c\u304c\u3042\u308b\u306e\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n Ruffle\u306ewiki\u3092\u53c2\u7167\u3059\u308b\u3053\u3068\u3067\u3001\u89e3\u6c7a\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\nerror-wasm-not-found =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306eWeb\u30b5\u30fc\u30d0\u30fc\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30dd\u30ea\u30b7\u30fc\u304c\u3001\u5b9f\u884c\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u5b9f\u884c\u3092\u8a31\u53ef\u3057\u3066\u3044\u307e\u305b\u3093\u3002\u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306e\u5834\u5408\u306f\u3001\u30d5\u30a1\u30a4\u30eb\u304c\u6b63\u3057\u304f\u30a2\u30c3\u30d7\u30ed\u30fc\u30c9\u3055\u308c\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3092\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3053\u306e\u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u300cpublicPath\u300d\u306e\u8a2d\u5b9a\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-mime-type =\n Ruffle\u306e\u521d\u671f\u5316\u306b\u5931\u6557\u3059\u308b\u5927\u304d\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306eWeb\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u3044MIME\u30bf\u30a4\u30d7\u306e\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u3092\u63d0\u4f9b\u3057\u3066\u3044\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-swf-fetch =\n Ruffle\u304cFlash SWF\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n \u6700\u3082\u8003\u3048\u3089\u308c\u308b\u539f\u56e0\u306f\u3001SWF\u30d5\u30a1\u30a4\u30eb\u304c\u65e2\u306b\u5b58\u5728\u3057\u306a\u3044\u4e8b\u3067Ruffle\u304c\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3059\u308b\u3068\u3044\u3046\u554f\u984c\u3067\u3059\u3002\n Web\u30b5\u30a4\u30c8\u306e\u7ba1\u7406\u8005\u306b\u304a\u554f\u3044\u5408\u308f\u305b\u304f\u3060\u3055\u3044\u3002\nerror-swf-cors =\n Ruffle\u306fSWF\u30d5\u30a1\u30a4\u30eb\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n CORS\u30dd\u30ea\u30b7\u30fc\u306e\u8a2d\u5b9a\u306b\u3088\u308a\u3001fetch\u3078\u306e\u30a2\u30af\u30bb\u30b9\u304c\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\n \u30b5\u30fc\u30d0\u30fc\u7ba1\u7406\u8005\u306e\u65b9\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-cors =\n Ruffle\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n CORS\u30dd\u30ea\u30b7\u30fc\u306b\u3088\u3063\u3066fetch\u3078\u306e\u30a2\u30af\u30bb\u30b9\u304c\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle wiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-invalid =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u30da\u30fc\u30b8\u306b\u306fRuffle\u3092\u5b9f\u884c\u3059\u308b\u305f\u3081\u306e\u30d5\u30a1\u30a4\u30eb\u304c\u5b58\u5728\u3057\u306a\u3044\u304b\u3001\u7121\u52b9\u306a\u30d5\u30a1\u30a4\u30eb\u304c\u3042\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-wasm-download =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u554f\u984c\u306f\u30da\u30fc\u30b8\u3092\u518d\u8aad\u307f\u8fbc\u307f\u3059\u308b\u4e8b\u3067\u5927\u62b5\u306f\u89e3\u6c7a\u3059\u308b\u306f\u305a\u306a\u306e\u3067\u884c\u306a\u3063\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n \u3082\u3057\u3082\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001Web\u30b5\u30a4\u30c8\u306e\u7ba1\u7406\u8005\u306b\u304a\u554f\u3044\u5408\u308f\u305b\u304f\u3060\u3055\u3044\u3002\nerror-wasm-disabled-on-edge =\n Ruffle\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u30d6\u30e9\u30a6\u30b6\u30fc\u306e\u8a2d\u5b9a\u3092\u958b\u304d\u3001\u300c\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3001\u691c\u7d22\u3001\u30b5\u30fc\u30d3\u30b9\u300d\u3092\u30af\u30ea\u30c3\u30af\u3057\u3001\u4e0b\u306b\u30b9\u30af\u30ed\u30fc\u30eb\u3067\u300cWeb\u4e0a\u306e\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5f37\u5316\u3059\u308b\u300d\u3092\u30aa\u30d5\u306b\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\n \u3053\u308c\u3067\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30d5\u30a1\u30a4\u30eb\u304c\u8aad\u307f\u8fbc\u307e\u308c\u308b\u3088\u3046\u306b\u306a\u308a\u307e\u3059\u3002\n \u305d\u308c\u3067\u3082\u554f\u984c\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u3001\u5225\u306e\u30d6\u30e9\u30a6\u30b6\u30fc\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\nerror-javascript-conflict =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306e\u30da\u30fc\u30b8\u3067\u306fRuffle\u3068\u7af6\u5408\u3059\u308bJavaScript\u30b3\u30fc\u30c9\u304c\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001\u7a7a\u767d\u306e\u30da\u30fc\u30b8\u3067\u30d5\u30a1\u30a4\u30eb\u3092\u8aad\u307f\u8fbc\u307f\u3057\u76f4\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002\nerror-csp-conflict =\n Ruffle\u306e\u521d\u671f\u5316\u6642\u306b\u91cd\u5927\u306a\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n \u3053\u306eWeb\u30b5\u30fc\u30d0\u30fc\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30dd\u30ea\u30b7\u30fc\u304c\u5b9f\u884c\u306b\u5fc5\u8981\u3068\u306a\u308b\u300c.wasm\u300d\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306e\u5b9f\u884c\u3092\u8a31\u53ef\u3057\u3066\u3044\u307e\u305b\u3093\u3002\n \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005\u306f\u3001Ruffle\u306ewiki\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\nerror-unknown =\n Flash\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u8868\u793a\u3059\u308b\u969b\u306bRuffle\u3067\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\n { $outdated ->\n [true] \u73fe\u5728\u4f7f\u7528\u3057\u3066\u3044\u308b\u30d3\u30eb\u30c9\u306f\u6700\u65b0\u3067\u306f\u306a\u3044\u305f\u3081\u3001\u30b5\u30fc\u30d0\u30fc\u7ba1\u7406\u8005\u306e\u65b9\u306f\u3001\u6700\u65b0\u7248\u306eRuffle\u306b\u66f4\u65b0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044(\u73fe\u5728\u5229\u7528\u4e2d\u306e\u30d3\u30eb\u30c9: { $buildDate })\u3002\n *[false] \u60f3\u5b9a\u5916\u306e\u554f\u984c\u306a\u306e\u3067\u3001\u30d0\u30b0\u3068\u3057\u3066\u5831\u544a\u3057\u3066\u3044\u305f\u3060\u3051\u308b\u3068\u5b09\u3057\u3044\u3067\u3059!\n }\n',"save-manager.ftl":"save-delete-prompt = \u3053\u306e\u30bb\u30fc\u30d6\u30d5\u30a1\u30a4\u30eb\u3092\u524a\u9664\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b?\nsave-reload-prompt =\n \u30bb\u30fc\u30d6\u30d5\u30a1\u30a4\u30eb\u3092\u7af6\u5408\u306e\u53ef\u80fd\u6027\u306a\u304f { $action ->\n [delete] \u524a\u9664\u3059\u308b\n *[replace] \u7f6e\u304d\u63db\u3048\u308b\n } \u305f\u3081\u306b\u3001\u3053\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u518d\u8aad\u307f\u8fbc\u307f\u3059\u308b\u3053\u3068\u3092\u63a8\u5968\u3057\u307e\u3059\u3002\u7d9a\u884c\u3057\u307e\u3059\u304b\uff1f\nsave-download = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\nsave-replace = \u7f6e\u304d\u63db\u3048\nsave-delete = \u524a\u9664\nsave-backup-all = \u5168\u3066\u306e\u30bb\u30fc\u30d6\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\n"},"ko-KR":{"context_menu.ftl":"context-menu-download-swf = .swf \ub2e4\uc6b4\ub85c\ub4dc\ncontext-menu-copy-debug-info = \ub514\ubc84\uadf8 \uc815\ubcf4 \ubcf5\uc0ac\ncontext-menu-open-save-manager = \uc800\uc7a5 \uad00\ub9ac\uc790 \uc5f4\uae30\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Ruffle \ud655\uc7a5 \ud504\ub85c\uadf8\ub7a8 \uc815\ubcf4 ({ $version })\n *[other] Ruffle \uc815\ubcf4 ({ $version })\n }\ncontext-menu-hide = \uc774 \uba54\ub274 \uc228\uae30\uae30\ncontext-menu-exit-fullscreen = \uc804\uccb4\ud654\uba74 \ub098\uac00\uae30\ncontext-menu-enter-fullscreen = \uc804\uccb4\ud654\uba74\uc73c\ub85c \uc5f4\uae30\n","messages.ftl":'message-cant-embed = Ruffle\uc774 \uc774 \ud398\uc774\uc9c0\uc5d0 \ud3ec\ud568\ub41c \ud50c\ub798\uc2dc\ub97c \uc2e4\ud589\ud560 \uc218 \uc5c6\uc5c8\uc2b5\ub2c8\ub2e4. \ubcc4\ub3c4\uc758 \ud0ed\uc5d0\uc11c \ud30c\uc77c\uc744 \uc5f4\uc5b4\ubd04\uc73c\ub85c\uc11c \uc774 \ubb38\uc81c\ub97c \ud574\uacb0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\npanic-title = \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4 :(\nmore-info = \ucd94\uac00 \uc815\ubcf4\nrun-anyway = \uadf8\ub798\ub3c4 \uc2e4\ud589\ud558\uae30\ncontinue = \uacc4\uc18d\ud558\uae30\nreport-bug = \ubc84\uadf8 \uc81c\ubcf4\nupdate-ruffle = Ruffle \uc5c5\ub370\uc774\ud2b8\nruffle-demo = \uc6f9 \ub370\ubaa8\nruffle-desktop = \ub370\uc2a4\ud06c\ud1b1 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\nruffle-wiki = Ruffle \uc704\ud0a4 \ubcf4\uae30\nview-error-details = \uc624\ub958 \uc138\ubd80 \uc815\ubcf4 \ubcf4\uae30\nopen-in-new-tab = \uc0c8 \ud0ed\uc5d0\uc11c \uc5f4\uae30\nclick-to-unmute = \ud074\ub9ad\ud558\uc5ec \uc74c\uc18c\uac70 \ud574\uc81c\nerror-file-protocol =\n Ruffle\uc744 "file:" \ud504\ub85c\ud1a0\ucf5c\uc5d0\uc11c \uc2e4\ud589\ud558\uace0 \uc788\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4.\n \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c\ub294 \uc774 \ud504\ub85c\ud1a0\ucf5c\uc744 \ubcf4\uc548\uc0c1\uc758 \uc774\uc720\ub85c \ub9ce\uc740 \uae30\ub2a5\uc744 \uc791\ub3d9\ud558\uc9c0 \uc54a\uac8c \ucc28\ub2e8\ud558\ubbc0\ub85c \uc774 \ubc29\ubc95\uc740 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n \ub300\uc2e0, \ub85c\uceec \uc11c\ubc84\ub97c \uc9c1\uc811 \uc5f4\uc5b4\uc11c \uc124\uc815\ud558\uac70\ub098 \uc6f9 \ub370\ubaa8 \ub610\ub294 \ub370\uc2a4\ud06c\ud1b1 \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc744 \uc0ac\uc6a9\ud558\uc2dc\uae30 \ubc14\ub78d\ub2c8\ub2e4.\nerror-javascript-config =\n \uc798\ubabb\ub41c \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \uc124\uc815\uc73c\ub85c \uc778\ud574 Ruffle\uc5d0\uc11c \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\uc778 \uacbd\uc6b0, \uc624\ub958 \uc138\ubd80\uc0ac\ud56d\uc744 \ud655\uc778\ud558\uc5ec \uc5b4\ub5a4 \ub9e4\uac1c\ubcc0\uc218\uac00 \uc798\ubabb\ub418\uc5c8\ub294\uc9c0 \uc54c\uc544\ubcf4\uc138\uc694.\n \ub610\ub294 Ruffle \uc704\ud0a4\ub97c \ud1b5\ud574 \ub3c4\uc6c0\uc744 \ubc1b\uc544 \ubcfc \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-not-found =\n Ruffle\uc774 ".wasm" \ud544\uc218 \ud30c\uc77c \uad6c\uc131\uc694\uc18c\ub97c \ub85c\ub4dc\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 \ud30c\uc77c\uc774 \uc62c\ubc14\ub974\uac8c \uc5c5\ub85c\ub4dc\ub418\uc5c8\ub294\uc9c0 \ud655\uc778\ud558\uc138\uc694.\n \ubb38\uc81c\uac00 \uc9c0\uc18d\ub41c\ub2e4\uba74 "publicPath" \uc635\uc158\uc744 \uc0ac\uc6a9\ud574\uc57c \ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4: Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc73c\uc138\uc694.\nerror-wasm-mime-type =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \uc6f9 \uc11c\ubc84\ub294 \uc62c\ubc14\ub978 MIME \uc720\ud615\uc758 ".wasm" \ud30c\uc77c\uc744 \uc81c\uacf5\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ud1b5\ud574 \ub3c4\uc6c0\uc744 \ubc1b\uc73c\uc138\uc694.\nerror-swf-fetch =\n Ruffle\uc774 \ud50c\ub798\uc2dc SWF \ud30c\uc77c\uc744 \ub85c\ub4dc\ud558\ub294 \ub370 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4.\n \uc774\ub294 \uc8fc\ub85c \ud30c\uc77c\uc774 \ub354 \uc774\uc0c1 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc544 Ruffle\uc774 \ub85c\ub4dc\ud560 \uc218 \uc788\ub294 \uac83\uc774 \uc5c6\uc744 \uac00\ub2a5\uc131\uc774 \ub192\uc2b5\ub2c8\ub2e4.\n \uc6f9\uc0ac\uc774\ud2b8 \uad00\ub9ac\uc790\uc5d0\uac8c \ubb38\uc758\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcf4\uc138\uc694.\nerror-swf-cors =\n Ruffle\uc774 \ud50c\ub798\uc2dc SWF \ud30c\uc77c\uc744 \ub85c\ub4dc\ud558\ub294 \ub370 \uc2e4\ud328\ud558\uc600\uc2b5\ub2c8\ub2e4.\n CORS \uc815\ucc45\uc5d0 \uc758\ud574 \ub370\uc774\ud130 \uac00\uc838\uc624\uae30\uc5d0 \ub300\ud55c \uc561\uc138\uc2a4\uac00 \ucc28\ub2e8\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-cors =\n Ruffle\uc774 ".wasm" \ud544\uc218 \ud30c\uc77c \uad6c\uc131\uc694\uc18c\ub97c \ub85c\ub4dc\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\n CORS \uc815\ucc45\uc5d0 \uc758\ud574 \ub370\uc774\ud130 \uac00\uc838\uc624\uae30\uc5d0 \ub300\ud55c \uc561\uc138\uc2a4\uac00 \ucc28\ub2e8\ub418\uc5c8\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-invalid =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \ud398\uc774\uc9c0\uc5d0 Ruffle\uc744 \uc2e4\ud589\ud558\uae30 \uc704\ud55c \ud30c\uc77c\uc774 \ub204\ub77d\ub418\uc5c8\uac70\ub098 \uc798\ubabb\ub41c \uac83 \uac19\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-wasm-download =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \ubb38\uc81c\ub294 \ub54c\ub54c\ub85c \ubc14\ub85c \ud574\uacb0\ub420 \uc218 \uc788\uc73c\ubbc0\ub85c \ud398\uc774\uc9c0\ub97c \uc0c8\ub85c\uace0\uce68\ud558\uc5ec \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694.\n \uadf8\ub798\ub3c4 \ubb38\uc81c\uac00 \uc9c0\uc18d\ub41c\ub2e4\uba74, \uc6f9\uc0ac\uc774\ud2b8 \uad00\ub9ac\uc790\uc5d0\uac8c \ubb38\uc758\ud574\uc8fc\uc138\uc694.\nerror-wasm-disabled-on-edge =\n Ruffle\uc774 ".wasm" \ud544\uc218 \ud30c\uc77c \uad6c\uc131\uc694\uc18c\ub97c \ub85c\ub4dc\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.\n \uc774\ub97c \ud574\uacb0\ud558\ub824\uba74 \ube0c\ub77c\uc6b0\uc800 \uc124\uc815\uc5d0\uc11c "\uac1c\uc778 \uc815\ubcf4, \uac80\uc0c9 \ubc0f \uc11c\ube44\uc2a4"\ub97c \ud074\ub9ad\ud55c \ud6c4, \ud558\ub2e8\uc73c\ub85c \uc2a4\ud06c\ub864\ud558\uc5ec "\uc6f9\uc5d0\uc11c \ubcf4\uc548 \uac15\ud654" \uae30\ub2a5\uc744 \uaebc\uc57c \ud569\ub2c8\ub2e4.\n \uc774\ub294 \ud544\uc694\ud55c ".wasm" \ud30c\uc77c\uc744 \ube0c\ub77c\uc6b0\uc800\uc5d0\uc11c \ub85c\ub4dc\ud560 \uc218 \uc788\ub3c4\ub85d \ud5c8\uc6a9\ud569\ub2c8\ub2e4.\n \uc774 \ubb38\uc81c\uac00 \uc9c0\uc18d\ub420 \uacbd\uc6b0 \ub2e4\ub978 \ube0c\ub77c\uc6b0\uc800\ub97c \uc0ac\uc6a9\ud574\uc57c \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-javascript-conflict =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \ud398\uc774\uc9c0\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \uc790\ubc14\uc2a4\ud06c\ub9bd\ud2b8 \ucf54\ub4dc\uac00 Ruffle\uacfc \ucda9\ub3cc\ud558\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 \ube48 \ud398\uc774\uc9c0\uc5d0\uc11c \ud30c\uc77c\uc744 \ub85c\ub4dc\ud574\ubcf4\uc138\uc694.\nerror-javascript-conflict-outdated = \ub610\ud55c Ruffle\uc758 \ucd5c\uc2e0 \ubc84\uc804\uc744 \uc5c5\ub85c\ub4dc\ud558\ub294 \uac83\uc744 \uc2dc\ub3c4\ud558\uc5ec \ubb38\uc81c\ub97c \uc6b0\ud68c\ud574\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. (\ud604\uc7ac \ube4c\ub4dc\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4: { $buildDate }).\nerror-csp-conflict =\n Ruffle\uc774 \ucd08\uae30\ud654\ub97c \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n \uc774 \uc6f9 \uc11c\ubc84\uc758 CSP(Content Security Policy) \uc815\ucc45\uc774 ".wasm" \ud544\uc218 \uad6c\uc131\uc694\uc18c\ub97c \uc2e4\ud589\ud558\ub294 \uac83\uc744 \ud5c8\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.\n \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74 Ruffle \uc704\ud0a4\ub97c \ucc38\uc870\ud558\uc5ec \ub3c4\uc6c0\uc744 \ubc1b\uc544\ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.\nerror-unknown =\n Ruffle\uc774 \ud50c\ub798\uc2dc \ucf58\ud150\uce20\ub97c \ud45c\uc2dc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\ub294 \ub3d9\uc548 \uc911\ub300\ud55c \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.\n { $outdated ->\n [true] \ub9cc\uc57d \ub2f9\uc2e0\uc774 \uc11c\ubc84 \uad00\ub9ac\uc790\ub77c\uba74, Ruffle\uc758 \ucd5c\uc2e0 \ubc84\uc804\uc744 \uc5c5\ub85c\ub4dc\ud558\uc5ec \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694. (\ud604\uc7ac \ube4c\ub4dc\uac00 \uc624\ub798\ub418\uc5c8\uc2b5\ub2c8\ub2e4: { $buildDate }).\n *[false] \uc774\ub7f0 \ud604\uc0c1\uc774 \ubc1c\uc0dd\ud574\uc11c\ub294 \uc548\ub418\ubbc0\ub85c, \ubc84\uadf8\ub97c \uc81c\ubcf4\ud574\uc8fc\uc2e0\ub2e4\uba74 \uac10\uc0ac\ud558\uaca0\uc2b5\ub2c8\ub2e4!\n }\n',"save-manager.ftl":"save-delete-prompt = \uc815\ub9d0\ub85c \uc774 \uc138\uc774\ube0c \ud30c\uc77c\uc744 \uc0ad\uc81c\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\nsave-reload-prompt =\n \b\uc774 \ud30c\uc77c\uc744 \uc7a0\uc7ac\uc801\uc778 \ucda9\ub3cc \uc5c6\uc774 { $action ->\n [delete] \uc0ad\uc81c\n *[replace] \uad50\uccb4\n }\ud558\ub824\uba74 \ucf58\ud150\uce20\ub97c \ub2e4\uc2dc \ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub798\ub3c4 \uacc4\uc18d\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\nsave-download = \ub2e4\uc6b4\ub85c\ub4dc\nsave-replace = \uad50\uccb4\nsave-delete = \uc0ad\uc81c\nsave-backup-all = \ubaa8\ub4e0 \uc800\uc7a5 \ud30c\uc77c \ub2e4\uc6b4\ub85c\ub4dc\n"},"nl-NL":{"context_menu.ftl":"context-menu-download-swf = .swf downloaden\ncontext-menu-copy-debug-info = Kopieer debuginformatie\ncontext-menu-open-save-manager = Open opgeslagen-data-manager\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Over Ruffle Uitbreiding ({ $version })\n *[other] Over Ruffle ({ $version })\n }\ncontext-menu-hide = Verberg dit menu\ncontext-menu-exit-fullscreen = Verlaat volledig scherm\ncontext-menu-enter-fullscreen = Naar volledig scherm\n","messages.ftl":'message-cant-embed =\n Ruffle kon de Flash-inhoud op de pagina niet draaien.\n Je kan proberen het bestand in een apart tabblad te openen, om hier omheen te werken.\npanic-title = Er ging iets mis :(\nmore-info = Meer informatie\nrun-anyway = Toch starten\ncontinue = Doorgaan\nreport-bug = Bug rapporteren\nupdate-ruffle = Ruffle updaten\nruffle-demo = Web Demo\nruffle-desktop = Desktopapplicatie\nruffle-wiki = Bekijk de Ruffle Wiki\nview-error-details = Foutdetails tonen\nopen-in-new-tab = Openen in een nieuw tabblad\nclick-to-unmute = Klik om te ontdempen\nerror-file-protocol =\n Het lijkt erop dat je Ruffle gebruikt met het "file" protocol.\n De meeste browsers blokkeren dit om veiligheidsredenen, waardoor het niet werkt.\n In plaats hiervan raden we aan om een lokale server te draaien, de web demo te gebruiken, of de desktopapplicatie.\nerror-javascript-config =\n Ruffle heeft een groot probleem ondervonden vanwege een onjuiste JavaScript configuratie.\n Als je de serverbeheerder bent, kijk dan naar de foutdetails om te zien wat er verkeerd is.\n Je kan ook in de Ruffle wiki kijken voor hulp.\nerror-wasm-not-found =\n Ruffle kon het vereiste ".wasm" bestandscomponent niet laden.\n Als je de serverbeheerder bent, controleer dan of het bestaand juist is ge\xfcpload.\n Mocht het probleem blijven voordoen, moet je misschien de "publicPath" instelling gebruiken: zie ook de Ruffle wiki voor hulp.\nerror-wasm-mime-type =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Deze webserver serveert ".wasm" bestanden niet met het juiste MIME type.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-swf-fetch =\n Ruffle kon het Flash SWF bestand niet inladen.\n De meest waarschijnlijke reden is dat het bestand niet langer bestaat, en er dus niets is om in te laden.\n Probeer contact op te nemen met de websitebeheerder voor hulp.\nerror-swf-cors =\n Ruffle kon het Flash SWD bestand niet inladen.\n Toegang is waarschijnlijk geblokeerd door het CORS beleid.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-wasm-cors =\n Ruffle kon het vereiste ".wasm" bestandscomponent niet laden.\n Toegang is waarschijnlijk geblokeerd door het CORS beleid.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-wasm-invalid =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Het lijkt erop dat de Ruffle bestanden ontbreken of ongeldig zijn.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-wasm-download =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Dit lost zichzelf vaak op als je de bladzijde opnieuw inlaadt.\n Zo niet, neem dan contact op met de websitebeheerder.\nerror-wasm-disabled-on-edge =\n Ruffle kon het vereiste ".wasm" bestandscomponent niet laden.\n Om dit op te lossen, ga naar je browserinstellingen, klik op "Privacy, zoeken en diensten", scroll omlaag, en schakel "Verbeter je veiligheid op he web" uit.\n Dan kan je browser wel de vereiste ".wasm" bestanden inladen.\n Als het probleem zich blijft voordoen, moet je misschien een andere browser gebruiken.\nerror-javascript-conflict =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Het lijkt erop dat deze pagina JavaScript code gebruikt die conflicteert met Ruffle.\n Als je de serverbeheerder bent, raden we aan om het bestand op een lege pagina te proberen in te laden.\nerror-javascript-conflict-outdated = Je kan ook proberen een nieuwe versie van Ruffle te installeren, om om het probleem heen te werken (huidige versie is oud: { $buildDate }).\nerror-csp-conflict =\n Ruffle heeft een groot probleem ondervonden tijdens het initialiseren.\n Het CSP-beleid staat niet toe dat het vereiste ".wasm" component kan draaien.\n Als je de serverbeheerder bent, kijk dan in de Ruffle wiki voor hulp.\nerror-unknown =\n Ruffle heeft een groot probleem onderbonden tijdens het weergeven van deze Flash-inhoud.\n { $outdated ->\n [true] Als je de serverbeheerder bent, upload dan een nieuwe versie van Ruffle (huidige versie is oud: { $buildDate }).\n *[false] Dit hoort niet te gebeuren, dus we stellen het op prijs als je de fout aan ons rapporteert!\n }\n',"save-manager.ftl":"save-delete-prompt = Weet je zeker dat je deze opgeslagen data wilt verwijderen?\nsave-reload-prompt =\n De enige manier om deze opgeslagen data te { $action ->\n [delete] verwijderen\n *[replace] vervangen\n } zonder potenti\xeble problemen is door de inhoud opnieuw te laden. Toch doorgaan?\nsave-download = Downloaden\nsave-replace = Vervangen\nsave-delete = Verwijderen\nsave-backup-all = Download alle opgeslagen data\n"},"pt-BR":{"context_menu.ftl":"context-menu-download-swf = Baixar .swf\ncontext-menu-copy-debug-info = Copiar informa\xe7\xe3o de depura\xe7\xe3o\ncontext-menu-open-save-manager = Abrir o Gerenciador de Salvamento\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Sobre a extens\xe3o do Ruffle ({ $version })\n *[other] Sobre o Ruffle ({ $version })\n }\ncontext-menu-hide = Esconder este menu\ncontext-menu-exit-fullscreen = Sair da tela cheia\ncontext-menu-enter-fullscreen = Entrar em tela cheia\n","messages.ftl":'message-cant-embed =\n Ruffle n\xe3o conseguiu executar o Flash incorporado nesta p\xe1gina.\n Voc\xea pode tentar abrir o arquivo em uma guia separada para evitar esse problema.\npanic-title = Algo deu errado :(\nmore-info = Mais informa\xe7\xe3o\nrun-anyway = Executar mesmo assim\ncontinue = Continuar\nreport-bug = Reportar Bug\nupdate-ruffle = Atualizar Ruffle\nruffle-demo = Demo Web\nruffle-desktop = Aplicativo de Desktop\nruffle-wiki = Ver Wiki do Ruffle\nview-error-details = Ver detalhes do erro\nopen-in-new-tab = Abrir em uma nova guia\nclick-to-unmute = Clique para ativar o som\nerror-file-protocol =\n Parece que voc\xea est\xe1 executando o Ruffle no protocolo "file:".\n Isto n\xe3o funciona como navegadores bloqueiam muitos recursos de funcionar por raz\xf5es de seguran\xe7a.\n Ao inv\xe9s disso, convidamos voc\xea a configurar um servidor local ou a usar a demonstra\xe7\xe3o da web, ou o aplicativo de desktop.\nerror-javascript-config =\n O Ruffle encontrou um grande problema devido a uma configura\xe7\xe3o incorreta do JavaScript.\n Se voc\xea for o administrador do servidor, convidamos voc\xea a verificar os detalhes do erro para descobrir qual par\xe2metro est\xe1 com falha.\n Voc\xea tamb\xe9m pode consultar o wiki do Ruffle para obter ajuda.\nerror-wasm-not-found =\n Ruffle falhou ao carregar o componente de arquivo ".wasm" necess\xe1rio.\n Se voc\xea \xe9 o administrador do servidor, por favor, certifique-se de que o arquivo foi carregado corretamente.\n Se o problema persistir, voc\xea pode precisar usar a configura\xe7\xe3o "publicPath": por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-mime-type =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Este servidor de web n\xe3o est\xe1 servindo ".wasm" arquivos com o tipo MIME correto.\n Se voc\xea \xe9 o administrador do servidor, por favor consulte o wiki do Ruffle para obter ajuda.\nerror-swf-fetch =\n Ruffle falhou ao carregar o arquivo Flash SWF.\n A raz\xe3o prov\xe1vel \xe9 que o arquivo n\xe3o existe mais, ent\xe3o n\xe3o h\xe1 nada para o Ruffle carregar.\n Tente contatar o administrador do site para obter ajuda.\nerror-swf-cors =\n Ruffle falhou ao carregar o arquivo Flash SWF.\n O acesso para fetch provavelmente foi bloqueado pela pol\xedtica CORS.\n Se voc\xea for o administrador do servidor, consulte o wiki do Ruffle para obter ajuda.\nerror-wasm-cors =\n Ruffle falhou ao carregar o componente de arquivo ".wasm" necess\xe1rio.\n O acesso para fetch foi provavelmente bloqueado pela pol\xedtica CORS.\n Se voc\xea \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-invalid =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Parece que esta p\xe1gina tem arquivos ausentes ou inv\xe1lidos para executar o Ruffle.\n Se voc\xea for o administrador do servidor, consulte o wiki do Ruffle para obter ajuda.\nerror-wasm-download =\n O Ruffle encontrou um grande problema ao tentar inicializar.\n Muitas vezes isso pode se resolver sozinho, ent\xe3o voc\xea pode tentar recarregar a p\xe1gina.\n Caso contr\xe1rio, contate o administrador do site.\nerror-wasm-disabled-on-edge =\n O Ruffle falhou ao carregar o componente de arquivo ".wasm" necess\xe1rio.\n Para corrigir isso, tente abrir configura\xe7\xf5es do seu navegador, clicando em "Privacidade, pesquisa e servi\xe7os", rolando para baixo e desativando "Melhore sua seguran\xe7a na web".\n Isso permitir\xe1 que seu navegador carregue os arquivos ".wasm" necess\xe1rios.\n Se o problema persistir, talvez seja necess\xe1rio usar um navegador diferente.\nerror-javascript-conflict =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Parece que esta p\xe1gina usa c\xf3digo JavaScript que entra em conflito com o Ruffle.\n Se voc\xea for o administrador do servidor, convidamos voc\xea a tentar carregar o arquivo em uma p\xe1gina em branco.\nerror-javascript-conflict-outdated = Voc\xea tamb\xe9m pode tentar fazer o upload de uma vers\xe3o mais recente do Ruffle que pode contornar o problema (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\nerror-csp-conflict =\n Ruffle encontrou um grande problema ao tentar inicializar.\n A pol\xedtica de seguran\xe7a de conte\xfado deste servidor da web n\xe3o permite a execu\xe7\xe3o do componente ".wasm" necess\xe1rio.\n Se voc\xea for o administrador do servidor, consulte o wiki do Ruffle para obter ajuda.\nerror-unknown =\n O Ruffle encontrou um grande problema enquanto tentava exibir este conte\xfado em Flash.\n { $outdated ->\n [true] Se voc\xea \xe9 o administrador do servidor, por favor tente fazer o upload de uma vers\xe3o mais recente do Ruffle (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\n *[false] Isso n\xe3o deveria acontecer, ent\xe3o apreciar\xedamos muito se voc\xea pudesse arquivar um bug!\n }\n',"save-manager.ftl":"save-delete-prompt = Tem certeza que deseja excluir este arquivo de salvamento?\nsave-reload-prompt =\n A \xfanica maneira de { $action ->\n [delete] excluir\n *[replace] substituir\n } este arquivo sem potencial conflito \xe9 recarregar este conte\xfado. Deseja continuar mesmo assim?\nsave-download = Baixar\nsave-replace = Substituir\nsave-delete = Excluir\nsave-backup-all = Baixar todos os arquivos de salvamento\n"},"pt-PT":{"context_menu.ftl":"context-menu-download-swf = Descarga.swf\ncontext-menu-copy-debug-info = Copiar informa\xe7\xf5es de depura\xe7\xe3o\ncontext-menu-open-save-manager = Abrir Gestor de Grava\xe7\xf5es\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Sobre a extens\xe3o do Ruffle ({ $version })\n *[other] Sobre o Ruffle ({ $version })\n }\ncontext-menu-hide = Esconder este menu\ncontext-menu-exit-fullscreen = Fechar Ecr\xe3 Inteiro\ncontext-menu-enter-fullscreen = Abrir Ecr\xe3 Inteiro\n","messages.ftl":'message-cant-embed =\n O Ruffle n\xe3o conseguiu abrir o Flash integrado nesta p\xe1gina.\n Para tentar resolver o problema, pode abrir o ficheiro num novo separador.\npanic-title = Algo correu mal :(\nmore-info = Mais informa\xe7\xf5es\nrun-anyway = Executar mesmo assim\ncontinue = Continuar\nreport-bug = Reportar falha\nupdate-ruffle = Atualizar o Ruffle\nruffle-demo = Demonstra\xe7\xe3o na Web\nruffle-desktop = Aplica\xe7\xe3o para Desktop\nruffle-wiki = Ver a Wiki do Ruffle\nview-error-details = Ver detalhes do erro\nopen-in-new-tab = Abrir num novo separador\nclick-to-unmute = Clique para ativar o som\nerror-file-protocol =\n Parece que executa o Ruffle no protocolo "file:".\n Isto n\xe3o funciona, j\xe1 que os navegadores bloqueiam muitas funcionalidades por raz\xf5es de seguran\xe7a.\n Em vez disto, recomendados configurar um servidor local ou usar a demonstra\xe7\xe3o na web, ou a aplica\xe7\xe3o para desktop.\nerror-javascript-config =\n O Ruffle encontrou um problema maior devido a uma configura\xe7\xe3o de JavaScript incorreta.\n Se \xe9 o administrador do servidor, convidamo-lo a verificar os detalhes do erro para descobrir o par\xe2metro problem\xe1tico.\n Pode ainda consultar a wiki do Ruffle para obter ajuda.\nerror-wasm-not-found =\n O Ruffle falhou ao carregar o componente de ficheiro ".wasm" necess\xe1rio.\n Se \xe9 o administrador do servidor, por favor certifique-se de que o ficheiro foi devidamente carregado.\n Se o problema persistir, poder\xe1 querer usar a configura\xe7\xe3o "publicPath": consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-mime-type =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n Este servidor de web n\xe3o suporta ficheiros ".wasm" com o tipo MIME correto.\n Se \xe9 o administrador do servidor, por favor consulte o wiki do Ruffle para obter ajuda.\nerror-swf-fetch =\n Ruffle falhou ao carregar o arquivo SWF do Flash\n A raz\xe3o mais prov\xe1vel \xe9 que o arquivo n\xe3o existe mais, ent\xe3o n\xe3o h\xe1 nada para o Ruffle carregar.\n Tente contactar o administrador do site para obter ajuda.\nerror-swf-cors =\n O Ruffle falhou ao carregar o ficheiro Flash SWF.\n Acesso a buscar foi provavelmente bloqueado pela pol\xedtica de CORS.\n Se \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-cors =\n O Ruffle falhou ao carregar o componente de ficheiro ".wasm" necess\xe1rio.\n O acesso a buscar foi provavelmente bloqueado pela pol\xedtica CORS.\n Se \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-invalid =\n Ruffle encontrou um grande problema ao tentar inicializar.\n Parece que esta p\xe1gina est\xe1 ausente ou arquivos inv\xe1lidos para executar o Ruffle.\n Se voc\xea \xe9 o administrador do servidor, por favor consulte a wiki do Ruffle para obter ajuda.\nerror-wasm-download =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n Isto frequentemente resolve-se sozinho, portanto experimente recarregar a p\xe1gina.\n Caso contr\xe1rio, por favor contacte o administrador do site.\nerror-wasm-disabled-on-edge =\n O Ruffle falhou ao carregar o componente de ficheiro ".wasm" necess\xe1rio.\n Para corrigir isso, tente abrir as op\xe7\xf5es do seu navegador, clicando em "Privacidade, pesquisa e servi\xe7os", rolando para baixo e desativando "Melhore a sua seguran\xe7a na web".\n Isto permitir\xe1 ao seu navegador carregar os ficheiros ".wasm" necess\xe1rios.\n Se o problema persistir, talvez seja necess\xe1rio usar um navegador diferente.\nerror-javascript-conflict =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n Parece que esta p\xe1gina usa c\xf3digo JavaScript que entra em conflito com o Ruffle.\n Se \xe9 o administrador do servidor, convidamo-lo a tentar carregar o ficheiro em numa p\xe1gina em branco.\nerror-javascript-conflict-outdated = Pode ainda tentar carregar uma vers\xe3o mais recente do Ruffle que talvez contorne o problema (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\nerror-csp-conflict =\n O Ruffle encontrou um problema maior ao tentar inicializar.\n A Pol\xedtica de Seguran\xe7a de Conte\xfado deste servidor n\xe3o permite que o componente ".wasm" necess\xe1rio seja executado.\n Se \xe9 o administrador do servidor, por favor consulte o wiki do Ruffle para obter ajuda.\nerror-unknown =\n O Ruffle encontrou um problema maior enquanto tentava mostrar este conte\xfado em Flash.\n { $outdated ->\n [true] Se \xe9 o administrador do servidor, por favor tente carregar uma vers\xe3o mais recente do Ruffle (a compila\xe7\xe3o atual est\xe1 desatualizada: { $buildDate }).\n *[false] N\xe3o era suposto isto ter acontecido, por isso agradecer\xedamos muito se pudesse reportar a falha!\n }\n',"save-manager.ftl":"save-delete-prompt = Tem a certeza de que quer apagar esta grava\xe7\xe3o?\nsave-reload-prompt =\n A \xfanica forma de { $action ->\n [delete] apagar\n *[replace] substituir\n } esta grava\xe7\xe3o sem um potencial conflito \xe9 recarregar este conte\xfado. Deseja continuar mesmo assim?\nsave-download = Descarregar\nsave-replace = Substituir\nsave-delete = Apagar\nsave-backup-all = Descarregar todas as grava\xe7\xf5es\n"},"ro-RO":{"context_menu.ftl":"context-menu-download-swf = Descarc\u0103 .swf\ncontext-menu-copy-debug-info = Copia\u021bi informa\u021biile de depanare\ncontext-menu-open-save-manager = Deschide manager de salv\u0103ri\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Despre extensia Ruffle ({ $version })\n *[other] Despre Ruffle ({ $version })\n }\ncontext-menu-hide = Ascunde acest meniu\ncontext-menu-exit-fullscreen = Ie\u0219i\u021bi din ecranul complet\ncontext-menu-enter-fullscreen = Intr\u0103 \xeen ecran complet\n","messages.ftl":'message-cant-embed =\n Ruffle nu a putut rula Flash \xeencorporat \xeen aceast\u0103 pagin\u0103.\n Pute\u021bi \xeencerca s\u0103 deschide\u021bi fi\u0219ierul \xeentr-o fil\u0103 separat\u0103, pentru a evita aceast\u0103 problem\u0103.\npanic-title = Ceva a mers prost :(\nmore-info = Mai multe informatii\nrun-anyway = Ruleaz\u0103 oricum\ncontinue = Continuare\nreport-bug = Raporteaz\u0103 o eroare\nupdate-ruffle = Actualizeaz\u0103\nruffle-demo = Demo Web\nruffle-desktop = Aplica\u021bie desktop\nruffle-wiki = Vezi Ruffle Wiki\nview-error-details = Vezi detaliile de eroare\nopen-in-new-tab = Deschidere in fil\u0103 nou\u0103\nclick-to-unmute = \xcenl\u0103tur\u0103 amu\u021birea\nerror-file-protocol =\n Se pare c\u0103 rula\u021bi Ruffle pe protocolul "fi\u0219ier:".\n Aceasta nu func\u021bioneaz\u0103 ca browsere blocheaz\u0103 multe caracteristici din motive de securitate.\n \xcen schimb, v\u0103 invit\u0103m s\u0103 configura\u021bi un server local sau s\u0103 folosi\u021bi aplica\u021bia web demo sau desktop.\nerror-javascript-config =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 din cauza unei configur\u0103ri incorecte a JavaScript.\n Dac\u0103 sunte\u021bi administratorul serverului, v\u0103 invit\u0103m s\u0103 verifica\u021bi detaliile de eroare pentru a afla care parametru este defect.\n Pute\u021bi consulta \u0219i Ruffle wiki pentru ajutor.\nerror-wasm-not-found =\n Ruffle a e\u0219uat la \xeenc\u0103rcarea componentei de fi\u0219ier ".wasm".\n Dac\u0103 sunte\u021bi administratorul serverului, v\u0103 rug\u0103m s\u0103 v\u0103 asigura\u021bi c\u0103 fi\u0219ierul a fost \xeenc\u0103rcat corect.\n Dac\u0103 problema persist\u0103, poate fi necesar s\u0103 utiliza\u0163i setarea "publicPath": v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-mime-type =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencerca ini\u021bializarea.\n Acest server web nu serve\u0219te ". asm" fi\u0219iere cu tipul corect MIME.\n Dac\u0103 sunte\u021bi administrator de server, v\u0103 rug\u0103m s\u0103 consulta\u021bi Ruffle wiki pentru ajutor.\nerror-swf-fetch =\n Ruffle a e\u0219uat la \xeenc\u0103rcarea fi\u0219ierului Flash SWF.\n Motivul cel mai probabil este c\u0103 fi\u015fierul nu mai exist\u0103, deci nu exist\u0103 nimic pentru Ruffle s\u0103 se \xeencarce.\n \xcencerca\u021bi s\u0103 contacta\u021bi administratorul site-ului web pentru ajutor.\nerror-swf-cors =\n Ruffle a e\u0219uat la \xeenc\u0103rcarea fi\u0219ierului Flash SWF.\n Accesul la preluare a fost probabil blocat de politica CORS.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-cors =\n Ruffle a e\u0219uat \xeen \xeenc\u0103rcarea componentei de fi\u0219ier ".wasm".\n Accesul la preluare a fost probabil blocat de politica CORS.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-invalid =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencearc\u0103 ini\u021bializarea.\n Se pare c\u0103 aceast\u0103 pagin\u0103 are fi\u0219iere lips\u0103 sau invalide pentru rularea Ruffle.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 rug\u0103m s\u0103 consulta\u0163i Ruffle wiki pentru ajutor.\nerror-wasm-download =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce \xeencerca s\u0103 ini\u021bializeze.\n Acest lucru se poate rezolva adesea, astfel \xeenc\xe2t pute\u0163i \xeencerca s\u0103 re\xeenc\u0103rca\u0163i pagina.\n Altfel, v\u0103 rug\u0103m s\u0103 contacta\u0163i administratorul site-ului.\nerror-wasm-disabled-on-edge =\n Ruffle nu a putut \xeenc\u0103rca componenta de fi\u0219ier ".wasm".\n Pentru a remedia acest lucru, \xeencerca\u021bi s\u0103 deschide\u021bi set\u0103rile browser-ului dvs., ap\u0103s\xe2nd pe "Confiden\u021bialitate, c\u0103utare \u0219i servicii", derul\xe2nd \xeen jos \u0219i \xeenchiz\xe2nd "\xcembun\u0103t\u0103\u021be\u0219te-\u021bi securitatea pe web".\n Acest lucru va permite browser-ului s\u0103 \xeencarce fi\u0219ierele ".wasm" necesare.\n Dac\u0103 problema persist\u0103, ar putea fi necesar s\u0103 folosi\u021bi un browser diferit.\nerror-javascript-conflict =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce \xeencerca s\u0103 ini\u021bializeze.\n Se pare c\u0103 aceast\u0103 pagin\u0103 folose\u0219te codul JavaScript care intr\u0103 \xeen conflict cu Ruffle.\n Dac\u0103 sunte\u0163i administratorul serverului, v\u0103 invit\u0103m s\u0103 \xeenc\u0103rca\u0163i fi\u015fierul pe o pagin\u0103 goal\u0103.\nerror-javascript-conflict-outdated = De asemenea, po\u021bi \xeencerca s\u0103 \xeencarci o versiune mai recent\u0103 de Ruffle care poate ocoli problema (versiunea curent\u0103 este expirat\u0103: { $buildDate }).\nerror-csp-conflict =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencerca ini\u021bializarea.\n Politica de securitate a con\u021binutului acestui server web nu permite serviciul necesar". asm" component\u0103 pentru a rula.\n Dac\u0103 sunte\u021bi administratorul de server, consulta\u021bi Ruffle wiki pentru ajutor.\nerror-unknown =\n Ruffle a \xeent\xe2mpinat o problem\u0103 major\u0103 \xeen timp ce se \xeencerca afi\u0219area con\u021binutului Flash.\n { $outdated ->\n [true] Dac\u0103 sunte\u021bi administratorul de server, v\u0103 rug\u0103m s\u0103 \xeencerca\u0163i s\u0103 \xeenc\u0103rca\u0163i o versiune mai recent\u0103 de Ruffle (versiunea curent\u0103 este dep\u0103\u015fit\u0103: { $buildDate }).\n *[false] Acest lucru nu ar trebui s\u0103 se \xeent\xe2mple, a\u0219a c\u0103 am aprecia foarte mult dac\u0103 ai putea trimite un bug!\n }\n',"save-manager.ftl":"save-delete-prompt = Sunte\u0163i sigur c\u0103 dori\u0163i s\u0103 \u015fterge\u0163i acest fi\u015fier salvat?\nsave-reload-prompt =\n Singura cale de a { $action ->\n [delete] \u0219terge\n *[replace] \xeenlocuie\u0219te\n } acest fi\u0219ier de salvare f\u0103r\u0103 un conflict poten\u021bial este de a re\xeenc\u0103rca acest con\u021binut. Dori\u021bi s\u0103 continua\u021bi oricum?\nsave-download = Desc\u0103rcare\nsave-replace = \xcenlocuie\u0219te\nsave-delete = \u0218tergere\n"},"ru-RU":{"context_menu.ftl":"context-menu-download-swf = \u0421\u043a\u0430\u0447\u0430\u0442\u044c .swf\ncontext-menu-copy-debug-info = \u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043b\u0430\u0434\u043e\u0447\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e\ncontext-menu-open-save-manager = \u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0439\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u041e \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0438 Ruffle ({ $version })\n *[other] \u041e Ruffle ({ $version })\n }\ncontext-menu-hide = \u0421\u043a\u0440\u044b\u0442\u044c \u044d\u0442\u043e \u043c\u0435\u043d\u044e\ncontext-menu-exit-fullscreen = \u041e\u043a\u043e\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c\ncontext-menu-enter-fullscreen = \u041f\u043e\u043b\u043d\u043e\u044d\u043a\u0440\u0430\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c\n","messages.ftl":'message-cant-embed =\n Ruffle \u043d\u0435 \u0441\u043c\u043e\u0433 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Flash, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435.\n \u0427\u0442\u043e\u0431\u044b \u043e\u0431\u043e\u0439\u0442\u0438 \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0444\u0430\u0439\u043b \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0435.\npanic-title = \u0427\u0442\u043e-\u0442\u043e \u043f\u043e\u0448\u043b\u043e \u043d\u0435 \u0442\u0430\u043a :(\nmore-info = \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435\nrun-anyway = \u0412\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c\ncontinue = \u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c\nreport-bug = \u0421\u043e\u043e\u0431\u0449\u0438\u0442\u044c \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435\nupdate-ruffle = \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c Ruffle\nruffle-demo = \u0412\u0435\u0431-\u0434\u0435\u043c\u043e\nruffle-desktop = \u041d\u0430\u0441\u0442\u043e\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\nruffle-wiki = \u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0438\u043a\u0438 Ruffle\nview-error-details = \u0421\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435\nopen-in-new-tab = \u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432 \u043d\u043e\u0432\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0435\nclick-to-unmute = \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0432\u0443\u043a\nerror-file-protocol =\n \u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u0432\u044b \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0435 Ruffle \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 "file:".\n \u042d\u0442\u043e \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u044b \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u044e\u0442 \u0440\u0430\u0431\u043e\u0442\u0443 \u043c\u043d\u043e\u0433\u0438\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u043f\u043e \u0441\u043e\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438.\n \u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0433\u043e \u043c\u044b \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u043e\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432\u0435\u0431-\u0434\u0435\u043c\u043e \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440.\nerror-javascript-config =\n \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 JavaScript.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u044f\u0432\u043b\u044f\u0435\u0442\u0435\u0441\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043c\u044b \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0434\u0435\u0442\u0430\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u044f\u0441\u043d\u0438\u0442\u044c, \u043a\u0430\u043a\u043e\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0434\u0430\u043b \u0441\u0431\u043e\u0439.\n \u0412\u044b \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-not-found =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0444\u0430\u0439\u043b\u0430 ".wasm".\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0444\u0430\u0439\u043b \u0431\u044b\u043b \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.\n \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f, \u0432\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 "publicPath": \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-mime-type =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u042d\u0442\u043e\u0442 \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0444\u0430\u0439\u043b\u044b ".wasm" \u0441 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u043c \u0442\u0438\u043f\u043e\u043c MIME.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-swf-fetch =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c SWF-\u0444\u0430\u0439\u043b Flash.\n \u0412\u0435\u0440\u043e\u044f\u0442\u043d\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0444\u0430\u0439\u043b \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 Ruffle \u043d\u0435\u0447\u0435\u0433\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044c.\n \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f \u0441 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0430\u0439\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u043c\u043e\u0449\u0438.\nerror-swf-cors =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c SWF-\u0444\u0430\u0439\u043b Flash.\n \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0444\u0430\u0439\u043b\u0443 \u0431\u044b\u043b \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u043e\u0439 CORS.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-cors =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0444\u0430\u0439\u043b\u0430 ".wasm".\n \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0444\u0430\u0439\u043b\u0443 \u0431\u044b\u043b \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u043e\u0439 CORS.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-invalid =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0444\u0430\u0439\u043b\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 Ruffle \u0438\u043b\u0438 \u043e\u043d\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-wasm-download =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u0427\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043e \u044d\u0442\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0443\u0441\u0442\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0441\u0430\u043c\u0430 \u0441\u043e\u0431\u043e\u044e, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.\n \u0415\u0441\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0435\u0442 \u043f\u043e\u044f\u0432\u043b\u044f\u0442\u044c\u0441\u044f, \u0441\u0432\u044f\u0436\u0438\u0442\u0435\u0441\u044c \u0441 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0430\u0439\u0442\u0430.\nerror-wasm-disabled-on-edge =\n Ruffle \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0444\u0430\u0439\u043b\u0430 ".wasm".\n \u0427\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c. \u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0443 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 WASM-\u0444\u0430\u0439\u043b\u044b.\n \u0415\u0441\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043e\u0441\u0442\u0430\u043b\u0430\u0441\u044c, \u0432\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0431\u0440\u0430\u0443\u0437\u0435\u0440.\nerror-javascript-conflict =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u044d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0438\u0439 \u0441 Ruffle \u043a\u043e\u0434 JavaScript.\n \u0415\u0441\u043b\u0438 \u0432\u044b \u044f\u0432\u043b\u044f\u0435\u0442\u0435\u0441\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043c\u044b \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u043c \u0432\u0430\u043c \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u043d\u0430 \u043f\u0443\u0441\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435.\nerror-javascript-conflict-outdated = \u0412\u044b \u0442\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044e\u044e \u0432\u0435\u0440\u0441\u0438\u044e Ruffle, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u043e\u0439\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 (\u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430: { $buildDate }).\nerror-csp-conflict =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438.\n \u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u044d\u0442\u043e\u0433\u043e \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 ".wasm".\n \u0415\u0441\u043b\u0438 \u0432\u044b \u044f\u0432\u043b\u044f\u0435\u0442\u0435\u0441\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0437\u0430 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a \u0432\u0438\u043a\u0438 Ruffle.\nerror-unknown =\n Ruffle \u0441\u0442\u043e\u043b\u043a\u043d\u0443\u043b\u0441\u044f \u0441 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0437\u0438\u0442\u044c \u044d\u0442\u043e\u0442 Flash-\u043a\u043e\u043d\u0442\u0435\u043d\u0442.\n { $outdated ->\n [true] \u0415\u0441\u043b\u0438 \u0432\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e Ruffle (\u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430: { $buildDate }).\n *[false] \u042d\u0442\u043e\u0433\u043e \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u0438\u0437\u043d\u0430\u0442\u0435\u043b\u044c\u043d\u044b, \u0435\u0441\u043b\u0438 \u0432\u044b \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043d\u0430\u043c \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435!\n }\n',"save-manager.ftl":"save-delete-prompt = \u0423\u0434\u0430\u043b\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f?\nsave-reload-prompt =\n \u0415\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 { $action ->\n [delete] \u0443\u0434\u0430\u043b\u0438\u0442\u044c\n *[replace] \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c\n } \u044d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0431\u0435\u0437 \u043f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0430 \u2013 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442. \u0412\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c?\nsave-download = \u0421\u043a\u0430\u0447\u0430\u0442\u044c\nsave-replace = \u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c\nsave-delete = \u0423\u0434\u0430\u043b\u0438\u0442\u044c\nsave-backup-all = \u0421\u043a\u0430\u0447\u0430\u0442\u044c \u0432\u0441\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f\n"},"sk-SK":{"context_menu.ftl":"context-menu-download-swf = Stiahnu\u0165 .swf\ncontext-menu-copy-debug-info = Skop\xedrova\u0165 debug info\ncontext-menu-open-save-manager = Otvori\u0165 spr\xe1vcu ulo\u017een\xed\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] O Ruffle roz\u0161\xedren\xed ({ $version })\n *[other] O Ruffle ({ $version })\n }\ncontext-menu-hide = Skry\u0165 menu\ncontext-menu-exit-fullscreen = Ukon\u010di\u0165 re\u017eim celej obrazovky\ncontext-menu-enter-fullscreen = Prejs\u0165 do re\u017eimu celej obrazovky\n","messages.ftl":'message-cant-embed =\n Ruffle nemohol spusti\u0165 Flash vlo\u017een\xfd na tejto str\xe1nke.\n M\xf4\u017eete sa pok\xfasi\u0165 otvori\u0165 s\xfabor na samostatnej karte, aby ste sa vyhli tomuto probl\xe9mu.\npanic-title = Nie\u010do sa pokazilo :(\nmore-info = Viac inform\xe1ci\xed\nrun-anyway = Spusti\u0165 aj tak\ncontinue = Pokra\u010dova\u0165\nreport-bug = Nahl\xe1si\u0165 chybu\nupdate-ruffle = Aktualizova\u0165 Ruffle\nruffle-demo = Web Demo\nruffle-desktop = Desktopov\xe1 aplik\xe1cia\nruffle-wiki = Zobrazi\u0165 Ruffle Wiki\nview-error-details = Zobrazi\u0165 podrobnosti o chybe\nopen-in-new-tab = Otvori\u0165 na novej karte\nclick-to-unmute = Kliknut\xedm zapnete zvuk\nerror-file-protocol =\n Zd\xe1 sa, \u017ee pou\u017e\xedvate Ruffle na protokole "file:".\n To nie je mo\u017en\xe9, preto\u017ee prehliada\u010de blokuj\xfa fungovanie mnoh\xfdch funkci\xed z bezpe\u010dnostn\xfdch d\xf4vodov.\n Namiesto toho v\xe1m odpor\xfa\u010dame nastavi\u0165 lok\xe1lny server alebo pou\u017ei\u0165 web demo \u010di desktopov\xfa aplik\xe1ciu.\nerror-javascript-config =\n Ruffle narazil na probl\xe9m v d\xf4sledku nespr\xe1vnej konfigur\xe1cie JavaScriptu.\n Ak ste spr\xe1vcom servera, odpor\xfa\u010dame v\xe1m skontrolova\u0165 podrobnosti o chybe, aby ste zistili, ktor\xfd parameter je chybn\xfd.\n Pomoc m\xf4\u017eete z\xedska\u0165 aj na wiki Ruffle.\nerror-wasm-not-found =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 po\u017eadovan\xfd komponent s\xfaboru \u201e.wasm\u201c.\n Ak ste spr\xe1vcom servera, skontrolujte, \u010di bol s\xfabor spr\xe1vne nahran\xfd.\n Ak probl\xe9m pretrv\xe1va, mo\u017eno budete musie\u0165 pou\u017ei\u0165 nastavenie \u201epublicPath\u201c: pomoc n\xe1jdete na wiki Ruffle.\nerror-wasm-mime-type =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Tento webov\xfd server neposkytuje s\xfabory \u201e.wasm\u201c so spr\xe1vnym typom MIME.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-swf-fetch =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 SWF s\xfabor Flash.\n Najpravdepodobnej\u0161\xedm d\xf4vodom je, \u017ee s\xfabor u\u017e neexistuje, tak\u017ee Ruffle nem\xe1 \u010do na\u010d\xedta\u0165.\n Sk\xfaste po\u017eiada\u0165 o pomoc spr\xe1vcu webovej lokality.\nerror-swf-cors =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 SWF s\xfabor Flash.\n Pr\xedstup k na\u010d\xedtaniu bol pravdepodobne zablokovan\xfd politikou CORS.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-wasm-cors =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 po\u017eadovan\xfd komponent s\xfaboru \u201e.wasm\u201c.\n Pr\xedstup k na\u010d\xedtaniu bol pravdepodobne zablokovan\xfd politikou CORS.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-wasm-invalid =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Zd\xe1 sa, \u017ee na tejto str\xe1nke ch\xfdbaj\xfa alebo s\xfa neplatn\xe9 s\xfabory na spustenie Ruffle.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-wasm-download =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Probl\xe9m sa m\xf4\u017ee vyrie\u0161i\u0165 aj s\xe1m, tak\u017ee m\xf4\u017eete sk\xfasi\u0165 str\xe1nku na\u010d\xedta\u0165 znova.\n V opa\u010dnom pr\xedpade kontaktujte administr\xe1tora str\xe1nky.\nerror-wasm-disabled-on-edge =\n Ruffle sa nepodarilo na\u010d\xedta\u0165 po\u017eadovan\xfd komponent s\xfaboru \u201e.wasm\u201c.\n Ak chcete tento probl\xe9m vyrie\u0161i\u0165, sk\xfaste otvori\u0165 nastavenia prehliada\u010da, kliknite na polo\u017eku \u201eOchrana osobn\xfdch \xfadajov, vyh\u013ead\xe1vanie a slu\u017eby\u201c, prejdite nadol a vypnite mo\u017enos\u0165 \u201eZv\xfd\u0161te svoju bezpe\u010dnos\u0165 na webe\u201c.\n V\xe1\u0161mu prehliada\u010du to umo\u017en\xed na\u010d\xedta\u0165 po\u017eadovan\xe9 s\xfabory \u201e.wasm\u201c.\n Ak probl\xe9m pretrv\xe1va, mo\u017eno budete musie\u0165 pou\u017ei\u0165 in\xfd prehliada\u010d.\nerror-javascript-conflict =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Zd\xe1 sa, \u017ee t\xe1to str\xe1nka pou\u017e\xedva k\xf3d JavaScript, ktor\xfd je v konflikte s Ruffle.\n Ak ste spr\xe1vcom servera, odpor\xfa\u010dame v\xe1m sk\xfasi\u0165 na\u010d\xedta\u0165 s\xfabor na pr\xe1zdnu str\xe1nku.\nerror-javascript-conflict-outdated = M\xf4\u017eete sa tie\u017e pok\xfasi\u0165 nahra\u0165 nov\u0161iu verziu Ruffle, ktor\xe1 m\xf4\u017ee dan\xfd probl\xe9m vyrie\u0161i\u0165 (aktu\xe1lny build je zastaran\xfd: { $buildDate }).\nerror-csp-conflict =\n Ruffle narazil na probl\xe9m pri pokuse o inicializ\xe1ciu.\n Z\xe1sady zabezpe\u010denia obsahu tohto webov\xe9ho servera nepovo\u013euj\xfa spustenie po\u017eadovan\xe9ho komponentu \u201e.wasm\u201c.\n Ak ste spr\xe1vcom servera, pomoc n\xe1jdete na Ruffle wiki.\nerror-unknown =\n Ruffle narazil na probl\xe9m pri pokuse zobrazi\u0165 tento Flash obsah.\n { $outdated ->\n [true] Ak ste spr\xe1vcom servera, sk\xfaste nahra\u0165 nov\u0161iu verziu Ruffle (aktu\xe1lny build je zastaran\xfd: { $buildDate }).\n *[false] Toto by sa nemalo sta\u0165, tak\u017ee by sme naozaj ocenili, keby ste mohli nahl\xe1si\u0165 chybu!\n }\n',"save-manager.ftl":"save-delete-prompt = Naozaj chcete odstr\xe1ni\u0165 tento s\xfabor s ulo\u017een\xfdmi poz\xedciami?\nsave-reload-prompt =\n Jedin\xfd sp\xf4sob, ako { $action ->\n [delete] vymaza\u0165\n *[replace] nahradi\u0165\n } tento s\xfabor s ulo\u017een\xfdmi poz\xedciami bez potenci\xe1lneho konfliktu je op\xe4tovn\xe9 na\u010d\xedtanie tohto obsahu. Chcete napriek tomu pokra\u010dova\u0165?\nsave-download = Stiahnu\u0165\nsave-replace = Nahradi\u0165\nsave-delete = Vymaza\u0165\nsave-backup-all = Stiahnu\u0165 v\u0161etky s\xfabory s ulo\u017een\xfdmi poz\xedciami\n"},"sv-SE":{"context_menu.ftl":"context-menu-download-swf = Ladda ner .swf\ncontext-menu-copy-debug-info = Kopiera fels\xf6kningsinfo\ncontext-menu-open-save-manager = \xd6ppna Sparhanteraren\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Om Ruffletill\xe4gget ({ $version })\n *[other] Om Ruffle ({ $version })\n }\ncontext-menu-hide = D\xf6lj denna meny\ncontext-menu-exit-fullscreen = Avsluta helsk\xe4rm\ncontext-menu-enter-fullscreen = G\xe5 in i helsk\xe4rm\n","messages.ftl":'message-cant-embed =\n Ruffle kunde inte k\xf6ra Flashinneh\xe5llet som \xe4r inb\xe4ddad p\xe5 denna sida.\n Du kan f\xf6rs\xf6ka \xf6ppna filen i en separat flik f\xf6r att kringg\xe5 problemet.\npanic-title = N\xe5got gick fel :(\nmore-info = Mer info\nrun-anyway = K\xf6r \xe4nd\xe5\ncontinue = Forts\xe4tt\nreport-bug = Rapportera Bugg\nupdate-ruffle = Uppdatera Ruffle\nruffle-demo = Webbdemo\nruffle-desktop = Skrivbordsprogram\nruffle-wiki = Se Rufflewiki\nview-error-details = Visa Felinformation\nopen-in-new-tab = \xd6ppna i ny flik\nclick-to-unmute = Klicka f\xf6r ljud\nerror-file-protocol =\n Det verkar som att du k\xf6r Ruffle p\xe5 "fil:"-protokollet.\n Detta fungerar inte eftersom webbl\xe4sare blockerar m\xe5nga funktioner fr\xe5n att fungera av s\xe4kerhetssk\xe4l.\n Ist\xe4llet bjuder vi in dig att s\xe4tta upp en lokal server eller antingen anv\xe4nda webbdemon eller skrivbordsprogrammet.\nerror-javascript-config =\n Ruffle har st\xf6tt p\xe5 ett stort fel p\xe5 grund av en felaktig JavaScriptkonfiguration.\n Om du \xe4r serveradministrat\xf6ren bjuder vi in dig att kontrollera feldetaljerna f\xf6r att ta reda p\xe5 vilken parameter som \xe4r felaktig.\n Du kan ocks\xe5 konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-not-found =\n Ruffle misslyckades ladda ".wasm"-filkomponenten.\n Om du \xe4r serveradministrat\xf6ren, se till att filen har laddats upp korrekt.\n Om problemet kvarst\xe5r kan du beh\xf6va anv\xe4nda inst\xe4llningen "publicPath": v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-mime-type =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Denna webbserver serverar inte ".wasm"-filer med korrekt MIME-typ.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-swf-fetch =\n Ruffle misslyckades ladda SWF-filen.\n Det mest sannolika sk\xe4let \xe4r att filen inte l\xe4ngre existerar, s\xe5 det finns inget f\xf6r Ruffle att ladda.\n F\xf6rs\xf6k att kontakta webbplatsadministrat\xf6ren f\xf6r hj\xe4lp.\nerror-swf-cors =\n Ruffle misslyckades ladda SWF-filen.\n \xc5tkomst att h\xe4mta har sannolikt blockerats av CORS-policy.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-cors =\n Ruffle misslyckades ladda ".wasm"-filkomponenten.\n \xc5tkomst att h\xe4mta har sannolikt blockerats av CORS-policy.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-invalid =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen\n Det verkar som att den h\xe4r sidan har saknade eller ogiltiga filer f\xf6r att k\xf6ra Ruffle.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-wasm-download =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Detta kan ofta l\xf6sas av sig sj\xe4lv, s\xe5 du kan prova att ladda om sidan.\n Annars, kontakta webbplatsens administrat\xf6r.\nerror-wasm-disabled-on-edge =\n Ruffle misslyckades ladda ".wasm"-filkomponenten.\n F\xf6r att \xe5tg\xe4rda detta, f\xf6rs\xf6k att \xf6ppna webbl\xe4sarens inst\xe4llningar, klicka p\xe5 "Sekretess, s\xf6kning och tj\xe4nster", bl\xe4ddra ner och st\xe4ng av "F\xf6rb\xe4ttra s\xe4kerheten p\xe5 webben".\n Detta till\xe5ter din webbl\xe4sare ladda ".wasm"-filerna.\n Om problemet kvarst\xe5r kan du beh\xf6va anv\xe4nda en annan webbl\xe4sare.\nerror-javascript-conflict =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Det verkar som att den h\xe4r sidan anv\xe4nder JavaScriptkod som st\xf6r Ruffle.\n Om du \xe4r serveradministrat\xf6ren bjuder vi in dig att f\xf6rs\xf6ka ladda filen p\xe5 en blank sida.\nerror-javascript-conflict-outdated = Du kan ocks\xe5 f\xf6rs\xf6ka ladda upp en nyare version av Ruffle, vilket kan kringg\xe5 problemet (nuvarande version \xe4r utdaterad: { $buildDate }).\nerror-csp-conflict =\n Ruffle har st\xf6tt p\xe5 ett stort fel under initialiseringen.\n Denna webbservers Content Security Policy till\xe5ter inte ".wasm"-komponenten att k\xf6ra.\n Om du \xe4r serveradministrat\xf6ren, v\xe4nligen konsultera Rufflewikin f\xf6r hj\xe4lp.\nerror-unknown =\n Ruffle har st\xf6tt p\xe5 ett stort fel medan den f\xf6rs\xf6kte visa Flashinneh\xe5llet.\n { $outdated ->\n [true] Om du \xe4r serveradministrat\xf6ren, f\xf6rs\xf6k att ladda upp en nyare version av Ruffle (nuvarande version \xe4r utdaterad: { $buildDate }).\n *[false] Detta \xe4r inte t\xe4nkt att h\xe4nda s\xe5 vi skulle verkligen uppskatta om du kunde rapportera in en bugg!\n }\n',"save-manager.ftl":"save-delete-prompt = \xc4r du s\xe4ker p\xe5 att du vill radera sparfilen?\nsave-reload-prompt =\n Det enda s\xe4ttet att { $action ->\n [delete] radera\n *[replace] ers\xe4tta\n } denna sparfil utan potentiell konflikt \xe4r att ladda om inneh\xe5llet. Vill du forts\xe4tta \xe4nd\xe5?\nsave-download = Ladda ner\nsave-replace = Ers\xe4tt\nsave-delete = Radera\nsave-backup-all = Ladda ner alla sparfiler\n"},"tr-TR":{"context_menu.ftl":"context-menu-download-swf = \u0130ndir .swf\ncontext-menu-copy-debug-info = Hata ay\u0131klama bilgisini kopyala\ncontext-menu-open-save-manager = Kay\u0131t Y\xf6neticisini A\xe7\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] Ruffle Uzant\u0131s\u0131 Hakk\u0131nda ({ $version })\n *[other] Ruffle Hakk\u0131nda ({ $version })\n }\ncontext-menu-hide = Bu men\xfcy\xfc gizle\ncontext-menu-exit-fullscreen = Tam ekrandan \xe7\u0131k\ncontext-menu-enter-fullscreen = Tam ekran yap\n","messages.ftl":'message-cant-embed =\n Ruffle, bu sayfaya g\xf6m\xfcl\xfc Flash\'\u0131 \xe7al\u0131\u015ft\u0131ramad\u0131.\n Bu sorunu ortadan kald\u0131rmak i\xe7in dosyay\u0131 ayr\u0131 bir sekmede a\xe7may\u0131 deneyebilirsiniz.\npanic-title = Bir \u015feyler yanl\u0131\u015f gitti :(\nmore-info = Daha fazla bilgi\nrun-anyway = Yine de \xe7al\u0131\u015ft\u0131r\ncontinue = Devam et\nreport-bug = Hata Bildir\nupdate-ruffle = Ruffle\'\u0131 G\xfcncelle\nruffle-demo = A\u011f Demosu\nruffle-desktop = Masa\xfcst\xfc Uygulamas\u0131\nruffle-wiki = Ruffle Wiki\'yi G\xf6r\xfcnt\xfcle\nview-error-details = Hata Ayr\u0131nt\u0131lar\u0131n\u0131 G\xf6r\xfcnt\xfcle\nopen-in-new-tab = Yeni sekmede a\xe7\nclick-to-unmute = Sesi a\xe7mak i\xe7in t\u0131klay\u0131n\nerror-file-protocol =\n G\xf6r\xfcn\xfc\u015fe g\xf6re Ruffle\'\u0131 "dosya:" protokol\xfcnde \xe7al\u0131\u015ft\u0131r\u0131yorsunuz.\n Taray\u0131c\u0131lar g\xfcvenlik nedenleriyle bir\xe7ok \xf6zelli\u011fin \xe7al\u0131\u015fmas\u0131n\u0131 engelledi\u011finden bu i\u015fe yaramaz.\n Bunun yerine, sizi yerel bir sunucu kurmaya veya a\u011f\u0131n demosunu ya da masa\xfcst\xfc uygulamas\u0131n\u0131 kullanmaya davet ediyoruz.\nerror-javascript-config =\n Ruffle, yanl\u0131\u015f bir JavaScript yap\u0131land\u0131rmas\u0131 nedeniyle \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Sunucu y\xf6neticisiyseniz, hangi parametrenin hatal\u0131 oldu\u011funu bulmak i\xe7in sizi hata ayr\u0131nt\u0131lar\u0131n\u0131 kontrol etmeye davet ediyoruz.\n Yard\u0131m i\xe7in Ruffle wiki\'sine de ba\u015fvurabilirsiniz.\nerror-wasm-not-found =\n Ruffle gerekli ".wasm" dosya bile\u015fenini y\xfckleyemedi.\n Sunucu y\xf6neticisi iseniz, l\xfctfen dosyan\u0131n do\u011fru bir \u015fekilde y\xfcklendi\u011finden emin olun.\n Sorun devam ederse, "publicPath" ayar\u0131n\u0131 kullanman\u0131z gerekebilir: yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-mime-type =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Bu web sunucusu, do\u011fru MIME tipinde ".wasm" dosyalar\u0131 sunmuyor.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-swf-fetch =\n Ruffle, Flash SWF dosyas\u0131n\u0131 y\xfckleyemedi.\n Bunun en olas\u0131 nedeni, dosyan\u0131n art\u0131k mevcut olmamas\u0131 ve bu nedenle Ruffle\'\u0131n y\xfckleyece\u011fi hi\xe7bir \u015feyin olmamas\u0131d\u0131r.\n Yard\u0131m i\xe7in web sitesi y\xf6neticisiyle ileti\u015fime ge\xe7meyi deneyin.\nerror-swf-cors =\n Ruffle, Flash SWF dosyas\u0131n\u0131 y\xfckleyemedi.\n Getirme eri\u015fimi muhtemelen CORS politikas\u0131 taraf\u0131ndan engellenmi\u015ftir.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-cors =\n Ruffle gerekli ".wasm" dosya bile\u015fenini y\xfckleyemedi.\n Getirme eri\u015fimi muhtemelen CORS politikas\u0131 taraf\u0131ndan engellenmi\u015ftir.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-invalid =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n G\xf6r\xfcn\xfc\u015fe g\xf6re bu sayfada Ruffle\'\u0131 \xe7al\u0131\u015ft\u0131rmak i\xe7in eksik veya ge\xe7ersiz dosyalar var.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine ba\u015fvurun.\nerror-wasm-download =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Bu genellikle kendi kendine \xe7\xf6z\xfclebilir, bu nedenle sayfay\u0131 yeniden y\xfcklemeyi deneyebilirsiniz.\n Aksi takdirde, l\xfctfen site y\xf6neticisiyle ileti\u015fime ge\xe7in.\nerror-wasm-disabled-on-edge =\n Ruffle gerekli ".wasm" dosya bile\u015fenini y\xfckleyemedi.\n Bunu d\xfczeltmek i\xe7in taray\u0131c\u0131n\u0131z\u0131n ayarlar\u0131n\u0131 a\xe7\u0131n, "Gizlilik, arama ve hizmetler"i t\u0131klay\u0131n, a\u015fa\u011f\u0131 kayd\u0131r\u0131n ve "Web\'de g\xfcvenli\u011finizi art\u0131r\u0131n"\u0131 kapatmay\u0131 deneyin.\n Bu, taray\u0131c\u0131n\u0131z\u0131n gerekli ".wasm" dosyalar\u0131n\u0131 y\xfcklemesine izin verecektir.\n Sorun devam ederse, farkl\u0131 bir taray\u0131c\u0131 kullanman\u0131z gerekebilir.\nerror-javascript-conflict =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n G\xf6r\xfcn\xfc\u015fe g\xf6re bu sayfa, Ruffle ile \xe7ak\u0131\u015fan JavaScript kodu kullan\u0131yor.\n Sunucu y\xf6neticisiyseniz, sizi dosyay\u0131 bo\u015f bir sayfaya y\xfcklemeyi denemeye davet ediyoruz.\nerror-javascript-conflict-outdated = Ayr\u0131ca sorunu giderebilecek daha yeni bir Ruffle s\xfcr\xfcm\xfc y\xfcklemeyi de deneyebilirsiniz (mevcut yap\u0131m eskimi\u015f: { $buildDate }).\nerror-csp-conflict =\n Ruffle, ba\u015flatmaya \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n Bu web sunucusunun \u0130\xe7erik G\xfcvenli\u011fi Politikas\u0131, gerekli ".wasm" bile\u015feninin \xe7al\u0131\u015fmas\u0131na izin vermiyor.\n Sunucu y\xf6neticisiyseniz, yard\u0131m i\xe7in l\xfctfen Ruffle wiki\'sine bak\u0131n.\nerror-unknown =\n Ruffle, bu Flash i\xe7eri\u011fini g\xf6r\xfcnt\xfclemeye \xe7al\u0131\u015f\u0131rken \xf6nemli bir sorunla kar\u015f\u0131la\u015ft\u0131.\n { $outdated ->\n [true] Sunucu y\xf6neticisiyseniz, l\xfctfen Ruffle\'\u0131n daha yeni bir s\xfcr\xfcm\xfcn\xfc y\xfcklemeyi deneyin (mevcut yap\u0131m eskimi\u015f: { $buildDate }).\n *[false] Bunun olmamas\u0131 gerekiyor, bu y\xfczden bir hata bildirebilirseniz \xe7ok memnun oluruz!\n }\n',"save-manager.ftl":"save-delete-prompt = Bu kay\u0131t dosyas\u0131n\u0131 silmek istedi\u011finize emin misiniz?\nsave-reload-prompt =\n Bu kaydetme dosyas\u0131n\u0131 potansiyel \xe7ak\u0131\u015fma olmadan { $action ->\n [delete] silmenin\n *[replace] de\u011fi\u015ftirmenin\n } tek yolu, bu i\xe7eri\u011fi yeniden y\xfcklemektir. Yine de devam etmek istiyor musunuz?\nsave-download = \u0130ndir\nsave-replace = De\u011fi\u015ftir\nsave-delete = Sil\nsave-backup-all = T\xfcm kay\u0131t dosyalar\u0131n\u0131 indir\n"},"zh-CN":{"context_menu.ftl":"context-menu-download-swf = \u4e0b\u8f7d .swf\ncontext-menu-copy-debug-info = \u590d\u5236\u8c03\u8bd5\u4fe1\u606f\ncontext-menu-open-save-manager = \u6253\u5f00\u5b58\u6863\u7ba1\u7406\u5668\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u5173\u4e8e Ruffle \u6269\u5c55 ({ $version })\n *[other] \u5173\u4e8e Ruffle ({ $version })\n }\ncontext-menu-hide = \u9690\u85cf\u6b64\u83dc\u5355\ncontext-menu-exit-fullscreen = \u9000\u51fa\u5168\u5c4f\ncontext-menu-enter-fullscreen = \u8fdb\u5165\u5168\u5c4f\n","messages.ftl":'message-cant-embed =\n Ruffle \u65e0\u6cd5\u8fd0\u884c\u5d4c\u5165\u5728\u6b64\u9875\u9762\u4e2d\u7684 Flash\u3002\n \u60a8\u53ef\u4ee5\u5c1d\u8bd5\u5728\u5355\u72ec\u7684\u6807\u7b7e\u9875\u4e2d\u6253\u5f00\u8be5\u6587\u4ef6\uff0c\u4ee5\u56de\u907f\u6b64\u95ee\u9898\u3002\npanic-title = \u51fa\u4e86\u4e9b\u95ee\u9898 :(\nmore-info = \u66f4\u591a\u4fe1\u606f\nrun-anyway = \u4ecd\u7136\u8fd0\u884c\ncontinue = \u7ee7\u7eed\nreport-bug = \u53cd\u9988\u95ee\u9898\nupdate-ruffle = \u66f4\u65b0 Ruffle\nruffle-demo = \u7f51\u9875\u6f14\u793a\nruffle-desktop = \u684c\u9762\u5e94\u7528\u7a0b\u5e8f\nruffle-wiki = \u67e5\u770b Ruffle Wiki\nview-error-details = \u67e5\u770b\u9519\u8bef\u8be6\u60c5\nopen-in-new-tab = \u5728\u65b0\u6807\u7b7e\u9875\u4e2d\u6253\u5f00\nclick-to-unmute = \u70b9\u51fb\u53d6\u6d88\u9759\u97f3\nerror-file-protocol =\n \u770b\u6765\u60a8\u6b63\u5728 "file:" \u534f\u8bae\u4e0a\u4f7f\u7528 Ruffle\u3002\n \u7531\u4e8e\u6d4f\u89c8\u5668\u4ee5\u5b89\u5168\u539f\u56e0\u963b\u6b62\u8bb8\u591a\u529f\u80fd\uff0c\u56e0\u6b64\u8fd9\u4e0d\u8d77\u4f5c\u7528\u3002\n \u76f8\u53cd\u6211\u4eec\u9080\u8bf7\u60a8\u8bbe\u7f6e\u672c\u5730\u670d\u52a1\u5668\u6216\u4f7f\u7528\u7f51\u9875\u6f14\u793a\u6216\u684c\u9762\u5e94\u7528\u7a0b\u5e8f\u3002\nerror-javascript-config =\n \u7531\u4e8e\u9519\u8bef\u7684 JavaScript \u914d\u7f6e\uff0cRuffle \u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u6211\u4eec\u9080\u8bf7\u60a8\u68c0\u67e5\u9519\u8bef\u8be6\u7ec6\u4fe1\u606f\uff0c\u4ee5\u627e\u51fa\u54ea\u4e2a\u53c2\u6570\u6709\u6545\u969c\u3002\n \u60a8\u4e5f\u53ef\u4ee5\u67e5\u9605 Ruffle \u7684 Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-not-found =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d\u6240\u9700\u7684 \u201c.wasm\u201d \u6587\u4ef6\u7ec4\u4ef6\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u786e\u4fdd\u6587\u4ef6\u5df2\u6b63\u786e\u4e0a\u4f20\u3002\n \u5982\u679c\u95ee\u9898\u4ecd\u7136\u5b58\u5728\uff0c\u60a8\u53ef\u80fd\u9700\u8981\u4f7f\u7528 \u201cpublicPath\u201d \u8bbe\u7f6e\uff1a\u8bf7\u67e5\u770b Ruffle \u7684 Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-mime-type =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8be5\u7f51\u7ad9\u670d\u52a1\u5668\u6ca1\u6709\u63d0\u4f9b ".asm\u201d \u6587\u4ef6\u6b63\u786e\u7684 MIME \u7c7b\u578b\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-swf-fetch =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d Flash SWF \u6587\u4ef6\u3002\n \u6700\u53ef\u80fd\u7684\u539f\u56e0\u662f\u6587\u4ef6\u4e0d\u518d\u5b58\u5728\u6240\u4ee5 Ruffle \u6ca1\u6709\u8981\u52a0\u8f7d\u7684\u5185\u5bb9\u3002\n \u8bf7\u5c1d\u8bd5\u8054\u7cfb\u7f51\u7ad9\u7ba1\u7406\u5458\u5bfb\u6c42\u5e2e\u52a9\u3002\nerror-swf-cors =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d Flash SWF \u6587\u4ef6\u3002\n \u83b7\u53d6\u6743\u9650\u53ef\u80fd\u88ab CORS \u7b56\u7565\u963b\u6b62\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u53c2\u8003 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-cors =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d\u6240\u9700\u7684\u201c.wasm\u201d\u6587\u4ef6\u7ec4\u4ef6\u3002\n \u83b7\u53d6\u6743\u9650\u53ef\u80fd\u88ab CORS \u7b56\u7565\u963b\u6b62\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-invalid =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8fd9\u4e2a\u9875\u9762\u4f3c\u4e4e\u7f3a\u5c11\u6587\u4ef6\u6765\u8fd0\u884c Curl\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-wasm-download =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8fd9\u901a\u5e38\u53ef\u4ee5\u81ea\u884c\u89e3\u51b3\uff0c\u56e0\u6b64\u60a8\u53ef\u4ee5\u5c1d\u8bd5\u91cd\u65b0\u52a0\u8f7d\u9875\u9762\u3002\n \u5426\u5219\u8bf7\u8054\u7cfb\u7f51\u7ad9\u7ba1\u7406\u5458\u3002\nerror-wasm-disabled-on-edge =\n Ruffle \u65e0\u6cd5\u52a0\u8f7d\u6240\u9700\u7684 \u201c.wasm\u201d \u6587\u4ef6\u7ec4\u4ef6\u3002\n \u8981\u89e3\u51b3\u8fd9\u4e2a\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6253\u5f00\u60a8\u7684\u6d4f\u89c8\u5668\u8bbe\u7f6e\uff0c\u5355\u51fb"\u9690\u79c1\u3001\u641c\u7d22\u548c\u670d\u52a1"\uff0c\u5411\u4e0b\u6eda\u52a8\u5e76\u5173\u95ed"\u589e\u5f3a\u60a8\u7684\u7f51\u7edc\u5b89\u5168"\u3002\n \u8fd9\u5c06\u5141\u8bb8\u60a8\u7684\u6d4f\u89c8\u5668\u52a0\u8f7d\u6240\u9700\u7684 \u201c.wasm\u201d \u6587\u4ef6\u3002\n \u5982\u679c\u95ee\u9898\u4ecd\u7136\u5b58\u5728\uff0c\u60a8\u53ef\u80fd\u5fc5\u987b\u4f7f\u7528\u4e0d\u540c\u7684\u6d4f\u89c8\u5668\u3002\nerror-javascript-conflict =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8fd9\u4e2a\u9875\u9762\u4f3c\u4e4e\u4f7f\u7528\u4e86\u4e0e Ruffle \u51b2\u7a81\u7684 JavaScript \u4ee3\u7801\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u6211\u4eec\u5efa\u8bae\u60a8\u5c1d\u8bd5\u5728\u7a7a\u767d\u9875\u9762\u4e0a\u52a0\u8f7d\u6587\u4ef6\u3002\nerror-javascript-conflict-outdated = \u60a8\u8fd8\u53ef\u4ee5\u5c1d\u8bd5\u4e0a\u4f20\u53ef\u80fd\u89c4\u907f\u8be5\u95ee\u9898\u7684\u6700\u65b0\u7248\u672c\u7684 (\u5f53\u524d\u6784\u5efa\u5df2\u8fc7\u65f6: { $buildDate })\u3002\nerror-csp-conflict =\n Ruffle \u5728\u8bd5\u56fe\u521d\u59cb\u5316\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n \u8be5\u7f51\u7ad9\u670d\u52a1\u5668\u7684\u5185\u5bb9\u5b89\u5168\u7b56\u7565\u4e0d\u5141\u8bb8\u8fd0\u884c\u6240\u9700\u7684 \u201c.wasm\u201d \u7ec4\u4ef6\u3002\n \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u67e5\u9605 Ruffle Wiki \u83b7\u53d6\u5e2e\u52a9\u3002\nerror-unknown =\n Ruffle \u5728\u8bd5\u56fe\u663e\u793a\u6b64 Flash \u5185\u5bb9\u65f6\u9047\u5230\u4e86\u4e00\u4e2a\u91cd\u5927\u95ee\u9898\u3002\n { $outdated ->\n [true] \u5982\u679c\u60a8\u662f\u670d\u52a1\u5668\u7ba1\u7406\u5458\uff0c\u8bf7\u5c1d\u8bd5\u4e0a\u4f20\u66f4\u65b0\u7684 Ruffle \u7248\u672c (\u5f53\u524d\u7248\u672c\u5df2\u8fc7\u65f6: { $buildDate }).\n *[false] \u8fd9\u4e0d\u5e94\u8be5\u53d1\u751f\uff0c\u56e0\u6b64\u5982\u679c\u60a8\u53ef\u4ee5\u62a5\u544a\u9519\u8bef\uff0c\u6211\u4eec\u5c06\u975e\u5e38\u611f\u8c22\uff01\n }\n',"save-manager.ftl":"save-delete-prompt = \u786e\u5b9a\u8981\u5220\u9664\u6b64\u5b58\u6863\u5417\uff1f\nsave-reload-prompt =\n \u4e3a\u4e86\u907f\u514d\u6f5c\u5728\u7684\u51b2\u7a81\uff0c{ $action ->\n [delete] \u5220\u9664\n *[replace] \u66ff\u6362\n } \u6b64\u5b58\u6863\u6587\u4ef6\u9700\u8981\u91cd\u65b0\u52a0\u8f7d\u5f53\u524d\u5185\u5bb9\u3002\u662f\u5426\u4ecd\u7136\u7ee7\u7eed\uff1f\nsave-download = \u4e0b\u8f7d\nsave-replace = \u66ff\u6362\nsave-delete = \u5220\u9664\nsave-backup-all = \u4e0b\u8f7d\u6240\u6709\u5b58\u6863\u6587\u4ef6\n"},"zh-TW":{"context_menu.ftl":"context-menu-download-swf = \u4e0b\u8f09SWF\u6a94\u6848\ncontext-menu-copy-debug-info = \u8907\u88fd\u9664\u932f\u8cc7\u8a0a\ncontext-menu-open-save-manager = \u6253\u958b\u5b58\u6a94\u7ba1\u7406\u5668\ncontext-menu-about-ruffle =\n { $flavor ->\n [extension] \u95dc\u65bcRuffle\u64f4\u5145\u529f\u80fd ({ $version })\n *[other] \u95dc\u65bcRuffle ({ $version })\n }\ncontext-menu-hide = \u96b1\u85cf\u83dc\u55ae\ncontext-menu-exit-fullscreen = \u9000\u51fa\u5168\u87a2\u5e55\ncontext-menu-enter-fullscreen = \u9032\u5165\u5168\u87a2\u5e55\n","messages.ftl":'message-cant-embed =\n \u76ee\u524dRuffle\u6c92\u8fa6\u6cd5\u57f7\u884c\u5d4c\u5165\u5f0fFlash\u3002\n \u4f60\u53ef\u4ee5\u5728\u65b0\u5206\u9801\u4e2d\u958b\u555f\u4f86\u89e3\u6c7a\u9019\u500b\u554f\u984c\u3002\npanic-title = \u5b8c\u86cb\uff0c\u51fa\u554f\u984c\u4e86 :(\nmore-info = \u66f4\u591a\u8cc7\u8a0a\nrun-anyway = \u76f4\u63a5\u57f7\u884c\ncontinue = \u7e7c\u7e8c\nreport-bug = \u56de\u5831BUG\nupdate-ruffle = \u66f4\u65b0Ruffle\nruffle-demo = \u7db2\u9801\u5c55\u793a\nruffle-desktop = \u684c\u9762\u61c9\u7528\u7a0b\u5f0f\nruffle-wiki = \u67e5\u770bRuffle Wiki\nview-error-details = \u6aa2\u8996\u932f\u8aa4\u8a73\u7d30\u8cc7\u6599\nopen-in-new-tab = \u958b\u555f\u65b0\u589e\u5206\u9801\nclick-to-unmute = \u9ede\u64ca\u4ee5\u53d6\u6d88\u975c\u97f3\nerror-file-protocol =\n \u770b\u8d77\u4f86\u4f60\u60f3\u8981\u7528Ruffle\u4f86\u57f7\u884c"file:"\u7684\u5354\u8b70\u3002\n \u56e0\u70ba\u700f\u89bd\u5668\u7981\u4e86\u5f88\u591a\u529f\u80fd\u4ee5\u8cc7\u5b89\u7684\u7406\u7531\u4f86\u8b1b\u3002\n \u6211\u5011\u5efa\u8b70\u4f60\u5efa\u7acb\u672c\u5730\u4f3a\u670d\u5668\u6216\u8457\u76f4\u63a5\u4f7f\u7528\u7db2\u9801\u5c55\u793a\u6216\u684c\u9762\u61c9\u7528\u7a0b\u5f0f\u3002\nerror-javascript-config =\n \u76ee\u524dRuffle\u9047\u5230\u4e0d\u6b63\u78ba\u7684JavaScript\u914d\u7f6e\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u6211\u5011\u5efa\u8b70\u4f60\u6aa2\u67e5\u54ea\u500b\u74b0\u7bc0\u51fa\u932f\u3002\n \u6216\u8457\u4f60\u53ef\u4ee5\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-not-found =\n \u76ee\u524dRuffle\u627e\u4e0d\u5230".wasm"\u6a94\u6848\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u78ba\u4fdd\u6a94\u6848\u662f\u5426\u653e\u5c0d\u4f4d\u7f6e\u3002\n \u5982\u679c\u9084\u662f\u6709\u554f\u984c\u7684\u8a71\uff0c\u4f60\u8981\u7528"publicPath"\u4f86\u8a2d\u5b9a: \u6216\u8457\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-mime-type =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u9019\u7db2\u9801\u4f3a\u670d\u5668\u4e26\u6c92\u6709\u670d\u52d9".wasm"\u6a94\u6848\u6216\u6b63\u78ba\u7684\u7db2\u969b\u7db2\u8def\u5a92\u9ad4\u985e\u578b\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-swf-fetch =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6Flash\u7684SWF\u6a94\u6848\u3002\n \u5f88\u6709\u53ef\u80fd\u8981\u8b80\u53d6\u7684\u6a94\u6848\u4e0d\u5b58\u5728\uff0c\u6240\u4ee5Ruffle\u8b80\u4e0d\u5230\u6771\u897f\u3002\n \u8acb\u5617\u8a66\u6e9d\u901a\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-swf-cors =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6Flash\u7684SWF\u6a94\u6848\u3002\n \u770b\u8d77\u4f86\u662f\u4f7f\u7528\u6b0a\u88ab\u8de8\u4f86\u6e90\u8cc7\u6e90\u5171\u7528\u6a5f\u5236\u88ab\u64cb\u5230\u4e86\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-cors =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6".wasm"\u6a94\u6848\u3002\n \u770b\u8d77\u4f86\u662f\u4f7f\u7528\u6b0a\u88ab\u8de8\u4f86\u6e90\u8cc7\u6e90\u5171\u7528\u6a5f\u5236\u88ab\u64cb\u5230\u4e86\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-invalid =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u770b\u8d77\u4f86\u9019\u7db2\u9801\u6709\u7f3a\u5931\u6a94\u6848\u5c0e\u81f4Ruffle\u7121\u6cd5\u904b\u884c\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-download =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u9019\u53ef\u4ee5\u4f60\u81ea\u5df1\u89e3\u6c7a\uff0c\u4f60\u53ea\u8981\u91cd\u65b0\u6574\u7406\u5c31\u597d\u4e86\u3002\n \u5426\u5247\uff0c\u8acb\u5617\u8a66\u6e9d\u901a\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-wasm-disabled-on-edge =\n \u76ee\u524dRuffle\u7121\u6cd5\u8b80\u53d6".wasm"\u6a94\u6848\u3002\n \u8981\u4fee\u6b63\u7684\u8a71\uff0c\u6253\u958b\u4f60\u7684\u700f\u89bd\u5668\u8a2d\u5b9a\uff0c\u9ede\u9078"\u96b1\u79c1\u6b0a\u3001\u641c\u5c0b\u8207\u670d\u52d9"\uff0c\u628a"\u9632\u6b62\u8ffd\u8e64"\u7d66\u95dc\u6389\u3002\n \u9019\u6a23\u4e00\u4f86\u4f60\u7684\u700f\u89bd\u5668\u6703\u8b80\u53d6\u9700\u8981\u7684".wasm"\u6a94\u6848\u3002\n \u5982\u679c\u554f\u984c\u4e00\u76f4\u9084\u5728\u7684\u8a71\uff0c\u4f60\u5fc5\u9808\u8981\u63db\u700f\u89bd\u5668\u4e86\u3002\nerror-javascript-conflict =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u770b\u8d77\u4f86\u9019\u7db2\u9801\u4f7f\u7528\u7684JavaScript\u6703\u8ddfRuffle\u8d77\u885d\u7a81\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u6211\u5011\u5efa\u8b70\u4f60\u958b\u500b\u7a7a\u767d\u9801\u4f86\u6e2c\u8a66\u3002\nerror-javascript-conflict-outdated = \u4f60\u4e5f\u53ef\u4ee5\u4e0a\u50b3\u6700\u65b0\u7248\u7684Ruffle\uff0c\u8aaa\u4e0d\u5b9a\u4f60\u8981\u8aaa\u7684\u7684\u554f\u984c\u5df2\u7d93\u4e0d\u898b\u4e86(\u73fe\u5728\u4f7f\u7528\u7684\u7248\u672c\u5df2\u7d93\u904e\u6642: { $buildDate })\u3002\nerror-csp-conflict =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\u3002\n \u9019\u7db2\u9801\u4f3a\u670d\u5668\u88ab\u8de8\u4f86\u6e90\u8cc7\u6e90\u5171\u7528\u6a5f\u5236\u7981\u6b62\u8b80\u53d6".wasm"\u6a94\u6848\u3002\n \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c\u8acb\u67e5\u8a62Ruffle wiki\u5f97\u5230\u9700\u6c42\u5e6b\u52a9\u3002\nerror-unknown =\n \u76ee\u524dRuffle\u521d\u59cb\u5316\u8981\u8b80\u53d6Flash\u5167\u5bb9\u6642\u9047\u5230\u91cd\u5927\u554f\u984c\n { $outdated ->\n [true] \u5982\u679c\u4f60\u662f\u4f3a\u670d\u5668\u7ba1\u7406\u54e1\uff0c \u8acb\u4e0a\u50b3\u6700\u65b0\u7248\u7684Ruffle(\u73fe\u5728\u4f7f\u7528\u7684\u7248\u672c\u5df2\u7d93\u904e\u6642: { $buildDate }).\n *[false] \u9019\u4e0d\u61c9\u8a72\u767c\u751f\u7684\uff0c\u6211\u5011\u4e5f\u5f88\u9ad8\u8208\u4f60\u544a\u77e5bug!\n }\n',"save-manager.ftl":"save-delete-prompt = \u4f60\u78ba\u5b9a\u8981\u522a\u9664\u9019\u500b\u5b58\u6a94\u55ce\uff1f\nsave-reload-prompt =\n \u552f\u4e00\u65b9\u6cd5\u53ea\u6709 { $action ->\n [delete] \u522a\u9664\n *[replace] \u53d6\u4ee3\n } \u9019\u500b\u5b58\u6a94\u4e0d\u6703\u5b8c\u5168\u53d6\u4ee3\u76f4\u5230\u91cd\u65b0\u555f\u52d5. \u4f60\u9700\u8981\u7e7c\u7e8c\u55ce?\nsave-download = \u4e0b\u8f09\nsave-replace = \u53d6\u4ee3\nsave-delete = \u522a\u9664\nsave-backup-all = \u4e0b\u8f09\u6240\u6709\u5b58\u6a94\u6a94\u6848\u3002\n"}},je={};for(const[e,n]of Object.entries(Ee)){const t=new K(e);if(n)for(const[r,a]of Object.entries(n))if(a)for(const n of t.addResource(new ke(a)))console.error(`Error in text for ${e} ${r}: ${n}`);je[e]=t}function Ae(e,n,t){const r=je[e];if(void 0!==r){const e=r.getMessage(n);if(void 0!==e&&e.value)return r.formatPattern(e.value,t)}return null}function Ce(e,n){const t=ze(navigator.languages,Object.keys(je),{defaultLocale:"en-US"});for(const r in t){const a=Ae(t[r],e,n);if(a)return a}return console.error(`Unknown text key '${e}'`),e}function Ie(e,n){const t=document.createElement("div");return Ce(e,n).split("\n").forEach((e=>{const n=document.createElement("p");n.innerText=e,t.appendChild(n)})),t.innerHTML}var Oe=a(297),De=a.n(Oe);const Fe="https://ruffle.rs",Pe=/^\s*(\d+(\.\d+)?(%)?)/;let Te=!1;function $e(e){if(null==e)return{};e instanceof URLSearchParams||(e=new URLSearchParams(e));const n={};for(const[t,r]of e)n[t]=r.toString();return n}class Be{constructor(e,n){this.x=e,this.y=n}distanceTo(e){const n=e.x-this.x,t=e.y-this.y;return Math.sqrt(n*n+t*t)}}class Me extends HTMLElement{get readyState(){return this._readyState}get metadata(){return this._metadata}constructor(){super(),this.contextMenuForceDisabled=!1,this.isTouch=!1,this.contextMenuSupported=!1,this.panicked=!1,this.rendererDebugInfo="",this.longPressTimer=null,this.pointerDownPosition=null,this.pointerMoveMaxDistance=0,this.config={},this.shadow=this.attachShadow({mode:"open"}),this.shadow.appendChild(p.content.cloneNode(!0)),this.dynamicStyles=this.shadow.getElementById("dynamic_styles"),this.container=this.shadow.getElementById("container"),this.playButton=this.shadow.getElementById("play_button"),this.playButton.addEventListener("click",(()=>this.play())),this.unmuteOverlay=this.shadow.getElementById("unmute_overlay"),this.splashScreen=this.shadow.getElementById("splash-screen"),this.virtualKeyboard=this.shadow.getElementById("virtual-keyboard"),this.virtualKeyboard.addEventListener("input",this.virtualKeyboardInput.bind(this)),this.saveManager=this.shadow.getElementById("save-manager"),this.saveManager.addEventListener("click",(()=>this.saveManager.classList.add("hidden")));const e=this.saveManager.querySelector("#modal-area");e&&e.addEventListener("click",(e=>e.stopPropagation()));const n=this.saveManager.querySelector("#close-modal");n&&n.addEventListener("click",(()=>this.saveManager.classList.add("hidden")));const t=this.saveManager.querySelector("#backup-saves");t&&(t.addEventListener("click",this.backupSaves.bind(this)),t.innerText=Ce("save-backup-all"));const r=this.unmuteOverlay.querySelector("#unmute_overlay_svg");if(r){r.querySelector("#unmute_text").textContent=Ce("click-to-unmute")}this.contextMenuOverlay=this.shadow.getElementById("context-menu-overlay"),this.contextMenuElement=this.shadow.getElementById("context-menu"),window.addEventListener("pointerdown",this.checkIfTouch.bind(this)),this.addEventListener("contextmenu",this.showContextMenu.bind(this)),this.container.addEventListener("pointerdown",this.pointerDown.bind(this)),this.container.addEventListener("pointermove",this.checkLongPressMovement.bind(this)),this.container.addEventListener("pointerup",this.checkLongPress.bind(this)),this.container.addEventListener("pointercancel",this.clearLongPressTimer.bind(this)),this.addEventListener("fullscreenchange",this.fullScreenChange.bind(this)),this.addEventListener("webkitfullscreenchange",this.fullScreenChange.bind(this)),this.instance=null,this.onFSCommand=null,this._readyState=0,this._metadata=null,this.lastActivePlayingState=!1,this.setupPauseOnTabHidden()}setupPauseOnTabHidden(){document.addEventListener("visibilitychange",(()=>{this.instance&&(document.hidden&&(this.lastActivePlayingState=this.instance.is_playing(),this.instance.pause()),document.hidden||!0!==this.lastActivePlayingState||this.instance.play())}),!1)}connectedCallback(){this.updateStyles()}static get observedAttributes(){return["width","height"]}attributeChangedCallback(e,n,t){"width"!==e&&"height"!==e||this.updateStyles()}disconnectedCallback(){this.destroy()}updateStyles(){if(this.dynamicStyles.sheet){if(this.dynamicStyles.sheet.rules)for(let e=0;e{if(console.error(`Serious error loading Ruffle: ${e}`),"file:"===window.location.protocol)e.ruffleIndexError=2;else{e.ruffleIndexError=9;const n=String(e.message).toLowerCase();n.includes("mime")?e.ruffleIndexError=8:n.includes("networkerror")||n.includes("failed to fetch")?e.ruffleIndexError=6:n.includes("disallowed by embedder")?e.ruffleIndexError=1:"CompileError"===e.name?e.ruffleIndexError=3:n.includes("could not download wasm module")&&"TypeError"===e.name?e.ruffleIndexError=7:"TypeError"===e.name?e.ruffleIndexError=5:navigator.userAgent.includes("Edg")&&n.includes("webassembly is not defined")&&(e.ruffleIndexError=10)}throw this.panic(e),e}));this.instance=await new n(this.container,this,this.loadedConfig),this.rendererDebugInfo=this.instance.renderer_debug_info();const t=this.instance.renderer_name();if(console.log("%cNew Ruffle instance created (WebAssembly extensions: "+(n.is_wasm_simd_used()?"ON":"OFF")+" | Used renderer: "+(null!=t?t:"")+")","background: #37528C; color: #FFAD33"),"running"!==this.audioState()&&(this.container.style.visibility="hidden",await new Promise((e=>{window.setTimeout((()=>{e()}),200)})),this.container.style.visibility=""),this.unmuteAudioContext(),navigator.userAgent.toLowerCase().includes("android")&&this.container.addEventListener("click",(()=>this.virtualKeyboard.blur())),!this.loadedConfig||"on"===this.loadedConfig.autoplay||"off"!==this.loadedConfig.autoplay&&"running"===this.audioState()){if(this.play(),"running"!==this.audioState()){this.loadedConfig&&"hidden"===this.loadedConfig.unmuteOverlay||(this.unmuteOverlay.style.display="block"),this.container.addEventListener("click",this.unmuteOverlayClicked.bind(this),{once:!0});const n=null===(e=this.instance)||void 0===e?void 0:e.audio_context();n&&(n.onstatechange=()=>{"running"===n.state&&this.unmuteOverlayClicked(),n.onstatechange=null})}}else this.playButton.style.display="block"}onRuffleDownloadProgress(e,n){const t=this.splashScreen.querySelector(".loadbar-inner"),r=this.splashScreen.querySelector(".loadbar");Number.isNaN(n)?r&&(r.style.display="none"):t.style.width=e/n*100+"%"}destroy(){this.instance&&(this.instance.destroy(),this.instance=null,this._metadata=null,this._readyState=0,console.log("Ruffle instance destroyed."))}checkOptions(e){if("string"==typeof e)return{url:e};const n=(e,n)=>{if(!e){const e=new TypeError(n);throw e.ruffleIndexError=4,this.panic(e),e}};return n(null!==e&&"object"==typeof e,"Argument 0 must be a string or object"),n("url"in e||"data"in e,"Argument 0 must contain a `url` or `data` key"),n(!("url"in e)||"string"==typeof e.url,"`url` must be a string"),e}getExtensionConfig(){var e;return window.RufflePlayer&&window.RufflePlayer.conflict&&("extension"===window.RufflePlayer.conflict.newestName||"extension"===window.RufflePlayer.newestName)?null===(e=window.RufflePlayer)||void 0===e?void 0:e.conflict.config:{}}async load(e){var n,t;if(e=this.checkOptions(e),this.isConnected&&!this.isUnusedFallbackObject()){if(!We(this))try{const r=this.getExtensionConfig();this.loadedConfig=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},b),r),null!==(t=null===(n=window.RufflePlayer)||void 0===n?void 0:n.config)&&void 0!==t?t:{}),this.config),e),this.loadedConfig.backgroundColor&&"transparent"!==this.loadedConfig.wmode&&(this.container.style.backgroundColor=this.loadedConfig.backgroundColor),await this.ensureFreshInstance(),"url"in e?(console.log(`Loading SWF file ${e.url}`),this.swfUrl=new URL(e.url,document.baseURI),this.instance.stream_from(this.swfUrl.href,$e(e.parameters))):"data"in e&&(console.log("Loading SWF data"),this.instance.load_data(new Uint8Array(e.data),$e(e.parameters),e.swfFileName||"movie.swf"))}catch(e){console.error(`Serious error occurred loading SWF file: ${e}`);const n=new Error(e);throw n.message.includes("Error parsing config")&&(n.ruffleIndexError=4),this.panic(n),n}}else console.warn("Ignoring attempt to play a disconnected or suspended Ruffle element")}play(){this.instance&&(this.instance.play(),this.playButton.style.display="none")}get isPlaying(){return!!this.instance&&this.instance.is_playing()}get volume(){return this.instance?this.instance.volume():1}set volume(e){this.instance&&this.instance.set_volume(e)}get fullscreenEnabled(){return!(!document.fullscreenEnabled&&!document.webkitFullscreenEnabled)}get isFullscreen(){return(document.fullscreenElement||document.webkitFullscreenElement)===this}setFullscreen(e){this.fullscreenEnabled&&(e?this.enterFullscreen():this.exitFullscreen())}enterFullscreen(){const e={navigationUI:"hide"};this.requestFullscreen?this.requestFullscreen(e):this.webkitRequestFullscreen?this.webkitRequestFullscreen(e):this.webkitRequestFullScreen&&this.webkitRequestFullScreen(e)}exitFullscreen(){document.exitFullscreen?document.exitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.webkitCancelFullScreen&&document.webkitCancelFullScreen()}fullScreenChange(){var e;null===(e=this.instance)||void 0===e||e.set_fullscreen(this.isFullscreen)}saveFile(e,n){const t=URL.createObjectURL(e),r=document.createElement("a");r.href=t,r.style.display="none",r.download=n,document.body.appendChild(r),r.click(),document.body.removeChild(r),URL.revokeObjectURL(t)}checkIfTouch(e){this.isTouch="touch"===e.pointerType||"pen"===e.pointerType}base64ToBlob(e,n){const t=atob(e),r=new ArrayBuffer(t.length),a=new Uint8Array(r);for(let e=0;e{if(r.result&&"string"==typeof r.result){const e=new RegExp("data:.*;base64,"),t=r.result.replace(e,"");this.confirmReloadSave(n,t,!0)}})),t&&t.files&&t.files.length>0&&t.files[0]&&r.readAsDataURL(t.files[0])}deleteSave(e){const n=localStorage.getItem(e);n&&this.confirmReloadSave(e,n,!1)}populateSaves(){const e=this.saveManager.querySelector("#local-saves");if(e){try{if(null===localStorage)return}catch(e){return}e.textContent="",Object.keys(localStorage).forEach((n=>{const t=n.split("/").pop(),r=localStorage.getItem(n);if(t&&r&&this.isB64SOL(r)){const a=document.createElement("TR"),i=document.createElement("TD");i.textContent=t,i.title=n;const o=document.createElement("TD"),s=document.createElement("SPAN");s.textContent=Ce("save-download"),s.className="save-option",s.addEventListener("click",(()=>{const e=this.base64ToBlob(r,"application/octet-stream");this.saveFile(e,t+".sol")})),o.appendChild(s);const l=document.createElement("TD"),u=document.createElement("INPUT");u.type="file",u.accept=".sol",u.className="replace-save",u.id="replace-save-"+n;const c=document.createElement("LABEL");c.htmlFor="replace-save-"+n,c.textContent=Ce("save-replace"),c.className="save-option",u.addEventListener("change",(e=>this.replaceSOL(e,n))),l.appendChild(u),l.appendChild(c);const d=document.createElement("TD"),f=document.createElement("SPAN");f.textContent=Ce("save-delete"),f.className="save-option",f.addEventListener("click",(()=>this.deleteSave(n))),d.appendChild(f),a.appendChild(i),a.appendChild(o),a.appendChild(l),a.appendChild(d),e.appendChild(a)}}))}}async backupSaves(){const e=new(De()),n=[];Object.keys(localStorage).forEach((t=>{let r=String(t.split("/").pop());const a=localStorage.getItem(t);if(a&&this.isB64SOL(a)){const t=this.base64ToBlob(a,"application/octet-stream"),i=n.filter((e=>e===r)).length;n.push(r),i>0&&(r+=` (${i+1})`),e.file(r+".sol",t)}}));const t=await e.generateAsync({type:"blob"});this.saveFile(t,"saves.zip")}openSaveManager(){this.saveManager.classList.remove("hidden")}async downloadSwf(){try{if(this.swfUrl){console.log("Downloading SWF: "+this.swfUrl);const e=await fetch(this.swfUrl.href);if(!e.ok)return void console.error("SWF download failed");const n=await e.blob();this.saveFile(n,function(e){const n=e.pathname;return n.substring(n.lastIndexOf("/")+1)}(this.swfUrl))}else console.error("SWF download failed")}catch(e){console.error("SWF download failed")}}virtualKeyboardInput(){const e=this.virtualKeyboard,n=e.value;for(const e of n)for(const n of["keydown","keyup"])this.dispatchEvent(new KeyboardEvent(n,{key:e,bubbles:!0}));e.value=""}openVirtualKeyboard(){navigator.userAgent.toLowerCase().includes("android")?setTimeout((()=>{this.virtualKeyboard.focus({preventScroll:!0})}),100):this.virtualKeyboard.focus({preventScroll:!0})}isVirtualKeyboardFocused(){return this.shadow.activeElement===this.virtualKeyboard}contextMenuItems(){const e=String.fromCharCode(10003),n=[],t=()=>{n.length>0&&null!==n[n.length-1]&&n.push(null)};if(this.instance&&this.isPlaying){this.instance.prepare_context_menu().forEach(((r,a)=>{r.separatorBefore&&t(),n.push({text:r.caption+(r.checked?` (${e})`:""),onClick:()=>{var e;return null===(e=this.instance)||void 0===e?void 0:e.run_context_menu_callback(a)},enabled:r.enabled})})),t()}this.fullscreenEnabled&&(this.isFullscreen?n.push({text:Ce("context-menu-exit-fullscreen"),onClick:()=>{var e;return null===(e=this.instance)||void 0===e?void 0:e.set_fullscreen(!1)}}):n.push({text:Ce("context-menu-enter-fullscreen"),onClick:()=>{var e;return null===(e=this.instance)||void 0===e?void 0:e.set_fullscreen(!0)}})),this.instance&&this.swfUrl&&this.loadedConfig&&!0===this.loadedConfig.showSwfDownload&&(t(),n.push({text:Ce("context-menu-download-swf"),onClick:this.downloadSwf.bind(this)})),window.isSecureContext&&n.push({text:Ce("context-menu-copy-debug-info"),onClick:()=>navigator.clipboard.writeText(this.getPanicData())}),this.populateSaves();const r=this.saveManager.querySelector("#local-saves");return r&&""!==r.textContent&&n.push({text:Ce("context-menu-open-save-manager"),onClick:this.openSaveManager.bind(this)}),t(),n.push({text:Ce("context-menu-about-ruffle",{flavor:d?"extension":"",version:S}),onClick(){window.open(Fe,"_blank")}}),this.isTouch&&(t(),n.push({text:Ce("context-menu-hide"),onClick:()=>this.contextMenuForceDisabled=!0})),n}pointerDown(e){this.pointerDownPosition=new Be(e.pageX,e.pageY),this.pointerMoveMaxDistance=0,this.startLongPressTimer()}clearLongPressTimer(){this.longPressTimer&&(clearTimeout(this.longPressTimer),this.longPressTimer=null)}startLongPressTimer(){this.clearLongPressTimer(),this.longPressTimer=setTimeout((()=>this.clearLongPressTimer()),800)}checkLongPressMovement(e){if(null!==this.pointerDownPosition){const n=new Be(e.pageX,e.pageY),t=this.pointerDownPosition.distanceTo(n);t>this.pointerMoveMaxDistance&&(this.pointerMoveMaxDistance=t)}}checkLongPress(e){this.longPressTimer?this.clearLongPressTimer():!this.contextMenuSupported&&"mouse"!==e.pointerType&&this.pointerMoveMaxDistance<15&&this.showContextMenu(e)}showContextMenu(e){var n,t,r;if(this.panicked||!this.saveManager.classList.contains("hidden"))return;if(e.preventDefault(),"contextmenu"===e.type?(this.contextMenuSupported=!0,window.addEventListener("click",this.hideContextMenu.bind(this),{once:!0})):(window.addEventListener("pointerup",this.hideContextMenu.bind(this),{once:!0}),e.stopPropagation()),[!1,"off"].includes(null!==(t=null===(n=this.loadedConfig)||void 0===n?void 0:n.contextMenu)&&void 0!==t?t:"on")||this.isTouch&&"rightClickOnly"===(null===(r=this.loadedConfig)||void 0===r?void 0:r.contextMenu)||this.contextMenuForceDisabled)return;for(;this.contextMenuElement.firstChild;)this.contextMenuElement.removeChild(this.contextMenuElement.firstChild);for(const e of this.contextMenuItems())if(null===e){const e=document.createElement("li");e.className="menu_separator";const n=document.createElement("hr");e.appendChild(n),this.contextMenuElement.appendChild(e)}else{const{text:n,onClick:t,enabled:r}=e,a=document.createElement("li");a.className="menu_item",a.textContent=n,this.contextMenuElement.appendChild(a),!1!==r?a.addEventListener(this.contextMenuSupported?"click":"pointerup",t):a.classList.add("disabled")}this.contextMenuElement.style.left="0",this.contextMenuElement.style.top="0",this.contextMenuOverlay.classList.remove("hidden");const a=this.getBoundingClientRect(),i=e.clientX-a.x,o=e.clientY-a.y,s=a.width-this.contextMenuElement.clientWidth-1,l=a.height-this.contextMenuElement.clientHeight-1;this.contextMenuElement.style.left=Math.floor(Math.min(i,s))+"px",this.contextMenuElement.style.top=Math.floor(Math.min(o,l))+"px"}hideContextMenu(){var e;null===(e=this.instance)||void 0===e||e.clear_custom_menu_items(),this.contextMenuOverlay.classList.add("hidden")}pause(){this.instance&&(this.instance.pause(),this.playButton.style.display="block")}audioState(){if(this.instance){const e=this.instance.audio_context();return e&&e.state||"running"}return"suspended"}unmuteOverlayClicked(){if(this.instance){if("running"!==this.audioState()){const e=this.instance.audio_context();e&&e.resume()}this.unmuteOverlay.style.display="none"}}unmuteAudioContext(){Te||(navigator.maxTouchPoints<1?Te=!0:this.container.addEventListener("click",(()=>{var e;if(Te)return;const n=null===(e=this.instance)||void 0===e?void 0:e.audio_context();if(!n)return;const t=new Audio;t.src=(()=>{const e=new ArrayBuffer(10),t=new DataView(e),r=n.sampleRate;t.setUint32(0,r,!0),t.setUint32(4,r,!0),t.setUint16(8,1,!0);return`data:audio/wav;base64,UklGRisAAABXQVZFZm10IBAAAAABAAEA${window.btoa(String.fromCharCode(...new Uint8Array(e))).slice(0,13)}AgAZGF0YQcAAACAgICAgICAAAA=`})(),t.load(),t.play().then((()=>{Te=!0})).catch((e=>{console.warn(`Failed to play dummy sound: ${e}`)}))}),{once:!0}))}copyElement(e){if(e){for(const n of e.attributes)if(n.specified){if("title"===n.name&&"Adobe Flash Player"===n.value)continue;try{this.setAttribute(n.name,n.value)}catch(e){console.warn(`Unable to set attribute ${n.name} on Ruffle instance`)}}for(const n of Array.from(e.children))this.appendChild(n)}}static htmlDimensionToCssDimension(e){if(e){const n=e.match(Pe);if(n){let e=n[1];return n[3]||(e+="px"),e}}return null}onCallbackAvailable(e){const n=this.instance;this[e]=(...t)=>null==n?void 0:n.call_exposed_callback(e,t)}set traceObserver(e){var n;null===(n=this.instance)||void 0===n||n.set_trace_observer(e)}getPanicData(){let e="\n# Player Info\n";if(e+=`Allows script access: ${!!this.loadedConfig&&this.loadedConfig.allowScriptAccess}\n`,e+=`${this.rendererDebugInfo}\n`,e+=this.debugPlayerInfo(),e+="\n# Page Info\n",e+=`Page URL: ${document.location.href}\n`,this.swfUrl&&(e+=`SWF URL: ${this.swfUrl}\n`),e+="\n# Browser Info\n",e+=`User Agent: ${window.navigator.userAgent}\n`,e+=`Platform: ${window.navigator.platform}\n`,e+=`Has touch support: ${window.navigator.maxTouchPoints>0}\n`,e+="\n# Ruffle Info\n",e+=`Version: ${x}\n`,e+=`Name: ${S}\n`,e+=`Channel: ${z}\n`,e+=`Built: ${E}\n`,e+=`Commit: ${j}\n`,e+=`Is extension: ${d}\n`,e+="\n# Metadata\n",this.metadata)for(const[n,t]of Object.entries(this.metadata))e+=`${n}: ${t}\n`;return e}panic(e){var n;if(this.panicked)return;if(this.panicked=!0,this.hideSplashScreen(),e instanceof Error&&("AbortError"===e.name||e.message.includes("AbortError")))return;const t=null!==(n=null==e?void 0:e.ruffleIndexError)&&void 0!==n?n:0,r=Object.assign([],{stackIndex:-1,avmStackIndex:-1});if(r.push("# Error Info\n"),e instanceof Error){if(r.push(`Error name: ${e.name}\n`),r.push(`Error message: ${e.message}\n`),e.stack){const n=r.push(`Error stack:\n\`\`\`\n${e.stack}\n\`\`\`\n`)-1;if(e.avmStack){const n=r.push(`AVM2 stack:\n\`\`\`\n ${e.avmStack.trim().replace(/\t/g," ")}\n\`\`\`\n`)-1;r.avmStackIndex=n}r.stackIndex=n}}else r.push(`Error: ${e}\n`);r.push(this.getPanicData());const a=r.join(""),i=new Date(E),o=new Date;o.setMonth(o.getMonth()-6);const s=o>i;let l,u,c;if(s)l=`${Ce("update-ruffle")}`;else{let e;e=document.location.protocol.includes("extension")?this.swfUrl.href:document.location.href,e=e.split(/[?#]/,1)[0];let n=`https://github.com/ruffle-rs/ruffle/issues/new?title=${encodeURIComponent(`Error on ${e}`)}&template=error_report.md&labels=error-report&body=`,t=encodeURIComponent(a);r.stackIndex>-1&&String(n+t).length>8195&&(r[r.stackIndex]=null,r.avmStackIndex>-1&&(r[r.avmStackIndex]=null),t=encodeURIComponent(r.join(""))),n+=t,l=`${Ce("report-bug")}`}switch(t){case 2:u=Ie("error-file-protocol"),c=`\n
  • ${Ce("ruffle-demo")}
  • \n
  • ${Ce("ruffle-desktop")}
  • \n `;break;case 4:u=Ie("error-javascript-config"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 9:u=Ie("error-wasm-not-found"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 8:u=Ie("error-wasm-mime-type"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 11:u=Ie("error-swf-fetch"),c=`\n
  • ${Ce("view-error-details")}
  • \n `;break;case 12:u=Ie("error-swf-cors"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 6:u=Ie("error-wasm-cors"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 3:u=Ie("error-wasm-invalid"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 7:u=Ie("error-wasm-download"),c=`\n
  • ${Ce("view-error-details")}
  • \n `;break;case 10:u=Ie("error-wasm-disabled-on-edge"),c=`\n
  • ${Ce("more-info")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 5:u=Ie("error-javascript-conflict"),s&&(u+=Ie("error-javascript-conflict-outdated",{buildDate:E})),c=`\n
  • ${l}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;case 1:u=Ie("error-csp-conflict"),c=`\n
  • ${Ce("ruffle-wiki")}
  • \n
  • ${Ce("view-error-details")}
  • \n `;break;default:u=Ie("error-unknown",{buildDate:E,outdated:String(s)}),c=`\n
  • ${l}
  • \n
  • ${Ce("view-error-details")}
  • \n `}this.container.innerHTML=`\n
    \n
    ${Ce("panic-title")}
    \n
    ${u}
    \n \n
    \n `;const d=this.container.querySelector("#panic-view-details");d&&(d.onclick=()=>{const e=this.container.querySelector("#panic-body");e.classList.add("details");const n=document.createElement("textarea");return n.readOnly=!0,n.value=a,e.replaceChildren(n),!1}),this.destroy()}displayRootMovieDownloadFailedMessage(){var e;if(d&&window.location.origin!==this.swfUrl.origin){const n=new URL(this.swfUrl);if(null===(e=this.loadedConfig)||void 0===e?void 0:e.parameters){const e=$e(this.loadedConfig.parameters);Object.entries(e).forEach((([e,t])=>{n.searchParams.set(e,t)}))}this.hideSplashScreen();const t=document.createElement("div");t.id="message_overlay",t.innerHTML=`
    \n ${Ie("message-cant-embed")}\n \n
    `,this.container.prepend(t)}else{const e=new Error("Failed to fetch: "+this.swfUrl);this.swfUrl.protocol.includes("http")?window.location.origin===this.swfUrl.origin?e.ruffleIndexError=11:e.ruffleIndexError=12:e.ruffleIndexError=2,this.panic(e)}}displayMessage(e){const n=document.createElement("div");n.id="message_overlay",n.innerHTML=`
    \n

    ${e}

    \n
    \n \n
    \n
    `,this.container.prepend(n),this.container.querySelector("#continue-btn").onclick=()=>{n.parentNode.removeChild(n)}}debugPlayerInfo(){return""}hideSplashScreen(){this.splashScreen.classList.add("hidden"),this.container.classList.remove("hidden")}showSplashScreen(){this.splashScreen.classList.remove("hidden"),this.container.classList.add("hidden")}setMetadata(e){this._metadata=e,this._readyState=2,this.hideSplashScreen(),this.dispatchEvent(new Event(Me.LOADED_METADATA)),this.dispatchEvent(new Event(Me.LOADED_DATA))}}function Le(e,n){switch(e||(e="sameDomain"),e.toLowerCase()){case"always":return!0;case"never":return!1;default:try{return new URL(window.location.href).origin===new URL(n,window.location.href).origin}catch(e){return!1}}}function Ne(e){return null===e||"true"===e.toLowerCase()}function Ue(e){if(e){let n="",t="";try{const r=new URL(e,Fe);n=r.pathname,t=r.hostname}catch(e){}if(n.startsWith("/v/")&&/^(?:(?:www\.|m\.)?youtube(?:-nocookie)?\.com)|(?:youtu\.be)$/i.test(t))return!0}return!1}function qe(e,n){var t,r;const a=e.getAttribute(n),i=null!==(r=null===(t=window.RufflePlayer)||void 0===t?void 0:t.config)&&void 0!==r?r:{};if(a)try{const t=new URL(a);"http:"!==t.protocol||"https:"!==window.location.protocol||"upgradeToHttps"in i&&!1===i.upgradeToHttps||(t.protocol="https:",e.setAttribute(n,t.toString()))}catch(e){}}function We(e){let n=e.parentElement;for(;null!==n;){switch(n.tagName){case"AUDIO":case"VIDEO":return!0}n=n.parentElement}return!1}Me.LOADED_METADATA="loadedmetadata",Me.LOADED_DATA="loadeddata";class Ze extends Me{constructor(){super()}connectedCallback(){var e,n,t,r,a,i,o,s,l,u,c,d,f,h,m,p,v,g,b,w;super.connectedCallback();const k=this.attributes.getNamedItem("src");if(k){const y=null!==(n=null===(e=this.attributes.getNamedItem("allowScriptAccess"))||void 0===e?void 0:e.value)&&void 0!==n?n:null,_=null!==(r=null===(t=this.attributes.getNamedItem("menu"))||void 0===t?void 0:t.value)&&void 0!==r?r:null;this.load({url:k.value,allowScriptAccess:Le(y,k.value),parameters:null!==(i=null===(a=this.attributes.getNamedItem("flashvars"))||void 0===a?void 0:a.value)&&void 0!==i?i:null,backgroundColor:null!==(s=null===(o=this.attributes.getNamedItem("bgcolor"))||void 0===o?void 0:o.value)&&void 0!==s?s:null,base:null!==(u=null===(l=this.attributes.getNamedItem("base"))||void 0===l?void 0:l.value)&&void 0!==u?u:null,menu:Ne(_),salign:null!==(d=null===(c=this.attributes.getNamedItem("salign"))||void 0===c?void 0:c.value)&&void 0!==d?d:"",quality:null!==(h=null===(f=this.attributes.getNamedItem("quality"))||void 0===f?void 0:f.value)&&void 0!==h?h:"high",scale:null!==(p=null===(m=this.attributes.getNamedItem("scale"))||void 0===m?void 0:m.value)&&void 0!==p?p:"showAll",wmode:null!==(g=null===(v=this.attributes.getNamedItem("wmode"))||void 0===v?void 0:v.value)&&void 0!==g?g:"window",allowNetworking:null!==(w=null===(b=this.attributes.getNamedItem("allowNetworking"))||void 0===b?void 0:b.value)&&void 0!==w?w:"all"})}}get src(){var e;return null===(e=this.attributes.getNamedItem("src"))||void 0===e?void 0:e.value}set src(e){if(e){const n=document.createAttribute("src");n.value=e,this.attributes.setNamedItem(n)}else this.attributes.removeNamedItem("src")}static get observedAttributes(){return["src","width","height"]}attributeChangedCallback(e,n,t){var r,a,i,o;if(super.attributeChangedCallback(e,n,t),this.isConnected&&"src"===e){const e=this.attributes.getNamedItem("src");e&&this.load({url:e.value,parameters:null!==(a=null===(r=this.attributes.getNamedItem("flashvars"))||void 0===r?void 0:r.value)&&void 0!==a?a:null,base:null!==(o=null===(i=this.attributes.getNamedItem("base"))||void 0===i?void 0:i.value)&&void 0!==o?o:null})}}static isInterdictable(e){const n=e.getAttribute("src"),t=e.getAttribute("type");return!!n&&(!We(e)&&(Ue(n)?(qe(e,"src"),!1):R(n,t)))}static fromNativeEmbedElement(e){const n=g("ruffle-embed",Ze),t=document.createElement(n);return t.copyElement(e),t}}function He(e,n,t){n=n.toLowerCase();for(const[t,r]of Object.entries(e))if(t.toLowerCase()===n)return r;return t}function Ve(e){var n,t;const r={};for(const a of e.children)if(a instanceof HTMLParamElement){const e=null===(n=a.attributes.getNamedItem("name"))||void 0===n?void 0:n.value,i=null===(t=a.attributes.getNamedItem("value"))||void 0===t?void 0:t.value;e&&i&&(r[e]=i)}return r}class Je extends Me{constructor(){super(),this.params={}}connectedCallback(){var e;super.connectedCallback(),this.params=Ve(this);let n=null;this.attributes.getNamedItem("data")?n=null===(e=this.attributes.getNamedItem("data"))||void 0===e?void 0:e.value:this.params.movie&&(n=this.params.movie);const t=He(this.params,"allowScriptAccess",null),r=He(this.params,"flashvars",this.getAttribute("flashvars")),a=He(this.params,"bgcolor",this.getAttribute("bgcolor")),i=He(this.params,"allowNetworking",this.getAttribute("allowNetworking")),o=He(this.params,"base",this.getAttribute("base")),s=He(this.params,"menu",null),l=He(this.params,"salign",""),u=He(this.params,"quality","high"),c=He(this.params,"scale","showAll"),d=He(this.params,"wmode","window");if(n){const e={url:n};e.allowScriptAccess=Le(t,n),r&&(e.parameters=r),a&&(e.backgroundColor=a),o&&(e.base=o),e.menu=Ne(s),l&&(e.salign=l),u&&(e.quality=u),c&&(e.scale=c),d&&(e.wmode=d),i&&(e.allowNetworking=i),this.load(e)}}debugPlayerInfo(){var e;let n="Player type: Object\n",t=null;return this.attributes.getNamedItem("data")?t=null===(e=this.attributes.getNamedItem("data"))||void 0===e?void 0:e.value:this.params.movie&&(t=this.params.movie),n+=`SWF URL: ${t}\n`,Object.keys(this.params).forEach((e=>{n+=`Param ${e}: ${this.params[e]}\n`})),Object.keys(this.attributes).forEach((e=>{var t;n+=`Attribute ${e}: ${null===(t=this.attributes.getNamedItem(e))||void 0===t?void 0:t.value}\n`})),n}get data(){return this.getAttribute("data")}set data(e){if(e){const n=document.createAttribute("data");n.value=e,this.attributes.setNamedItem(n)}else this.attributes.removeNamedItem("data")}static isInterdictable(e){var n,t,r,a;if(We(e))return!1;if(e.getElementsByTagName("ruffle-object").length>0||e.getElementsByTagName("ruffle-embed").length>0)return!1;const i=null===(n=e.attributes.getNamedItem("data"))||void 0===n?void 0:n.value.toLowerCase(),o=null!==(r=null===(t=e.attributes.getNamedItem("type"))||void 0===t?void 0:t.value)&&void 0!==r?r:null,s=Ve(e);let l;if(i){if(Ue(i))return qe(e,"data"),!1;l=i}else{if(!s||!s.movie)return!1;if(Ue(s.movie)){const n=e.querySelector("param[name='movie']");if(n){qe(n,"value");const t=n.getAttribute("value");t&&e.setAttribute("data",t)}return!1}l=s.movie}const u=null===(a=e.attributes.getNamedItem("classid"))||void 0===a?void 0:a.value.toLowerCase();return u==="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000".toLowerCase()?!Array.from(e.getElementsByTagName("object")).some(Je.isInterdictable)&&!Array.from(e.getElementsByTagName("embed")).some(Ze.isInterdictable):!u&&R(l,o)}static fromNativeObjectElement(e){const n=g("ruffle-object",Je),t=document.createElement(n);for(const n of Array.from(e.getElementsByTagName("embed")))Ze.isInterdictable(n)&&n.remove();for(const n of Array.from(e.getElementsByTagName("object")))Je.isInterdictable(n)&&n.remove();return t.copyElement(e),t}}class Ke{constructor(e){if(this.__mimeTypes=[],this.__namedMimeTypes={},e)for(let n=0;n>>0]}namedItem(e){return this.__namedMimeTypes[e]}get length(){return this.__mimeTypes.length}[Symbol.iterator](){return this.__mimeTypes[Symbol.iterator]()}}class Ge extends Ke{constructor(e,n,t){super(),this.name=e,this.description=n,this.filename=t}}class Ye{constructor(e){this.__plugins=[],this.__namedPlugins={};for(let n=0;n>>0]}namedItem(e){return this.__namedPlugins[e]}refresh(){}[Symbol.iterator](){return this.__plugins[Symbol.iterator]()}get length(){return this.__plugins.length}}const Xe=new Ge("Shockwave Flash","Shockwave Flash 32.0 r0","ruffle.js"),Qe=new Ge("Ruffle Extension","Ruffle Extension","ruffle.js");var en,nn;Xe.install({type:k,description:"Shockwave Flash",suffixes:"spl",enabledPlugin:Xe}),Xe.install({type:w,description:"Shockwave Flash",suffixes:"swf",enabledPlugin:Xe}),Xe.install({type:y,description:"Shockwave Flash",suffixes:"swf",enabledPlugin:Xe}),Xe.install({type:_,description:"Shockwave Flash",suffixes:"swf",enabledPlugin:Xe}),Qe.install({type:"",description:"Ruffle Detection",suffixes:"",enabledPlugin:Qe});const tn=null!==(nn=null===(en=window.RufflePlayer)||void 0===en?void 0:en.config)&&void 0!==nn?nn:{},rn=f(tn)+"ruffle.js";let an,on,sn,ln;function un(){var e,n;return(!("favorFlash"in tn)||!1!==tn.favorFlash)&&"ruffle.js"!==(null!==(n=null===(e=navigator.plugins.namedItem("Shockwave Flash"))||void 0===e?void 0:e.filename)&&void 0!==n?n:"ruffle.js")}function cn(){try{an=null!=an?an:document.getElementsByTagName("object"),on=null!=on?on:document.getElementsByTagName("embed");for(const e of Array.from(an))if(Je.isInterdictable(e)){const n=Je.fromNativeObjectElement(e);e.replaceWith(n)}for(const e of Array.from(on))if(Ze.isInterdictable(e)){const n=Ze.fromNativeEmbedElement(e);e.replaceWith(n)}}catch(e){console.error(`Serious error encountered when polyfilling native Flash elements: ${e}`)}}function dn(){sn=null!=sn?sn:document.getElementsByTagName("iframe"),ln=null!=ln?ln:document.getElementsByTagName("frame"),[sn,ln].forEach((e=>{for(const n of e){if(void 0!==n.dataset.rufflePolyfilled)continue;n.dataset.rufflePolyfilled="";const e=n.contentWindow,t=`Couldn't load Ruffle into ${n.tagName}[${n.src}]: `;try{"complete"===e.document.readyState&&fn(e,t)}catch(e){d||console.warn(t+e)}n.addEventListener("load",(()=>{fn(e,t)}),!1)}}))}async function fn(e,n){var t;let r;await new Promise((e=>{window.setTimeout((()=>{e()}),100)}));try{if(r=e.document,!r)return}catch(e){return void(d||console.warn(n+e))}if(d||void 0===r.documentElement.dataset.ruffleOptout)if(d)e.RufflePlayer||(e.RufflePlayer={}),e.RufflePlayer.config=Object.assign(Object.assign({},tn),null!==(t=e.RufflePlayer.config)&&void 0!==t?t:{});else if(!e.RufflePlayer){const n=r.createElement("script");n.setAttribute("src",rn),n.onload=()=>{e.RufflePlayer={},e.RufflePlayer.config=tn},r.head.appendChild(n)}}function hn(){un()||function(e){"install"in navigator.plugins&&navigator.plugins.install||Object.defineProperty(navigator,"plugins",{value:new Ye(navigator.plugins),writable:!1}),navigator.plugins.install(e),!(e.length>0)||"install"in navigator.mimeTypes&&navigator.mimeTypes.install||Object.defineProperty(navigator,"mimeTypes",{value:new Ke(navigator.mimeTypes),writable:!1});const n=navigator.mimeTypes;for(let t=0;te.addedNodes.length>0))&&(cn(),dn())})).observe(document,{childList:!0,subtree:!0}))}const pn={version:x,polyfill(){mn()},pluginPolyfill(){hn()},createPlayer(){const e=g("ruffle-player",Me);return document.createElement(e)}};class vn{constructor(e){this.sources={},this.config={},this.invoked=!1,this.newestName=null,this.conflict=null,null!=e&&(e instanceof vn?(this.sources=e.sources,this.config=e.config,this.invoked=e.invoked,this.conflict=e.conflict,this.newestName=e.newestName,e.superseded()):e.constructor===Object&&e.config instanceof Object?this.config=e.config:this.conflict=e),"loading"===document.readyState?document.addEventListener("readystatechange",this.init.bind(this)):window.setTimeout(this.init.bind(this),0)}get version(){return"0.1.0"}registerSource(e){this.sources[e]=pn}newestSourceName(){let n=null,t=e.fromSemver("0.0.0");for(const r in this.sources)if(Object.prototype.hasOwnProperty.call(this.sources,r)){const a=e.fromSemver(this.sources[r].version);a.hasPrecedenceOver(t)&&(n=r,t=a)}return n}init(){if(!this.invoked){if(this.invoked=!0,this.newestName=this.newestSourceName(),null===this.newestName)throw new Error("No registered Ruffle source!");!1!==(!("polyfills"in this.config)||this.config.polyfills)&&this.sources[this.newestName].polyfill()}}newest(){const e=this.newestSourceName();return null!==e?this.sources[e]:null}satisfying(t){const r=n.fromRequirementString(t);let a=null;for(const n in this.sources)if(Object.prototype.hasOwnProperty.call(this.sources,n)){const t=e.fromSemver(this.sources[n].version);r.satisfiedBy(t)&&(a=this.sources[n])}return a}localCompatible(){return void 0!==this.sources.local?this.satisfying("^"+this.sources.local.version):this.newest()}local(){return void 0!==this.sources.local?this.satisfying("="+this.sources.local.version):this.newest()}superseded(){this.invoked=!0}static negotiate(e,n){let t;if(t=e instanceof vn?e:new vn(e),void 0!==n){t.registerSource(n);!1!==(!("polyfills"in t.config)||t.config.polyfills)&&pn.pluginPolyfill()}return t}}window.RufflePlayer=vn.negotiate(window.RufflePlayer,"local")})()})(); -//# sourceMappingURL=ruffle.js.map \ No newline at end of file diff --git a/doc/ax25-2p0/index_files/seal.js b/doc/ax25-2p0/index_files/seal.js deleted file mode 100644 index 3108bad..0000000 --- a/doc/ax25-2p0/index_files/seal.js +++ /dev/null @@ -1,114 +0,0 @@ -var _____WB$wombat$assign$function_____ = function(name) {return (self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init(name)) || self[name]; }; -if (!self.__WB_pmw) { self.__WB_pmw = function(obj) { this.__WB_source = obj; return this; } } -{ - let window = _____WB$wombat$assign$function_____("window"); - let self = _____WB$wombat$assign$function_____("self"); - let document = _____WB$wombat$assign$function_____("document"); - let location = _____WB$wombat$assign$function_____("location"); - let top = _____WB$wombat$assign$function_____("top"); - let parent = _____WB$wombat$assign$function_____("parent"); - let frames = _____WB$wombat$assign$function_____("frames"); - let opener = _____WB$wombat$assign$function_____("opener"); - -// (c) 2006. Authorize.Net is a registered trademark of Lightbridge, Inc. -var ANSVerificationURL = "http://web.archive.org/web/20190831185324/https://verify.authorize.net/anetseal/"; // String must start with "//" and end with "/" -var AuthorizeNetSeal = -{ - verification_parameters: "", - id_parameter_name: "pid", - url_parameter_name: "rurl", - seal_image_file: (ANSVerificationURL + "images/secure90x72.gif"), - seal_width: "90", - seal_height: "72", - seal_alt_text: "Authorize.Net Merchant - Click to Verify", - display_url: "http://web.archive.org/web/20190831185324/http://www.authorize.net/", - text_color: "black", - text_size: "9px", - line_spacing: "10px", - new_window_height: "430", - new_window_width: "600", - current_url: "", - display_location: true, - no_click: false, - debug: false -}; - -document.writeln( '' ); - -if( window.ANS_customer_id ) -{ - AuthorizeNetSeal.verification_parameters = '?' + AuthorizeNetSeal.id_parameter_name + '=' + escape( ANS_customer_id ); - if( window.location.href ) - { - AuthorizeNetSeal.current_url = window.location.href; - } - else if( document.URL ) - { - AuthorizeNetSeal.current_url = document.URL; - } - - if( AuthorizeNetSeal.current_url ) - { - AuthorizeNetSeal.verification_parameters += '&' + AuthorizeNetSeal.url_parameter_name + '=' + escape( AuthorizeNetSeal.current_url ); - } - - if( !AuthorizeNetSeal.no_click ) - { - document.write( '' ); - } - - document.writeln( '' + AuthorizeNetSeal.seal_alt_text + '' ); - - if( !AuthorizeNetSeal.no_click ) - { - document.writeln( '' ); - } -} - - -} -/* - FILE ARCHIVED ON 18:53:24 Aug 31, 2019 AND RETRIEVED FROM THE - INTERNET ARCHIVE ON 06:36:00 Oct 23, 2023. - JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE. - - ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C. - SECTION 108(a)(3)). -*/ -/* -playback timings (ms): - captures_list: 1475.342 - exclusion.robots: 0.121 - exclusion.robots.policy: 0.109 - cdx.remote: 0.082 - esindex: 0.01 - LoadShardBlock: 132.682 (4) - PetaboxLoader3.datanode: 100.818 (5) - load_resource: 59.75 - PetaboxLoader3.resolve: 43.32 -*/ \ No newline at end of file diff --git a/doc/ax25-2p0/index_files/wombat.js b/doc/ax25-2p0/index_files/wombat.js deleted file mode 100644 index 9f2c553..0000000 --- a/doc/ax25-2p0/index_files/wombat.js +++ /dev/null @@ -1,21 +0,0 @@ -/* -Wombat.js client-side rewriting engine for web archive replay -Copyright (C) 2014-2023 Webrecorder Software, Rhizome, and Contributors. Released under the GNU Affero General Public License. - -This file is part of wombat.js, see https://github.com/webrecorder/wombat.js for the full source -Wombat.js is part of the Webrecorder project (https://github.com/webrecorder) - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - */ -(function(){"use strict";function FuncMap(){this._map=[]}function ensureNumber(maybeNumber){try{switch(typeof maybeNumber){case"number":case"bigint":return maybeNumber;}var converted=Number(maybeNumber);return isNaN(converted)?null:converted}catch(e){}return null}function addToStringTagToClass(clazz,tag){typeof self.Symbol!=="undefined"&&typeof self.Symbol.toStringTag!=="undefined"&&Object.defineProperty(clazz.prototype,self.Symbol.toStringTag,{value:tag,enumerable:false})}function autobind(clazz){for(var prop,propValue,proto=clazz.__proto__||clazz.constructor.prototype||clazz.prototype,clazzProps=Object.getOwnPropertyNames(proto),len=clazzProps.length,i=0;i(r+=String.fromCharCode(n),r),""):t?t.toString():"";try{return"__wb_post_data="+btoa(e)}catch{return"__wb_post_data="}}function w(t){function o(a){return a instanceof Uint8Array&&(a=new TextDecoder().decode(a)),a}let{method:e,headers:r,postData:n}=t;if(e==="GET")return!1;let i=(r.get("content-type")||"").split(";")[0],s="";switch(i){case"application/x-www-form-urlencoded":s=o(n);break;case"application/json":s=c(o(n));break;case"text/plain":try{s=c(o(n),!1)}catch{s=u(n)}break;case"multipart/form-data":{let a=r.get("content-type");if(!a)throw new Error("utils cannot call postToGetURL when missing content-type header");s=g(o(n),a);break}default:s=u(n);}return s!==null&&(t.url=f(t.url,s,t.method),t.method="GET",t.requestBody=s,!0)}function f(t,e,r){if(!r)return t;let n=t.indexOf("?")>0?"&":"?";return`${t}${n}__wb_method=${r}&${e}`}function p(t,e=!0){if(typeof t=="string")try{t=JSON.parse(t)}catch{t={}}let r=new URLSearchParams,n={},i=o=>r.has(o)?(o in n||(n[o]=1),o+"."+ ++n[o]+"_"):o;try{JSON.stringify(t,(o,s)=>(["object","function"].includes(typeof s)||r.set(i(o),s),s))}catch(o){if(!e)throw o}return r}function y(t,e){let r=new URLSearchParams;t instanceof Uint8Array&&(t=new TextDecoder().decode(t));try{let n=e.split("boundary=")[1],i=t.split(new RegExp("-*"+n+"-*","mi"));for(let o of i){let s=o.trim().match(/name="([^"]+)"\r\n\r\n(.*)/im);s&&r.set(s[1],s[2])}}catch{}return r}function c(t,e=!0){return p(t,e).toString()}function g(t,e){return y(t,e).toString()}function Wombat($wbwindow,wbinfo){if(!(this instanceof Wombat))return new Wombat($wbwindow,wbinfo);this.debug_rw=false,this.$wbwindow=$wbwindow,this.WBWindow=Window,this.origHost=$wbwindow.location.host,this.origHostname=$wbwindow.location.hostname,this.origProtocol=$wbwindow.location.protocol,this.HTTP_PREFIX="http://",this.HTTPS_PREFIX="https://",this.REL_PREFIX="//",this.VALID_PREFIXES=[this.HTTP_PREFIX,this.HTTPS_PREFIX,this.REL_PREFIX],this.IGNORE_PREFIXES=["#","about:","data:","blob:","mailto:","javascript:","{","*"],"ignore_prefixes"in wbinfo&&(this.IGNORE_PREFIXES=this.IGNORE_PREFIXES.concat(wbinfo.ignore_prefixes)),this.WB_CHECK_THIS_FUNC="_____WB$wombat$check$this$function_____",this.WB_ASSIGN_FUNC="_____WB$wombat$assign$function_____",this.wb_setAttribute=$wbwindow.Element.prototype.setAttribute,this.wb_getAttribute=$wbwindow.Element.prototype.getAttribute,this.wb_funToString=Function.prototype.toString,this.WBAutoFetchWorker=null,this.wbUseAFWorker=wbinfo.enable_auto_fetch&&$wbwindow.Worker!=null&&wbinfo.is_live,this.wb_rel_prefix="",this.wb_wombat_updating=false,this.message_listeners=new FuncMap,this.storage_listeners=new FuncMap,this.linkAsTypes={script:"js_",worker:"js_",style:"cs_",image:"im_",document:"if_",fetch:"mp_",font:"oe_",audio:"oe_",video:"oe_",embed:"oe_",object:"oe_",track:"oe_","":"mp_",null:"mp_",undefined:"mp_"},this.linkTagMods={linkRelToAs:{import:this.linkAsTypes,preload:this.linkAsTypes},stylesheet:"cs_",null:"mp_",undefined:"mp_","":"mp_"},this.tagToMod={A:{href:"mp_"},AREA:{href:"mp_"},AUDIO:{src:"oe_",poster:"im_"},BASE:{href:"mp_"},EMBED:{src:"oe_"},FORM:{action:"mp_"},FRAME:{src:"fr_"},IFRAME:{src:"if_"},IMAGE:{href:"im_","xlink:href":"im_"},IMG:{src:"im_",srcset:"im_"},INPUT:{src:"oe_"},INS:{cite:"mp_"},META:{content:"mp_"},OBJECT:{data:"oe_",codebase:"oe_"},Q:{cite:"mp_"},SCRIPT:{src:"js_","xlink:href":"js_"},SOURCE:{src:"oe_",srcset:"oe_"},TRACK:{src:"oe_"},VIDEO:{src:"oe_",poster:"im_"},image:{href:"im_","xlink:href":"im_"}},this.URL_PROPS=["href","hash","pathname","host","hostname","protocol","origin","search","port"],this.wb_info=wbinfo,this.wb_opts=wbinfo.wombat_opts,this.wb_replay_prefix=wbinfo.prefix,this.wb_is_proxy=this.wb_info.proxy_magic||!this.wb_replay_prefix,this.wb_info.top_host=this.wb_info.top_host||"*",this.wb_curr_host=$wbwindow.location.protocol+"//"+$wbwindow.location.host,this.wb_info.wombat_opts=this.wb_info.wombat_opts||{},this.wb_orig_scheme=this.wb_info.wombat_scheme+"://",this.wb_orig_origin=this.wb_orig_scheme+this.wb_info.wombat_host,this.wb_abs_prefix=this.wb_replay_prefix,this.wb_capture_date_part="",!this.wb_info.is_live&&this.wb_info.wombat_ts&&(this.wb_capture_date_part="/"+this.wb_info.wombat_ts+"/"),this.BAD_PREFIXES=["http:"+this.wb_replay_prefix,"https:"+this.wb_replay_prefix,"http:/"+this.wb_replay_prefix,"https:/"+this.wb_replay_prefix],this.hostnamePortRe=/^[\w-]+(\.[\w-_]+)+(:\d+)(\/|$)/,this.ipPortRe=/^\d+\.\d+\.\d+\.\d+(:\d+)?(\/|$)/,this.workerBlobRe=/__WB_pmw\(.*?\)\.(?=postMessage\()/g,this.rmCheckThisInjectRe=/_____WB\$wombat\$check\$this\$function_____\(.*?\)/g,this.STYLE_REGEX=/(url\s*\(\s*[\\"']*)([^)'"]+)([\\"']*\s*\))/gi,this.IMPORT_REGEX=/(@import\s*[\\"']*)([^)'";]+)([\\"']*\s*;?)/gi,this.IMPORT_JS_REGEX=/^(import\s*\(['"]+)([^'"]+)(["'])/i,this.no_wombatRe=/WB_wombat_/g,this.srcsetRe=/\s*(\S*\s+[\d.]+[wx]),|(?:\s*,(?:\s+|(?=https?:)))/,this.cookie_path_regex=/\bPath='?"?([^;'"\s]+)/i,this.cookie_domain_regex=/\bDomain=([^;'"\s]+)/i,this.cookie_expires_regex=/\bExpires=([^;'"]+)/gi,this.SetCookieRe=/,(?![|])/,this.IP_RX=/^(\d)+\.(\d)+\.(\d)+\.(\d)+$/,this.FullHTMLRegex=/^\s*<(?:html|head|body|!doctype html)/i,this.IsTagRegex=/^\s*=0){var fnMapping=this._map.splice(idx,1);return fnMapping[0][1]}return null},FuncMap.prototype.map=function(param){for(var i=0;i0&&afw.preserveMedia(media)})},AutoFetcher.prototype.terminate=function(){this.worker.terminate()},AutoFetcher.prototype.justFetch=function(urls){this.worker.postMessage({type:"fetch-all",values:urls})},AutoFetcher.prototype.fetchAsPage=function(url,originalUrl,title){if(url){var headers={"X-Wombat-History-Page":originalUrl};if(title){var encodedTitle=encodeURIComponent(title.trim());title&&(headers["X-Wombat-History-Title"]=encodedTitle)}var fetchData={url:url,options:{headers:headers,cache:"no-store"}};this.justFetch([fetchData])}},AutoFetcher.prototype.postMessage=function(msg,deferred){if(deferred){var afWorker=this;return void Promise.resolve().then(function(){afWorker.worker.postMessage(msg)})}this.worker.postMessage(msg)},AutoFetcher.prototype.preserveSrcset=function(srcset,mod){this.postMessage({type:"values",srcset:{value:srcset,mod:mod,presplit:true}},true)},AutoFetcher.prototype.preserveDataSrcset=function(elem){this.postMessage({type:"values",srcset:{value:elem.dataset.srcset,mod:this.rwMod(elem),presplit:false}},true)},AutoFetcher.prototype.preserveMedia=function(media){this.postMessage({type:"values",media:media},true)},AutoFetcher.prototype.getSrcset=function(elem){return this.wombat.wb_getAttribute?this.wombat.wb_getAttribute.call(elem,"srcset"):elem.getAttribute("srcset")},AutoFetcher.prototype.rwMod=function(elem){switch(elem.tagName){case"SOURCE":return elem.parentElement&&elem.parentElement.tagName==="PICTURE"?"im_":"oe_";case"IMG":return"im_";}return"oe_"},AutoFetcher.prototype.extractFromLocalDoc=function(){var afw=this;Promise.resolve().then(function(){for(var msg={type:"values",context:{docBaseURI:document.baseURI}},media=[],i=0,sheets=document.styleSheets;i=0||scriptType.indexOf("ecmascript")>=0)&&(!!(scriptType.indexOf("json")>=0)||!!(scriptType.indexOf("text/")>=0))},Wombat.prototype.skipWrapScriptTextBasedOnText=function(text){if(!text||text.indexOf(this.WB_ASSIGN_FUNC)>=0||text.indexOf("<")===0)return true;for(var override_props=["window","self","document","location","top","parent","frames","opener"],i=0;i=0)return false;return true},Wombat.prototype.nodeHasChildren=function(node){if(!node)return false;if(typeof node.hasChildNodes==="function")return node.hasChildNodes();var kids=node.children||node.childNodes;return!!kids&&kids.length>0},Wombat.prototype.rwModForElement=function(elem,attrName){if(!elem)return undefined;var mod="mp_";if(!(elem.tagName==="LINK"&&attrName==="href")){var maybeMod=this.tagToMod[elem.tagName];maybeMod!=null&&(mod=maybeMod[attrName])}else if(elem.rel){var relV=elem.rel.trim().toLowerCase(),asV=this.wb_getAttribute.call(elem,"as");if(asV&&this.linkTagMods.linkRelToAs[relV]!=null){var asMods=this.linkTagMods.linkRelToAs[relV];mod=asMods[asV.toLowerCase()]}else this.linkTagMods[relV]!=null&&(mod=this.linkTagMods[relV])}return mod},Wombat.prototype.removeWBOSRC=function(elem){elem.tagName!=="SCRIPT"||elem.__$removedWBOSRC$__||(elem.hasAttribute("__wb_orig_src")&&elem.removeAttribute("__wb_orig_src"),elem.__$removedWBOSRC$__=true)},Wombat.prototype.retrieveWBOSRC=function(elem){if(elem.tagName==="SCRIPT"&&!elem.__$removedWBOSRC$__){var maybeWBOSRC;return maybeWBOSRC=this.wb_getAttribute?this.wb_getAttribute.call(elem,"__wb_orig_src"):elem.getAttribute("__wb_orig_src"),maybeWBOSRC==null&&(elem.__$removedWBOSRC$__=true),maybeWBOSRC}return undefined},Wombat.prototype.wrapScriptTextJsProxy=function(scriptText){return"var _____WB$wombat$assign$function_____ = function(name) {return (self._wb_wombat && self._wb_wombat.local_init && self._wb_wombat.local_init(name)) || self[name]; };\nif (!self.__WB_pmw) { self.__WB_pmw = function(obj) { this.__WB_source = obj; return this; } }\n{\nlet window = _____WB$wombat$assign$function_____(\"window\");\nlet globalThis = _____WB$wombat$assign$function_____(\"globalThis\");\nlet self = _____WB$wombat$assign$function_____(\"self\");\nlet document = _____WB$wombat$assign$function_____(\"document\");\nlet location = _____WB$wombat$assign$function_____(\"location\");\nlet top = _____WB$wombat$assign$function_____(\"top\");\nlet parent = _____WB$wombat$assign$function_____(\"parent\");\nlet frames = _____WB$wombat$assign$function_____(\"frames\");\nlet opener = _____WB$wombat$assign$function_____(\"opener\");\n{\n"+scriptText.replace(this.DotPostMessageRe,".__WB_pmw(self.window)$1")+"\n\n}}"},Wombat.prototype.watchElem=function(elem,func){if(!this.$wbwindow.MutationObserver)return false;var m=new this.$wbwindow.MutationObserver(function(records,observer){for(var r,i=0;i"},Wombat.prototype.getFinalUrl=function(useRel,mod,url){var prefix=useRel?this.wb_rel_prefix:this.wb_abs_prefix;return mod==null&&(mod=this.wb_info.mod),this.wb_info.is_live||(prefix+=this.wb_info.wombat_ts),prefix+=mod,prefix[prefix.length-1]!=="/"&&(prefix+="/"),prefix+url},Wombat.prototype.resolveRelUrl=function(url,doc){var docObj=doc||this.$wbwindow.document,parser=this.makeParser(docObj.baseURI,docObj),hash=parser.href.lastIndexOf("#"),href=hash>=0?parser.href.substring(0,hash):parser.href,lastslash=href.lastIndexOf("/");return parser.href=lastslash>=0&&lastslash!==href.length-1?href.substring(0,lastslash+1)+url:href+url,parser.href},Wombat.prototype.extractOriginalURL=function(rewrittenUrl){if(!rewrittenUrl)return"";if(this.wb_is_proxy)return rewrittenUrl;var rwURLString=rewrittenUrl.toString(),url=rwURLString;if(this.startsWithOneOf(url,this.IGNORE_PREFIXES))return url;if(url.startsWith(this.wb_info.static_prefix))return url;var start;start=this.startsWith(url,this.wb_abs_prefix)?this.wb_abs_prefix.length:this.wb_rel_prefix&&this.startsWith(url,this.wb_rel_prefix)?this.wb_rel_prefix.length:this.wb_rel_prefix?1:0;var index=url.indexOf("/http",start);return index<0&&(index=url.indexOf("///",start)),index<0&&(index=url.indexOf("/blob:",start)),index<0&&(index=url.indexOf("/about:blank",start)),index>=0?url=url.substr(index+1):(index=url.indexOf(this.wb_replay_prefix),index>=0&&(url=url.substr(index+this.wb_replay_prefix.length)),url.length>4&&url.charAt(2)==="_"&&url.charAt(3)==="/"&&(url=url.substr(4)),url!==rwURLString&&!this.startsWithOneOf(url,this.VALID_PREFIXES)&&!this.startsWith(url,"blob:")&&(url=this.wb_orig_scheme+url)),rwURLString.charAt(0)==="/"&&rwURLString.charAt(1)!=="/"&&this.startsWith(url,this.wb_orig_origin)&&(url=url.substr(this.wb_orig_origin.length)),this.startsWith(url,this.REL_PREFIX)?this.wb_info.wombat_scheme+":"+url:url},Wombat.prototype.makeParser=function(maybeRewrittenURL,doc){var originalURL=this.extractOriginalURL(maybeRewrittenURL),docElem=doc;return doc||(this.$wbwindow.location.href==="about:blank"&&this.$wbwindow.opener?docElem=this.$wbwindow.opener.document:docElem=this.$wbwindow.document),this._makeURLParser(originalURL,docElem)},Wombat.prototype._makeURLParser=function(url,docElem){try{return new this.$wbwindow.URL(url,docElem.baseURI)}catch(e){}var p=docElem.createElement("a");return p._no_rewrite=true,p.href=url,p},Wombat.prototype.defProp=function(obj,prop,setFunc,getFunc,enumerable){var existingDescriptor=Object.getOwnPropertyDescriptor(obj,prop);if(existingDescriptor&&!existingDescriptor.configurable)return false;if(!getFunc)return false;var descriptor={configurable:true,enumerable:enumerable||false,get:getFunc};setFunc&&(descriptor.set=setFunc);try{return Object.defineProperty(obj,prop,descriptor),true}catch(e){return console.warn("Failed to redefine property %s",prop,e.message),false}},Wombat.prototype.defGetterProp=function(obj,prop,getFunc,enumerable){var existingDescriptor=Object.getOwnPropertyDescriptor(obj,prop);if(existingDescriptor&&!existingDescriptor.configurable)return false;if(!getFunc)return false;try{return Object.defineProperty(obj,prop,{configurable:true,enumerable:enumerable||false,get:getFunc}),true}catch(e){return console.warn("Failed to redefine property %s",prop,e.message),false}},Wombat.prototype.getOrigGetter=function(obj,prop){var orig_getter;if(obj.__lookupGetter__&&(orig_getter=obj.__lookupGetter__(prop)),!orig_getter&&Object.getOwnPropertyDescriptor){var props=Object.getOwnPropertyDescriptor(obj,prop);props&&(orig_getter=props.get)}return orig_getter},Wombat.prototype.getOrigSetter=function(obj,prop){var orig_setter;if(obj.__lookupSetter__&&(orig_setter=obj.__lookupSetter__(prop)),!orig_setter&&Object.getOwnPropertyDescriptor){var props=Object.getOwnPropertyDescriptor(obj,prop);props&&(orig_setter=props.set)}return orig_setter},Wombat.prototype.getAllOwnProps=function(obj){for(var ownProps=[],props=Object.getOwnPropertyNames(obj),i=0;i "+final_href),actualLocation.href=final_href}}},Wombat.prototype.checkLocationChange=function(wombatLoc,isTop){var locType=typeof wombatLoc,actual_location=isTop?this.$wbwindow.__WB_replay_top.location:this.$wbwindow.location;locType==="string"?this.updateLocation(wombatLoc,actual_location.href,actual_location):locType==="object"&&this.updateLocation(wombatLoc.href,wombatLoc._orig_href,actual_location)},Wombat.prototype.checkAllLocations=function(){return!this.wb_wombat_updating&&void(this.wb_wombat_updating=true,this.checkLocationChange(this.$wbwindow.WB_wombat_location,false),this.$wbwindow.WB_wombat_location!=this.$wbwindow.__WB_replay_top.WB_wombat_location&&this.checkLocationChange(this.$wbwindow.__WB_replay_top.WB_wombat_location,true),this.wb_wombat_updating=false)},Wombat.prototype.proxyToObj=function(source){if(source)try{var proxyRealObj=source.__WBProxyRealObj__;if(proxyRealObj)return proxyRealObj}catch(e){}return source},Wombat.prototype.objToProxy=function(obj){if(obj)try{var maybeWbProxy=obj._WB_wombat_obj_proxy;if(maybeWbProxy)return maybeWbProxy}catch(e){}return obj},Wombat.prototype.defaultProxyGet=function(obj,prop,ownProps,fnCache){switch(prop){case"__WBProxyRealObj__":return obj;case"location":case"WB_wombat_location":return obj.WB_wombat_location;case"_WB_wombat_obj_proxy":return obj._WB_wombat_obj_proxy;case"__WB_pmw":case this.WB_ASSIGN_FUNC:case this.WB_CHECK_THIS_FUNC:return obj[prop];case"origin":return obj.WB_wombat_location.origin;case"constructor":return obj.constructor;}var retVal=obj[prop],type=typeof retVal;if(type==="function"&&ownProps.indexOf(prop)!==-1){switch(prop){case"requestAnimationFrame":case"cancelAnimationFrame":{if(!this.isNativeFunction(retVal))return retVal;break}case"eval":if(this.isNativeFunction(retVal))return this.wrappedEval(retVal);}var cachedFN=fnCache[prop];return cachedFN&&cachedFN.original===retVal||(cachedFN={original:retVal,boundFn:retVal.bind(obj)},fnCache[prop]=cachedFN),cachedFN.boundFn}return type==="object"&&retVal&&retVal._WB_wombat_obj_proxy?(retVal instanceof this.WBWindow&&this.initNewWindowWombat(retVal),retVal._WB_wombat_obj_proxy):retVal},Wombat.prototype.setLoc=function(loc,originalURL){var parser=this.makeParser(originalURL,loc.ownerDocument);loc._orig_href=originalURL,loc._parser=parser;var href=parser.href;loc._hash=parser.hash,loc._href=href,loc._host=parser.host,loc._hostname=parser.hostname,loc._origin=parser.origin?parser.host?parser.origin:"null":parser.protocol+"//"+parser.hostname+(parser.port?":"+parser.port:""),loc._pathname=parser.pathname,loc._port=parser.port,loc._protocol=parser.protocol,loc._search=parser.search,Object.defineProperty||(loc.href=href,loc.hash=parser.hash,loc.host=loc._host,loc.hostname=loc._hostname,loc.origin=loc._origin,loc.pathname=loc._pathname,loc.port=loc._port,loc.protocol=loc._protocol,loc.search=loc._search)},Wombat.prototype.makeGetLocProp=function(prop,origGetter){var wombat=this;return function newGetLocProp(){if(this._no_rewrite)return origGetter.call(this,prop);var curr_orig_href=origGetter.call(this,"href");return prop==="href"?wombat.extractOriginalURL(curr_orig_href):prop==="ancestorOrigins"?[]:(this._orig_href!==curr_orig_href&&wombat.setLoc(this,curr_orig_href),this["_"+prop])}},Wombat.prototype.makeSetLocProp=function(prop,origSetter,origGetter){var wombat=this;return function newSetLocProp(value){if(this._no_rewrite)return origSetter.call(this,prop,value);if(this["_"+prop]!==value){if(this["_"+prop]=value,!this._parser){var href=origGetter.call(this);this._parser=wombat.makeParser(href,this.ownerDocument)}var rel=false;if(prop==="href"&&typeof value==="string")if(value&&this._parser instanceof URL)try{value=new URL(value,this._parser).href}catch(e){console.warn("Error resolving URL",e)}else value&&(value[0]==="."||value[0]==="#"?value=wombat.resolveRelUrl(value,this.ownerDocument):value[0]==="/"&&(value.length>1&&value[1]==="/"?value=this._parser.protocol+value:(rel=true,value=WB_wombat_location.origin+value)));try{this._parser[prop]=value}catch(e){console.log("Error setting "+prop+" = "+value)}prop==="hash"?(value=this._parser[prop],origSetter.call(this,"hash",value)):(rel=rel||value===this._parser.pathname,value=wombat.rewriteUrl(this._parser.href,rel),origSetter.call(this,"href",value))}}},Wombat.prototype.styleReplacer=function(match,n1,n2,n3,offset,string){return n1+this.rewriteUrl(n2)+n3},Wombat.prototype.domConstructorErrorChecker=function(thisObj,what,args,numRequiredArgs){var errorMsg,needArgs=typeof numRequiredArgs==="number"?numRequiredArgs:1;if(thisObj instanceof this.WBWindow?errorMsg="Failed to construct '"+what+"': Please use the 'new' operator, this DOM object constructor cannot be called as a function.":args&&args.length=0)return url;if(url.indexOf(this.wb_rel_prefix)===0&&url.indexOf("http")>1){var scheme_sep=url.indexOf(":/");return scheme_sep>0&&url[scheme_sep+2]!=="/"?url.substring(0,scheme_sep+2)+"/"+url.substring(scheme_sep+2):url}return this.getFinalUrl(true,mod,this.wb_orig_origin+url)}url.charAt(0)==="."&&(url=this.resolveRelUrl(url,doc));var prefix=this.startsWithOneOf(url.toLowerCase(),this.VALID_PREFIXES);if(prefix){var orig_host=this.replayTopHost,orig_protocol=this.replayTopProtocol,prefix_host=prefix+orig_host+"/";if(this.startsWith(url,prefix_host)){if(this.startsWith(url,this.wb_replay_prefix))return url;var curr_scheme=orig_protocol+"//",path=url.substring(prefix_host.length),rebuild=false;return path.indexOf(this.wb_rel_prefix)<0&&url.indexOf("/static/")<0&&(path=this.getFinalUrl(true,mod,WB_wombat_location.origin+"/"+path),rebuild=true),prefix!==curr_scheme&&prefix!==this.REL_PREFIX&&(rebuild=true),rebuild&&(url=useRel?"":curr_scheme+orig_host,path&&path[0]!=="/"&&(url+="/"),url+=path),url}return this.getFinalUrl(useRel,mod,url)}return prefix=this.startsWithOneOf(url,this.BAD_PREFIXES),prefix?this.getFinalUrl(useRel,mod,this.extractOriginalURL(url)):url},Wombat.prototype.rewriteUrl=function(url,useRel,mod,doc){var rewritten=this.rewriteUrl_(url,useRel,mod,doc);return this.debug_rw&&(url===rewritten?console.log("NOT REWRITTEN "+url):console.log("REWRITE: "+url+" -> "+rewritten)),rewritten},Wombat.prototype.performAttributeRewrite=function(elem,name,value,absUrlOnly){switch(name){case"innerHTML":case"outerHTML":return this.rewriteHtml(value);case"filter":return this.rewriteInlineStyle(value);case"style":return this.rewriteStyle(value);case"srcset":return this.rewriteSrcset(value,elem);}if(absUrlOnly&&!this.startsWithOneOf(value,this.VALID_PREFIXES))return value;var mod=this.rwModForElement(elem,name);return this.wbUseAFWorker&&this.WBAutoFetchWorker&&this.isSavedDataSrcSrcset(elem)&&this.WBAutoFetchWorker.preserveDataSrcset(elem),this.rewriteUrl(value,false,mod,elem.ownerDocument)},Wombat.prototype.rewriteAttr=function(elem,name,absUrlOnly){var changed=false;if(!elem||!elem.getAttribute||elem._no_rewrite||elem["_"+name])return changed;var value=this.wb_getAttribute.call(elem,name);if(!value||this.startsWith(value,"javascript:"))return changed;var new_value=this.performAttributeRewrite(elem,name,value,absUrlOnly);return new_value!==value&&(this.removeWBOSRC(elem),this.wb_setAttribute.call(elem,name,new_value),changed=true),changed},Wombat.prototype.noExceptRewriteStyle=function(style){try{return this.rewriteStyle(style)}catch(e){return style}},Wombat.prototype.rewriteStyle=function(style){if(!style)return style;var value=style;return typeof style==="object"&&(value=style.toString()),typeof value==="string"?value.replace(this.STYLE_REGEX,this.styleReplacer).replace(this.IMPORT_REGEX,this.styleReplacer).replace(this.no_wombatRe,""):value},Wombat.prototype.rewriteSrcset=function(value,elem){if(!value)return"";for(var v,split=value.split(this.srcsetRe),values=[],mod=this.rwModForElement(elem,"srcset"),i=0;i=0){var JS="javascript:";new_value="javascript:window.parent._wb_wombat.initNewWindowWombat(window);"+value.substr(11)}return new_value||(new_value=this.rewriteUrl(value,false,this.rwModForElement(elem,attrName))),new_value!==value&&(this.wb_setAttribute.call(elem,attrName,new_value),true)},Wombat.prototype.rewriteScript=function(elem){if(elem.hasAttribute("src")||!elem.textContent||!this.$wbwindow.Proxy)return this.rewriteAttr(elem,"src");if(this.skipWrapScriptBasedOnType(elem.type))return false;var text=elem.textContent.trim();return!this.skipWrapScriptTextBasedOnText(text)&&(elem.textContent=this.wrapScriptTextJsProxy(text),true)},Wombat.prototype.rewriteSVGElem=function(elem){var changed=this.rewriteAttr(elem,"filter");return changed=this.rewriteAttr(elem,"style")||changed,changed=this.rewriteAttr(elem,"xlink:href")||changed,changed=this.rewriteAttr(elem,"href")||changed,changed=this.rewriteAttr(elem,"src")||changed,changed},Wombat.prototype.rewriteElem=function(elem){var changed=false;if(!elem)return changed;if(elem instanceof SVGElement)changed=this.rewriteSVGElem(elem);else switch(elem.tagName){case"META":var maybeCSP=this.wb_getAttribute.call(elem,"http-equiv");maybeCSP&&maybeCSP.toLowerCase()==="content-security-policy"&&(this.wb_setAttribute.call(elem,"http-equiv","_"+maybeCSP),changed=true);break;case"STYLE":var new_content=this.rewriteStyle(elem.textContent);elem.textContent!==new_content&&(elem.textContent=new_content,changed=true,this.wbUseAFWorker&&this.WBAutoFetchWorker&&elem.sheet!=null&&this.WBAutoFetchWorker.deferredSheetExtraction(elem.sheet));break;case"LINK":changed=this.rewriteAttr(elem,"href"),this.wbUseAFWorker&&elem.rel==="stylesheet"&&this._addEventListener(elem,"load",this.utilFns.wbSheetMediaQChecker);break;case"IMG":changed=this.rewriteAttr(elem,"src"),changed=this.rewriteAttr(elem,"srcset")||changed,changed=this.rewriteAttr(elem,"style")||changed,this.wbUseAFWorker&&this.WBAutoFetchWorker&&elem.dataset.srcset&&this.WBAutoFetchWorker.preserveDataSrcset(elem);break;case"OBJECT":if(this.wb_info.isSW&&elem.parentElement&&elem.getAttribute("type")==="application/pdf"){for(var iframe=this.$wbwindow.document.createElement("IFRAME"),i=0;i0;)for(var child,children=rewriteQ.shift(),i=0;i"+rwString+"","text/html");if(!inner_doc||!this.nodeHasChildren(inner_doc.head)||!inner_doc.head.children[0].content)return rwString;var template=inner_doc.head.children[0];if(template._no_rewrite=true,this.recurseRewriteElem(template.content)){var new_html=template.innerHTML;if(checkEndTag){var first_elem=template.content.children&&template.content.children[0];if(first_elem){var end_tag="";this.endsWith(new_html,end_tag)&&!this.endsWith(rwString.toLowerCase(),end_tag)&&(new_html=new_html.substring(0,new_html.length-end_tag.length))}else if(rwString[0]!=="<"||rwString[rwString.length-1]!==">")return this.write_buff+=rwString,undefined}return new_html}return rwString},Wombat.prototype.rewriteHtmlFull=function(string,checkEndTag){var inner_doc=new DOMParser().parseFromString(string,"text/html");if(!inner_doc)return string;for(var changed=false,i=0;i=0)inner_doc.documentElement._no_rewrite=true,new_html=this.reconstructDocType(inner_doc.doctype)+inner_doc.documentElement.outerHTML;else{inner_doc.head._no_rewrite=true,inner_doc.body._no_rewrite=true;var headHasKids=this.nodeHasChildren(inner_doc.head),bodyHasKids=this.nodeHasChildren(inner_doc.body);if(new_html=(headHasKids?inner_doc.head.outerHTML:"")+(bodyHasKids?inner_doc.body.outerHTML:""),checkEndTag)if(inner_doc.all.length>3){var end_tag="";this.endsWith(new_html,end_tag)&&!this.endsWith(string.toLowerCase(),end_tag)&&(new_html=new_html.substring(0,new_html.length-end_tag.length))}else if(string[0]!=="<"||string[string.length-1]!==">")return void(this.write_buff+=string);new_html=this.reconstructDocType(inner_doc.doctype)+new_html}return new_html}return string},Wombat.prototype.rewriteInlineStyle=function(orig){var decoded;try{decoded=decodeURIComponent(orig)}catch(e){decoded=orig}if(decoded!==orig){var parts=this.rewriteStyle(decoded).split(",",2);return parts[0]+","+encodeURIComponent(parts[1])}return this.rewriteStyle(orig)},Wombat.prototype.rewriteCookie=function(cookie){var wombat=this,rwCookie=cookie.replace(this.wb_abs_prefix,"").replace(this.wb_rel_prefix,"");return rwCookie=rwCookie.replace(this.cookie_domain_regex,function(m,m1){var message={domain:m1,cookie:rwCookie,wb_type:"cookie"};return wombat.sendTopMessage(message,true),wombat.$wbwindow.location.hostname.indexOf(".")>=0&&!wombat.IP_RX.test(wombat.$wbwindow.location.hostname)?"Domain=."+wombat.$wbwindow.location.hostname:""}).replace(this.cookie_path_regex,function(m,m1){var rewritten=wombat.rewriteUrl(m1);return rewritten.indexOf(wombat.wb_curr_host)===0&&(rewritten=rewritten.substring(wombat.wb_curr_host.length)),"Path="+rewritten}),wombat.$wbwindow.location.protocol!=="https:"&&(rwCookie=rwCookie.replace("secure","")),rwCookie.replace(",|",",")},Wombat.prototype.rewriteWorker=function(workerUrl){if(!workerUrl)return workerUrl;workerUrl=workerUrl.toString();var isBlob=workerUrl.indexOf("blob:")===0,isJS=workerUrl.indexOf("javascript:")===0;if(!isBlob&&!isJS){if(!this.startsWithOneOf(workerUrl,this.VALID_PREFIXES)&&!this.startsWith(workerUrl,"/")&&!this.startsWithOneOf(workerUrl,this.BAD_PREFIXES)){var rurl=this.resolveRelUrl(workerUrl,this.$wbwindow.document);return this.rewriteUrl(rurl,false,"wkr_",this.$wbwindow.document)}return this.rewriteUrl(workerUrl,false,"wkr_",this.$wbwindow.document)}var workerCode=isJS?workerUrl.replace("javascript:",""):null;if(isBlob){var x=new XMLHttpRequest;this.utilFns.XHRopen.call(x,"GET",workerUrl,false),this.utilFns.XHRsend.call(x),workerCode=x.responseText.replace(this.workerBlobRe,"").replace(this.rmCheckThisInjectRe,"this")}if(this.wb_info.static_prefix||this.wb_info.ww_rw_script){var originalURL=this.$wbwindow.document.baseURI,ww_rw=this.wb_info.ww_rw_script||this.wb_info.static_prefix+"wombatWorkers.js",rw="(function() { self.importScripts('"+ww_rw+"'); new WBWombat({'prefix': '"+this.wb_abs_prefix+"', 'prefixMod': '"+this.wb_abs_prefix+"wkrf_/', 'originalURL': '"+originalURL+"'}); })();";workerCode=rw+workerCode}var blob=new Blob([workerCode],{type:"application/javascript"});return URL.createObjectURL(blob)},Wombat.prototype.rewriteTextNodeFn=function(fnThis,originalFn,argsObj){var args,deproxiedThis=this.proxyToObj(fnThis);if(argsObj.length>0&&deproxiedThis.parentElement&&deproxiedThis.parentElement.tagName==="STYLE"){args=new Array(argsObj.length);var dataIndex=argsObj.length-1;dataIndex===2?(args[0]=argsObj[0],args[1]=argsObj[1]):dataIndex===1&&(args[0]=argsObj[0]),args[dataIndex]=this.rewriteStyle(argsObj[dataIndex])}else args=argsObj;return originalFn.__WB_orig_apply?originalFn.__WB_orig_apply(deproxiedThis,args):originalFn.apply(deproxiedThis,args)},Wombat.prototype.rewriteDocWriteWriteln=function(fnThis,originalFn,argsObj){var string,thisObj=this.proxyToObj(fnThis),argLen=argsObj.length;if(argLen===0)return originalFn.call(thisObj);string=argLen===1?argsObj[0]:Array.prototype.join.call(argsObj,"");var new_buff=this.rewriteHtml(string,true),res=originalFn.call(thisObj,new_buff);return this.initNewWindowWombat(thisObj.defaultView),res},Wombat.prototype.rewriteChildNodeFn=function(fnThis,originalFn,argsObj){var thisObj=this.proxyToObj(fnThis);if(argsObj.length===0)return originalFn.call(thisObj);var newArgs=this.rewriteElementsInArguments(argsObj);return originalFn.__WB_orig_apply?originalFn.__WB_orig_apply(thisObj,newArgs):originalFn.apply(thisObj,newArgs)},Wombat.prototype.rewriteInsertAdjHTMLOrElemArgs=function(fnThis,originalFn,position,textOrElem,rwHTML){var fnThisObj=this.proxyToObj(fnThis);return fnThisObj._no_rewrite?originalFn.call(fnThisObj,position,textOrElem):rwHTML?originalFn.call(fnThisObj,position,this.rewriteHtml(textOrElem)):(this.rewriteElemComplete(textOrElem),originalFn.call(fnThisObj,position,textOrElem))},Wombat.prototype.rewriteSetTimeoutInterval=function(fnThis,originalFn,argsObj){var rw=this.isString(argsObj[0]),args=rw?new Array(argsObj.length):argsObj;if(rw){args[0]=this.$wbwindow.Proxy?this.wrapScriptTextJsProxy(argsObj[0]):argsObj[0].replace(/\blocation\b/g,"WB_wombat_$&");for(var i=1;i0&&cssStyleValueOverride(this.$wbwindow.CSSStyleValue,"parse"),this.$wbwindow.CSSStyleValue.parseAll&&this.$wbwindow.CSSStyleValue.parseAll.toString().indexOf("[native code]")>0&&cssStyleValueOverride(this.$wbwindow.CSSStyleValue,"parseAll")}if(this.$wbwindow.CSSKeywordValue&&this.$wbwindow.CSSKeywordValue.prototype){var oCSSKV=this.$wbwindow.CSSKeywordValue;this.$wbwindow.CSSKeywordValue=function(CSSKeywordValue_){return function CSSKeywordValue(cssValue){return wombat.domConstructorErrorChecker(this,"CSSKeywordValue",arguments),new CSSKeywordValue_(wombat.rewriteStyle(cssValue))}}(this.$wbwindow.CSSKeywordValue),this.$wbwindow.CSSKeywordValue.prototype=oCSSKV.prototype,Object.defineProperty(this.$wbwindow.CSSKeywordValue.prototype,"constructor",{value:this.$wbwindow.CSSKeywordValue}),addToStringTagToClass(this.$wbwindow.CSSKeywordValue,"CSSKeywordValue")}if(this.$wbwindow.StylePropertyMap&&this.$wbwindow.StylePropertyMap.prototype){var originalSet=this.$wbwindow.StylePropertyMap.prototype.set;this.$wbwindow.StylePropertyMap.prototype.set=function set(){if(arguments.length<=1)return originalSet.__WB_orig_apply?originalSet.__WB_orig_apply(this,arguments):originalSet.apply(this,arguments);var newArgs=new Array(arguments.length);newArgs[0]=arguments[0];for(var i=1;i")&&(array[0]=wombat.rewriteHtml(array[0]),options.type="text/html"),new Blob_(array,options)}}(this.$wbwindow.Blob),this.$wbwindow.Blob.prototype=orig_blob.prototype}},Wombat.prototype.initWSOverride=function(){this.$wbwindow.WebSocket&&this.$wbwindow.WebSocket.prototype&&(this.$wbwindow.WebSocket=function(WebSocket_){function WebSocket(url,protocols){this.addEventListener=function(){},this.removeEventListener=function(){},this.close=function(){},this.send=function(data){console.log("ws send",data)},this.protocol=protocols&&protocols.length?protocols[0]:"",this.url=url,this.readyState=0}return WebSocket.CONNECTING=0,WebSocket.OPEN=1,WebSocket.CLOSING=2,WebSocket.CLOSED=3,WebSocket}(this.$wbwindow.WebSocket),Object.defineProperty(this.$wbwindow.WebSocket.prototype,"constructor",{value:this.$wbwindow.WebSocket}),addToStringTagToClass(this.$wbwindow.WebSocket,"WebSocket"))},Wombat.prototype.initDocTitleOverride=function(){var orig_get_title=this.getOrigGetter(this.$wbwindow.document,"title"),orig_set_title=this.getOrigSetter(this.$wbwindow.document,"title"),wombat=this,set_title=function title(value){var res=orig_set_title.call(this,value),message={wb_type:"title",title:value};return wombat.sendTopMessage(message),res};this.defProp(this.$wbwindow.document,"title",set_title,orig_get_title)},Wombat.prototype.initFontFaceOverride=function(){if(this.$wbwindow.FontFace){var wombat=this,origFontFace=this.$wbwindow.FontFace;this.$wbwindow.FontFace=function(FontFace_){return function FontFace(family,source,descriptors){wombat.domConstructorErrorChecker(this,"FontFace",arguments,2);var rwSource=source;return source!=null&&(typeof source==="string"?rwSource=wombat.rewriteInlineStyle(source):rwSource=wombat.rewriteInlineStyle(source.toString())),new FontFace_(family,rwSource,descriptors)}}(this.$wbwindow.FontFace),this.$wbwindow.FontFace.prototype=origFontFace.prototype,Object.defineProperty(this.$wbwindow.FontFace.prototype,"constructor",{value:this.$wbwindow.FontFace}),addToStringTagToClass(this.$wbwindow.FontFace,"FontFace")}},Wombat.prototype.initFixedRatio=function(value){try{this.$wbwindow.devicePixelRatio=value}catch(e){}if(Object.defineProperty)try{Object.defineProperty(this.$wbwindow,"devicePixelRatio",{value:value,writable:false})}catch(e){}},Wombat.prototype.initPaths=function(wbinfo){wbinfo.wombat_opts=wbinfo.wombat_opts||{},Object.assign(this.wb_info,wbinfo),this.wb_opts=wbinfo.wombat_opts,this.wb_replay_prefix=wbinfo.prefix,this.wb_is_proxy=wbinfo.proxy_magic||!this.wb_replay_prefix,this.wb_info.top_host=this.wb_info.top_host||"*",this.wb_curr_host=this.$wbwindow.location.protocol+"//"+this.$wbwindow.location.host,this.wb_info.wombat_opts=this.wb_info.wombat_opts||{},this.wb_orig_scheme=wbinfo.wombat_scheme+"://",this.wb_orig_origin=this.wb_orig_scheme+wbinfo.wombat_host,this.wb_abs_prefix=this.wb_replay_prefix,this.wb_capture_date_part=!wbinfo.is_live&&wbinfo.wombat_ts?"/"+wbinfo.wombat_ts+"/":"",this.initBadPrefixes(this.wb_replay_prefix),this.initCookiePreset()},Wombat.prototype.initSeededRandom=function(seed){this.$wbwindow.Math.seed=parseInt(seed);var wombat=this;this.$wbwindow.Math.random=function random(){return wombat.$wbwindow.Math.seed=(wombat.$wbwindow.Math.seed*9301+49297)%233280,wombat.$wbwindow.Math.seed/233280}},Wombat.prototype.initHistoryOverrides=function(){this.overrideHistoryFunc("pushState"),this.overrideHistoryFunc("replaceState");var wombat=this;this.$wbwindow.addEventListener("popstate",function(event){wombat.sendHistoryUpdate(wombat.$wbwindow.WB_wombat_location.href,wombat.$wbwindow.document.title)})},Wombat.prototype.initCookiePreset=function(){if(this.wb_info.presetCookie)for(var splitCookies=this.wb_info.presetCookie.split(";"),i=0;i2&&!this.__WB_xhr_open_arguments[2]&&navigator.userAgent.indexOf("Firefox")===-1&&(this.__WB_xhr_open_arguments[2]=true,console.warn("wombat.js: Sync XHR not supported in SW-based replay in this browser, converted to async")),this._no_rewrite||(this.__WB_xhr_open_arguments[1]=wombat.rewriteUrl(this.__WB_xhr_open_arguments[1])),origOpen.apply(this,this.__WB_xhr_open_arguments),!wombat.startsWith(this.__WB_xhr_open_arguments[1],"data:")){for(const[name,value]of this.__WB_xhr_headers.entries())origSetRequestHeader.call(this,name,value);origSetRequestHeader.call(this,"X-Pywb-Requested-With","XMLHttpRequest")}return origSend.call(this,value)}}else if(this.$wbwindow.XMLHttpRequest.prototype.open){var origXMLHttpOpen=this.$wbwindow.XMLHttpRequest.prototype.open;this.utilFns.XHRopen=origXMLHttpOpen,this.utilFns.XHRsend=this.$wbwindow.XMLHttpRequest.prototype.send,this.$wbwindow.XMLHttpRequest.prototype.open=function open(method,url,async,user,password){var rwURL=this._no_rewrite?url:wombat.rewriteUrl(url),openAsync=true;async==null||async||(openAsync=false),origXMLHttpOpen.call(this,method,rwURL,openAsync,user,password),wombat.startsWith(rwURL,"data:")||this.setRequestHeader("X-Pywb-Requested-With","XMLHttpRequest")}}if(this.$wbwindow.fetch){var orig_fetch=this.$wbwindow.fetch;this.$wbwindow.fetch=function fetch(input,init_opts){var rwInput=input,inputType=typeof input;if(inputType==="string")rwInput=wombat.rewriteUrl(input);else if(inputType==="object"&&input.url){var new_url=wombat.rewriteUrl(input.url);new_url!==input.url&&(rwInput=new Request(new_url,init_opts))}else inputType==="object"&&input.href&&(rwInput=wombat.rewriteUrl(input.href));if(init_opts||(init_opts={}),init_opts.credentials===undefined)try{init_opts.credentials="include"}catch(e){}return orig_fetch.call(wombat.proxyToObj(this),rwInput,init_opts)}}if(this.$wbwindow.Request&&this.$wbwindow.Request.prototype){var orig_request=this.$wbwindow.Request;this.$wbwindow.Request=function(Request_){return function Request(input,init_opts){wombat.domConstructorErrorChecker(this,"Request",arguments);var newInitOpts=init_opts||{},newInput=input,inputType=typeof input;switch(inputType){case"string":newInput=wombat.rewriteUrl(input);break;case"object":if(newInput=input,input.url){var new_url=wombat.rewriteUrl(input.url);new_url!==input.url&&(newInput=new Request_(new_url,input))}else input.href&&(newInput=wombat.rewriteUrl(input.toString(),true));}return newInitOpts.credentials="include",new Request_(newInput,newInitOpts)}}(this.$wbwindow.Request),this.$wbwindow.Request.prototype=orig_request.prototype,Object.defineProperty(this.$wbwindow.Request.prototype,"constructor",{value:this.$wbwindow.Request})}if(this.$wbwindow.Response&&this.$wbwindow.Response.prototype){var originalRedirect=this.$wbwindow.Response.prototype.redirect;this.$wbwindow.Response.prototype.redirect=function redirect(url,status){var rwURL=wombat.rewriteUrl(url,true,null,wombat.$wbwindow.document);return originalRedirect.call(this,rwURL,status)}}if(this.$wbwindow.EventSource&&this.$wbwindow.EventSource.prototype){var origEventSource=this.$wbwindow.EventSource;this.$wbwindow.EventSource=function(EventSource_){return function EventSource(url,configuration){wombat.domConstructorErrorChecker(this,"EventSource",arguments);var rwURL=url;return url!=null&&(rwURL=wombat.rewriteUrl(url)),new EventSource_(rwURL,configuration)}}(this.$wbwindow.EventSource),this.$wbwindow.EventSource.prototype=origEventSource.prototype,Object.defineProperty(this.$wbwindow.EventSource.prototype,"constructor",{value:this.$wbwindow.EventSource}),addToStringTagToClass(this.$wbwindow.EventSource,"EventSource")}},Wombat.prototype.initElementGetSetAttributeOverride=function(){if(!this.wb_opts.skip_setAttribute&&this.$wbwindow.Element&&this.$wbwindow.Element.prototype){var wombat=this,ElementProto=this.$wbwindow.Element.prototype;if(ElementProto.setAttribute){var orig_setAttribute=ElementProto.setAttribute;ElementProto._orig_setAttribute=orig_setAttribute,ElementProto.setAttribute=function setAttribute(name,value){var rwValue=value;if(name&&typeof rwValue==="string"){var lowername=name.toLowerCase();if(this.tagName==="LINK"&&lowername==="href"&&rwValue.indexOf("data:text/css")===0)rwValue=wombat.rewriteInlineStyle(value);else if(lowername==="style")rwValue=wombat.rewriteStyle(value);else if(lowername==="srcset"||lowername==="imagesrcset"&&this.tagName==="LINK")rwValue=wombat.rewriteSrcset(value,this);else{var shouldRW=wombat.shouldRewriteAttr(this.tagName,lowername);shouldRW&&(wombat.removeWBOSRC(this),!this._no_rewrite&&(rwValue=wombat.rewriteUrl(value,false,wombat.rwModForElement(this,lowername))))}}return orig_setAttribute.call(this,name,rwValue)}}if(ElementProto.getAttribute){var orig_getAttribute=ElementProto.getAttribute;this.wb_getAttribute=orig_getAttribute,ElementProto.getAttribute=function getAttribute(name){var result=orig_getAttribute.call(this,name);if(result===null)return result;var lowerName=name;if(name&&(lowerName=name.toLowerCase()),wombat.shouldRewriteAttr(this.tagName,lowerName)){var maybeWBOSRC=wombat.retrieveWBOSRC(this);return maybeWBOSRC?maybeWBOSRC:wombat.extractOriginalURL(result)}return wombat.startsWith(lowerName,"data-")&&wombat.startsWithOneOf(result,wombat.wb_prefixes)?wombat.extractOriginalURL(result):result}}}},Wombat.prototype.initSvgImageOverrides=function(){if(this.$wbwindow.SVGImageElement){var svgImgProto=this.$wbwindow.SVGImageElement.prototype,orig_getAttr=svgImgProto.getAttribute,orig_getAttrNS=svgImgProto.getAttributeNS,orig_setAttr=svgImgProto.setAttribute,orig_setAttrNS=svgImgProto.setAttributeNS,wombat=this;svgImgProto.getAttribute=function getAttribute(name){var value=orig_getAttr.call(this,name);return name.indexOf("xlink:href")>=0||name==="href"?wombat.extractOriginalURL(value):value},svgImgProto.getAttributeNS=function getAttributeNS(ns,name){var value=orig_getAttrNS.call(this,ns,name);return name.indexOf("xlink:href")>=0||name==="href"?wombat.extractOriginalURL(value):value},svgImgProto.setAttribute=function setAttribute(name,value){var rwValue=value;return(name.indexOf("xlink:href")>=0||name==="href")&&(rwValue=wombat.rewriteUrl(value)),orig_setAttr.call(this,name,rwValue)},svgImgProto.setAttributeNS=function setAttributeNS(ns,name,value){var rwValue=value;return(name.indexOf("xlink:href")>=0||name==="href")&&(rwValue=wombat.rewriteUrl(value)),orig_setAttrNS.call(this,ns,name,rwValue)}}},Wombat.prototype.initCreateElementNSFix=function(){if(this.$wbwindow.document.createElementNS&&this.$wbwindow.Document.prototype.createElementNS){var orig_createElementNS=this.$wbwindow.document.createElementNS,wombat=this,createElementNS=function createElementNS(namespaceURI,qualifiedName){return orig_createElementNS.call(wombat.proxyToObj(this),wombat.extractOriginalURL(namespaceURI),qualifiedName)};this.$wbwindow.Document.prototype.createElementNS=createElementNS,this.$wbwindow.document.createElementNS=createElementNS}},Wombat.prototype.initInsertAdjacentElementHTMLOverrides=function(){var Element=this.$wbwindow.Element;if(Element&&Element.prototype){var elementProto=Element.prototype,rewriteFn=this.rewriteInsertAdjHTMLOrElemArgs;if(elementProto.insertAdjacentHTML){var origInsertAdjacentHTML=elementProto.insertAdjacentHTML;elementProto.insertAdjacentHTML=function insertAdjacentHTML(position,text){return rewriteFn(this,origInsertAdjacentHTML,position,text,true)}}if(elementProto.insertAdjacentElement){var origIAdjElem=elementProto.insertAdjacentElement;elementProto.insertAdjacentElement=function insertAdjacentElement(position,element){return rewriteFn(this,origIAdjElem,position,element,false)}}}},Wombat.prototype.initDomOverride=function(){var Node=this.$wbwindow.Node;if(Node&&Node.prototype){var rewriteFn=this.rewriteNodeFuncArgs;if(Node.prototype.appendChild){var originalAppendChild=Node.prototype.appendChild;Node.prototype.appendChild=function appendChild(newNode,oldNode){return rewriteFn(this,originalAppendChild,newNode,oldNode)}}if(Node.prototype.insertBefore){var originalInsertBefore=Node.prototype.insertBefore;Node.prototype.insertBefore=function insertBefore(newNode,oldNode){return rewriteFn(this,originalInsertBefore,newNode,oldNode)}}if(Node.prototype.replaceChild){var originalReplaceChild=Node.prototype.replaceChild;Node.prototype.replaceChild=function replaceChild(newNode,oldNode){return rewriteFn(this,originalReplaceChild,newNode,oldNode)}}this.overridePropToProxy(Node.prototype,"ownerDocument"),this.overridePropToProxy(this.$wbwindow.HTMLHtmlElement.prototype,"parentNode"),this.overridePropToProxy(this.$wbwindow.Event.prototype,"target")}this.$wbwindow.Element&&this.$wbwindow.Element.prototype&&(this.overrideParentNodeAppendPrepend(this.$wbwindow.Element),this.overrideChildNodeInterface(this.$wbwindow.Element,false)),this.$wbwindow.DocumentFragment&&this.$wbwindow.DocumentFragment.prototype&&this.overrideParentNodeAppendPrepend(this.$wbwindow.DocumentFragment)},Wombat.prototype.initDocOverrides=function($document){if(Object.defineProperty){this.overrideReferrer($document),this.defGetterProp($document,"origin",function origin(){return this.WB_wombat_location.origin}),this.defGetterProp(this.$wbwindow,"origin",function origin(){return this.WB_wombat_location.origin});var wombat=this,domain_setter=function domain(val){var loc=this.WB_wombat_location;loc&&wombat.endsWith(loc.hostname,val)&&(this.__wb_domain=val)},domain_getter=function domain(){return this.__wb_domain||this.WB_wombat_location.hostname};this.defProp($document,"domain",domain_setter,domain_getter)}},Wombat.prototype.initDocWriteOpenCloseOverride=function(){if(this.$wbwindow.DOMParser){var DocumentProto=this.$wbwindow.Document.prototype,$wbDocument=this.$wbwindow.document,docWriteWritelnRWFn=this.rewriteDocWriteWriteln,orig_doc_write=$wbDocument.write,new_write=function write(){return docWriteWritelnRWFn(this,orig_doc_write,arguments)};$wbDocument.write=new_write,DocumentProto.write=new_write;var orig_doc_writeln=$wbDocument.writeln,new_writeln=function writeln(){return docWriteWritelnRWFn(this,orig_doc_writeln,arguments)};$wbDocument.writeln=new_writeln,DocumentProto.writeln=new_writeln;var wombat=this,orig_doc_open=$wbDocument.open,new_open=function open(){var res,thisObj=wombat.proxyToObj(this);if(arguments.length===3){var rwUrl=wombat.rewriteUrl(arguments[0],false,"mp_");res=orig_doc_open.call(thisObj,rwUrl,arguments[1],arguments[2]),wombat.initNewWindowWombat(res,arguments[0])}else res=orig_doc_open.call(thisObj),wombat.initNewWindowWombat(thisObj.defaultView);return res};$wbDocument.open=new_open,DocumentProto.open=new_open;var originalClose=$wbDocument.close,newClose=function close(){var thisObj=wombat.proxyToObj(this);return wombat.initNewWindowWombat(thisObj.defaultView),originalClose.__WB_orig_apply?originalClose.__WB_orig_apply(thisObj,arguments):originalClose.apply(thisObj,arguments)};$wbDocument.close=newClose,DocumentProto.close=newClose;var oBodyGetter=this.getOrigGetter(DocumentProto,"body"),oBodySetter=this.getOrigSetter(DocumentProto,"body");oBodyGetter&&oBodySetter&&this.defProp(DocumentProto,"body",function body(newBody){return newBody&&(newBody instanceof HTMLBodyElement||newBody instanceof HTMLFrameSetElement)&&wombat.rewriteElemComplete(newBody),oBodySetter.call(wombat.proxyToObj(this),newBody)},oBodyGetter)}},Wombat.prototype.initIframeWombat=function(iframe){var win;win=iframe._get_contentWindow?iframe._get_contentWindow.call(iframe):iframe.contentWindow;try{if(!win||win===this.$wbwindow||win._skip_wombat||win._wb_wombat)return}catch(e){return}var src=iframe.src;this.initNewWindowWombat(win,src)},Wombat.prototype.initNewWindowWombat=function(win,src){var fullWombat=false;if(win&&!win._wb_wombat){if((!src||src===""||this.startsWithOneOf(src,["about:blank","javascript:"]))&&(fullWombat=true),!fullWombat&&this.wb_info.isSW){var origURL=this.extractOriginalURL(src);(origURL==="about:blank"||origURL.startsWith("srcdoc:")||origURL.startsWith("blob:"))&&(fullWombat=true)}if(fullWombat){var newInfo={};Object.assign(newInfo,this.wb_info);var wombat=new Wombat(win,newInfo);win._wb_wombat=wombat.wombatInit()}else this.initProtoPmOrigin(win),this.initPostMessageOverride(win),this.initMessageEventOverride(win),this.initCheckThisFunc(win),this.initImportWrapperFunc(win)}},Wombat.prototype.initTimeoutIntervalOverrides=function(){var rewriteFn=this.rewriteSetTimeoutInterval;if(this.$wbwindow.setTimeout&&!this.$wbwindow.setTimeout.__$wbpatched$__){var originalSetTimeout=this.$wbwindow.setTimeout;this.$wbwindow.setTimeout=function setTimeout(){return rewriteFn(this,originalSetTimeout,arguments)},this.$wbwindow.setTimeout.__$wbpatched$__=true}if(this.$wbwindow.setInterval&&!this.$wbwindow.setInterval.__$wbpatched$__){var originalSetInterval=this.$wbwindow.setInterval;this.$wbwindow.setInterval=function setInterval(){return rewriteFn(this,originalSetInterval,arguments)},this.$wbwindow.setInterval.__$wbpatched$__=true}},Wombat.prototype.initWorkerOverrides=function(){var wombat=this;if(this.$wbwindow.Worker&&!this.$wbwindow.Worker._wb_worker_overriden){var orig_worker=this.$wbwindow.Worker;this.$wbwindow.Worker=function(Worker_){return function Worker(url,options){return wombat.domConstructorErrorChecker(this,"Worker",arguments),new Worker_(wombat.rewriteWorker(url),options)}}(orig_worker),this.$wbwindow.Worker.prototype=orig_worker.prototype,Object.defineProperty(this.$wbwindow.Worker.prototype,"constructor",{value:this.$wbwindow.Worker}),this.$wbwindow.Worker._wb_worker_overriden=true}if(this.$wbwindow.SharedWorker&&!this.$wbwindow.SharedWorker.__wb_sharedWorker_overriden){var oSharedWorker=this.$wbwindow.SharedWorker;this.$wbwindow.SharedWorker=function(SharedWorker_){return function SharedWorker(url,options){return wombat.domConstructorErrorChecker(this,"SharedWorker",arguments),new SharedWorker_(wombat.rewriteWorker(url),options)}}(oSharedWorker),this.$wbwindow.SharedWorker.prototype=oSharedWorker.prototype,Object.defineProperty(this.$wbwindow.SharedWorker.prototype,"constructor",{value:this.$wbwindow.SharedWorker}),this.$wbwindow.SharedWorker.__wb_sharedWorker_overriden=true}if(this.$wbwindow.ServiceWorkerContainer&&this.$wbwindow.ServiceWorkerContainer.prototype&&this.$wbwindow.ServiceWorkerContainer.prototype.register){var orig_register=this.$wbwindow.ServiceWorkerContainer.prototype.register;this.$wbwindow.ServiceWorkerContainer.prototype.register=function register(scriptURL,options){var newScriptURL=new URL(scriptURL,wombat.$wbwindow.document.baseURI).href,mod=wombat.getPageUnderModifier();return options&&options.scope?options.scope=wombat.rewriteUrl(options.scope,false,mod):options={scope:wombat.rewriteUrl("/",false,mod)},orig_register.call(this,wombat.rewriteUrl(newScriptURL,false,"sw_"),options)}}if(this.$wbwindow.Worklet&&this.$wbwindow.Worklet.prototype&&this.$wbwindow.Worklet.prototype.addModule&&!this.$wbwindow.Worklet.__wb_workerlet_overriden){var oAddModule=this.$wbwindow.Worklet.prototype.addModule;this.$wbwindow.Worklet.prototype.addModule=function addModule(moduleURL,options){var rwModuleURL=wombat.rewriteUrl(moduleURL,false,"js_");return oAddModule.call(this,rwModuleURL,options)},this.$wbwindow.Worklet.__wb_workerlet_overriden=true}},Wombat.prototype.initLocOverride=function(loc,oSetter,oGetter){if(Object.defineProperty)for(var prop,i=0;i=0&&props.splice(foundInx,1);return props}})}catch(e){console.log(e)}},Wombat.prototype.initHashChange=function(){if(this.$wbwindow.__WB_top_frame){var wombat=this,receive_hash_change=function receive_hash_change(event){if(event.data&&event.data.from_top){var message=event.data.message;message.wb_type&&(message.wb_type!=="outer_hashchange"||wombat.$wbwindow.location.hash==message.hash||(wombat.$wbwindow.location.hash=message.hash))}},send_hash_change=function send_hash_change(){var message={wb_type:"hashchange",hash:wombat.$wbwindow.location.hash};wombat.sendTopMessage(message)};this.$wbwindow.addEventListener("message",receive_hash_change),this.$wbwindow.addEventListener("hashchange",send_hash_change)}},Wombat.prototype.initPostMessageOverride=function($wbwindow){if($wbwindow.postMessage&&!$wbwindow.__orig_postMessage){var orig=$wbwindow.postMessage,wombat=this;$wbwindow.__orig_postMessage=orig;var postmessage_rewritten=function postMessage(message,targetOrigin,transfer,from_top){var from,src_id,this_obj=wombat.proxyToObj(this);if(this_obj||(this_obj=$wbwindow,this_obj.__WB_source=$wbwindow),this_obj.__WB_source&&this_obj.__WB_source.WB_wombat_location){var source=this_obj.__WB_source;if(from=source.WB_wombat_location.origin,this_obj.__WB_win_id||(this_obj.__WB_win_id={},this_obj.__WB_counter=0),!source.__WB_id){var id=this_obj.__WB_counter;source.__WB_id=id+source.WB_wombat_location.href,this_obj.__WB_counter+=1}this_obj.__WB_win_id[source.__WB_id]=source,src_id=source.__WB_id,this_obj.__WB_source=undefined}else from=window.WB_wombat_location.origin;var to_origin=targetOrigin;to_origin===this_obj.location.origin&&(to_origin=from);var new_message={from:from,to_origin:to_origin,src_id:src_id,message:message,from_top:from_top};if(targetOrigin!=="*"){if(this_obj.location.origin==="null"||this_obj.location.origin==="")return;targetOrigin=this_obj.location.origin}return orig.call(this_obj,new_message,targetOrigin,transfer)};$wbwindow.postMessage=postmessage_rewritten,$wbwindow.Window.prototype.postMessage=postmessage_rewritten;var eventTarget=null;eventTarget=$wbwindow.EventTarget&&$wbwindow.EventTarget.prototype?$wbwindow.EventTarget.prototype:$wbwindow;var _oAddEventListener=eventTarget.addEventListener;eventTarget.addEventListener=function addEventListener(type,listener,useCapture){var rwListener,obj=wombat.proxyToObj(this);if(type==="message"?rwListener=wombat.message_listeners.add_or_get(listener,function(){return wrapEventListener(listener,obj,wombat)}):type==="storage"?wombat.storage_listeners.add_or_get(listener,function(){return wrapSameOriginEventListener(listener,obj)}):rwListener=listener,rwListener)return _oAddEventListener.call(obj,type,rwListener,useCapture)};var _oRemoveEventListener=eventTarget.removeEventListener;eventTarget.removeEventListener=function removeEventListener(type,listener,useCapture){var rwListener,obj=wombat.proxyToObj(this);if(type==="message"?rwListener=wombat.message_listeners.remove(listener):type==="storage"?wombat.storage_listeners.remove(listener):rwListener=listener,rwListener)return _oRemoveEventListener.call(obj,type,rwListener,useCapture)};var override_on_prop=function(onevent,wrapperFN){var orig_setter=wombat.getOrigSetter($wbwindow,onevent),setter=function(value){this["__orig_"+onevent]=value;var obj=wombat.proxyToObj(this),listener=value?wrapperFN(value,obj,wombat):value;return orig_setter.call(obj,listener)},getter=function(){return this["__orig_"+onevent]};wombat.defProp($wbwindow,onevent,setter,getter)};override_on_prop("onmessage",wrapEventListener),override_on_prop("onstorage",wrapSameOriginEventListener)}},Wombat.prototype.initMessageEventOverride=function($wbwindow){!$wbwindow.MessageEvent||$wbwindow.MessageEvent.prototype.__extended||(this.addEventOverride("target"),this.addEventOverride("srcElement"),this.addEventOverride("currentTarget"),this.addEventOverride("eventPhase"),this.addEventOverride("path"),this.overridePropToProxy($wbwindow.MessageEvent.prototype,"source"),$wbwindow.MessageEvent.prototype.__extended=true)},Wombat.prototype.initUIEventsOverrides=function(){this.overrideAnUIEvent("UIEvent"),this.overrideAnUIEvent("MouseEvent"),this.overrideAnUIEvent("TouchEvent"),this.overrideAnUIEvent("FocusEvent"),this.overrideAnUIEvent("KeyboardEvent"),this.overrideAnUIEvent("WheelEvent"),this.overrideAnUIEvent("InputEvent"),this.overrideAnUIEvent("CompositionEvent")},Wombat.prototype.initOpenOverride=function(){var orig=this.$wbwindow.open;this.$wbwindow.Window.prototype.open&&(orig=this.$wbwindow.Window.prototype.open);var wombat=this,open_rewritten=function open(strUrl,strWindowName,strWindowFeatures){strWindowName&&(strWindowName=wombat.rewriteAttrTarget(strWindowName));var rwStrUrl=wombat.rewriteUrl(strUrl,false),res=orig.call(wombat.proxyToObj(this),rwStrUrl,strWindowName,strWindowFeatures);return wombat.initNewWindowWombat(res,strUrl),res};this.$wbwindow.open=open_rewritten,this.$wbwindow.Window.prototype.open&&(this.$wbwindow.Window.prototype.open=open_rewritten);for(var i=0;i Date: Tue, 24 Oct 2023 10:56:47 +1000 Subject: [PATCH 161/207] README.md: document standards With links to mirrors of the docs used to produce this library since some have been removed from their original locations. --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index d87531b..e6daf87 100644 --- a/README.md +++ b/README.md @@ -314,3 +314,11 @@ The `APRSDigipeater` class constructor can take a single parameter, `digipeater_timeout`, which sets an expiry (default of 5 seconds) on queued digipeat messages. If a message is not sent by the time this timeout expires, the message is quietly dropped, preventing the memory effect. + +## Specifications + +This library is built on the following specifications: + +* [AX.25 2.0](https://htmlpreview.github.io/?https://github.com/sjlongland/aioax25/blob/feature/connected-mode/doc/ax25-2p0/index.html) +* [AX.25 2.2](doc/ax25-2p2/ax25-2p2.pdf) +* [APRS 1.01](http://www.aprs.org/doc/APRS101.PDF) From 073fa88b4210c2791af1c255941486ea2ab15306 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Sat, 4 May 2024 14:44:24 +1000 Subject: [PATCH 162/207] frame: Fix UA stringification tests Seems this was copied from a UI frame test, UI frames have a PID field, but UA does not. Nor do they have a payload. --- tests/test_frame/test_uframe.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_frame/test_uframe.py b/tests/test_frame/test_uframe.py index dee7c4e..98933ac 100644 --- a/tests/test_frame/test_uframe.py +++ b/tests/test_frame/test_uframe.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 from aioax25.frame import ( + AX25DisconnectModeFrame, AX25Frame, - AX25UnnumberedInformationFrame, AX25FrameRejectFrame, - AX25UnnumberedFrame, - AX25DisconnectModeFrame, AX25SetAsyncBalancedModeFrame, AX25SetAsyncBalancedModeExtendedFrame, AX25TestFrame, + AX25UnnumberedAcknowledgeFrame, + AX25UnnumberedFrame, + AX25UnnumberedInformationFrame, ) from ..hex import from_hex, hex_cmp @@ -686,12 +687,10 @@ def test_ua_str(): destination="VK4BWI", source="VK4MSL", cr=True, - pid=0xF0, ) assert str(frame) == ( "AX25UnnumberedAcknowledgeFrame VK4MSL>VK4BWI: " - "Control=0x03 P/F=False Modifier=0x03 PID=0xf0\n" - "Payload=b'This is a test'" + "Control=0x73 P/F=True Modifier=0x63" ) From 9508652e5fa894de21bd29dbdc9599dab0a12a45 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 10:08:04 +1000 Subject: [PATCH 163/207] frame: Allow `AX25Address` to return "normalised copies" with customisations For connected mode, it is observed the SABM-sending station has the C/H bit cleared, but the receiving station has it set. So it'd be easier if we can have a function that "normalises" those bits the way we expect. --- aioax25/frame.py | 20 +++++++++++++++++--- tests/test_frame/test_ax25address.py | 12 ++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/aioax25/frame.py b/aioax25/frame.py index cedf733..2f416e1 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -2611,10 +2611,24 @@ def copy(self, **overrides): mydata.update(overrides) return self.__class__(**mydata) + def normcopy(self, **overrides): + """ + Return a normalised copy of this address. By default, reserved bits + are set to 1s, C/H and extension bits are set to 0s. + """ + + # Define defaults for the overrides + overrides.setdefault("res0", True) + overrides.setdefault("res1", True) + overrides.setdefault("ch", False) + overrides.setdefault("extension", False) + + return self.copy(**overrides) + @property def normalised(self): """ - Return a normalised copy of this address. (Set reserved bits to ones, - clear the CH bit and extension bit.) + Return a normalised copy of this address. """ - return self.copy(res0=True, res1=True, ch=False, extension=False) + + return self.normcopy() diff --git a/tests/test_frame/test_ax25address.py b/tests/test_frame/test_ax25address.py index 9952ec0..82702cd 100644 --- a/tests/test_frame/test_ax25address.py +++ b/tests/test_frame/test_ax25address.py @@ -338,6 +338,18 @@ def test_copy(): assert getattr(a, field) == getattr(b, field) +def test_normcopy(): + """ + Test we can get normalised copies with specific bits set. + """ + a = AX25Address("VK4MSL", 15, ch=True, res0=False, res1=False) + b = a.normcopy(res0=False, ch=True) + + assert b._ch is True + assert b._res0 is False + assert b._res1 is True + + def test_normalised(): """ Test we can get normalised copies for comparison. From 167a330ebf064a8d5af4b41720b414f2bdfbf345 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 10:09:36 +1000 Subject: [PATCH 164/207] kiss: Describe the other kinds of KISS frames Handy for packet dissection purposes. --- aioax25/kiss.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/aioax25/kiss.py b/aioax25/kiss.py index 41657c8..c002e64 100644 --- a/aioax25/kiss.py +++ b/aioax25/kiss.py @@ -176,6 +176,49 @@ def __init__(self, port, payload, cmd=CMD_DATA): KISSCommand._register(CMD_DATA, KISSCmdData) +# Remaining types for identification purposes only +class KISSCmdTXDelay(KISSCommand): + pass + + +KISSCommand._register(CMD_TXDELAY, KISSCmdTXDelay) + + +class KISSCmdP(KISSCommand): + pass + + +KISSCommand._register(CMD_P, KISSCmdP) + + +class KISSCmdSlotTime(KISSCommand): + pass + + +KISSCommand._register(CMD_SLOTTIME, KISSCmdSlotTime) + + +class KISSCmdTXTail(KISSCommand): + pass + + +KISSCommand._register(CMD_TXTAIL, KISSCmdTXTail) + + +class KISSCmdFDuplex(KISSCommand): + pass + + +KISSCommand._register(CMD_FDUPLEX, KISSCmdFDuplex) + + +class KISSCmdSetHW(KISSCommand): + pass + + +KISSCommand._register(CMD_SETHW, KISSCmdSetHW) + + # KISS device interface From 426c0b11e23e1d1cf71eee8cb817370285dbd6cf Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 10:20:59 +1000 Subject: [PATCH 165/207] tools: Add a KISS frame dump tool Very crude at this point, assumes 8-bit mode, but it's enough to figure out some of AX.25's secrets at this point. --- aioax25/tools/__init__.py | 0 aioax25/tools/dumphex.py | 99 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 +++ 3 files changed, 105 insertions(+) create mode 100644 aioax25/tools/__init__.py create mode 100644 aioax25/tools/dumphex.py diff --git a/aioax25/tools/__init__.py b/aioax25/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aioax25/tools/dumphex.py b/aioax25/tools/dumphex.py new file mode 100644 index 0000000..63b8342 --- /dev/null +++ b/aioax25/tools/dumphex.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +""" +Very crude AX.25 KISS packet dump dissector. + +This code takes the KISS traffic seen through `socat -x` (so it could be a +real serial port, a virtual one on a VM, or network sockets), and dumps the +traffic it saw to the output. + +Usage: + python3 -m aioax25.tools.dumphex > +""" + +from aioax25.kiss import BaseKISSDevice, KISSDeviceState, KISSCmdData +from aioax25.frame import AX25Frame +from binascii import a2b_hex +from argparse import ArgumentParser +import logging +import asyncio +import re + + +SOCAT_HEX_RE = re.compile(r"^ [0-9a-f]{2}[0-9a-f ]*\n*$") +NON_HEX_RE = re.compile(r"[^0-9a-f]") + + +class FileKISSDevice(BaseKISSDevice): + def __init__(self, filename, **kwargs): + super(FileKISSDevice, self).__init__(**kwargs) + self._filename = filename + self._read_finished = False + self._frames = [] + self._future = asyncio.Future() + + async def dump(self): + self.open() + await self._future + self.close() + return self._frames + + def _open(self): + with open(self._filename, "r") as f: + self._log.info("Reading frames from %r", self._filename) + self._state = KISSDeviceState.OPEN + for line in f: + match = SOCAT_HEX_RE.match(line) + if match: + self._log.debug("Parsing %r", line) + self._receive(a2b_hex(NON_HEX_RE.sub("", line))) + else: + self._log.debug("Ignoring %r", line) + + self._log.info("Read %r", self._filename) + self._read_finished = True + + def _receive_frame(self): + super(FileKISSDevice, self)._receive_frame() + + if not self._read_finished: + return + + if self._future.done(): + return + + if len(self._rx_buffer) < 2: + self._log.info("Buffer is now empty") + self._future.set_result(None) + + def _send_raw_data(self, data): + pass + + def _dispatch_rx_frame(self, frame): + self._frames.append(frame) + + def _close(self): + self._state = KISSDeviceState.CLOSED + + +async def main(): + ap = ArgumentParser() + ap.add_argument("hexfile", nargs="+") + + args = ap.parse_args() + + logging.basicConfig(level=logging.DEBUG) + + for filename in args.hexfile: + kissdev = FileKISSDevice(filename) + frames = await kissdev.dump() + + for frame in frames: + print(frame) + if isinstance(frame, KISSCmdData): + axframe = AX25Frame.decode(frame.payload, modulo128=False) + print(axframe) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 7cf391e..ed84b8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,3 +41,9 @@ log_cli = true [tool.setuptools.dynamic] version = {attr = "aioax25.__version__"} + +[tool.coverage.run] +omit = [ + # Debugging tool for dumping KISS traffic from socat hex dumps. + "aioax25/tools/dumphex.py" +] From fca874c421aa8b83cd1f24b1945d87ff897acea7 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 10:37:00 +1000 Subject: [PATCH 166/207] kiss: Silence annoying "RECV FRAME start at 0" when buffer == [0xc0] If the buffer only contains a single `0xc0` byte, then we're wasting our time checking for another frame. --- aioax25/kiss.py | 8 ++++++++ tests/test_kiss/test_base.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/aioax25/kiss.py b/aioax25/kiss.py index c002e64..f943976 100644 --- a/aioax25/kiss.py +++ b/aioax25/kiss.py @@ -292,6 +292,10 @@ def _receive_frame(self): underlying device. If more than one frame is present, schedule ourselves again with the IO loop. """ + # Skip if all we have is a FEND byte + if bytes(self._rx_buffer) == bytearray([BYTE_FEND]): + return + # Locate the first FEND byte try: start = self._rx_buffer.index(BYTE_FEND) @@ -336,11 +340,15 @@ def _receive_frame(self): # If we just have a FEND, stop here. if bytes(self._rx_buffer) == bytearray([BYTE_FEND]): + self._log.debug("FEND byte in receive buffer, wait for more.") return # If there is more to send, call ourselves via the IO loop if len(self._rx_buffer): + self._log.debug("More data in receive buffer, will check again.") self._loop.call_soon(self._receive_frame) + else: + self._log.debug("No data in receive buffer. Wait for more.") def _dispatch_rx_frame(self, frame): """ diff --git a/tests/test_kiss/test_base.py b/tests/test_kiss/test_base.py index d256e10..15ad7ff 100644 --- a/tests/test_kiss/test_base.py +++ b/tests/test_kiss/test_base.py @@ -142,6 +142,22 @@ def test_receive_frame_garbage_start(): assert len(loop.calls) == 0 +def test_receive_frame_single_fend(): + """ + Test _receive_frame does nothing if there's only a FEND byte. + """ + loop = DummyLoop() + kissdev = DummyKISSDevice(loop=loop, reset_on_close=True) + kissdev._rx_buffer += b"\xc0" + kissdev._receive_frame() + + # We should just have the last FEND + assert bytes(kissdev._rx_buffer) == b"\xc0" + + # It should leave the last FEND there and wait for more data. + assert len(loop.calls) == 0 + + def test_receive_frame_empty(): """ Test _receive_frame discards empty frames. From d2cf0efcf652021db3fff07e5f959605a8c5b455 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 10:46:49 +1000 Subject: [PATCH 167/207] station: Store and consider command bit state This appears to be indicative of which side sent the SABM: sender sends SABM with its own SSID field clearing the C/H bit, and the destination SSID setting the C/H bit. So in the station context, we really have two separate peers: - Peer with C/H cleared: is a peer that sent US a `SABM(E)` - Peer with C/H set: is a peer that WE sent a `SABM(E)` Default to C/H set to `True`, since most users will be making outbound connections. --- aioax25/station.py | 14 +++++++--- tests/test_station/test_getpeer.py | 41 +++++++++++++++++++++++++--- tests/test_station/test_receive.py | 43 ++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/aioax25/station.py b/aioax25/station.py index 9b7d6b9..1b42261 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -142,14 +142,20 @@ def detach(self): ) def getpeer( - self, callsign, ssid=None, repeaters=None, create=True, **kwargs + self, + callsign, + ssid=None, + repeaters=None, + command=True, + create=True, + **kwargs ): """ Retrieve an instance of a peer context. This creates the peer object if it doesn't already exist unless create is set to False (in which case it will raise KeyError). """ - address = AX25Address.decode(callsign, ssid).normalised + address = AX25Address.decode(callsign, ssid).normcopy(ch=command) try: return self._peers[address] except KeyError: @@ -199,7 +205,9 @@ def _on_receive(self, frame, **kwargs): # If we're still here, then we don't handle unsolicited frames # of this type, so pass it to a handler if we have one. peer = self.getpeer( - frame.header.source, repeaters=frame.header.repeaters.reply + frame.header.source, + repeaters=frame.header.repeaters.reply, + command=frame.header.source.ch, ) self._log.debug("Passing frame to peer %s: %s", peer.address, frame) peer._on_receive(frame) diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py index db572e5..2b36025 100644 --- a/tests/test_station/test_getpeer.py +++ b/tests/test_station/test_getpeer.py @@ -18,17 +18,29 @@ def test_unknown_peer_nocreate_keyerror(): except KeyError as e: assert str(e) == ( "AX25Address(callsign=VK4BWI, ssid=0, " - "ch=False, res0=True, res1=True, extension=False)" + "ch=True, res0=True, res1=True, extension=False)" ) -def test_unknown_peer_create_instance(): +def test_unknown_peer_create_instance_ch(): """ - Test fetching an unknown peer with create=True generates peer + Test fetching an unknown peer with create=True generates peer with C/H set """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") peer = station.getpeer("VK4BWI", create=True) assert isinstance(peer, AX25Peer) + assert peer.address.ch is True + + +def test_unknown_peer_create_instance_noch(): + """ + Test fetching an unknown peer with create=True and command=False generates + peer with C/H clear + """ + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") + peer = station.getpeer("VK4BWI", create=True, command=False) + assert isinstance(peer, AX25Peer) + assert peer.address.ch is False def test_known_peer_fetch_instance(): @@ -36,7 +48,7 @@ def test_known_peer_fetch_instance(): Test fetching an known peer returns that known peer """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - mypeer = DummyPeer(station, AX25Address("VK4BWI")) + mypeer = DummyPeer(station, AX25Address("VK4BWI", ch=True)) # Inject the peer station._peers[mypeer._address] = mypeer @@ -44,3 +56,24 @@ def test_known_peer_fetch_instance(): # Retrieve the peer instance peer = station.getpeer("VK4BWI") assert peer is mypeer + + +def test_known_peer_fetch_instance_ch(): + """ + Test fetching peers differentiates command bits + """ + station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") + mypeer_in = DummyPeer(station, AX25Address("VK4BWI", ch=False)) + mypeer_out = DummyPeer(station, AX25Address("VK4BWI", ch=True)) + + # Inject the peers + station._peers[mypeer_in._address] = mypeer_in + station._peers[mypeer_out._address] = mypeer_out + + # Retrieve the peer instance + peer = station.getpeer("VK4BWI", command=True) + assert peer is mypeer_out + + # Retrieve the other peer instance + peer = station.getpeer("VK4BWI", command=False) + assert peer is mypeer_in diff --git a/tests/test_station/test_receive.py b/tests/test_station/test_receive.py index 4bcaac6..c8ef135 100644 --- a/tests/test_station/test_receive.py +++ b/tests/test_station/test_receive.py @@ -132,3 +132,46 @@ def stub_on_test_frame(*args, **kwargs): assert rx_call_kwargs == {} assert len(rx_call_args) == 1 assert rx_call_args[0] is txframe + + +def test_route_incoming_msg_ch(): + """ + Test passing a frame considers C/H bits. + """ + interface = DummyInterface() + station = AX25Station(interface=interface, callsign="VK4MSL-5") + + # Stub out _on_test_frame + def stub_on_test_frame(*args, **kwargs): + assert False, "Should not have been called" + + station._on_test_frame = stub_on_test_frame + + # Inject a couple of peers + peer1 = DummyPeer(station, AX25Address("VK4BWI", ssid=7, ch=False)) + peer2 = DummyPeer(station, AX25Address("VK4BWI", ssid=7, ch=True)) + station._peers[peer1._address] = peer1 + station._peers[peer2._address] = peer2 + + # Pass in the message + txframe = AX25UnnumberedInformationFrame( + destination="VK4MSL-5", + source="VK4BWI-7*", + cr=True, + pid=0xAB, + payload=b"This is a test frame", + ) + station._on_receive(frame=txframe) + + # There should be no replies queued + assert interface.bind_calls == [] + assert interface.unbind_calls == [] + assert interface.transmit_calls == [] + + # This should have gone to peer2, not peer1 + assert peer1.on_receive_calls == [] + assert len(peer2.on_receive_calls) == 1 + (rx_call_args, rx_call_kwargs) = peer2.on_receive_calls.pop() + assert rx_call_kwargs == {} + assert len(rx_call_args) == 1 + assert rx_call_args[0] is txframe From 5d98ab161f9634ff984279beb3c6c5c3153f2e9d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 12:20:07 +1000 Subject: [PATCH 168/207] peer: Move enumerations out of the `AX25Peer` class I realised how cumbersome it is to put it here! --- aioax25/peer.py | 140 +++++++++++++++-------------- aioax25/station.py | 6 +- tests/test_peer/peer.py | 4 +- tests/test_peer/test_cleanup.py | 9 +- tests/test_peer/test_connection.py | 51 +++++------ tests/test_peer/test_disc.py | 13 +-- tests/test_peer/test_dm.py | 9 +- tests/test_peer/test_frmr.py | 9 +- tests/test_peer/test_state.py | 13 +-- tests/test_peer/test_xid.py | 47 +++++----- 10 files changed, 155 insertions(+), 146 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 7aa1c74..383b2f4 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -66,46 +66,48 @@ _REJECT_MODE_PRECEDENCE = {"selective_rr": 2, "selective": 1, "implicit": 0} -class AX25Peer(object): - """ - This class is a proxy representation of the remote AX.25 peer that may be - connected to this station. The factory for these objects is the - AX25Station's getpeer method. - """ +class AX25RejectMode(enum.Enum): + IMPLICIT = "implicit" + SELECTIVE = "selective" + SELECTIVE_RR = "selective_rr" - class AX25RejectMode(enum.Enum): - IMPLICIT = "implicit" - SELECTIVE = "selective" - SELECTIVE_RR = "selective_rr" + @property + def precedence(self): + """ + Get the precedence of this mode. + """ + return _REJECT_MODE_PRECEDENCE[self.value] - @property - def precedence(self): - """ - Get the precedence of this mode. - """ - return _REJECT_MODE_PRECEDENCE[self.value] - class AX25PeerState(enum.Enum): - # DISCONNECTED: No connection has been established - DISCONNECTED = 0 +class AX25PeerState(enum.Enum): + # DISCONNECTED: No connection has been established + DISCONNECTED = 0 - # Awaiting response to XID request - NEGOTIATING = 1 + # Awaiting response to XID request + NEGOTIATING = 1 - # Awaiting response to SABM(E) request - CONNECTING = 2 + # Awaiting response to SABM(E) request + CONNECTING = 2 - # Connection is established - CONNECTED = 3 + # Connection is established + CONNECTED = 3 - # Awaiting response to DISC request - DISCONNECTING = 4 + # Awaiting response to DISC request + DISCONNECTING = 4 - # FRMR condition entered - FRMR = 5 + # FRMR condition entered + FRMR = 5 - # Incomming connection, awaiting our UA or DM response - INCOMING_CONNECTION = 6 + # Incomming connection, awaiting our UA or DM response + INCOMING_CONNECTION = 6 + + +class AX25Peer(object): + """ + This class is a proxy representation of the remote AX.25 peer that may be + connected to this station. The factory for these objects is the + AX25Station's getpeer method. + """ def __init__( self, @@ -159,7 +161,7 @@ def __init__( self._loop = loop # Internal state (see AX.25 2.2 spec 4.2.4) - self._state = self.AX25PeerState.DISCONNECTED + self._state = AX25PeerState.DISCONNECTED self._max_outstanding = None # Decided when SABM(E) received self._modulo = None # Set when SABM(E) received self._negotiated = False # Set to True after XID negotiation @@ -314,7 +316,7 @@ def connect(self): """ Connect to the remote node. """ - if self._state is self.AX25PeerState.DISCONNECTED: + if self._state is AX25PeerState.DISCONNECTED: self._log.info("Initiating connection to remote peer") handler = AX25PeerConnectionHandler(self) handler.done_sig.connect(self._on_connect_response) @@ -329,13 +331,13 @@ def accept(self): """ Accept an incoming connection from the peer. """ - if self._state is self.AX25PeerState.INCOMING_CONNECTION: + if self._state is AX25PeerState.INCOMING_CONNECTION: self._log.info("Accepting incoming connection") # Send a UA and set ourselves as connected self._stop_ack_timer() self._send_ua() self._log.info("Connection accepted") - self._set_conn_state(self.AX25PeerState.CONNECTED) + self._set_conn_state(AX25PeerState.CONNECTED) else: self._log.info( "Will not accept connection from peer now, " @@ -347,11 +349,11 @@ def reject(self): """ Reject an incoming connection from the peer. """ - if self._state is self.AX25PeerState.INCOMING_CONNECTION: + if self._state is AX25PeerState.INCOMING_CONNECTION: self._log.info("Rejecting incoming connection") # Send a DM and set ourselves as disconnected self._stop_ack_timer() - self._set_conn_state(self.AX25PeerState.DISCONNECTED) + self._set_conn_state(AX25PeerState.DISCONNECTED) self._send_dm() else: self._log.info( @@ -364,9 +366,9 @@ def disconnect(self): """ Disconnect from the remote node. """ - if self._state is self.AX25PeerState.CONNECTED: + if self._state is AX25PeerState.CONNECTED: self._uaframe_handler = self._on_disconnect - self._set_conn_state(self.AX25PeerState.DISCONNECTING) + self._set_conn_state(AX25PeerState.DISCONNECTING) self._send_disc() self._start_disconnect_ack_timer() else: @@ -410,11 +412,11 @@ def _cleanup(self): Clean up the instance of this peer as the activity has expired. """ if self._state not in ( - self.AX25PeerState.DISCONNECTED, - self.AX25PeerState.DISCONNECTING, + AX25PeerState.DISCONNECTED, + AX25PeerState.DISCONNECTING, ): self._log.warning("Disconnecting peer due to inactivity") - if self._state is self.AX25PeerState.CONNECTED: + if self._state is AX25PeerState.CONNECTED: self.disconnect() else: self._send_dm() @@ -450,7 +452,7 @@ def _on_receive(self, frame): # AX.25 2.2 sect 6.3.1: "The originating TNC sending a SABM(E) command # ignores and discards any frames except SABM, DISC, UA and DM frames # from the distant TNC." - if (self._state is self.AX25PeerState.CONNECTING) and not isinstance( + if (self._state is AX25PeerState.CONNECTING) and not isinstance( frame, ( AX25SetAsyncBalancedModeFrame, # SABM @@ -471,7 +473,7 @@ def _on_receive(self, frame): # a DM response frame. Any other command received while the DXE is in # the frame reject state will cause another FRMR to be sent out with # the same information field as originally sent." - if (self._state is self.AX25PeerState.FRMR) and not isinstance( + if (self._state is AX25PeerState.FRMR) and not isinstance( frame, (AX25SetAsyncBalancedModeFrame, AX25DisconnectFrame), # SABM ): # DISC @@ -512,7 +514,7 @@ def _on_receive(self, frame): elif isinstance(frame, AX25RawFrame): # This is either an I or S frame. We should know enough now to # decode it properly. - if self._state is self.AX25PeerState.CONNECTED: + if self._state is AX25PeerState.CONNECTED: # A connection is in progress, we can decode this frame = AX25Frame.decode( frame, modulo128=(self._modulo == 128) @@ -783,7 +785,7 @@ def _on_receive_sabm(self, frame): # Are we already connecting ourselves to this station? If yes, # we should just treat their SABM(E) as a UA, since _clearly_ both # parties wish to connect. - if self._state == self.AX25PeerState.CONNECTING: + if self._state == AX25PeerState.CONNECTING: self._log.info( "Auto-accepting incoming connection as we are waiting for " "UA from our connection attempt." @@ -793,7 +795,7 @@ def _on_receive_sabm(self, frame): # Set the incoming connection state, and emit a signal via the # station's 'connection_request' signal. self._log.debug("Preparing incoming connection") - self._set_conn_state(self.AX25PeerState.INCOMING_CONNECTION) + self._set_conn_state(AX25PeerState.INCOMING_CONNECTION) self._start_connect_ack_timer() self._log.debug("Announcing incoming connection") self._station().connection_request.emit(peer=self) @@ -816,7 +818,7 @@ def _stop_ack_timer(self): self._ack_timeout_handle = None def _on_incoming_connect_timeout(self): - if self._state is self.AX25PeerState.INCOMING_CONNECTION: + if self._state is AX25PeerState.INCOMING_CONNECTION: self._log.info("Incoming connection timed out") self._ack_timeout_handle = None self.reject() @@ -830,10 +832,10 @@ def _on_connect_response(self, response, **kwargs): self._log.debug("Connection response: %r", response) if response == "ack": # We're in. - self._set_conn_state(self.AX25PeerState.CONNECTED) + self._set_conn_state(AX25PeerState.CONNECTED) else: # Didn't work - self._set_conn_state(self.AX25PeerState.DISCONNECTED) + self._set_conn_state(AX25PeerState.DISCONNECTED) def _negotiate(self, callback): """ @@ -851,7 +853,7 @@ def _negotiate(self, callback): handler.done_sig.connect(callback) handler._go() - self._set_conn_state(self.AX25PeerState.NEGOTIATING) + self._set_conn_state(AX25PeerState.NEGOTIATING) return def _on_negotiate_result(self, response, **kwargs): @@ -881,7 +883,7 @@ def _on_negotiate_result(self, response, **kwargs): self._negotiated = True self._log.debug("XID negotiation complete") - self._set_conn_state(self.AX25PeerState.DISCONNECTED) + self._set_conn_state(AX25PeerState.DISCONNECTED) def _init_connection(self, extended): """ @@ -950,7 +952,7 @@ def _set_conn_state(self, state): ) def _on_disc_ua_timeout(self): - if self._state is self.AX25PeerState.DISCONNECTING: + if self._state is AX25PeerState.DISCONNECTING: self._log.info("DISC timed out, assuming we're disconnected") # Assume we are disconnected. self._ack_timeout_handle = None @@ -968,7 +970,7 @@ def _on_disconnect(self): self._stop_ack_timer() # Set ourselves as disconnected - self._set_conn_state(self.AX25PeerState.DISCONNECTED) + self._set_conn_state(AX25PeerState.DISCONNECTED) # Reset our state self._reset_connection_state() @@ -999,7 +1001,7 @@ def _on_receive_dm(self): """ Handle a disconnect request from this peer. """ - if self._state is self.AX25PeerState.CONNECTED: + if self._state is AX25PeerState.CONNECTED: # Set ourselves as disconnected self._log.info("Received DM from peer") self._on_disconnect() @@ -1027,8 +1029,8 @@ def _on_receive_xid(self, frame): return self._send_frmr(frame, w=True) if self._state in ( - self.AX25PeerState.CONNECTING, - self.AX25PeerState.DISCONNECTING, + AX25PeerState.CONNECTING, + AX25PeerState.DISCONNECTING, ): # AX.25 2.2 sect 4.3.3.7: "A station receiving an XID command # returns an XID response unless a UA response to a mode setting @@ -1139,13 +1141,13 @@ def _process_xid_hdlcoptfunc(self, param): # Negotiable parts of this parameter are: # - SREJ/REJ bits if param.srej and param.rej: - reject_mode = self.AX25RejectMode.SELECTIVE_RR + reject_mode = AX25RejectMode.SELECTIVE_RR elif param.srej: - reject_mode = self.AX25RejectMode.SELECTIVE + reject_mode = AX25RejectMode.SELECTIVE else: # Technically this means also the invalid SREJ=0 REJ=0, # we'll assume they meant REJ=1 in that case. - reject_mode = self.AX25RejectMode.IMPLICIT + reject_mode = AX25RejectMode.IMPLICIT # We take the option with the lowest precedence if self._reject_mode.precedence > reject_mode.precedence: @@ -1238,8 +1240,8 @@ def _send_sabm(self): repeaters=self.reply_path, ) ) - if self._state is not self.AX25PeerState.INCOMING_CONNECTION: - self._set_conn_state(self.AX25PeerState.CONNECTING) + if self._state is not AX25PeerState.INCOMING_CONNECTION: + self._set_conn_state(AX25PeerState.CONNECTING) def _send_xid(self, cr): self._transmit_frame( @@ -1256,15 +1258,15 @@ def _send_xid(self, cr): rej=( self._reject_mode in ( - self.AX25RejectMode.IMPLICIT, - self.AX25RejectMode.SELECTIVE_RR, + AX25RejectMode.IMPLICIT, + AX25RejectMode.SELECTIVE_RR, ) ), srej=( self._reject_mode in ( - self.AX25RejectMode.SELECTIVE, - self.AX25RejectMode.SELECTIVE_RR, + AX25RejectMode.SELECTIVE, + AX25RejectMode.SELECTIVE_RR, ) ), modulo8=(not self._modulo128), @@ -1342,7 +1344,7 @@ def _send_frmr(self, frame, w=False, x=False, y=False, z=False): # a DM response frame. Any other command received while the DXE is in # the frame reject state will cause another FRMR to be sent out with # the same information field as originally sent." - self._set_conn_state(self.AX25PeerState.FRMR) + self._set_conn_state(AX25PeerState.FRMR) # See https://www.tapr.org/pub_ax25.html self._transmit_frame( @@ -1389,7 +1391,7 @@ def _send_rr_notification(self): # additional I frames are not being transmitted." self._cancel_rr_notification() - if self._state is self.AX25PeerState.CONNECTED: + if self._state is AX25PeerState.CONNECTED: self._log.debug( "Sending RR with N(R) == V(R) == %d", self._recv_state ) @@ -1407,7 +1409,7 @@ def _send_rnr_notification(self): """ Send a RNR notification if the RNR interval has elapsed. """ - if self._state is self.AX25PeerState.CONNECTED: + if self._state is AX25PeerState.CONNECTED: now = self._loop.time() if (now - self._last_rnr_sent) > self._rnr_interval: self._transmit_frame( diff --git a/aioax25/station.py b/aioax25/station.py index 1b42261..019a77b 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -15,7 +15,7 @@ from .frame import AX25Address, AX25Path, AX25TestFrame -from .peer import AX25Peer +from .peer import AX25Peer, AX25RejectMode from .version import AX25Version @@ -41,7 +41,7 @@ def __init__( full_duplex=False, # HDLC Optional Functions modulo128=False, # Whether to use Mod128 by default - reject_mode=AX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, # What reject mode to use? # Parameters (AX.25 2.2 sect 6.7.2) max_ifield=256, # aka N1 @@ -83,7 +83,7 @@ def __init__( self._protocol = protocol self._ack_timeout = ack_timeout self._idle_timeout = idle_timeout - self._reject_mode = AX25Peer.AX25RejectMode(reject_mode) + self._reject_mode = AX25RejectMode(reject_mode) self._modulo128 = modulo128 self._max_ifield = max_ifield self._max_ifield_rx = max_ifield_rx diff --git a/tests/test_peer/peer.py b/tests/test_peer/peer.py index c0b4d66..cef2a5a 100644 --- a/tests/test_peer/peer.py +++ b/tests/test_peer/peer.py @@ -4,7 +4,7 @@ Fixture for initialising an AX25 Peer """ -from aioax25.peer import AX25Peer +from aioax25.peer import AX25Peer, AX25RejectMode from aioax25.version import AX25Version from ..mocks import DummyIOLoop, DummyLogger @@ -27,7 +27,7 @@ def __init__( idle_timeout=900.0, protocol=AX25Version.UNKNOWN, modulo128=False, - reject_mode=AX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, full_duplex=False, reply_path=None, locked_path=False, diff --git a/tests/test_peer/test_cleanup.py b/tests/test_peer/test_cleanup.py index 258d805..86998a9 100644 --- a/tests/test_peer/test_cleanup.py +++ b/tests/test_peer/test_cleanup.py @@ -5,6 +5,7 @@ """ from aioax25.frame import AX25Address, AX25Path +from aioax25.peer import AX25PeerState from .peer import TestingAX25Peer from ..mocks import DummyStation, DummyTimeout @@ -119,7 +120,7 @@ def _send_dm(): peer._send_dm = _send_dm # Set state - peer._state = peer.AX25PeerState.DISCONNECTED + peer._state = AX25PeerState.DISCONNECTED # Do clean-up peer._cleanup() @@ -162,7 +163,7 @@ def _send_dm(): peer._send_dm = _send_dm # Set state - peer._state = peer.AX25PeerState.DISCONNECTING + peer._state = AX25PeerState.DISCONNECTING # Do clean-up peer._cleanup() @@ -202,7 +203,7 @@ def _send_dm(): peer._send_dm = _send_dm # Set state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Do clean-up peer._cleanup() @@ -242,7 +243,7 @@ def _send_dm(): peer._send_dm = _send_dm # Set state - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED # Do clean-up peer._cleanup() diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index d741dd5..0654176 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -26,6 +26,7 @@ AX2516BitRejectFrame, AX2516BitSelectiveRejectFrame, ) +from aioax25.peer import AX25PeerState from .peer import TestingAX25Peer from ..mocks import DummyStation, DummyTimeout @@ -54,7 +55,7 @@ def _negotiate(*args, **kwargs): peer._negotiated = False # Override the state to ensure connection attempt never happens - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED # Now try connecting peer.connect() @@ -85,7 +86,7 @@ def _negotiate(*args, **kwargs): peer._negotiated = False # Ensure disconnected state - peer._state = peer.AX25PeerState.DISCONNECTED + peer._state = AX25PeerState.DISCONNECTED # Now try connecting try: @@ -131,7 +132,7 @@ def _transmit_frame(frame): assert str(frame.header.repeaters) == "VK4RZB" assert len(sent) == 0 - assert peer._state == peer.AX25PeerState.CONNECTING + assert peer._state == AX25PeerState.CONNECTING def test_send_sabme(): @@ -168,7 +169,7 @@ def _transmit_frame(frame): assert str(frame.header.repeaters) == "VK4RZB" assert len(sent) == 0 - assert peer._state == peer.AX25PeerState.CONNECTING + assert peer._state == AX25PeerState.CONNECTING # SABM response handling @@ -198,7 +199,7 @@ def _on_receive_frmr(): peer._on_receive_frmr = _on_receive_frmr # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame peer._on_receive( @@ -242,7 +243,7 @@ def _on_receive_test(): peer._on_receive_test = _on_receive_test # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame peer._on_receive( @@ -279,7 +280,7 @@ def _on_receive_ua(): peer._uaframe_handler = _on_receive_ua # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame peer._on_receive( @@ -323,7 +324,7 @@ def _on_disconnect(): peer._on_disconnect = _on_disconnect # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame peer._on_receive( @@ -367,7 +368,7 @@ def _on_disconnect(): peer._on_disconnect = _on_disconnect # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame peer._on_receive( @@ -409,7 +410,7 @@ def _on_receive_sabm(frame): peer._on_receive_sabm = _on_receive_sabm # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame frame = AX25SetAsyncBalancedModeFrame( @@ -446,7 +447,7 @@ def _on_receive_sabm(frame): peer._on_receive_sabm = _on_receive_sabm # Set the state - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Inject a frame frame = AX25SetAsyncBalancedModeExtendedFrame( @@ -475,7 +476,7 @@ def test_on_receive_sabm_while_connecting(): ) # Assume we're already connecting to the station - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Stub _init_connection count = dict(init=0, sabmframe_handler=0) @@ -841,7 +842,7 @@ def test_accept_connected_noop(): ) # Set the state to known value - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED # Stub functions that should not be called def _stop_ack_timer(): @@ -857,7 +858,7 @@ def _send_ua(): # Try accepting a ficticious connection peer.accept() - assert peer._state == peer.AX25PeerState.CONNECTED + assert peer._state == AX25PeerState.CONNECTED def test_accept_incoming_ua(): @@ -873,7 +874,7 @@ def test_accept_incoming_ua(): ) # Set the state to known value - peer._state = peer.AX25PeerState.INCOMING_CONNECTION + peer._state = AX25PeerState.INCOMING_CONNECTION # Stub functions that should be called actions = [] @@ -885,7 +886,7 @@ def _stop_ack_timer(): def _send_ua(): # At this time, we should be in the INCOMING_CONNECTION state - assert peer._state is peer.AX25PeerState.INCOMING_CONNECTION + assert peer._state is AX25PeerState.INCOMING_CONNECTION actions.append("sent-ua") peer._send_ua = _send_ua @@ -893,7 +894,7 @@ def _send_ua(): # Try accepting a ficticious connection peer.accept() - assert peer._state is peer.AX25PeerState.CONNECTED + assert peer._state is AX25PeerState.CONNECTED assert actions == ["stop-connect-timer", "sent-ua"] assert peer._uaframe_handler is None @@ -911,7 +912,7 @@ def test_reject_connected_noop(): ) # Set the state to known value - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED # Stub functions that should not be called def _stop_ack_timer(): @@ -927,7 +928,7 @@ def _send_dm(): # Try rejecting a ficticious connection peer.reject() - assert peer._state == peer.AX25PeerState.CONNECTED + assert peer._state == AX25PeerState.CONNECTED def test_reject_incoming_dm(): @@ -943,7 +944,7 @@ def test_reject_incoming_dm(): ) # Set the state to known value - peer._state = peer.AX25PeerState.INCOMING_CONNECTION + peer._state = AX25PeerState.INCOMING_CONNECTION # Stub functions that should be called actions = [] @@ -961,7 +962,7 @@ def _send_dm(): # Try rejecting a ficticious connection peer.reject() - assert peer._state == peer.AX25PeerState.DISCONNECTED + assert peer._state == AX25PeerState.DISCONNECTED assert actions == ["stop-connect-timer", "sent-dm"] @@ -981,7 +982,7 @@ def test_disconnect_disconnected_noop(): ) # Set the state to known value - peer._state = peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # A dummy UA handler def _dummy_ua_handler(): @@ -1003,7 +1004,7 @@ def _start_disconnect_ack_timer(): # Try disconnecting a ficticious connection peer.disconnect() - assert peer._state == peer.AX25PeerState.CONNECTING + assert peer._state == AX25PeerState.CONNECTING assert peer._uaframe_handler == _dummy_ua_handler @@ -1020,7 +1021,7 @@ def test_disconnect_connected_disc(): ) # Set the state to known value - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED # A dummy UA handler def _dummy_ua_handler(): @@ -1044,6 +1045,6 @@ def _start_disconnect_ack_timer(): # Try disconnecting a ficticious connection peer.disconnect() - assert peer._state == peer.AX25PeerState.DISCONNECTING + assert peer._state == AX25PeerState.DISCONNECTING assert actions == ["sent-disc", "start-ack-timer"] assert peer._uaframe_handler == peer._on_disconnect diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index d08ee64..4ec6dac 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -10,6 +10,7 @@ AX25DisconnectFrame, AX25UnnumberedAcknowledgeFrame, ) +from aioax25.peer import AX25PeerState from aioax25.version import AX25Version from .peer import TestingAX25Peer from ..mocks import DummyStation, DummyTimeout @@ -35,7 +36,7 @@ def test_peer_recv_disc(): # Set some dummy data in fields -- this should be cleared out. ack_timer = DummyTimeout(None, None) peer._ack_timeout_handle = ack_timer - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED peer._send_state = 1 peer._send_seq = 2 peer._recv_state = 3 @@ -67,7 +68,7 @@ def test_peer_recv_disc(): # We should now be "disconnected" assert peer._ack_timeout_handle is None - assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._state is AX25PeerState.DISCONNECTED assert peer._send_state == 0 assert peer._send_seq == 0 assert peer._recv_state == 0 @@ -127,12 +128,12 @@ def test_peer_ua_timeout_disconnecting(): full_duplex=True, ) - peer._state = peer.AX25PeerState.DISCONNECTING + peer._state = AX25PeerState.DISCONNECTING peer._ack_timeout_handle = "time-out handle" peer._on_disc_ua_timeout() - assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._state is AX25PeerState.DISCONNECTED assert peer._ack_timeout_handle is None @@ -148,10 +149,10 @@ def test_peer_ua_timeout_notdisconnecting(): full_duplex=True, ) - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED peer._ack_timeout_handle = "time-out handle" peer._on_disc_ua_timeout() - assert peer._state is peer.AX25PeerState.CONNECTED + assert peer._state is AX25PeerState.CONNECTED assert peer._ack_timeout_handle == "time-out handle" diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index be262d6..613bcb2 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -5,6 +5,7 @@ """ from aioax25.frame import AX25Address, AX25Path, AX25DisconnectModeFrame +from aioax25.peer import AX25PeerState from aioax25.version import AX25Version from .peer import TestingAX25Peer from ..mocks import DummyStation, DummyTimeout @@ -29,7 +30,7 @@ def test_peer_recv_dm(): # Set some dummy data in fields -- this should be cleared out. ack_timer = DummyTimeout(None, None) peer._ack_timeout_handle = ack_timer - peer._state = peer.AX25PeerState.CONNECTED + peer._state = AX25PeerState.CONNECTED peer._send_state = 1 peer._send_seq = 2 peer._recv_state = 3 @@ -47,7 +48,7 @@ def test_peer_recv_dm(): # We should now be "disconnected" assert peer._ack_timeout_handle is None - assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._state is AX25PeerState.DISCONNECTED assert peer._send_state == 0 assert peer._send_seq == 0 assert peer._recv_state == 0 @@ -73,7 +74,7 @@ def test_peer_recv_dm_disconnected(): # Set some dummy data in fields -- this should be cleared out. ack_timer = DummyTimeout(None, None) peer._ack_timeout_handle = ack_timer - peer._state = peer.AX25PeerState.NEGOTIATING + peer._state = AX25PeerState.NEGOTIATING peer._send_state = 1 peer._send_seq = 2 peer._recv_state = 3 @@ -91,7 +92,7 @@ def test_peer_recv_dm_disconnected(): # State should be unchanged from before assert peer._ack_timeout_handle is ack_timer - assert peer._state is peer.AX25PeerState.NEGOTIATING + assert peer._state is AX25PeerState.NEGOTIATING assert peer._send_state == 1 assert peer._send_seq == 2 assert peer._recv_state == 3 diff --git a/tests/test_peer/test_frmr.py b/tests/test_peer/test_frmr.py index fe0caa7..3708ae7 100644 --- a/tests/test_peer/test_frmr.py +++ b/tests/test_peer/test_frmr.py @@ -16,6 +16,7 @@ AX25UnnumberedAcknowledgeFrame, AX25TestFrame, ) +from aioax25.peer import AX25PeerState from ..mocks import DummyPeer, DummyStation from .peer import TestingAX25Peer @@ -117,7 +118,7 @@ def test_on_receive_in_frmr_drop_test(): locked_path=True, ) - peer._state = peer.AX25PeerState.FRMR + peer._state = AX25PeerState.FRMR def _on_receive_test(*a, **kwa): assert False, "Should have ignored frame" @@ -147,7 +148,7 @@ def test_on_receive_in_frmr_drop_ua(): locked_path=True, ) - peer._state = peer.AX25PeerState.FRMR + peer._state = AX25PeerState.FRMR def _on_receive_ua(*a, **kwa): assert False, "Should have ignored frame" @@ -176,7 +177,7 @@ def test_on_receive_in_frmr_pass_sabm(): locked_path=True, ) - peer._state = peer.AX25PeerState.FRMR + peer._state = AX25PeerState.FRMR frames = [] @@ -208,7 +209,7 @@ def test_on_receive_in_frmr_pass_disc(): locked_path=True, ) - peer._state = peer.AX25PeerState.FRMR + peer._state = AX25PeerState.FRMR events = [] diff --git a/tests/test_peer/test_state.py b/tests/test_peer/test_state.py index 73c265d..309140a 100644 --- a/tests/test_peer/test_state.py +++ b/tests/test_peer/test_state.py @@ -5,6 +5,7 @@ """ from aioax25.frame import AX25Address, AX25Path +from aioax25.peer import AX25PeerState from .peer import TestingAX25Peer from ..mocks import DummyStation @@ -30,9 +31,9 @@ def _on_state_change(**kwargs): peer.connect_state_changed.connect(_on_state_change) - assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._state is AX25PeerState.DISCONNECTED - peer._set_conn_state(peer.AX25PeerState.DISCONNECTED) + peer._set_conn_state(AX25PeerState.DISCONNECTED) assert state_changes == [] @@ -56,15 +57,15 @@ def _on_state_change(**kwargs): peer.connect_state_changed.connect(_on_state_change) - assert peer._state is peer.AX25PeerState.DISCONNECTED + assert peer._state is AX25PeerState.DISCONNECTED - peer._set_conn_state(peer.AX25PeerState.CONNECTED) + peer._set_conn_state(AX25PeerState.CONNECTED) - assert peer._state is peer.AX25PeerState.CONNECTED + assert peer._state is AX25PeerState.CONNECTED assert state_changes[1:] == [] change = state_changes.pop(0) assert change.pop("station") is station assert change.pop("peer") is peer - assert change.pop("state") is peer.AX25PeerState.CONNECTED + assert change.pop("state") is AX25PeerState.CONNECTED assert change == {} diff --git a/tests/test_peer/test_xid.py b/tests/test_peer/test_xid.py index b550b0f..90fd233 100644 --- a/tests/test_peer/test_xid.py +++ b/tests/test_peer/test_xid.py @@ -18,6 +18,7 @@ AX25ExchangeIdentificationFrame, AX25FrameRejectFrame, ) +from aioax25.peer import AX25PeerState, AX25RejectMode from aioax25.version import AX25Version from .peer import TestingAX25Peer from ..mocks import DummyStation @@ -162,7 +163,7 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peerssr(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter @@ -171,7 +172,7 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peerssr(): ) # Selective Reject-Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE_RR + assert peer._reject_mode == AX25RejectMode.SELECTIVE_RR def test_peer_process_xid_hdlcoptfunc_stnsr_peerssr(): @@ -183,7 +184,7 @@ def test_peer_process_xid_hdlcoptfunc_stnsr_peerssr(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE, + reject_mode=AX25RejectMode.SELECTIVE, ) # Pass in a HDLC Optional Functions XID parameter @@ -192,7 +193,7 @@ def test_peer_process_xid_hdlcoptfunc_stnsr_peerssr(): ) # Selective Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE + assert peer._reject_mode == AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_stnssr_peersr(): @@ -204,7 +205,7 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peersr(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter @@ -213,7 +214,7 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peersr(): ) # Selective Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE + assert peer._reject_mode == AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_stnsr_peersr(): @@ -225,7 +226,7 @@ def test_peer_process_xid_hdlcoptfunc_stnsr_peersr(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE, + reject_mode=AX25RejectMode.SELECTIVE, ) # Pass in a HDLC Optional Functions XID parameter @@ -234,7 +235,7 @@ def test_peer_process_xid_hdlcoptfunc_stnsr_peersr(): ) # Selective Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE + assert peer._reject_mode == AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_stnir_peersr(): @@ -246,7 +247,7 @@ def test_peer_process_xid_hdlcoptfunc_stnir_peersr(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT, + reject_mode=AX25RejectMode.IMPLICIT, ) # Pass in a HDLC Optional Functions XID parameter @@ -255,7 +256,7 @@ def test_peer_process_xid_hdlcoptfunc_stnir_peersr(): ) # Implicit Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT + assert peer._reject_mode == AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_stnsr_peerir(): @@ -267,7 +268,7 @@ def test_peer_process_xid_hdlcoptfunc_stnsr_peerir(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE, + reject_mode=AX25RejectMode.SELECTIVE, ) # Pass in a HDLC Optional Functions XID parameter @@ -276,7 +277,7 @@ def test_peer_process_xid_hdlcoptfunc_stnsr_peerir(): ) # Implicit Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT + assert peer._reject_mode == AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_stnir_peerssr(): @@ -288,7 +289,7 @@ def test_peer_process_xid_hdlcoptfunc_stnir_peerssr(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.IMPLICIT, + reject_mode=AX25RejectMode.IMPLICIT, ) # Pass in a HDLC Optional Functions XID parameter @@ -297,7 +298,7 @@ def test_peer_process_xid_hdlcoptfunc_stnir_peerssr(): ) # Implicit Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT + assert peer._reject_mode == AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_stnssr_peerir(): @@ -309,7 +310,7 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peerir(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter @@ -318,7 +319,7 @@ def test_peer_process_xid_hdlcoptfunc_stnssr_peerir(): ) # Implicit Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT + assert peer._reject_mode == AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_malformed_rej_srej(): @@ -330,7 +331,7 @@ def test_peer_process_xid_hdlcoptfunc_malformed_rej_srej(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter @@ -339,7 +340,7 @@ def test_peer_process_xid_hdlcoptfunc_malformed_rej_srej(): ) # Implicit Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.IMPLICIT + assert peer._reject_mode == AX25RejectMode.IMPLICIT def test_peer_process_xid_hdlcoptfunc_default_rej_srej(): @@ -351,7 +352,7 @@ def test_peer_process_xid_hdlcoptfunc_default_rej_srej(): station=station, address=AX25Address("VK4MSL"), repeaters=None, - reject_mode=TestingAX25Peer.AX25RejectMode.SELECTIVE_RR, + reject_mode=AX25RejectMode.SELECTIVE_RR, ) # Pass in a HDLC Optional Functions XID parameter @@ -362,7 +363,7 @@ def test_peer_process_xid_hdlcoptfunc_default_rej_srej(): ) # Selective Reject should be chosen. - assert peer._reject_mode == TestingAX25Peer.AX25RejectMode.SELECTIVE + assert peer._reject_mode == AX25RejectMode.SELECTIVE def test_peer_process_xid_hdlcoptfunc_s128_p128(): @@ -770,7 +771,7 @@ def test_peer_on_receive_xid_ax20_mode(): assert frame.w # We should now be in the FRMR state - assert peer._state == peer.AX25PeerState.FRMR + assert peer._state == AX25PeerState.FRMR def test_peer_on_receive_xid_connecting(): @@ -787,7 +788,7 @@ def test_peer_on_receive_xid_connecting(): assert interface.transmit_calls == [] # Set state - peer._state = TestingAX25Peer.AX25PeerState.CONNECTING + peer._state = AX25PeerState.CONNECTING # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive( @@ -818,7 +819,7 @@ def test_peer_on_receive_xid_disconnecting(): assert interface.transmit_calls == [] # Set state - peer._state = TestingAX25Peer.AX25PeerState.DISCONNECTING + peer._state = AX25PeerState.DISCONNECTING # Pass in the XID frame to our AX.25 2.2 station. peer._on_receive( From 856ce1d6aa1b81810c62bea8894af30b5acd2e98 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 12:41:20 +1000 Subject: [PATCH 169/207] peer: Expose connection state --- aioax25/peer.py | 7 +++++++ tests/test_peer/test_state.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 383b2f4..b780ad9 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -245,6 +245,13 @@ def address(self): """ return self._address + @property + def state(self): + """ + Return the peer connection state + """ + return self._state + @property def reply_path(self): """ diff --git a/tests/test_peer/test_state.py b/tests/test_peer/test_state.py index 309140a..453fa88 100644 --- a/tests/test_peer/test_state.py +++ b/tests/test_peer/test_state.py @@ -32,6 +32,7 @@ def _on_state_change(**kwargs): peer.connect_state_changed.connect(_on_state_change) assert peer._state is AX25PeerState.DISCONNECTED + assert peer.state is AX25PeerState.DISCONNECTED peer._set_conn_state(AX25PeerState.DISCONNECTED) @@ -58,10 +59,12 @@ def _on_state_change(**kwargs): peer.connect_state_changed.connect(_on_state_change) assert peer._state is AX25PeerState.DISCONNECTED + assert peer.state is AX25PeerState.DISCONNECTED peer._set_conn_state(AX25PeerState.CONNECTED) assert peer._state is AX25PeerState.CONNECTED + assert peer.state is AX25PeerState.CONNECTED assert state_changes[1:] == [] change = state_changes.pop(0) From ec7340a616fa14cd0125bfac325937885c38789e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 13:34:06 +1000 Subject: [PATCH 170/207] frame: Catch non-integer/boolean input on XID parameters --- aioax25/frame.py | 3 +++ tests/test_frame/test_xid.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/aioax25/frame.py b/aioax25/frame.py index 2f416e1..b4e5432 100644 --- a/aioax25/frame.py +++ b/aioax25/frame.py @@ -1910,6 +1910,9 @@ def __init__(self, value): """ Create a big-endian integer parameter. """ + if not (isinstance(value, int) or isinstance(value, bool)): + raise TypeError("value must be an integer or boolean") + self._value = value super(AX25XIDBigEndianParameter, self).__init__(pi=self.PI) diff --git a/tests/test_frame/test_xid.py b/tests/test_frame/test_xid.py index 10efa5f..2e31895 100644 --- a/tests/test_frame/test_xid.py +++ b/tests/test_frame/test_xid.py @@ -13,6 +13,8 @@ from ..hex import from_hex, hex_cmp +from pytest import mark + def test_encode_xid(): """ @@ -419,6 +421,18 @@ def test_encode_hdlcfunc_param(): hex_cmp(param.pv, from_hex("8c a8 83")) +@mark.parametrize("value", [("96", None)]) +def test_encode_badtype(value): + """ + Test that AX25XIDBigEndianParameter refuses bad input types. + """ + try: + AX25XIDRetriesParameter(value) + assert False, "Should not have been accepted" + except TypeError as e: + assert str(e) == "value must be an integer or boolean" + + def test_encode_retries_param(): """ Test we can encode a Retries parameter. From 77b8b0da88310e636e678db20ce9cbf0a19a1c32 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 13:34:29 +1000 Subject: [PATCH 171/207] peer: Send maximum outstanding frames in XID. --- aioax25/peer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index b780ad9..ba4fce2 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1285,7 +1285,11 @@ def _send_xid(self, cr): AX25XIDIFieldLengthReceiveParameter( self._max_ifield_rx * 8 ), - AX25XIDWindowSizeTransmitParameter(self._max_outstanding), + AX25XIDWindowSizeTransmitParameter( + self._max_outstanding_mod128 + if self._modulo128 + else self._max_outstanding_mod8 + ), AX25XIDWindowSizeReceiveParameter( self._max_outstanding_mod128 if self._modulo128 From 3eb754380835b9fc863606264d55e5c7c5070eb5 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 14:22:01 +1000 Subject: [PATCH 172/207] Revert "station: Store and consider command bit state" This reverts commit d2cf0efcf652021db3fff07e5f959605a8c5b455. --- aioax25/station.py | 14 +++------- tests/test_station/test_getpeer.py | 41 +++------------------------- tests/test_station/test_receive.py | 43 ------------------------------ 3 files changed, 7 insertions(+), 91 deletions(-) diff --git a/aioax25/station.py b/aioax25/station.py index 019a77b..f4a84b9 100644 --- a/aioax25/station.py +++ b/aioax25/station.py @@ -142,20 +142,14 @@ def detach(self): ) def getpeer( - self, - callsign, - ssid=None, - repeaters=None, - command=True, - create=True, - **kwargs + self, callsign, ssid=None, repeaters=None, create=True, **kwargs ): """ Retrieve an instance of a peer context. This creates the peer object if it doesn't already exist unless create is set to False (in which case it will raise KeyError). """ - address = AX25Address.decode(callsign, ssid).normcopy(ch=command) + address = AX25Address.decode(callsign, ssid).normalised try: return self._peers[address] except KeyError: @@ -205,9 +199,7 @@ def _on_receive(self, frame, **kwargs): # If we're still here, then we don't handle unsolicited frames # of this type, so pass it to a handler if we have one. peer = self.getpeer( - frame.header.source, - repeaters=frame.header.repeaters.reply, - command=frame.header.source.ch, + frame.header.source, repeaters=frame.header.repeaters.reply ) self._log.debug("Passing frame to peer %s: %s", peer.address, frame) peer._on_receive(frame) diff --git a/tests/test_station/test_getpeer.py b/tests/test_station/test_getpeer.py index 2b36025..db572e5 100644 --- a/tests/test_station/test_getpeer.py +++ b/tests/test_station/test_getpeer.py @@ -18,29 +18,17 @@ def test_unknown_peer_nocreate_keyerror(): except KeyError as e: assert str(e) == ( "AX25Address(callsign=VK4BWI, ssid=0, " - "ch=True, res0=True, res1=True, extension=False)" + "ch=False, res0=True, res1=True, extension=False)" ) -def test_unknown_peer_create_instance_ch(): +def test_unknown_peer_create_instance(): """ - Test fetching an unknown peer with create=True generates peer with C/H set + Test fetching an unknown peer with create=True generates peer """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") peer = station.getpeer("VK4BWI", create=True) assert isinstance(peer, AX25Peer) - assert peer.address.ch is True - - -def test_unknown_peer_create_instance_noch(): - """ - Test fetching an unknown peer with create=True and command=False generates - peer with C/H clear - """ - station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - peer = station.getpeer("VK4BWI", create=True, command=False) - assert isinstance(peer, AX25Peer) - assert peer.address.ch is False def test_known_peer_fetch_instance(): @@ -48,7 +36,7 @@ def test_known_peer_fetch_instance(): Test fetching an known peer returns that known peer """ station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - mypeer = DummyPeer(station, AX25Address("VK4BWI", ch=True)) + mypeer = DummyPeer(station, AX25Address("VK4BWI")) # Inject the peer station._peers[mypeer._address] = mypeer @@ -56,24 +44,3 @@ def test_known_peer_fetch_instance(): # Retrieve the peer instance peer = station.getpeer("VK4BWI") assert peer is mypeer - - -def test_known_peer_fetch_instance_ch(): - """ - Test fetching peers differentiates command bits - """ - station = AX25Station(interface=DummyInterface(), callsign="VK4MSL-5") - mypeer_in = DummyPeer(station, AX25Address("VK4BWI", ch=False)) - mypeer_out = DummyPeer(station, AX25Address("VK4BWI", ch=True)) - - # Inject the peers - station._peers[mypeer_in._address] = mypeer_in - station._peers[mypeer_out._address] = mypeer_out - - # Retrieve the peer instance - peer = station.getpeer("VK4BWI", command=True) - assert peer is mypeer_out - - # Retrieve the other peer instance - peer = station.getpeer("VK4BWI", command=False) - assert peer is mypeer_in diff --git a/tests/test_station/test_receive.py b/tests/test_station/test_receive.py index c8ef135..4bcaac6 100644 --- a/tests/test_station/test_receive.py +++ b/tests/test_station/test_receive.py @@ -132,46 +132,3 @@ def stub_on_test_frame(*args, **kwargs): assert rx_call_kwargs == {} assert len(rx_call_args) == 1 assert rx_call_args[0] is txframe - - -def test_route_incoming_msg_ch(): - """ - Test passing a frame considers C/H bits. - """ - interface = DummyInterface() - station = AX25Station(interface=interface, callsign="VK4MSL-5") - - # Stub out _on_test_frame - def stub_on_test_frame(*args, **kwargs): - assert False, "Should not have been called" - - station._on_test_frame = stub_on_test_frame - - # Inject a couple of peers - peer1 = DummyPeer(station, AX25Address("VK4BWI", ssid=7, ch=False)) - peer2 = DummyPeer(station, AX25Address("VK4BWI", ssid=7, ch=True)) - station._peers[peer1._address] = peer1 - station._peers[peer2._address] = peer2 - - # Pass in the message - txframe = AX25UnnumberedInformationFrame( - destination="VK4MSL-5", - source="VK4BWI-7*", - cr=True, - pid=0xAB, - payload=b"This is a test frame", - ) - station._on_receive(frame=txframe) - - # There should be no replies queued - assert interface.bind_calls == [] - assert interface.unbind_calls == [] - assert interface.transmit_calls == [] - - # This should have gone to peer2, not peer1 - assert peer1.on_receive_calls == [] - assert len(peer2.on_receive_calls) == 1 - (rx_call_args, rx_call_kwargs) = peer2.on_receive_calls.pop() - assert rx_call_kwargs == {} - assert len(rx_call_args) == 1 - assert rx_call_args[0] is txframe From 9b094ed17355aacbb6982ff820175d1d1c23fe6d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:44:41 +1000 Subject: [PATCH 173/207] mocks: Ignore multiple `TimeoutHandle.cancel` calls The real `TimeoutHandle` doesn't seem to care, so neither should we. --- tests/mocks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/mocks.py b/tests/mocks.py index 3b893f9..94d231b 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -68,7 +68,6 @@ def __init__(self, delay, callback, *args, **kwargs): self.cancelled = False def cancel(self): - assert not self.cancelled, "Cancel called twice!" self.cancelled = True From ae2ace4f4372a874597990e3fe84d7cd12aab7db Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:46:32 +1000 Subject: [PATCH 174/207] peer: Don't expect SABM in reply to our SABM. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flow is… we send SABM(E) they send UA -- we're connected -- Not "send SABM(E)", "receive UA", then get a SABM(E) coming the other way! --- aioax25/peer.py | 41 +++++++---------------------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index ba4fce2..3facd1e 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1584,8 +1584,6 @@ def __init__(self, peer): peer, peer._ack_timeout ) self._retries = peer._max_retries - self._our_sabm_acked = False - self._their_sabm_acked = False def _go(self): if self.peer._negotiated: @@ -1619,7 +1617,6 @@ def _on_negotiated(self, response, **kwargs): (self.peer._uaframe_handler is not None) or (self.peer._frmrframe_handler is not None) or (self.peer._dmframe_handler is not None) - or (self.peer._sabmframe_handler is not None) ): # We're handling another frame now. self._log.debug("Received XID, but we're busy") @@ -1629,7 +1626,6 @@ def _on_negotiated(self, response, **kwargs): self._log.debug( "XID done (state %s), beginning connection", response ) - self.peer._sabmframe_handler = self._on_receive_sabm self.peer._uaframe_handler = self._on_receive_ua self.peer._frmrframe_handler = self._on_receive_frmr self.peer._dmframe_handler = self._on_receive_dm @@ -1642,30 +1638,9 @@ def _on_negotiated(self, response, **kwargs): def _on_receive_ua(self): # Peer just acknowledged our connection self._log.debug("UA received") - self._our_sabm_acked = True - self._check_connection_init() - - def _on_receive_sabm(self): - # Peer sent us a SABM. - self._log.debug("SABM received, sending UA") - self.peer._send_ua() - self._their_sabm_acked = True - self._check_connection_init() - - def _check_connection_init(self): - self._log.debug( - "UA status: ours=%s theirs=%s", - self._our_sabm_acked, - self._their_sabm_acked, - ) - if not self._our_sabm_acked: - self._log.debug("Waiting for peer to send UA for our SABM") - elif not self._their_sabm_acked: - self._log.debug("Waiting for peer's SABM to us") - else: - self._log.info("Connection is established") - self.peer._init_connection(self.peer._modulo128) - self._finish(response="ack") + self._log.info("Connection is established") + self.peer._init_connection(self.peer._modulo128) + self._finish(response="ack") def _on_receive_frmr(self): # Peer just rejected our connect frame, begin FRMR recovery. @@ -1679,6 +1654,7 @@ def _on_receive_dm(self): self._finish(response="dm") def _on_timeout(self): + self._unhook() if self._retries: self._retries -= 1 self._log.debug("Retrying, remaining=%d", self._retries) @@ -1687,12 +1663,7 @@ def _on_timeout(self): self._log.debug("Giving up") self._finish(response="timeout") - def _finish(self, **kwargs): - # Clean up hooks - if self.peer._sabmframe_handler == self._on_receive_sabm: - self._log.debug("Unhooking SABM handler") - self.peer._sabmframe_handler = None - + def _unhook(self): if self.peer._uaframe_handler == self._on_receive_ua: self._log.debug("Unhooking UA handler") self.peer._uaframe_handler = None @@ -1705,6 +1676,8 @@ def _finish(self, **kwargs): self._log.debug("Unhooking DM handler") self.peer._dmframe_handler = None + def _finish(self, **kwargs): + self._unhook() super(AX25PeerConnectionHandler, self)._finish(**kwargs) From 45a2c46484da00871cf8d88a320b0b2fe37845d9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:49:28 +1000 Subject: [PATCH 175/207] peer: Always set C/H bits appropriately Seems we should always set it on the destination, and never on the source, regardless of which way the traffic is flowing. --- aioax25/peer.py | 38 ++++++++++++++++-------------- tests/test_peer/test_connection.py | 8 +++---- tests/test_peer/test_disc.py | 4 ++-- tests/test_peer/test_dm.py | 2 +- tests/test_peer/test_ua.py | 2 +- 5 files changed, 28 insertions(+), 26 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 3facd1e..1d91396 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1242,8 +1242,8 @@ def _send_sabm(self): self._transmit_frame( SABMClass( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, ) ) @@ -1253,8 +1253,8 @@ def _send_sabm(self): def _send_xid(self, cr): self._transmit_frame( AX25ExchangeIdentificationFrame( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, parameters=[ AX25XIDClassOfProceduresParameter( @@ -1311,8 +1311,8 @@ def _send_dm(self): self._log.info("Sending DM") self._transmit_frame( AX25DisconnectModeFrame( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, ) ) @@ -1324,8 +1324,8 @@ def _send_disc(self): self._log.info("Sending DISC") self._transmit_frame( AX25DisconnectFrame( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, ) ) @@ -1337,8 +1337,8 @@ def _send_ua(self): self._log.info("Sending UA") self._transmit_frame( AX25UnnumberedAcknowledgeFrame( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, ) ) @@ -1360,8 +1360,8 @@ def _send_frmr(self, frame, w=False, x=False, y=False, z=False): # See https://www.tapr.org/pub_ax25.html self._transmit_frame( AX25FrameRejectFrame( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, w=w, x=x, @@ -1406,10 +1406,11 @@ def _send_rr_notification(self): self._log.debug( "Sending RR with N(R) == V(R) == %d", self._recv_state ) + self._update_recv_seq() self._transmit_frame( self._RRFrameClass( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, pf=False, nr=self._recv_state, @@ -1423,10 +1424,11 @@ def _send_rnr_notification(self): if self._state is AX25PeerState.CONNECTED: now = self._loop.time() if (now - self._last_rnr_sent) > self._rnr_interval: + self._update_recv_seq() self._transmit_frame( self._RNRFrameClass( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, nr=self._recv_seq, pf=False, @@ -1491,8 +1493,8 @@ def _transmit_iframe(self, ns): ) self._transmit_frame( self._IFrameClass( - destination=self.address, - source=self._station().address, + destination=self.address.normcopy(ch=True), + source=self._station().address.normcopy(ch=False), repeaters=self.reply_path, nr=self._recv_state, # N(R) == V(R) ns=ns, diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 0654176..04d1190 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -127,8 +127,8 @@ def _transmit_frame(frame): assert False, "No frames were sent" assert isinstance(frame, AX25SetAsyncBalancedModeFrame) - assert str(frame.header.destination) == "VK4MSL" - assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.destination) == "VK4MSL*" # CONTROL set + assert str(frame.header.source) == "VK4MSL-1" # CONTROL clear assert str(frame.header.repeaters) == "VK4RZB" assert len(sent) == 0 @@ -164,8 +164,8 @@ def _transmit_frame(frame): assert False, "No frames were sent" assert isinstance(frame, AX25SetAsyncBalancedModeExtendedFrame) - assert str(frame.header.destination) == "VK4MSL" - assert str(frame.header.source) == "VK4MSL-1" + assert str(frame.header.destination) == "VK4MSL*" # CONTROL set + assert str(frame.header.source) == "VK4MSL-1" # CONTROL clear assert str(frame.header.repeaters) == "VK4RZB" assert len(sent) == 0 diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index 4ec6dac..11a5364 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -62,7 +62,7 @@ def test_peer_recv_disc(): (frame,) = tx_args assert isinstance(frame, AX25UnnumberedAcknowledgeFrame) - assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.destination) == "VK4MSL*" assert str(frame.header.source) == "VK4MSL-1" assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" @@ -107,7 +107,7 @@ def test_peer_send_disc(): (frame,) = tx_args assert isinstance(frame, AX25DisconnectFrame) - assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.destination) == "VK4MSL*" assert str(frame.header.source) == "VK4MSL-1" assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index 613bcb2..a77eceb 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -131,6 +131,6 @@ def test_peer_send_dm(): (frame,) = tx_args assert isinstance(frame, AX25DisconnectModeFrame) - assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.destination) == "VK4MSL*" assert str(frame.header.source) == "VK4MSL-1" assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" diff --git a/tests/test_peer/test_ua.py b/tests/test_peer/test_ua.py index 3fe8ef5..75c2cb9 100644 --- a/tests/test_peer/test_ua.py +++ b/tests/test_peer/test_ua.py @@ -63,6 +63,6 @@ def test_peer_send_ua(): (frame,) = tx_args assert isinstance(frame, AX25UnnumberedAcknowledgeFrame) - assert str(frame.header.destination) == "VK4MSL" + assert str(frame.header.destination) == "VK4MSL*" assert str(frame.header.source) == "VK4MSL-1" assert str(frame.header.repeaters) == "VK4MSL-2,VK4MSL-3" From d53b595b48d4e89e23361d8db86a7839d56ad823 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:51:33 +1000 Subject: [PATCH 176/207] peer: Drop unused _ack_state counter Was seen in some other implementations of AX.25, but doesn't seem to be needed. Also, clarify the exact purpose of each state variable, with quotations from the AX.25 2.0 spec. --- aioax25/peer.py | 36 +++++++++++++++++++++++++----- tests/test_peer/test_connection.py | 4 ---- tests/test_peer/test_disc.py | 2 -- tests/test_peer/test_dm.py | 4 ---- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 1d91396..e519cee 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -167,16 +167,40 @@ def __init__( self._negotiated = False # Set to True after XID negotiation self._connected = False # Set to true on SABM UA self._last_act = 0 # Time of last activity - self._send_state = 0 # AKA V(S) + + # 2.3.2.4.1 Send State Variable V(S) + # The send state variable is a variable that is internal to the DXE + # and is never sent. It contains the next sequential number to be + # assigned to the next transmitted I frame. This variable is updated + # upon the transmission of each I frame. + self._send_state = 0 self._send_state_name = "V(S)" - self._send_seq = 0 # AKA N(S) + + # 2.3.2.4.2 Send Sequence Number N(S) + # The send sequence number is found in the control field of all I + # frames. It contains the sequence number of the I frame being sent. + # Just prior to the transmission of the I frame, N(S) is updated to + # equal the send state variable. + self._send_seq = 0 self._send_seq_name = "N(S)" - self._recv_state = 0 # AKA V(R) + + # 2.3.2.4.3 Receive State Variable V(R) + # The receive state variable is a variable that is internal to the + # DXE. It contains the sequence number of the next expected received I + # frame. This variable is updated upon the reception of an error-free + # I frame whose send sequence number equals the present received state + # variable value. + self._recv_state = 0 self._recv_state_name = "V(R)" - self._recv_seq = 0 # AKA N(R) + + # 2.3.2.4.4 Received Sequence Number N(R) + # The received sequence number is in both I and S frames. Prior to + # sending an I or S frame, this variable is updated to equal that of + # the received state variable, thus implicitly acknowledging the + # proper reception of all frames up to and including N(R)-1. + self._recv_seq = 0 self._recv_seq_name = "N(R)" - self._ack_state = 0 # AKA V(A) - self._ack_state_name = "V(A)" + self._local_busy = False # Local end busy, respond to # RR and I-frames with RNR. self._peer_busy = False # Peer busy, await RR. diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 04d1190..ba9e658 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -743,7 +743,6 @@ def test_init_connection_mod8(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 - peer._ack_state = 5 peer._modulo = 6 peer._max_outstanding = 7 peer._IFrameClass = None @@ -770,7 +769,6 @@ def test_init_connection_mod8(): assert peer._send_seq == 0 assert peer._recv_state == 0 assert peer._recv_seq == 0 - assert peer._ack_state == 0 assert peer._pending_iframes == {} assert peer._pending_data == [] @@ -794,7 +792,6 @@ def test_init_connection_mod128(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 - peer._ack_state = 5 peer._modulo = 6 peer._max_outstanding = 7 peer._IFrameClass = None @@ -821,7 +818,6 @@ def test_init_connection_mod128(): assert peer._send_seq == 0 assert peer._recv_state == 0 assert peer._recv_seq == 0 - assert peer._ack_state == 0 assert peer._pending_iframes == {} assert peer._pending_data == [] diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index 11a5364..8c87b47 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -41,7 +41,6 @@ def test_peer_recv_disc(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 - peer._ack_state = 5 peer._pending_iframes = dict(comment="pending data") peer._pending_data = ["pending data"] @@ -73,7 +72,6 @@ def test_peer_recv_disc(): assert peer._send_seq == 0 assert peer._recv_state == 0 assert peer._recv_seq == 0 - assert peer._ack_state == 0 assert peer._pending_iframes == {} assert peer._pending_data == [] diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index a77eceb..bb29ae4 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -35,7 +35,6 @@ def test_peer_recv_dm(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 - peer._ack_state = 5 peer._pending_iframes = dict(comment="pending data") peer._pending_data = ["pending data"] @@ -53,7 +52,6 @@ def test_peer_recv_dm(): assert peer._send_seq == 0 assert peer._recv_state == 0 assert peer._recv_seq == 0 - assert peer._ack_state == 0 assert peer._pending_iframes == {} assert peer._pending_data == [] @@ -79,7 +77,6 @@ def test_peer_recv_dm_disconnected(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 - peer._ack_state = 5 peer._pending_iframes = dict(comment="pending data") peer._pending_data = ["pending data"] @@ -97,7 +94,6 @@ def test_peer_recv_dm_disconnected(): assert peer._send_seq == 2 assert peer._recv_state == 3 assert peer._recv_seq == 4 - assert peer._ack_state == 5 assert peer._pending_iframes == dict(comment="pending data") assert peer._pending_data == ["pending data"] From 57f8fb727dc38392e1154ec6271adbcf52a4b92f Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:53:41 +1000 Subject: [PATCH 177/207] peer: Add comment to state updates, always apply modulo. --- aioax25/peer.py | 14 +++++++++++--- tests/test_peer/test_disc.py | 4 ++++ tests/test_peer/test_dm.py | 2 ++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index e519cee..3ad1635 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1538,15 +1538,23 @@ def _transmit_frame(self, frame, callback=None): self._reset_idle_timeout() return self._station()._interface().transmit(frame, callback=None) - def _update_state(self, prop, delta=None, value=None): + def _update_state(self, prop, delta=None, value=None, comment=""): + if comment: + comment = " " + comment + if value is None: value = getattr(self, prop) if delta is not None: value += delta - value %= self._modulo + comment += " delta=%s" % delta + + # Always apply modulo op + value %= self._modulo - self._log.debug("%s = %s", getattr(self, "%s_name" % prop), value) + self._log.debug( + "%s = %s" + comment, getattr(self, "%s_name" % prop), value + ) setattr(self, prop, value) diff --git a/tests/test_peer/test_disc.py b/tests/test_peer/test_disc.py index 8c87b47..2d7f518 100644 --- a/tests/test_peer/test_disc.py +++ b/tests/test_peer/test_disc.py @@ -37,6 +37,7 @@ def test_peer_recv_disc(): ack_timer = DummyTimeout(None, None) peer._ack_timeout_handle = ack_timer peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 peer._send_state = 1 peer._send_seq = 2 peer._recv_state = 3 @@ -91,6 +92,7 @@ def test_peer_send_disc(): repeaters=AX25Path("VK4MSL-2", "VK4MSL-3"), full_duplex=True, ) + peer._modulo = 8 # Request a DISC frame be sent peer._send_disc() @@ -127,6 +129,7 @@ def test_peer_ua_timeout_disconnecting(): ) peer._state = AX25PeerState.DISCONNECTING + peer._modulo = 8 peer._ack_timeout_handle = "time-out handle" peer._on_disc_ua_timeout() @@ -149,6 +152,7 @@ def test_peer_ua_timeout_notdisconnecting(): peer._state = AX25PeerState.CONNECTED peer._ack_timeout_handle = "time-out handle" + peer._modulo = 8 peer._on_disc_ua_timeout() diff --git a/tests/test_peer/test_dm.py b/tests/test_peer/test_dm.py index bb29ae4..dd15bd3 100644 --- a/tests/test_peer/test_dm.py +++ b/tests/test_peer/test_dm.py @@ -31,6 +31,7 @@ def test_peer_recv_dm(): ack_timer = DummyTimeout(None, None) peer._ack_timeout_handle = ack_timer peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 peer._send_state = 1 peer._send_seq = 2 peer._recv_state = 3 @@ -73,6 +74,7 @@ def test_peer_recv_dm_disconnected(): ack_timer = DummyTimeout(None, None) peer._ack_timeout_handle = ack_timer peer._state = AX25PeerState.NEGOTIATING + peer._modulo = 8 peer._send_state = 1 peer._send_seq = 2 peer._recv_state = 3 From 1b98163b108537fbcfd06a6fd5dbf648e4775e83 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:54:55 +1000 Subject: [PATCH 178/207] peer: Update N(S) and N(R) prior to I-frame TX --- aioax25/peer.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 3ad1635..8add1d2 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1515,6 +1515,10 @@ def _transmit_iframe(self, ns): pid, payload, ) + + self._update_send_seq() + self._update_recv_seq() + self._transmit_frame( self._IFrameClass( destination=self.address.normcopy(ch=True), @@ -1557,6 +1561,30 @@ def _update_state(self, prop, delta=None, value=None, comment=""): ) setattr(self, prop, value) + def _update_send_seq(self): + """ + Update the send sequence. Call this just prior to sending an + I-frame. + """ + # "Just prior to the transmission of the I frame, N(S) is updated to + # equal the send state variable." § 2.3.2.4.2 + # _send_seq (aka N(S)) ← _send_state (aka V(S)) + self._update_state( + "_send_seq", value=self._send_state, comment="from V(S)" + ) + + def _update_recv_seq(self): + """ + Update the send sequence. Call this just prior to sending an + I-frame or S-frame. + """ + # "Prior to sending an I or S frame, this variable is updated to equal + # that of the received state variable" § 2.3.2.4.4 + # _recv_seq (aka N(R)) ← _recv_state (aka V(R)) + self._update_state( + "_recv_seq", value=self._recv_state, comment="from V(R)" + ) + class AX25PeerHelper(object): """ From afeaca6f1935c3267964a38597f820675cefb59b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:55:46 +1000 Subject: [PATCH 179/207] peer: ACK outstanding frames on every I or S frame --- aioax25/peer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aioax25/peer.py b/aioax25/peer.py index 8add1d2..991df22 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -603,6 +603,7 @@ def _on_receive_iframe(self, frame): # increments its receive state variable, and acts in one of the # following manners:…" self._update_state("_recv_state", delta=1) + self._on_receive_isframe_nr_ns(frame) # TODO: the payload here may be a repeat of data already seen, or # for _future_ data (i.e. there's an I-frame that got missed in between @@ -628,6 +629,7 @@ def _on_receive_sframe(self, frame): """ Handle a S-frame from the peer. """ + self._on_receive_isframe_nr_ns(frame) if isinstance(frame, self._RRFrameClass): self._on_receive_rr(frame) elif isinstance(frame, self._RNRFrameClass): From d7a0e7ed1da89df22c20fd40b3bcca32aba1d7e6 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:57:11 +1000 Subject: [PATCH 180/207] peer: Set V(R) from I-frame N(S) + 1 --- aioax25/peer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 991df22..d6a8399 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -602,7 +602,9 @@ def _on_receive_iframe(self, frame): # "…it accepts the received I frame, # increments its receive state variable, and acts in one of the # following manners:…" - self._update_state("_recv_state", delta=1) + self._update_state( + "_recv_state", value=frame.ns + 1, comment="from I-frame N(S)" + ) self._on_receive_isframe_nr_ns(frame) # TODO: the payload here may be a repeat of data already seen, or From aab3882566093ac5c421c34aea7233892345e1bd Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:58:23 +1000 Subject: [PATCH 181/207] peer: Fix logic for ACKing outstanding I-frames. --- aioax25/peer.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index d6a8399..7aa9cad 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -728,22 +728,33 @@ def _on_receive_rr_rnr_rej_query(self): def _ack_outstanding(self, nr): """ - Receive all frames up to N(R) + Receive all frames up to N(R)-1 """ - self._log.debug("%d through to %d are received", self._send_state, nr) - while self._send_seq != nr: + self._log.debug("%d through to %d are received", self._recv_seq, nr) + while self._recv_seq != nr: if self._log.isEnabledFor(logging.DEBUG): self._log.debug("Pending frames: %r", self._pending_iframes) - frame = self._pending_iframes.pop(self._send_seq) - if self._log.isEnabledFor(logging.DEBUG): - self._log.debug( - "Popped %s off pending queue, N(R)s pending: %r", - frame, - self._pending_iframes, + self._log.debug("ACKing N(R)=%s", self._recv_seq) + try: + frame = self._pending_iframes.pop(self._recv_seq) + if self._log.isEnabledFor(logging.DEBUG): + self._log.debug( + "Popped %s off pending queue, N(R)s pending: %r", + frame, + self._pending_iframes, + ) + except KeyError: + if self._log.isEnabledFor(logging.DEBUG): + self._log.debug( + "ACK to unexpected N(R) number %s, pending: %r", + self._recv_seq, + self._pending_iframes, + ) + finally: + self._update_state( + "_recv_seq", delta=1, comment="ACKed by peer N(R)" ) - self._log.debug("Increment N(S) due to ACK") - self._update_state("_send_seq", delta=1) def _on_receive_test(self, frame): self._log.debug("Received TEST response: %s", frame) From c9f0828bc817f65d6940b7f8e303badea73446a4 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:59:15 +1000 Subject: [PATCH 182/207] peer: Clarify where state variables are being updated --- aioax25/peer.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 7aa9cad..33daacc 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -700,7 +700,9 @@ def _on_receive_rej(self, frame): # AX.25 2.2 section 6.4.7 says we set V(S) to this frame's # N(R) and begin re-transmission. self._log.debug("Set state V(S) from frame N(R) = %d", frame.nr) - self._update_state("_send_state", value=frame.nr) + self._update_state( + "_send_state", value=frame.nr, comment="from REJ N(R)" + ) self._send_next_iframe() def _on_receive_srej(self, frame): @@ -971,11 +973,10 @@ def _reset_connection_state(self): self._log.debug("Resetting the peer state") # Reset our state - self._update_state("_send_state", value=0) # AKA V(S) - self._send_seq = 0 # AKA N(S) - self._update_state("_recv_state", value=0) # AKA V(R) - self._recv_seq = 0 # AKA N(R) - self._ack_state = 0 # AKA V(A) + self._update_state("_send_state", value=0, comment="reset") + self._update_state("_send_seq", value=0, comment="reset") + self._update_state("_recv_state", value=0, comment="reset") + self._update_state("_recv_seq", value=0, comment="reset") # Unacknowledged I-frames to be ACKed self._pending_iframes = {} @@ -1503,8 +1504,9 @@ def _send_next_iframe(self): # "After the I frame is sent, the send state variable is incremented # by one." - self._log.debug("Increment send state V(S) by one") - self._update_state("_send_state", delta=1) + self._update_state( + "_send_state", delta=1, comment="send next I-frame" + ) if self._log.isEnabledFor(logging.DEBUG): self._log.debug("Pending frames: %r", self._pending_iframes) From 15267793098cf6a089eaa145d27c9c7f68c63620 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 16:59:47 +1000 Subject: [PATCH 183/207] peer: Always cancel helper time-out timer Don't check if it's been cancelled, `asyncio` doesn't seem to care. --- aioax25/peer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 33daacc..76fe0a9 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1636,9 +1636,7 @@ def _stop_timer(self): if self._timeout_handle is None: return - if not self._timeout_handle.cancelled: - self._timeout_handle.cancel() - + self._timeout_handle.cancel() self._timeout_handle = None def _finish(self, **kwargs): From 23ce89d54bf45c38130d2efc76c888eb062a4c3e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:00:20 +1000 Subject: [PATCH 184/207] peer: Add a numeric suffix to the handler for identification --- aioax25/peer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 76fe0a9..2d60bcf 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1609,10 +1609,16 @@ class AX25PeerHelper(object): negotiating parameters or sending test frames. """ + # Class-level counter so we can differentiate instances + _IDX = 0 + def __init__(self, peer, timeout): self._peer = peer self._loop = peer._loop - self._log = peer._log.getChild(self.__class__.__name__) + self._log = peer._log.getChild( + "%s-%d" % (self.__class__.__name__, self.__class__._IDX) + ) + self.__class__._IDX += 1 self._done = False self._timeout = timeout self._timeout_handle = None From 12769e806c9ed044d6d9a62bf2e9ad66c375612b Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:00:52 +1000 Subject: [PATCH 185/207] peer: Add some debugging logs --- aioax25/peer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 2d60bcf..7469d43 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -593,7 +593,7 @@ def _on_receive_iframe(self, frame): if frame.ns != self._recv_seq: # TODO: should we send a REJ/SREJ after a time-out? self._log.debug( - "I-frame sequence is %s, expecting %s, ignoring", + "I-frame sequence N(S) is %s, expecting N(R) %s, ignoring", frame.ns, self._recv_seq, ) @@ -621,10 +621,12 @@ def _on_receive_iframe(self, frame): if len(self._pending_data) and ( len(self._pending_iframes) < self._max_outstanding ): + self._log.debug("Data pending, will send I-frame in reply") return self._send_next_iframe() # "b) If there are no outstanding I frames, the receiving TNC sends # an RR frame with N(R) equal to V(R)." + self._log.debug("No data pending, will send RR S-frame in reply") self._schedule_rr_notification() def _on_receive_sframe(self, frame): @@ -1637,13 +1639,16 @@ def _start_timer(self): self._timeout_handle = self._loop.call_later( self._timeout, self._on_timeout ) + self._log.debug("time-out timer started") def _stop_timer(self): if self._timeout_handle is None: + self._log.debug("no time-out timer to cancel") return self._timeout_handle.cancel() self._timeout_handle = None + self._log.debug("time-out timer cancelled") def _finish(self, **kwargs): if self._done: From a93f8699f4ea549e4774a2693ec63743e0cfa3af Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:01:08 +1000 Subject: [PATCH 186/207] test mocks: Add some missing bits to the mock peer. --- tests/mocks.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/mocks.py b/tests/mocks.py index 94d231b..a60e285 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -127,6 +127,9 @@ def __init__(self, station, address): self._negotiated = False self._protocol = AX25Version.UNKNOWN + self._modulo128 = False + self._init_connection_modulo = None + # Our fake weakref def _station(self): return self._station_ref @@ -136,6 +139,14 @@ def address(self): self.address_read = True return self._address + def _init_connection(self, extended): + if extended is True: + self._init_connection_modulo = 128 + elif extended is False: + self._init_connection_modulo = 8 + else: + raise ValueError("Invalid extended value %r" % extended) + def _negotiate(self, callback): self._negotiate_calls.append(callback) From 1b844b947a40c6d92031435193d131db795ba752 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:01:38 +1000 Subject: [PATCH 187/207] peer tests: Clean up SABM handling in connection helper --- tests/test_peer/test_peerconnection.py | 282 +------------------------ 1 file changed, 5 insertions(+), 277 deletions(-) diff --git a/tests/test_peer/test_peerconnection.py b/tests/test_peer/test_peerconnection.py index 01a621e..f9a7e09 100644 --- a/tests/test_peer/test_peerconnection.py +++ b/tests/test_peer/test_peerconnection.py @@ -159,31 +159,6 @@ def test_peerconn_on_negotiated_failed(): assert done_evts == [{"response": "whoopsie"}] -def test_peerconn_on_negotiated_sabmframe_handler(): - """ - Test _on_negotiated refuses to run if another SABM frame handler is hooked. - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - - # Nothing should be set up - assert helper._timeout_handle is None - assert not helper._done - assert peer.transmit_calls == [] - - # Hook the SABM handler - peer._sabmframe_handler = lambda *a, **kwa: None - - # Hook the done signal - done_evts = [] - helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - - # Try to connect - helper._on_negotiated("xid") - assert done_evts == [{"response": "station_busy"}] - - def test_peerconn_on_negotiated_uaframe_handler(): """ Test _on_negotiated refuses to run if another UA frame handler is hooked. @@ -287,241 +262,28 @@ def test_peerconn_on_negotiated_xid(): assert callback is None -def test_peerconn_check_connection_init(): - """ - Test _check_connection_init finalises connection if both SABMs ACKed - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - - # Assume we're using modulo-8 mode - peer._modulo128 = False - - # Mark SABMs ACKed - helper._our_sabm_acked = True - helper._their_sabm_acked = True - - # Nothing should be set up - assert helper._timeout_handle is None - assert not helper._done - - # Stub peer _init_connection - count = dict(init=0) - - def _init_connection(extended): - assert extended is False, "Should be in Modulo-8 mode" - count["init"] += 1 - - peer._init_connection = _init_connection - - # Hook the done signal - done_evts = [] - helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - - # Call _check_connection_init - helper._check_connection_init() - - # We should have initialised the connection - assert count == dict(init=1) - - # See that the helper finished - assert helper._done is True - assert done_evts == [{"response": "ack"}] - - -def test_peerconn_check_connection_init_mod128(): - """ - Test _check_connection_init finalises mod128 connections too - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - - # Assume we're using modulo-128 mode - peer._modulo128 = True - - # Mark SABMs ACKed - helper._our_sabm_acked = True - helper._their_sabm_acked = True - - # Nothing should be set up - assert helper._timeout_handle is None - assert not helper._done - - # Stub peer _init_connection - count = dict(init=0) - - def _init_connection(extended): - assert extended is True, "Should be in Modulo-128 mode" - count["init"] += 1 - - peer._init_connection = _init_connection - - # Hook the done signal - done_evts = [] - helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - - # Call _check_connection_init - helper._check_connection_init() - - # We should have initialised the connection - assert count == dict(init=1) - - # See that the helper finished - assert helper._done is True - assert done_evts == [{"response": "ack"}] - - -def test_peerconn_check_connection_init_notoursabm(): - """ - Test _check_connection_init does nothing if our SABM not ACKed - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - - # Assume we're using modulo-8 mode - peer._modulo128 = False - - # Mark their SABM ACKed, but not ours - helper._our_sabm_acked = False - helper._their_sabm_acked = True - - # Nothing should be set up - assert helper._timeout_handle is None - assert not helper._done - - # Stub peer _init_connection - count = dict(init=0) - - def _init_connection(extended): - assert extended is False, "Should be in Modulo-8 mode" - count["init"] += 1 - - peer._init_connection = _init_connection - - # Hook the done signal - done_evts = [] - helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - - # Call _check_connection_init - helper._check_connection_init() - - # We should NOT have initialised the connection - assert count == dict(init=0) - - # See that the helper is NOT finished - assert helper._done is False - assert done_evts == [] - - -def test_peerconn_check_connection_init_nottheirsabm(): +def test_peerconn_receive_ua(): """ - Test _check_connection_init does nothing if their SABM not ACKed + Test _on_receive_ua marks the SABM as ACKed """ station = DummyStation(AX25Address("VK4MSL", ssid=1)) peer = DummyPeer(station, AX25Address("VK4MSL")) helper = AX25PeerConnectionHandler(peer) - # Assume we're using modulo-8 mode - peer._modulo128 = False - - # Mark our SABM ACKed, but not theirs - helper._our_sabm_acked = True - helper._their_sabm_acked = False - - # Nothing should be set up - assert helper._timeout_handle is None - assert not helper._done - - # Stub peer _init_connection - count = dict(init=0) - - def _init_connection(extended): - assert extended is False, "Should be in Modulo-8 mode" - count["init"] += 1 - - peer._init_connection = _init_connection - # Hook the done signal done_evts = [] helper.done_sig.connect(lambda **kw: done_evts.append(kw)) - # Call _check_connection_init - helper._check_connection_init() - - # We should NOT have initialised the connection - assert count == dict(init=0) - - # See that the helper is NOT finished - assert helper._done is False - assert done_evts == [] - - -def test_peerconn_receive_ua(): - """ - Test _on_receive_ua marks the SABM as ACKed - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - # Nothing should be set up assert helper._timeout_handle is None assert not helper._done - # Stub helper _check_connection_init - count = dict(check=0) - - def _check_connection_init(): - count["check"] += 1 - - helper._check_connection_init = _check_connection_init - - assert helper._our_sabm_acked is False - # Call _on_receive_ua helper._on_receive_ua() - # Our SABM should be marked as ACKed - assert helper._our_sabm_acked is True - - # We should have checked the ACK status - assert count == dict(check=1) - - -def test_peerconn_receive_sabm(): - """ - Test _on_receive_sabm ends the helper - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - - # Nothing should be set up - assert helper._timeout_handle is None - assert not helper._done - - # Stub peer _send_ua - count = dict(send_ua=0, check=0) - - def _send_ua(): - count["send_ua"] += 1 - - peer._send_ua = _send_ua - - # Stub helper _check_connection_init - def _check_connection_init(): - count["check"] += 1 - - helper._check_connection_init = _check_connection_init - - # Call _on_receive_sabm - helper._on_receive_sabm() - - # We should have ACKed the SABM and checked the connection status - assert count == dict(send_ua=1, check=1) + # We should be connected + assert helper._done is True + assert done_evts == [{"response": "ack"}] def test_peerconn_receive_frmr(): @@ -607,7 +369,6 @@ def test_peerconn_on_timeout_first(): assert helper._retries == 1 # Helper should have hooked the handler events - assert peer._sabmframe_handler == helper._on_receive_sabm assert peer._uaframe_handler == helper._on_receive_ua assert peer._frmrframe_handler == helper._on_receive_frmr assert peer._dmframe_handler == helper._on_receive_dm @@ -638,7 +399,6 @@ def test_peerconn_on_timeout_last(): helper._retries = 0 # Pretend we're hooked up - peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = helper._on_receive_dm @@ -654,7 +414,6 @@ def test_peerconn_on_timeout_last(): assert helper._timeout_handle is None # Helper should have unhooked the handler events - assert peer._sabmframe_handler is None assert peer._uaframe_handler is None assert peer._frmrframe_handler is None assert peer._dmframe_handler is None @@ -677,7 +436,6 @@ def test_peerconn_finish_disconnect_ua(): # Pretend we're hooked up dummy_uaframe_handler = lambda *a, **kw: None - peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = dummy_uaframe_handler peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = helper._on_receive_dm @@ -686,37 +444,11 @@ def test_peerconn_finish_disconnect_ua(): helper._finish() # All except UA (which is not ours) should be disconnected - assert peer._sabmframe_handler is None assert peer._uaframe_handler == dummy_uaframe_handler assert peer._frmrframe_handler is None assert peer._dmframe_handler is None -def test_peerconn_finish_disconnect_sabm(): - """ - Test _finish leaves other SABM hooks intact - """ - station = DummyStation(AX25Address("VK4MSL", ssid=1)) - peer = DummyPeer(station, AX25Address("VK4MSL")) - helper = AX25PeerConnectionHandler(peer) - - # Pretend we're hooked up - dummy_sabmframe_handler = lambda *a, **kw: None - peer._sabmframe_handler = dummy_sabmframe_handler - peer._uaframe_handler = helper._on_receive_ua - peer._frmrframe_handler = helper._on_receive_frmr - peer._dmframe_handler = helper._on_receive_dm - - # Call the finish routine - helper._finish() - - # All except SABM (which is not ours) should be disconnected - assert peer._sabmframe_handler == dummy_sabmframe_handler - assert peer._uaframe_handler is None - assert peer._frmrframe_handler is None - assert peer._dmframe_handler is None - - def test_peerconn_finish_disconnect_frmr(): """ Test _finish leaves other FRMR hooks intact @@ -727,7 +459,6 @@ def test_peerconn_finish_disconnect_frmr(): # Pretend we're hooked up dummy_frmrframe_handler = lambda *a, **kw: None - peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = dummy_frmrframe_handler peer._dmframe_handler = helper._on_receive_dm @@ -736,7 +467,6 @@ def test_peerconn_finish_disconnect_frmr(): helper._finish() # All except FRMR (which is not ours) should be disconnected - assert peer._sabmframe_handler is None assert peer._uaframe_handler is None assert peer._frmrframe_handler == dummy_frmrframe_handler assert peer._dmframe_handler is None @@ -752,7 +482,6 @@ def test_peerconn_finish_disconnect_dm(): # Pretend we're hooked up dummy_dmframe_handler = lambda *a, **kw: None - peer._sabmframe_handler = helper._on_receive_sabm peer._uaframe_handler = helper._on_receive_ua peer._frmrframe_handler = helper._on_receive_frmr peer._dmframe_handler = dummy_dmframe_handler @@ -761,7 +490,6 @@ def test_peerconn_finish_disconnect_dm(): helper._finish() # All except DM (which is not ours) should be disconnected - assert peer._sabmframe_handler is None assert peer._uaframe_handler is None assert peer._frmrframe_handler is None assert peer._dmframe_handler == dummy_dmframe_handler From 37282ab52ad72760967a7c44825e7d55b95fa5ee Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:10:40 +1000 Subject: [PATCH 188/207] tools: Add `call` tool This calls a remote AX.25 station, very crude at the moment, but it seems to be able to interact with BPQ32. --- aioax25/tools/call.py | 133 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 aioax25/tools/call.py diff --git a/aioax25/tools/call.py b/aioax25/tools/call.py new file mode 100644 index 0000000..521450b --- /dev/null +++ b/aioax25/tools/call.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +import asyncio +import argparse +import logging + +from prompt_toolkit import PromptSession +from prompt_toolkit.patch_stdout import patch_stdout +from yaml import safe_load + +# aioax25 imports +# from aioax25.kiss import … +# from aioax25.interface import … +# etc… if you're copying this for your own code +from ..kiss import make_device, KISSDeviceState +from ..interface import AX25Interface +from ..station import AX25Station +from ..peer import AX25PeerState +from ..version import AX25Version + + +class AX25Call(object): + def __init__(self, source, destination, kissparams, port=0): + log = logging.getLogger(self.__class__.__name__) + kisslog = log.getChild("kiss") + kisslog.setLevel(logging.INFO) # KISS logs are verbose! + intflog = log.getChild("interface") + intflog.setLevel(logging.INFO) # interface logs are verbose too! + stnlog = log.getChild("station") + + self._log = log + self._device = make_device(**kissparams, log=kisslog) + self._interface = AX25Interface(self._device[port], log=intflog) + self._station = AX25Station( + self._interface, + source, + protocol=AX25Version.AX25_20, + log=stnlog, + ) + self._station.attach() + self._peer = self._station.getpeer(destination) + self._peer.received_information.connect(self._on_receive) + + def _on_receive(self, frame, **kwargs): + with patch_stdout(): + if frame.pid == 0xF0: + # No L3 protocol + print("\n".join(frame.payload.decode().split("\r"))) + else: + print("[PID=0x%02x] %r" % (frame.pid, frame.payload)) + + async def interact(self): + # Open the KISS interface + self._device.open() + + # TODO: implement async functions on KISS device to avoid this! + while self._device.state != KISSDeviceState.OPEN: + await asyncio.sleep(0.1) + + # Connect to the remote station + future = asyncio.Future() + + def _state_change_fn(state, **kwa): + if state is AX25PeerState.CONNECTED: + future.set_result(None) + elif state is AX25PeerState.DISCONNECTED: + future.set_exception(IOError("Connection refused")) + + self._peer.connect_state_changed.connect(_state_change_fn) + self._peer.connect() + await future + self._peer.connect_state_changed.disconnect(_state_change_fn) + + # We should now be connected + self._log.info("CONNECTED to %s", self._peer.address) + finished = False + session = PromptSession() + while not finished: + if self._peer.state is not AX25PeerState.CONNECTED: + finished = True + + with patch_stdout(): + # Prompt for user input + txinput = await session.prompt_async( + "%s>" % self._peer.address + ) + if txinput: + self._peer.send(("%s\r" % txinput).encode()) + + if self._peer.state is not AX25PeerState.DISCONNECTED: + self._log.info("DISCONNECTING") + future = asyncio.Future() + + def _state_change_fn(state, **kwa): + if state is AX25PeerState.DISCONNECTED: + future.set_result(None) + + self._peer.connect_state_changed.connect(_state_change_fn) + self._peer.disconnect() + await future + self._peer.connect_state_changed.disconnect(_state_change_fn) + + self._log.info("Finished") + + +async def main(): + ap = argparse.ArgumentParser() + + ap.add_argument("--log-level", default="info", type=str, help="Log level") + ap.add_argument("--port", default=0, type=int, help="KISS port number") + ap.add_argument( + "config", type=str, help="KISS serial port configuration file" + ) + ap.add_argument("source", type=str, help="Source callsign/SSID") + ap.add_argument("destination", type=str, help="Source callsign/SSID") + + args = ap.parse_args() + + logging.basicConfig( + level=args.log_level.upper(), + format=( + "%(asctime)s %(name)s[%(filename)s:%(lineno)4d] " + "%(levelname)s %(message)s" + ), + ) + config = safe_load(open(args.config, "r").read()) + + ax25call = AX25Call(args.source, args.destination, config, args.port) + await ax25call.interact() + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) From bbaab1ec364934e9af36e36f8bdfd72d39d2bbeb Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:11:17 +1000 Subject: [PATCH 189/207] coverage: Exclude interactive tools These are not practically unit testable. --- .coveragerc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index ce87a9d..2e20669 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,6 @@ [run] branch=true source=aioax25 -omit=tests +omit= + tests + aioax25/tools/** From 38b4982fce37e57ed2a134a7a019d3ee968d7224 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:11:47 +1000 Subject: [PATCH 190/207] pyproject.toml: Set dependencies for the `call` tool. --- pyproject.toml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed84b8a..8f8ecb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,11 @@ dependencies = [ "signalslot" ] +[project.optional-dependencies] +call = [ + "prompt_toolkit" +] + [project.readme] file = "README.md" content-type = "text/markdown" @@ -41,9 +46,3 @@ log_cli = true [tool.setuptools.dynamic] version = {attr = "aioax25.__version__"} - -[tool.coverage.run] -omit = [ - # Debugging tool for dumping KISS traffic from socat hex dumps. - "aioax25/tools/dumphex.py" -] From 5b2ead616ebe36204a90e926051f643e4b0c6a0e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 17:22:09 +1000 Subject: [PATCH 191/207] README.md update --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e6daf87..3ef440c 100644 --- a/README.md +++ b/README.md @@ -29,27 +29,31 @@ Python 3.5 support is planned to continue until it too, becomes infeasible [NWDR UDRC-II](https://nw-digital-radio.groups.io/g/udrc/wiki/home)) * We can receive AX.25 UI frames * We can send AX.25 UI frames +* Connecting to AX.25 nodes (*experimental*) ## What doesn't work -* Connecting to AX.25 nodes -* Accepting connections from AX.25 nodes +* Nothing yet? ## What isn't tested * Platforms other than GNU/Linux +* Accepting connections from AX.25 nodes ## Current plans -Right now, I intend to get enough going for APRS operation, as that is my -immediate need now. Hence the focus on UI frames. +Right now, I have the smarts to deal with basic APRS messaging. Hence the +focus on UI frames. We can send and receive APRS message frames, parse some +kinds of position frames, and do basic UI frame stuff. -I intend to write a core class that will take care of some core AX.25 message -handling work and provide the basis of what's needed to implement APRS. +Preliminary support for AX.25 connected mode is present, but is _experimental_. +It was tested connecting to a BPQ32 node, but not a lot of testing has been +done at this stage. **More feedback would be appreciated.** At this stage, +there is code for accepting a connection, but no testing has been done of this +code so there could be glaring bugs. After that, some things I'd like to tackle in no particular order: -* Connected mode operation * NET/ROM support Supported platforms will be GNU/Linux, and possibly BSD variants. I don't From 7bf880d8e6db0cf3ad0513a11cbf9733575ba272 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 19:06:26 +1000 Subject: [PATCH 192/207] peer unit tests: Test send segmentation --- tests/test_peer/peer.py | 2 + tests/test_peer/test_send.py | 118 +++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 tests/test_peer/test_send.py diff --git a/tests/test_peer/peer.py b/tests/test_peer/peer.py index cef2a5a..6ab0f68 100644 --- a/tests/test_peer/peer.py +++ b/tests/test_peer/peer.py @@ -31,6 +31,7 @@ def __init__( full_duplex=False, reply_path=None, locked_path=False, + paclen=128, ): super(TestingAX25Peer, self).__init__( station, @@ -54,4 +55,5 @@ def __init__( full_duplex, reply_path, locked_path, + paclen, ) diff --git a/tests/test_peer/test_send.py b/tests/test_peer/test_send.py new file mode 100644 index 0000000..fed475a --- /dev/null +++ b/tests/test_peer/test_send.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +""" +Tests for AX25Peer transmit segmentation +""" + +from aioax25.frame import ( + AX25Address, +) +from .peer import TestingAX25Peer +from ..mocks import DummyStation + + +# UA reception + + +def test_peer_send_short(): + """ + Test send accepts short payloads and enqueues a single transmission. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=[], + full_duplex=True, + ) + + peer._send_next_iframe_scheduled = False + + def _send_next_iframe(): + peer._send_next_iframe_scheduled = True + + peer._send_next_iframe = _send_next_iframe + + peer.send(b"Testing 1 2 3 4") + + assert peer._send_next_iframe_scheduled is True + assert peer._pending_data == [(0xF0, b"Testing 1 2 3 4")] + + +def test_peer_send_long(): + """ + Test send accepts long payloads and enqueues multiple transmissions. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=[], + full_duplex=True, + ) + + peer._send_next_iframe_scheduled = False + + def _send_next_iframe(): + peer._send_next_iframe_scheduled = True + + peer._send_next_iframe = _send_next_iframe + + peer.send( + b"(0) Testing 1 2 3 4 5\n(1) Testing 1 2 3 4 5\n(2) Testing 1 2 3 4 5" + b"\n(3) Testing 1 2 3 4 5\n(4) Testing 1 2 3 4 5\n(5) Testing 1 2 3 4" + b" 5\n(6) Testing 1 2 3 4 5\n(7) Testing 1 2 3 4 5\n" + ) + + assert peer._send_next_iframe_scheduled is True + assert peer._pending_data == [ + ( + 0xF0, + b"(0) Testing 1 2 3 4 5\n(1) Testing 1 2 3 4 5\n(2) Testing " + b"1 2 3 4 5\n(3) Testing 1 2 3 4 5\n(4) Testing 1 2 3 4 5\n(5) " + b"Testing 1 2 3 ", + ), + (0xF0, b"4 5\n(6) Testing 1 2 3 4 5\n(7) Testing 1 2 3 4 5\n"), + ] + + +def test_peer_send_paclen(): + """ + Test send respects PACLEN. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=[], + full_duplex=True, + paclen=16, + ) + + peer._send_next_iframe_scheduled = False + + def _send_next_iframe(): + peer._send_next_iframe_scheduled = True + + peer._send_next_iframe = _send_next_iframe + + peer.send( + b"(0) Testing 1 2 3 4 5\n(1) Testing 1 2 3 4 5\n(2) Testing 1 2 3 4 5" + b"\n(3) Testing 1 2 3 4 5\n(4) Testing 1 2 3 4 5\n(5) Testing 1 2 3 4" + b" 5\n(6) Testing 1 2 3 4 5\n(7) Testing 1 2 3 4 5\n" + ) + + assert peer._send_next_iframe_scheduled is True + assert peer._pending_data == [ + (0xF0, b"(0) Testing 1 2 "), + (0xF0, b"3 4 5\n(1) Testin"), + (0xF0, b"g 1 2 3 4 5\n(2) "), + (0xF0, b"Testing 1 2 3 4 "), + (0xF0, b"5\n(3) Testing 1 "), + (0xF0, b"2 3 4 5\n(4) Test"), + (0xF0, b"ing 1 2 3 4 5\n(5"), + (0xF0, b") Testing 1 2 3 "), + (0xF0, b"4 5\n(6) Testing "), + (0xF0, b"1 2 3 4 5\n(7) Te"), + (0xF0, b"sting 1 2 3 4 5\n"), + ] From e926d6b6b650834d7013a63eeaa6fc307bf7f19d Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 19:28:06 +1000 Subject: [PATCH 193/207] peer unit tests: Test decoding of I/S-frames. --- tests/test_peer/test_connection.py | 300 +++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index ba9e658..b36ca0c 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -12,6 +12,8 @@ AX25DisconnectModeFrame, AX25FrameRejectFrame, AX25UnnumberedAcknowledgeFrame, + AX25UnnumberedInformationFrame, + AX25RawFrame, AX25TestFrame, AX25SetAsyncBalancedModeFrame, AX25SetAsyncBalancedModeExtendedFrame, @@ -295,6 +297,304 @@ def _on_receive_ua(): assert count == dict(ua=1) +def test_recv_ui(): + """ + Test that UI is emitted by the received frame signal. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Create a handler for receiving the UI + rx_frames = [] + + def _on_receive_frame(frame, **kwargs): + assert "peer" in kwargs + assert kwargs.pop("peer") is peer + assert kwargs == {} + rx_frames.append(frame) + + peer.received_frame.connect(_on_receive_frame) + + # Set the state + peer._state = AX25PeerState.CONNECTED + + # Inject a frame + frame = AX25UnnumberedInformationFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + pid=0xF0, + payload=b"Testing 1 2 3 4", + ) + + peer._on_receive(frame) + + # Our handler should have been called + assert len(rx_frames) == 1 + assert rx_frames[0] is frame + + +def test_recv_raw_noconn(): + """ + Test that a raw frame without a connection triggers a DM frame. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub _send_dm + count = dict(send_dm=0) + + def _send_dm(): + count["send_dm"] += 1 + + peer._send_dm = _send_dm + + # Set the state + peer._state = AX25PeerState.DISCONNECTED + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x00\x00Testing 1 2 3 4", + ) + ) + + +def test_recv_raw_mod8_iframe(): + """ + Test that a I-frame with Mod8 connection is handled. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub _on_receive_iframe + iframes = [] + + def _on_receive_iframe(frame): + iframes.append(frame) + + peer._on_receive_iframe = _on_receive_iframe + + # Stub _on_receive_sframe + sframes = [] + + def _on_receive_sframe(frame): + sframes.append(frame) + + peer._on_receive_sframe = _on_receive_sframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # Our I-frame handler should have been called + assert len(iframes) == 1 + assert isinstance(iframes[0], AX258BitInformationFrame) + assert iframes[0].pid == 0xf0 + assert iframes[0].payload == b"Testing 1 2 3 4" + + # Our S-frame handler should NOT have been called + assert sframes == [] + + +def test_recv_raw_mod128_iframe(): + """ + Test that a I-frame with Mod128 connection is handled. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub _on_receive_iframe + iframes = [] + + def _on_receive_iframe(frame): + iframes.append(frame) + + peer._on_receive_iframe = _on_receive_iframe + + # Stub _on_receive_sframe + sframes = [] + + def _on_receive_sframe(frame): + sframes.append(frame) + + peer._on_receive_sframe = _on_receive_sframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 128 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x04\x0d\xf0Testing 1 2 3 4", + ) + ) + + # Our I-frame handler should have been called + assert len(iframes) == 1 + assert isinstance(iframes[0], AX2516BitInformationFrame) + assert iframes[0].pid == 0xf0 + assert iframes[0].payload == b"Testing 1 2 3 4" + + # Our S-frame handler should NOT have been called + assert sframes == [] + + +def test_recv_raw_mod8_sframe(): + """ + Test that a S-frame with Mod8 connection is handled. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub _on_receive_iframe + iframes = [] + + def _on_receive_iframe(frame): + iframes.append(frame) + + peer._on_receive_iframe = _on_receive_iframe + + # Stub _on_receive_sframe + sframes = [] + + def _on_receive_sframe(frame): + sframes.append(frame) + + peer._on_receive_sframe = _on_receive_sframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x41", + ) + ) + + # Our S-frame handler should have been called + assert len(sframes) == 1 + assert isinstance(sframes[0], AX258BitReceiveReadyFrame) + + # Our I-frame handler should NOT have been called + assert iframes == [] + + +def test_recv_raw_mod128_sframe(): + """ + Test that a S-frame with Mod128 connection is handled. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub _on_receive_iframe + iframes = [] + + def _on_receive_iframe(frame): + iframes.append(frame) + + peer._on_receive_iframe = _on_receive_iframe + + # Stub _on_receive_sframe + sframes = [] + + def _on_receive_sframe(frame): + sframes.append(frame) + + peer._on_receive_sframe = _on_receive_sframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 128 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x01\x5c", + ) + ) + + # Our S-frame handler should have been called + assert len(sframes) == 1 + assert isinstance(sframes[0], AX2516BitReceiveReadyFrame) + + # Our I-frame handler should NOT have been called + assert iframes == [] + + def test_recv_disc(): """ Test that DISC is handled. From 6a68be3b4532e10630b5e6dde8c1b79ad1190dc9 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 20:04:39 +1000 Subject: [PATCH 194/207] peer unit tests: Test I-frame handling --- tests/test_peer/test_connection.py | 515 ++++++++++++++++++++++++++++- 1 file changed, 513 insertions(+), 2 deletions(-) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index b36ca0c..a058ed2 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -427,7 +427,7 @@ def _on_receive_sframe(frame): # Our I-frame handler should have been called assert len(iframes) == 1 assert isinstance(iframes[0], AX258BitInformationFrame) - assert iframes[0].pid == 0xf0 + assert iframes[0].pid == 0xF0 assert iframes[0].payload == b"Testing 1 2 3 4" # Our S-frame handler should NOT have been called @@ -482,7 +482,7 @@ def _on_receive_sframe(frame): # Our I-frame handler should have been called assert len(iframes) == 1 assert isinstance(iframes[0], AX2516BitInformationFrame) - assert iframes[0].pid == 0xf0 + assert iframes[0].pid == 0xF0 assert iframes[0].payload == b"Testing 1 2 3 4" # Our S-frame handler should NOT have been called @@ -595,6 +595,517 @@ def _on_receive_sframe(frame): assert iframes == [] +def test_recv_iframe_busy(): + """ + Test that an I-frame received while we're busy triggers RNR. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub _send_rnr_notification and _cancel_rr_notification + count = dict(send_rnr=0, cancel_rr=0) + + def _cancel_rr_notification(): + count["cancel_rr"] += 1 + + peer._cancel_rr_notification = _cancel_rr_notification + + def _send_rnr_notification(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr_notification + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + peer._local_busy = True + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # RR notification should be cancelled and there should be a RNR queued + assert count == dict(cancel_rr=1, send_rnr=1) + + +def test_recv_iframe_mismatched_seq(): + """ + Test that an I-frame with a mismatched sequence number is dropped. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub the functions called + count = dict(send_rnr=0, cancel_rr=0, send_next_iframe=0, schedule_rr=0) + isframes = [] + iframes = [] + state_updates = [] + + def _cancel_rr_notification(): + count["cancel_rr"] += 1 + + peer._cancel_rr_notification = _cancel_rr_notification + + def _schedule_rr_notification(): + count["schedule_rr"] += 1 + + peer._schedule_rr_notification = _schedule_rr_notification + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _send_rnr_notification(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr_notification + + def _on_receive_isframe_nr_ns(frame): + isframes.append(frame) + + peer._on_receive_isframe_nr_ns = _on_receive_isframe_nr_ns + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + # Hook received_information signal + + def _received_information(frame, payload, **kwargs): + assert kwargs == {} + assert payload == frame.payload + iframes.append(frame) + + peer.received_information.connect(_received_information) + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # RR notification should be cancelled, no other actions pending + assert count == dict( + cancel_rr=1, send_rnr=0, schedule_rr=0, send_next_iframe=0 + ) + + assert isframes == [] + assert iframes == [] + assert state_updates == [] + + +def test_recv_iframe_mismatched_seq(): + """ + Test that an I-frame with a mismatched sequence number is dropped. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub the functions called + count = dict(send_rnr=0, cancel_rr=0, send_next_iframe=0, schedule_rr=0) + isframes = [] + iframes = [] + state_updates = [] + + def _cancel_rr_notification(): + count["cancel_rr"] += 1 + + peer._cancel_rr_notification = _cancel_rr_notification + + def _schedule_rr_notification(): + count["schedule_rr"] += 1 + + peer._schedule_rr_notification = _schedule_rr_notification + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _send_rnr_notification(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr_notification + + def _on_receive_isframe_nr_ns(frame): + isframes.append(frame) + + peer._on_receive_isframe_nr_ns = _on_receive_isframe_nr_ns + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + # Hook received_information signal + + def _received_information(frame, payload, **kwargs): + assert kwargs == {} + assert payload == frame.payload + iframes.append(frame) + + peer.received_information.connect(_received_information) + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # RR notification should be cancelled, no other actions pending + assert count == dict( + cancel_rr=1, send_rnr=0, schedule_rr=0, send_next_iframe=0 + ) + + assert isframes == [] + assert iframes == [] + assert state_updates == [] + + +def test_recv_iframe_matched_seq_nopending(): + """ + Test that an I-frame with a matched sequence number is handled. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub the functions called + count = dict(send_rnr=0, cancel_rr=0, send_next_iframe=0, schedule_rr=0) + isframes = [] + iframes = [] + state_updates = [] + + def _cancel_rr_notification(): + count["cancel_rr"] += 1 + + peer._cancel_rr_notification = _cancel_rr_notification + + def _schedule_rr_notification(): + count["schedule_rr"] += 1 + + peer._schedule_rr_notification = _schedule_rr_notification + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _send_rnr_notification(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr_notification + + def _on_receive_isframe_nr_ns(frame): + isframes.append(frame) + + peer._on_receive_isframe_nr_ns = _on_receive_isframe_nr_ns + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + # Hook received_information signal + + def _received_information(frame, payload, **kwargs): + assert kwargs == {} + assert payload == frame.payload + iframes.append(frame) + + peer.received_information.connect(_received_information) + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + peer._recv_seq = 2 + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # RR notification should be re-scheduled, no I-frame transmissions + assert count == dict( + cancel_rr=1, send_rnr=0, schedule_rr=1, send_next_iframe=0 + ) + + assert len(isframes) == 1 + + frame = isframes.pop(0) + assert frame.pid == 0xF0 + assert frame.payload == b"Testing 1 2 3 4" + + assert iframes == [frame] + assert state_updates == [ + {"comment": "from I-frame N(S)", "prop": "_recv_state", "value": 3} + ] + + +def test_recv_iframe_matched_seq_lotspending(): + """ + Test that an I-frame with lots of pending I-frames sends RR instead. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub the functions called + count = dict(send_rnr=0, cancel_rr=0, send_next_iframe=0, schedule_rr=0) + isframes = [] + iframes = [] + state_updates = [] + + def _cancel_rr_notification(): + count["cancel_rr"] += 1 + + peer._cancel_rr_notification = _cancel_rr_notification + + def _schedule_rr_notification(): + count["schedule_rr"] += 1 + + peer._schedule_rr_notification = _schedule_rr_notification + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _send_rnr_notification(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr_notification + + def _on_receive_isframe_nr_ns(frame): + isframes.append(frame) + + peer._on_receive_isframe_nr_ns = _on_receive_isframe_nr_ns + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + # Hook received_information signal + + def _received_information(frame, payload, **kwargs): + assert kwargs == {} + assert payload == frame.payload + iframes.append(frame) + + peer.received_information.connect(_received_information) + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + peer._max_outstanding = 8 + peer._recv_seq = 2 + peer._pending_data = [(0xF0, b"Test outgoing")] + peer._pending_iframes = { + 0: (0xF0, b"Test outgoing 1"), + 1: (0xF0, b"Test outgoing 2"), + 2: (0xF0, b"Test outgoing 3"), + 3: (0xF0, b"Test outgoing 4"), + 4: (0xF0, b"Test outgoing 5"), + 5: (0xF0, b"Test outgoing 6"), + 6: (0xF0, b"Test outgoing 7"), + 7: (0xF0, b"Test outgoing 8"), + } + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # RR notification should be re-scheduled, no I-frame transmissions + assert count == dict( + cancel_rr=1, send_rnr=0, schedule_rr=1, send_next_iframe=0 + ) + + assert len(isframes) == 1 + + frame = isframes.pop(0) + assert frame.pid == 0xF0 + assert frame.payload == b"Testing 1 2 3 4" + + assert iframes == [frame] + assert state_updates == [ + {"comment": "from I-frame N(S)", "prop": "_recv_state", "value": 3} + ] + + +def test_recv_iframe_matched_seq_iframepending(): + """ + Test that an I-frame reception triggers I-frame transmission if data is + pending. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub idle time-out handling + peer._reset_idle_timeout = lambda: None + + # Stub the functions called + count = dict(send_rnr=0, cancel_rr=0, send_next_iframe=0, schedule_rr=0) + isframes = [] + iframes = [] + state_updates = [] + + def _cancel_rr_notification(): + count["cancel_rr"] += 1 + + peer._cancel_rr_notification = _cancel_rr_notification + + def _schedule_rr_notification(): + count["schedule_rr"] += 1 + + peer._schedule_rr_notification = _schedule_rr_notification + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _send_rnr_notification(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr_notification + + def _on_receive_isframe_nr_ns(frame): + isframes.append(frame) + + peer._on_receive_isframe_nr_ns = _on_receive_isframe_nr_ns + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + # Hook received_information signal + + def _received_information(frame, payload, **kwargs): + assert kwargs == {} + assert payload == frame.payload + iframes.append(frame) + + peer.received_information.connect(_received_information) + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._modulo = 8 + peer._max_outstanding = 8 + peer._recv_seq = 2 + peer._pending_data = [(0xF0, b"Test outgoing")] + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\xd4\xf0Testing 1 2 3 4", + ) + ) + + # RR notification should be cancelled, no I-frame transmissions + assert count == dict( + cancel_rr=1, send_rnr=0, schedule_rr=0, send_next_iframe=1 + ) + + assert len(isframes) == 1 + + frame = isframes.pop(0) + assert frame.pid == 0xF0 + assert frame.payload == b"Testing 1 2 3 4" + + assert iframes == [frame] + assert state_updates == [ + {"comment": "from I-frame N(S)", "prop": "_recv_state", "value": 3} + ] + + def test_recv_disc(): """ Test that DISC is handled. From 236c9d0b8fb16828243764ce8f3b61dd426e2e7a Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 23:02:49 +1000 Subject: [PATCH 195/207] peer unit tests: Test S-frame handling --- aioax25/peer.py | 48 ++- tests/mocks.py | 3 + tests/test_peer/test_connection.py | 584 +++++++++++++++++++++++++++++ 3 files changed, 609 insertions(+), 26 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 7469d43..5d7e30b 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -570,6 +570,23 @@ def _on_receive(self, frame): self.received_frame.emit(frame=frame, peer=self) return self._send_dm() + def _on_receive_isframe_nr_ns(self, frame): + """ + Handle the N(R) / N(S) fields from an I or S frame from the peer. + """ + # "Whenever an I or S frame is correctly received, even in a busy + # condition, the N(R) of the received frame should be checked to see + # if it includes an acknowledgement of outstanding sent I frames. The + # T1 timer should be cancelled if the received frame actually + # acknowledges previously unacknowledged frames. If the T1 timer is + # cancelled and there are still some frames that have been sent that + # are not acknowledged, T1 should be started again. If the T1 timer + # runs out before an acknowledgement is received, the device should + # proceed to the retransmission procedure in 2.4.4.9." + + # Check N(R) for received frames. + self._ack_outstanding((frame.nr - 1) % self._modulo) + def _on_receive_iframe(self, frame): """ Handle an incoming I-frame @@ -642,23 +659,9 @@ def _on_receive_sframe(self, frame): self._on_receive_rej(frame) elif isinstance(frame, self._SREJFrameClass): self._on_receive_srej(frame) - - def _on_receive_isframe_nr_ns(self, frame): - """ - Handle the N(R) / N(S) fields from an I or S frame from the peer. - """ - # "Whenever an I or S frame is correctly received, even in a busy - # condition, the N(R) of the received frame should be checked to see - # if it includes an acknowledgement of outstanding sent I frames. The - # T1 timer should be cancelled if the received frame actually - # acknowledges previously unacknowledged frames. If the T1 timer is - # cancelled and there are still some frames that have been sent that - # are not acknowledged, T1 should be started again. If the T1 timer - # runs out before an acknowledgement is received, the device should - # proceed to the retransmission procedure in 2.4.4.9." - - # Check N(R) for received frames. - self._ack_outstanding((frame.nr - 1) % self._modulo) + else: # pragma: no cover + # Should be impossible to get here! + raise TypeError("Unhandled frame: %r" % frame) def _on_receive_rr(self, frame): if frame.pf: @@ -670,9 +673,6 @@ def _on_receive_rr(self, frame): self._log.debug( "RR notification received from peer N(R)=%d", frame.nr ) - # AX.25 sect 4.3.2.1: "acknowledges properly received - # I frames up to and including N(R)-1" - self._ack_outstanding((frame.nr - 1) % self._modulo) self._peer_busy = False self._send_next_iframe() @@ -684,8 +684,6 @@ def _on_receive_rnr(self, frame): else: # Received peer's RNR status, peer is busy self._log.debug("RNR notification received from peer") - # AX.25 sect 4.3.2.2: "Frames up to N(R)-1 are acknowledged." - self._ack_outstanding((frame.nr - 1) % self._modulo) self._peer_busy = True def _on_receive_rej(self, frame): @@ -696,9 +694,6 @@ def _on_receive_rej(self, frame): else: # Reject reject. self._log.debug("REJ notification received from peer") - # AX.25 sect 4.3.2.3: "Any frames sent with a sequence number - # of N(R)-1 or less are acknowledged." - self._ack_outstanding((frame.nr - 1) % self._modulo) # AX.25 2.2 section 6.4.7 says we set V(S) to this frame's # N(R) and begin re-transmission. self._log.debug("Set state V(S) from frame N(R) = %d", frame.nr) @@ -713,7 +708,8 @@ def _on_receive_srej(self, frame): # '1', then I frames numbered up to N(R)-1 inclusive are considered # as acknowledged." self._log.debug("SREJ received with P/F=1") - self._ack_outstanding((frame.nr - 1) % self._modulo) + # TODO: but we always ACK up to N(R)-1 on receipt of a S-frame? + # What if the P/F bit is 0? # Re-send the outstanding frame self._log.debug("Re-sending I-frame %d due to SREJ", frame.nr) diff --git a/tests/mocks.py b/tests/mocks.py index a60e285..93eef60 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -57,6 +57,9 @@ def warning(self, msg, *args, **kwargs): def getChild(self, name): return DummyLogger(self.name + "." + name, parent=self) + def isEnabledFor(self, level): + return True + class DummyTimeout(object): def __init__(self, delay, callback, *args, **kwargs): diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index a058ed2..ec858df 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -1106,6 +1106,590 @@ def _received_information(frame, payload, **kwargs): ] +def test_recv_sframe_rr_req_busy(): + """ + Test that RR with P/F set while busy sends RNR + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._local_busy = True + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x51", + ) + ) + + # We should send a RNR in reply + assert count == dict(send_rr=0, send_rnr=1, send_next_iframe=0) + + +def test_recv_sframe_rr_req_notbusy(): + """ + Test that RR with P/F set while not busy sends RR + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._local_busy = False + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x51", + ) + ) + + # We should send a RR in reply + assert count == dict(send_rr=1, send_rnr=0, send_next_iframe=0) + + +def test_recv_sframe_rr_rep(): + """ + Test that RR with P/F clear marks peer not busy + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._peer_busy = True + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x41", + ) + ) + + # Busy flag should be cleared + assert peer._peer_busy is False + + # We should send the next I-frame in reply + assert count == dict(send_rr=0, send_rnr=0, send_next_iframe=1) + + +def test_recv_sframe_rnr_req_busy(): + """ + Test that RNR with P/F set while busy sends RNR + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._local_busy = True + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x55", + ) + ) + + # We should send a RNR in reply + assert count == dict(send_rr=0, send_rnr=1, send_next_iframe=0) + + +def test_recv_sframe_rnr_req_notbusy(): + """ + Test that RNR with P/F set while not busy sends RR + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._local_busy = False + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x55", + ) + ) + + # We should send a RR in reply + assert count == dict(send_rr=1, send_rnr=0, send_next_iframe=0) + + +def test_recv_sframe_rnr_rep(): + """ + Test that RNR with P/F clear marks peer busy + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._peer_busy = False + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x45", + ) + ) + + # Busy flag should be set + assert peer._peer_busy is True + + +def test_recv_sframe_rej_req_busy(): + """ + Test that REJ with P/F set while busy sends RNR + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + state_updates = [] + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + setattr( + peer, + prop, + kwargs.get("value", getattr(peer, prop)) + kwargs.get("delta", 0), + ) + + peer._update_state = _update_state + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._local_busy = True + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x59", + ) + ) + + # We should update due to resets and peer ACKs + assert state_updates == [ + {"comment": "reset", "prop": "_send_state", "value": 0}, + {"comment": "reset", "prop": "_send_seq", "value": 0}, + {"comment": "reset", "prop": "_recv_state", "value": 0}, + {"comment": "reset", "prop": "_recv_seq", "value": 0}, + {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_recv_seq"}, + ] + + # We should send a RNR in reply + assert count == dict(send_rr=0, send_rnr=1, send_next_iframe=0) + + +def test_recv_sframe_rej_req_notbusy(): + """ + Test that REJ with P/F set while not busy sends RR + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + state_updates = [] + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + setattr( + peer, + prop, + kwargs.get("value", getattr(peer, prop)) + kwargs.get("delta", 0), + ) + + peer._update_state = _update_state + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._local_busy = False + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x59", + ) + ) + + # State updates should be a reset and peer ACK + assert state_updates == [ + {"comment": "reset", "prop": "_send_state", "value": 0}, + {"comment": "reset", "prop": "_send_seq", "value": 0}, + {"comment": "reset", "prop": "_recv_state", "value": 0}, + {"comment": "reset", "prop": "_recv_seq", "value": 0}, + {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_recv_seq"}, + ] + + # We should send a RR in reply + assert count == dict(send_rr=1, send_rnr=0, send_next_iframe=0) + + +def test_recv_sframe_rej_rep(): + """ + Test that REJ with P/F clear marks peer busy + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + count = dict(send_rr=0, send_rnr=0, send_next_iframe=0) + state_updates = [] + + def _send_rr(): + count["send_rr"] += 1 + + peer._send_rr_notification = _send_rr + + def _send_rnr(): + count["send_rnr"] += 1 + + peer._send_rnr_notification = _send_rnr + + def _send_next_iframe(): + count["send_next_iframe"] += 1 + + peer._send_next_iframe = _send_next_iframe + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + setattr( + peer, + prop, + kwargs.get("value", getattr(peer, prop)) + kwargs.get("delta", 0), + ) + + peer._update_state = _update_state + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(False) + peer._peer_busy = False + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x49", + ) + ) + + assert state_updates == [ + # Reset state + {"comment": "reset", "prop": "_send_state", "value": 0}, + {"comment": "reset", "prop": "_send_seq", "value": 0}, + {"comment": "reset", "prop": "_recv_state", "value": 0}, + {"comment": "reset", "prop": "_recv_seq", "value": 0}, + # Peer ACK + {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_recv_seq"}, + # REJ handling + {"comment": "from REJ N(R)", "prop": "_send_state", "value": 2}, + ] + + # We should send an I-frame in reply + assert count == dict(send_rr=0, send_rnr=0, send_next_iframe=1) + + +def test_recv_sframe_srej_pf(): + """ + Test that REJ with P/F set retransmits specified frame + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + iframes_rqd = [] + + def _transmit_iframe(nr): + iframes_rqd.append(nr) + + peer._transmit_iframe = _transmit_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(True) + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x0d\x55", + ) + ) + + assert iframes_rqd == [42] + + +def test_recv_sframe_srej_nopf(): + """ + Test that REJ with P/F clear retransmits specified frame + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub the functions called + iframes_rqd = [] + + def _transmit_iframe(nr): + iframes_rqd.append(nr) + + peer._transmit_iframe = _transmit_iframe + + # Set the state + peer._state = AX25PeerState.CONNECTED + peer._init_connection(True) + + # Inject a frame + peer._on_receive( + AX25RawFrame( + destination=AX25Address("VK4MSL-1"), + source=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + payload=b"\x0d\x54", + ) + ) + + assert iframes_rqd == [42] + + def test_recv_disc(): """ Test that DISC is handled. From 1dd227e5ec0f23dcb7d3477eb5db49c8f239d57e Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 23:13:22 +1000 Subject: [PATCH 196/207] peer: Require connection attempt before XID The only time we do an XID is just prior to a connection attempt, we should enforce this to simplify logic. --- aioax25/peer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 5d7e30b..11112aa 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -349,6 +349,7 @@ def connect(self): """ if self._state is AX25PeerState.DISCONNECTED: self._log.info("Initiating connection to remote peer") + self._set_conn_state(AX25PeerState.CONNECTING) handler = AX25PeerConnectionHandler(self) handler.done_sig.connect(self._on_connect_response) handler._go() @@ -887,6 +888,9 @@ def _negotiate(self, callback): """ Undertake negotiation with the peer station. """ + # Sanity check, ensure we are in the CONNECTING state + assert self._state is AX25PeerState.CONNECTING + # Sanity check, don't call this if we know the station won't take it. if self._protocol not in (AX25Version.UNKNOWN, AX25Version.AX25_22): raise RuntimeError( @@ -928,8 +932,8 @@ def _on_negotiate_result(self, response, **kwargs): self._protocol = AX25Version.AX25_22 self._negotiated = True - self._log.debug("XID negotiation complete") - self._set_conn_state(AX25PeerState.DISCONNECTED) + self._log.debug("XID negotiation complete, resume connection") + self._set_conn_state(AX25PeerState.CONNECTING) def _init_connection(self, extended): """ From 42022236f201376430b4a41129abf8c8bcd076a8 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Tue, 7 May 2024 23:14:25 +1000 Subject: [PATCH 197/207] tools.call: Drop AX.25 2.0 limitation BPQ32 responds with FRMR, so fallback to AX.25 2.0 seems to work. --- aioax25/tools/call.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aioax25/tools/call.py b/aioax25/tools/call.py index 521450b..3099fd5 100644 --- a/aioax25/tools/call.py +++ b/aioax25/tools/call.py @@ -34,7 +34,6 @@ def __init__(self, source, destination, kissparams, port=0): self._station = AX25Station( self._interface, source, - protocol=AX25Version.AX25_20, log=stnlog, ) self._station.attach() From c11b5877d8daec3921d5ba69a02ce2abdabf85dc Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 09:42:29 +1000 Subject: [PATCH 198/207] tools.call: Don't use magic numbers for PID. --- aioax25/tools/call.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aioax25/tools/call.py b/aioax25/tools/call.py index 3099fd5..af92da3 100644 --- a/aioax25/tools/call.py +++ b/aioax25/tools/call.py @@ -12,6 +12,7 @@ # from aioax25.kiss import … # from aioax25.interface import … # etc… if you're copying this for your own code +from ..frame import AX25Frame from ..kiss import make_device, KISSDeviceState from ..interface import AX25Interface from ..station import AX25Station @@ -42,7 +43,7 @@ def __init__(self, source, destination, kissparams, port=0): def _on_receive(self, frame, **kwargs): with patch_stdout(): - if frame.pid == 0xF0: + if frame.pid == AX25Frame.PID_NO_L3: # No L3 protocol print("\n".join(frame.payload.decode().split("\r"))) else: From 83d36407ac1e73894d8cc4ab90e7fde950b927f1 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 11:52:08 +1000 Subject: [PATCH 199/207] peer: Re-instate `_ack_state`, use it for handling peer `N(R)` Now its purpose makes sense! Not part of the AX.25 2.0 spec, but it seems to make more sense than the other variables for tracking this point. --- aioax25/peer.py | 19 +++++++++++++------ tests/test_peer/test_connection.py | 13 ++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 11112aa..bc8c132 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -201,6 +201,12 @@ def __init__( self._recv_seq = 0 self._recv_seq_name = "N(R)" + # ACK state number. + # Used to track the sequence number previously ACKed in an I or S + # frame N(R) field. + self._ack_state = 0 # AKA V(A) + self._ack_state_name = "V(A)" + self._local_busy = False # Local end busy, respond to # RR and I-frames with RNR. self._peer_busy = False # Peer busy, await RR. @@ -731,14 +737,14 @@ def _ack_outstanding(self, nr): """ Receive all frames up to N(R)-1 """ - self._log.debug("%d through to %d are received", self._recv_seq, nr) - while self._recv_seq != nr: + self._log.debug("%d through to %d are received", self._ack_state, nr) + while self._ack_state != nr: if self._log.isEnabledFor(logging.DEBUG): self._log.debug("Pending frames: %r", self._pending_iframes) - self._log.debug("ACKing N(R)=%s", self._recv_seq) + self._log.debug("ACKing N(R)=%s", self._ack_state) try: - frame = self._pending_iframes.pop(self._recv_seq) + frame = self._pending_iframes.pop(self._ack_state) if self._log.isEnabledFor(logging.DEBUG): self._log.debug( "Popped %s off pending queue, N(R)s pending: %r", @@ -749,12 +755,12 @@ def _ack_outstanding(self, nr): if self._log.isEnabledFor(logging.DEBUG): self._log.debug( "ACK to unexpected N(R) number %s, pending: %r", - self._recv_seq, + self._ack_state, self._pending_iframes, ) finally: self._update_state( - "_recv_seq", delta=1, comment="ACKed by peer N(R)" + "_ack_state", delta=1, comment="ACKed by peer N(R)" ) def _on_receive_test(self, frame): @@ -979,6 +985,7 @@ def _reset_connection_state(self): self._update_state("_send_seq", value=0, comment="reset") self._update_state("_recv_state", value=0, comment="reset") self._update_state("_recv_seq", value=0, comment="reset") + self._update_state("_ack_state", value=0, comment="reset") # Unacknowledged I-frames to be ACKed self._pending_iframes = {} diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index ec858df..16a9fa4 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -1466,7 +1466,8 @@ def _update_state(prop, **kwargs): {"comment": "reset", "prop": "_send_seq", "value": 0}, {"comment": "reset", "prop": "_recv_state", "value": 0}, {"comment": "reset", "prop": "_recv_seq", "value": 0}, - {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_recv_seq"}, + {"comment": "reset", "prop": "_ack_state", "value": 0}, + {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_ack_state"}, ] # We should send a RNR in reply @@ -1536,7 +1537,8 @@ def _update_state(prop, **kwargs): {"comment": "reset", "prop": "_send_seq", "value": 0}, {"comment": "reset", "prop": "_recv_state", "value": 0}, {"comment": "reset", "prop": "_recv_seq", "value": 0}, - {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_recv_seq"}, + {"comment": "reset", "prop": "_ack_state", "value": 0}, + {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_ack_state"}, ] # We should send a RR in reply @@ -1606,8 +1608,9 @@ def _update_state(prop, **kwargs): {"comment": "reset", "prop": "_send_seq", "value": 0}, {"comment": "reset", "prop": "_recv_state", "value": 0}, {"comment": "reset", "prop": "_recv_seq", "value": 0}, + {"comment": "reset", "prop": "_ack_state", "value": 0}, # Peer ACK - {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_recv_seq"}, + {"comment": "ACKed by peer N(R)", "delta": 1, "prop": "_ack_state"}, # REJ handling {"comment": "from REJ N(R)", "prop": "_send_state", "value": 2}, ] @@ -2138,6 +2141,7 @@ def test_init_connection_mod8(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 + peer._ack_state = 5 peer._modulo = 6 peer._max_outstanding = 7 peer._IFrameClass = None @@ -2164,6 +2168,7 @@ def test_init_connection_mod8(): assert peer._send_seq == 0 assert peer._recv_state == 0 assert peer._recv_seq == 0 + assert peer._ack_state == 0 assert peer._pending_iframes == {} assert peer._pending_data == [] @@ -2187,6 +2192,7 @@ def test_init_connection_mod128(): peer._send_seq = 2 peer._recv_state = 3 peer._recv_seq = 4 + peer._ack_state = 5 peer._modulo = 6 peer._max_outstanding = 7 peer._IFrameClass = None @@ -2213,6 +2219,7 @@ def test_init_connection_mod128(): assert peer._send_seq == 0 assert peer._recv_state == 0 assert peer._recv_seq == 0 + assert peer._ack_state == 0 assert peer._pending_iframes == {} assert peer._pending_data == [] From bc8bee586245cd3e4f849890b36e3b49d4effbbe Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 11:57:37 +1000 Subject: [PATCH 200/207] tools.listen: Add basic tool for "listening" for a connection This runs a program and redirects stdin/stdout/stderr to the packet link. CR is translated to LF and back again. --- aioax25/tools/listen.py | 244 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 aioax25/tools/listen.py diff --git a/aioax25/tools/listen.py b/aioax25/tools/listen.py new file mode 100644 index 0000000..5a5c26f --- /dev/null +++ b/aioax25/tools/listen.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 + +""" +Very crude program for listening for an AX.25 connection, then launching a +program for the remote caller to interact with. e.g. to make a Python +interpreter available over the packet network (and open a remote code +execution hole in the process!), use something like: + +``` +$ python3 -m aioax25.tools.listen kiss-config.yml N0CALL-12 -- python -i +``` + +""" + +import asyncio +import argparse +import logging +import subprocess + +from yaml import safe_load + +# aioax25 imports +# from aioax25.kiss import … +# from aioax25.interface import … +# etc… if you're copying this for your own code +from ..kiss import make_device, KISSDeviceState +from ..interface import AX25Interface +from ..station import AX25Station +from ..peer import AX25PeerState +from ..version import AX25Version + + +class SubprocProtocol(asyncio.Protocol): + """ + SubprocProtocol manages the link to the sub-process on behalf of the peer + session. + """ + + def __init__(self, on_connect, on_receive, on_close, log): + super(SubprocProtocol, self).__init__() + + self._on_connect = on_connect + self._on_receive = on_receive + self._on_close = on_close + self._log = log + + def connection_made(self, transport): + try: + self._log.debug("Announcing connection: %r", transport) + self._on_connect(transport) + except Exception as e: + self._log.exception("Failed to handle connection establishment") + transport.close() + self._on_connect(None) + + def pipe_data_received(self, fd, data): + try: + if fd == 1: # stdout + self._on_receive(data) + else: + self._log.debug("Data received on fd=%d: %r", fd, data) + except: + self._log.exception( + "Failed to handle incoming data %r on fd=%d", data, fd + ) + + def pipe_connection_lost(self, fd, exc): + self._log.debug("FD %d closed (exc=%s)", fd, exc) + try: + self._on_close(exc) + except: + self._log.exception("Failed to handle process pipe close") + + def process_exited(self): + try: + self._on_close(None) + except: + self._log.exception("Failed to handle process exit") + + +class PeerSession(object): + def __init__(self, peer, command, log): + self._peer = peer + self._log = log + self._command = command + self._cmd_transport = None + + peer.received_information.connect(self._on_peer_received) + peer.connect_state_changed.connect(self._on_peer_state_change) + + async def init(self): + self._log.info("Launching sub-process") + await asyncio.get_event_loop().subprocess_exec( + self._make_protocol, *self._command, + stderr=subprocess.STDOUT + ) + + def _make_protocol(self): + """ + Return a SubprocessProtocol instance that will handle the KISS traffic for the + asyncio transport. + """ + + def _on_connect(transport): + self._log.info("Sub-process transport now open") + self._cmd_transport = transport + + return SubprocProtocol( + _on_connect, + self._on_subproc_received, + self._on_subproc_closed, + self._log.getChild("protocol"), + ) + + def _on_subproc_received(self, data): + """ + Pass data from the sub-process to the AX.25 peer. + """ + self._log.debug("Received from subprocess: %r", data) + if self._peer.state is AX25PeerState.CONNECTED: + # Peer still connected, pass to the peer, translating newline with + # CR as per AX.25 conventions. + data = b"\r".join(data.split(b"\n")) + self._log.debug("Writing to peer: %r", data) + self._peer.send(data) + elif self._peer.state is AX25PeerState.DISCONNECTED: + # Peer is not connected, close the subprocess. + self._log.info("Peer no longer connected, shutting down") + self._cmd_transport.close() + + def _on_subproc_closed(self, exc=None): + if exc is not None: + self._log.error("Closing port due to error %r", exc) + + self._log.info("Sub-process has exited") + self._cmd_transport = None + if self._peer.state is not AX25PeerState.DISCONNECTED: + self._log.info("Closing peer connection") + self._peer.disconnect() + + def _on_peer_received(self, payload, **kwargs): + """ + Pass data from the AX.25 peer to the sub-process. + """ + self._log.debug("Received from peer: %r", payload) + if self._cmd_transport: + payload = b"\n".join(payload.split(b"\r")) + self._log.debug("Writing to subprocess: %r", payload) + self._cmd_transport.get_pipe_transport(0).write(payload) + else: + # Subprocess no longer running, so shut it down. + self._log.info("Sub-process no longer running, disconnecting") + self._peer.disconnect() + + def _on_peer_state_change(self, state, **kwargs): + """ + Handle peer connection state change. + """ + if state is AX25PeerState.DISCONNECTED: + self._log.info("Peer has disconnected") + if self._cmd_transport: + self._cmd_transport.close() + + +class AX25Listen(object): + def __init__(self, source, command, kissparams, port=0): + log = logging.getLogger(self.__class__.__name__) + kisslog = log.getChild("kiss") + kisslog.setLevel(logging.INFO) # KISS logs are verbose! + intflog = log.getChild("interface") + intflog.setLevel(logging.INFO) # interface logs are verbose too! + stnlog = log.getChild("station") + + self._log = log + self._device = make_device(**kissparams, log=kisslog) + self._interface = AX25Interface(self._device[port], log=intflog) + self._station = AX25Station( + self._interface, + source, + log=stnlog, + ) + self._station.attach() + self._command = command + self._station.connection_request.connect(self._on_connection_request) + + async def listen(self): + # Open the KISS interface + self._device.open() + + # TODO: implement async functions on KISS device to avoid this! + while self._device.state != KISSDeviceState.OPEN: + await asyncio.sleep(0.1) + + self._log.info("Listening for connections") + while True: + await asyncio.sleep(1) + + def _on_connection_request(self, peer, **kwargs): + # Bounce to the I/O loop + asyncio.ensure_future(self._connect_peer(peer)) + + async def _connect_peer(self, peer): + self._log.info("Incoming connection from %s", peer.address) + try: + session = PeerSession(peer, self._command, self._log.getChild(str(peer.address))) + await session.init() + except: + self._log.exception("Failed to initialise peer connection") + peer.reject() + return + + # All good? Accept the connection. + peer.accept() + + +async def main(): + ap = argparse.ArgumentParser() + + ap.add_argument("--log-level", default="info", type=str, help="Log level") + ap.add_argument("--port", default=0, type=int, help="KISS port number") + ap.add_argument( + "config", type=str, help="KISS serial port configuration file" + ) + ap.add_argument("source", type=str, help="Source callsign/SSID") + ap.add_argument("command", type=str, nargs="+", + help="Program + args to run") + + args = ap.parse_args() + + logging.basicConfig( + level=args.log_level.upper(), + format=( + "%(asctime)s %(name)s[%(filename)s:%(lineno)4d] " + "%(levelname)s %(message)s" + ), + ) + config = safe_load(open(args.config, "r").read()) + + ax25listen = AX25Listen(args.source, args.command, config, args.port) + await ax25listen.listen() + + +if __name__ == "__main__": + asyncio.get_event_loop().run_until_complete(main()) From d50744d9ade4ab221fcb289f4c1e5b0ee895bc90 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 12:03:32 +1000 Subject: [PATCH 201/207] README.md update --- README.md | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3ef440c..ff0eb52 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,6 @@ The aim of this project is to implement a simple-to-understand asynchronous AX.25 library built on `asyncio` and `pyserial`, implementing a AX.25 and APRS stack in pure Python. -## Python 3.5+ and above is required as of 2021-11-12 - -I did try to support 3.4, but this proved to be infeasible for the following -reasons: - -* Python 3.8+ makes `asyncio.coroutine` deprecated (apparently will be removed - in 3.10). This meant I needed `coroutine` and `async def` versions of some - API functions, and the necessary logic to "hide" the latter from Python 3.4. -* Trying to coax generator-based coroutines to run "in the background" for unit - test purposes proved to be a pain in the arse. - -Python 3.5 support is planned to continue until it too, becomes infeasible -(e.g. if type annotations become required). - ## What works * We can put a Kantronics KPC-3 TNC into KISS mode automatically @@ -30,6 +16,7 @@ Python 3.5 support is planned to continue until it too, becomes infeasible * We can receive AX.25 UI frames * We can send AX.25 UI frames * Connecting to AX.25 nodes (*experimental*) +* Accepting connections from AX.25 nodes (*experimental*) ## What doesn't work @@ -38,7 +25,6 @@ Python 3.5 support is planned to continue until it too, becomes infeasible ## What isn't tested * Platforms other than GNU/Linux -* Accepting connections from AX.25 nodes ## Current plans @@ -47,10 +33,9 @@ focus on UI frames. We can send and receive APRS message frames, parse some kinds of position frames, and do basic UI frame stuff. Preliminary support for AX.25 connected mode is present, but is _experimental_. -It was tested connecting to a BPQ32 node, but not a lot of testing has been -done at this stage. **More feedback would be appreciated.** At this stage, -there is code for accepting a connection, but no testing has been done of this -code so there could be glaring bugs. +It was tested connecting to a BPQ32 node, and being connected to by a BPQ32 +node, but not a lot of testing has been done at this stage. **More feedback +would be appreciated.** The API is also very much a work-in-progress. After that, some things I'd like to tackle in no particular order: From ae18276a7e29870ab1061a6badd7724efafb85e8 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 12:58:12 +1000 Subject: [PATCH 202/207] peer: Assume modulo 8 if not set --- aioax25/peer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index bc8c132..892be6b 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1579,8 +1579,8 @@ def _update_state(self, prop, delta=None, value=None, comment=""): value += delta comment += " delta=%s" % delta - # Always apply modulo op - value %= self._modulo + # Always apply modulo op (assume 8 if not set) + value %= (self._modulo or 8) self._log.debug( "%s = %s" + comment, getattr(self, "%s_name" % prop), value From 1de9e4a09a17fa999bb193fd7af93271307e30af Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 12:59:11 +1000 Subject: [PATCH 203/207] tools.listen: Add line echo, tweak buffering --- aioax25/tools/listen.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/aioax25/tools/listen.py b/aioax25/tools/listen.py index 5a5c26f..ed66242 100644 --- a/aioax25/tools/listen.py +++ b/aioax25/tools/listen.py @@ -13,9 +13,9 @@ """ import asyncio +from asyncio import subprocess import argparse import logging -import subprocess from yaml import safe_load @@ -79,11 +79,12 @@ def process_exited(self): class PeerSession(object): - def __init__(self, peer, command, log): + def __init__(self, peer, command, echo, log): self._peer = peer self._log = log self._command = command self._cmd_transport = None + self._echo = echo peer.received_information.connect(self._on_peer_received) peer.connect_state_changed.connect(self._on_peer_state_change) @@ -92,7 +93,8 @@ async def init(self): self._log.info("Launching sub-process") await asyncio.get_event_loop().subprocess_exec( self._make_protocol, *self._command, - stderr=subprocess.STDOUT + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, bufsize=0 ) def _make_protocol(self): @@ -143,6 +145,10 @@ def _on_peer_received(self, payload, **kwargs): Pass data from the AX.25 peer to the sub-process. """ self._log.debug("Received from peer: %r", payload) + if self._echo: + # Echo back to peer + self._peer.send(payload) + if self._cmd_transport: payload = b"\n".join(payload.split(b"\r")) self._log.debug("Writing to subprocess: %r", payload) @@ -163,7 +169,7 @@ def _on_peer_state_change(self, state, **kwargs): class AX25Listen(object): - def __init__(self, source, command, kissparams, port=0): + def __init__(self, source, command, kissparams, port=0, echo=False): log = logging.getLogger(self.__class__.__name__) kisslog = log.getChild("kiss") kisslog.setLevel(logging.INFO) # KISS logs are verbose! @@ -182,6 +188,7 @@ def __init__(self, source, command, kissparams, port=0): self._station.attach() self._command = command self._station.connection_request.connect(self._on_connection_request) + self._echo = echo async def listen(self): # Open the KISS interface @@ -202,7 +209,7 @@ def _on_connection_request(self, peer, **kwargs): async def _connect_peer(self, peer): self._log.info("Incoming connection from %s", peer.address) try: - session = PeerSession(peer, self._command, self._log.getChild(str(peer.address))) + session = PeerSession(peer, self._command, self._echo, self._log.getChild(str(peer.address))) await session.init() except: self._log.exception("Failed to initialise peer connection") @@ -218,6 +225,8 @@ async def main(): ap.add_argument("--log-level", default="info", type=str, help="Log level") ap.add_argument("--port", default=0, type=int, help="KISS port number") + ap.add_argument("--echo", default=False, action="store_const", const=True, + help="Echo input back to the caller") ap.add_argument( "config", type=str, help="KISS serial port configuration file" ) @@ -236,7 +245,8 @@ async def main(): ) config = safe_load(open(args.config, "r").read()) - ax25listen = AX25Listen(args.source, args.command, config, args.port) + ax25listen = AX25Listen(args.source, args.command, config, args.port, + args.echo) await ax25listen.listen() From 4f25ce91b4eae5774a686cc6cf406b3bee73e36a Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 12:59:29 +1000 Subject: [PATCH 204/207] Apply `black` code formatting --- aioax25/peer.py | 2 +- aioax25/tools/listen.py | 32 +++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index 892be6b..b155982 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -1580,7 +1580,7 @@ def _update_state(self, prop, delta=None, value=None, comment=""): comment += " delta=%s" % delta # Always apply modulo op (assume 8 if not set) - value %= (self._modulo or 8) + value %= self._modulo or 8 self._log.debug( "%s = %s" + comment, getattr(self, "%s_name" % prop), value diff --git a/aioax25/tools/listen.py b/aioax25/tools/listen.py index ed66242..a68e9b7 100644 --- a/aioax25/tools/listen.py +++ b/aioax25/tools/listen.py @@ -92,9 +92,11 @@ def __init__(self, peer, command, echo, log): async def init(self): self._log.info("Launching sub-process") await asyncio.get_event_loop().subprocess_exec( - self._make_protocol, *self._command, + self._make_protocol, + *self._command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, bufsize=0 + stderr=subprocess.STDOUT, + bufsize=0 ) def _make_protocol(self): @@ -209,7 +211,12 @@ def _on_connection_request(self, peer, **kwargs): async def _connect_peer(self, peer): self._log.info("Incoming connection from %s", peer.address) try: - session = PeerSession(peer, self._command, self._echo, self._log.getChild(str(peer.address))) + session = PeerSession( + peer, + self._command, + self._echo, + self._log.getChild(str(peer.address)), + ) await session.init() except: self._log.exception("Failed to initialise peer connection") @@ -225,14 +232,20 @@ async def main(): ap.add_argument("--log-level", default="info", type=str, help="Log level") ap.add_argument("--port", default=0, type=int, help="KISS port number") - ap.add_argument("--echo", default=False, action="store_const", const=True, - help="Echo input back to the caller") + ap.add_argument( + "--echo", + default=False, + action="store_const", + const=True, + help="Echo input back to the caller", + ) ap.add_argument( "config", type=str, help="KISS serial port configuration file" ) ap.add_argument("source", type=str, help="Source callsign/SSID") - ap.add_argument("command", type=str, nargs="+", - help="Program + args to run") + ap.add_argument( + "command", type=str, nargs="+", help="Program + args to run" + ) args = ap.parse_args() @@ -245,8 +258,9 @@ async def main(): ) config = safe_load(open(args.config, "r").read()) - ax25listen = AX25Listen(args.source, args.command, config, args.port, - args.echo) + ax25listen = AX25Listen( + args.source, args.command, config, args.port, args.echo + ) await ax25listen.listen() From 2cc665a23c2dfa53dff369e7b9714ad7d7acbb57 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 14:11:09 +1000 Subject: [PATCH 205/207] peer unit tests: Test frame scheduling logic --- tests/test_peer/test_connection.py | 562 +++++++++++++++++++++++++++++ 1 file changed, 562 insertions(+) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 16a9fa4..6713209 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -1858,6 +1858,568 @@ def _on_receive_sabm(frame): assert frames == [frame] +# RR Notification transmission, scheduling and cancellation + + +def test_cancel_rr_notification_notpending(): + """ + Test _cancel_rr_notification does nothing if not pending. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + assert peer._rr_notification_timeout_handle is None + + peer._cancel_rr_notification() + + assert peer._rr_notification_timeout_handle is None + + +def test_cancel_rr_notification_ispending(): + """ + Test _cancel_rr_notification cancels a pending notification. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + timeout = DummyTimeout(0, lambda: None) + peer._rr_notification_timeout_handle = timeout + + peer._cancel_rr_notification() + + assert peer._rr_notification_timeout_handle is None + assert timeout.cancelled is True + + +def test_schedule_rr_notification(): + """ + Test _schedule_rr_notification schedules a notification. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._schedule_rr_notification() + + assert peer._rr_notification_timeout_handle is not None + + +def test_send_rr_notification_connected(): + """ + Test _send_rr_notification sends a notification if connected. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + peer._state = AX25PeerState.CONNECTED + + peer._send_rr_notification() + + assert count == dict(update_recv_seq=1) + assert len(transmitted) == 1 + assert isinstance(transmitted[0], AX258BitReceiveReadyFrame) + + +def test_send_rr_notification_disconnected(): + """ + Test _send_rr_notification sends a notification if connected. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + peer._state = AX25PeerState.DISCONNECTED + + peer._send_rr_notification() + + assert count == dict(update_recv_seq=0) + assert len(transmitted) == 0 + + +# RNR transmission + + +def test_send_rnr_notification_connected(): + """ + Test _send_rnr_notification sends a notification if connected. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + peer._state = AX25PeerState.CONNECTED + + peer._send_rnr_notification() + + assert count == dict(update_recv_seq=1) + assert len(transmitted) == 1 + assert isinstance(transmitted[0], AX258BitReceiveNotReadyFrame) + + +def test_send_rnr_notification_connected_recent(): + """ + Test _send_rnr_notification skips notification if the last was recent. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + peer._state = AX25PeerState.CONNECTED + peer._last_rnr_sent = peer._loop.time() - (peer._rnr_interval / 2) + + peer._send_rnr_notification() + + assert count == dict(update_recv_seq=0) + assert len(transmitted) == 0 + + +def test_send_rnr_notification_disconnected(): + """ + Test _send_rnr_notification sends a notification if connected. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + peer._state = AX25PeerState.DISCONNECTED + + peer._send_rnr_notification() + + assert count == dict(update_recv_seq=0) + assert len(transmitted) == 0 + + +# I-Frame transmission + + +def test_send_next_iframe_max_outstanding(): + """ + Test I-frame transmission is suppressed if too many frames are pending. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_send_seq=0, update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + def _update_send_seq(): + count["update_send_seq"] += 1 + + peer._update_send_seq = _update_send_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + state_updates = [] + + def _update_state(**kwargs): + state_updates.append(kwargs) + + peer._update_state = _update_state + + peer._state = AX25PeerState.CONNECTED + peer._pending_iframes = { + 0: (0xF0, b"Frame 1"), + 1: (0xF0, b"Frame 2"), + 2: (0xF0, b"Frame 3"), + 3: (0xF0, b"Frame 4"), + 4: (0xF0, b"Frame 5"), + 5: (0xF0, b"Frame 6"), + 6: (0xF0, b"Frame 7"), + 7: (0xF0, b"Frame 8"), + } + peer._max_outstanding = 8 + + peer._send_next_iframe() + + assert count == dict(update_send_seq=0, update_recv_seq=0) + assert state_updates == [] + assert transmitted == [] + + +def test_send_next_iframe_nothing_pending(): + """ + Test I-frame transmission is suppressed no data is pending. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_send_seq=0, update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + def _update_send_seq(): + count["update_send_seq"] += 1 + + peer._update_send_seq = _update_send_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + state_updates = [] + + def _update_state(**kwargs): + state_updates.append(kwargs) + + peer._update_state = _update_state + + peer._state = AX25PeerState.CONNECTED + peer._pending_iframes = { + 0: (0xF0, b"Frame 1"), + 1: (0xF0, b"Frame 2"), + 2: (0xF0, b"Frame 3"), + 3: (0xF0, b"Frame 4"), + } + peer._max_outstanding = 8 + peer._send_state = 4 + + peer._send_next_iframe() + + assert count == dict(update_send_seq=0, update_recv_seq=0) + assert state_updates == [] + assert transmitted == [] + + +def test_send_next_iframe_create_next(): + """ + Test I-frame transmission creates a new I-frame if there's data to send. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_send_seq=0, update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + def _update_send_seq(): + count["update_send_seq"] += 1 + + peer._update_send_seq = _update_send_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + state_updates = [] + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + peer._state = AX25PeerState.CONNECTED + peer._pending_iframes = { + 0: (0xF0, b"Frame 1"), + 1: (0xF0, b"Frame 2"), + 2: (0xF0, b"Frame 3"), + 3: (0xF0, b"Frame 4"), + } + peer._pending_data = [ + (0xF0, b"Frame 5"), + ] + peer._max_outstanding = 8 + peer._send_state = 4 + + peer._send_next_iframe() + + assert peer._pending_iframes == { + 0: (0xF0, b"Frame 1"), + 1: (0xF0, b"Frame 2"), + 2: (0xF0, b"Frame 3"), + 3: (0xF0, b"Frame 4"), + 4: (0xF0, b"Frame 5"), + } + assert peer._pending_data == [] + assert count == dict(update_send_seq=1, update_recv_seq=1) + assert state_updates == [ + dict(prop="_send_state", delta=1, comment="send next I-frame") + ] + assert transmitted[1:] == [] + frame = transmitted.pop(0) + assert isinstance(frame, AX258BitInformationFrame) + assert frame.payload == b"Frame 5" + + +def test_send_next_iframe_existing_next(): + """ + Test I-frame transmission sends existing next frame. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + peer._init_connection(False) + + count = dict(update_send_seq=0, update_recv_seq=0) + + def _update_recv_seq(): + count["update_recv_seq"] += 1 + + peer._update_recv_seq = _update_recv_seq + + def _update_send_seq(): + count["update_send_seq"] += 1 + + peer._update_send_seq = _update_send_seq + + transmitted = [] + + def _transmit_frame(frame): + transmitted.append(frame) + + peer._transmit_frame = _transmit_frame + + state_updates = [] + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + peer._state = AX25PeerState.CONNECTED + peer._pending_iframes = { + 0: (0xF0, b"Frame 1"), + 1: (0xF0, b"Frame 2"), + 2: (0xF0, b"Frame 3"), + 3: (0xF0, b"Frame 4"), + } + peer._pending_data = [ + (0xF0, b"Frame 5"), + ] + peer._max_outstanding = 8 + peer._send_state = 3 + + peer._send_next_iframe() + + assert peer._pending_iframes == { + 0: (0xF0, b"Frame 1"), + 1: (0xF0, b"Frame 2"), + 2: (0xF0, b"Frame 3"), + 3: (0xF0, b"Frame 4"), + } + assert peer._pending_data == [ + (0xF0, b"Frame 5"), + ] + assert count == dict(update_send_seq=1, update_recv_seq=1) + assert state_updates == [ + dict(prop="_send_state", delta=1, comment="send next I-frame") + ] + assert transmitted[1:] == [] + frame = transmitted.pop(0) + assert isinstance(frame, AX258BitInformationFrame) + assert frame.payload == b"Frame 4" + + +# Sequence number state updates + + +def test_update_send_seq(): + """ + Test _update_send_seq copies V(S) to N(S). + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + state_updates = [] + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + peer._send_seq = 2 + peer._send_state = 6 + + peer._update_send_seq() + assert state_updates == [ + dict(prop="_send_seq", value=6, comment="from V(S)") + ] + + +def test_update_recv_seq(): + """ + Test _update_recv_seq copies V(R) to N(R). + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path(), + ) + + state_updates = [] + + def _update_state(prop, **kwargs): + kwargs["prop"] = prop + state_updates.append(kwargs) + + peer._update_state = _update_state + + peer._recv_state = 6 + peer._recv_seq = 2 + + peer._update_recv_seq() + assert state_updates == [ + dict(prop="_recv_seq", value=6, comment="from V(R)") + ] + + # SABM(E) handling From 76f1aaea0fe21122b52b5cb2b12e1c6e8cf0c380 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 14:26:26 +1000 Subject: [PATCH 206/207] peer unit tests: Test ACK timer handling --- tests/test_peer/test_connection.py | 115 +++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 6713209..2dd9425 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -3008,3 +3008,118 @@ def _start_disconnect_ack_timer(): assert peer._state == AX25PeerState.DISCONNECTING assert actions == ["sent-disc", "start-ack-timer"] assert peer._uaframe_handler == peer._on_disconnect + + +# ACK timer handling + + +def test_start_connect_ack_timer(): + """ + Test _start_connect_ack_timer schedules _on_incoming_connect_timeout + to fire after _ack_timeout. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + count = dict(on_incoming_connect_timeout=0, on_disc_ua_timeout=0) + + def _on_incoming_connect_timeout(): + count["on_incoming_connect_timeout"] += 1 + + peer._on_incoming_connect_timeout = _on_incoming_connect_timeout + + def _on_disc_ua_timeout(): + count["on_disc_ua_timeout"] += 1 + + peer._on_disc_ua_timeout = _on_disc_ua_timeout + + assert peer._ack_timeout_handle is None + + peer._start_connect_ack_timer() + + assert peer._ack_timeout_handle is not None + assert peer._ack_timeout_handle.delay == peer._ack_timeout + + assert count == dict(on_incoming_connect_timeout=0, on_disc_ua_timeout=0) + peer._ack_timeout_handle.callback() + assert count == dict(on_incoming_connect_timeout=1, on_disc_ua_timeout=0) + + +def test_start_disconnect_ack_timer(): + """ + Test _start_disconnect_ack_timer schedules _on_disc_ua_timeout + to fire after _ack_timeout. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + count = dict(on_incoming_connect_timeout=0, on_disc_ua_timeout=0) + + def _on_incoming_connect_timeout(): + count["on_incoming_connect_timeout"] += 1 + + peer._on_incoming_connect_timeout = _on_incoming_connect_timeout + + def _on_disc_ua_timeout(): + count["on_disc_ua_timeout"] += 1 + + peer._on_disc_ua_timeout = _on_disc_ua_timeout + + assert peer._ack_timeout_handle is None + + peer._start_disconnect_ack_timer() + + assert peer._ack_timeout_handle is not None + assert peer._ack_timeout_handle.delay == peer._ack_timeout + + assert count == dict(on_incoming_connect_timeout=0, on_disc_ua_timeout=0) + peer._ack_timeout_handle.callback() + assert count == dict(on_incoming_connect_timeout=0, on_disc_ua_timeout=1) + + +def test_stop_ack_timer_existing(): + """ + Test _stop_ack_timer cancels the existing time-out. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + timeout = DummyTimeout(None, None) + peer._ack_timeout_handle = timeout + + peer._stop_ack_timer() + + assert peer._ack_timeout_handle is None + assert timeout.cancelled is True + + +def test_stop_ack_timer_notexisting(): + """ + Test _stop_ack_timer does nothing if no time-out pending. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + peer._ack_timeout_handle = None + + peer._stop_ack_timer() From 3152c52c43ef811b808b9a50e2e74ac92b92c5a0 Mon Sep 17 00:00:00 2001 From: Stuart Longland Date: Wed, 8 May 2024 15:12:04 +1000 Subject: [PATCH 207/207] peer unit tests: Finish off unit tests --- aioax25/peer.py | 7 +- tests/test_peer/test_connection.py | 339 +++++++++++++++++++++++++++++ 2 files changed, 345 insertions(+), 1 deletion(-) diff --git a/aioax25/peer.py b/aioax25/peer.py index b155982..a084e7d 100644 --- a/aioax25/peer.py +++ b/aioax25/peer.py @@ -928,10 +928,15 @@ def _on_negotiate_result(self, response, **kwargs): self._process_xid_winszrx(AX25_20_DEFAULT_XID_WINDOWSZRX) self._process_xid_acktimer(AX25_20_DEFAULT_XID_ACKTIMER) self._process_xid_retrycounter(AX25_20_DEFAULT_XID_RETRIES) + + # Downgrade 2.2 to 2.0, do not unwittingly "upgrade" 1.0 to 2.0! if self._protocol in (AX25Version.UNKNOWN, AX25Version.AX25_22): self._log.info("Downgrading to AX.25 2.0 due to failed XID") self._protocol = AX25Version.AX25_20 - self._modulo128 = False + + # AX.25 2.2 is required for Mod128, so if we get FRMR here, + # disable this unconditionally. + self._modulo128 = False elif self._protocol != AX25Version.AX25_22: # Clearly this station understands AX.25 2.2 self._log.info("Upgrading to AX.25 2.2 due to successful XID") diff --git a/tests/test_peer/test_connection.py b/tests/test_peer/test_connection.py index 2dd9425..cde9769 100644 --- a/tests/test_peer/test_connection.py +++ b/tests/test_peer/test_connection.py @@ -31,6 +31,9 @@ from aioax25.peer import AX25PeerState from .peer import TestingAX25Peer from ..mocks import DummyStation, DummyTimeout +from functools import partial + +from pytest import mark # Connection establishment @@ -98,6 +101,100 @@ def _negotiate(*args, **kwargs): pass +def test_on_incoming_connect_timeout_incoming(): + """ + Test if the application does not accept within the time-out, we reject the + connection. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) + + count = dict(reject=0) + + def _reject(): + count["reject"] += 1 + + peer.reject = _reject + + peer._state = AX25PeerState.INCOMING_CONNECTION + peer._ack_timeout_handle = DummyTimeout(None, None) + + peer._on_incoming_connect_timeout() + + assert peer._ack_timeout_handle is None + assert count == dict(reject=1) + + +def test_on_incoming_connect_timeout_otherstate(): + """ + Test if the incoming connection time-out fires whilst in another state, it + is ignored + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) + + count = dict(reject=0) + + def _reject(): + count["reject"] += 1 + + peer.reject = _reject + + peer._state = AX25PeerState.CONNECTED + peer._ack_timeout_handle = DummyTimeout(None, None) + + peer._on_incoming_connect_timeout() + + assert peer._ack_timeout_handle is not None + assert count == dict(reject=0) + + +def test_on_connect_response_ack(): + """ + Test if _on_connect_response receives an ACK, we enter the connected + state. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) + + peer._state = AX25PeerState.CONNECTING + + peer._on_connect_response(response="ack") + + assert peer._state == AX25PeerState.CONNECTED + + +def test_on_connect_response_other(): + """ + Test if _on_connect_response receives another response, we enter the + disconnected state. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + ) + + peer._state = AX25PeerState.CONNECTING + + peer._on_connect_response(response="nope") + + assert peer._state == AX25PeerState.DISCONNECTED + + # SABM(E) transmission @@ -3123,3 +3220,245 @@ def test_stop_ack_timer_notexisting(): peer._ack_timeout_handle = None peer._stop_ack_timer() + + +# AX.25 2.2 XID negotiation + + +@mark.parametrize("version", [AX25Version.AX25_10, AX25Version.AX25_20]) +def test_negotiate_notsupported(version): + """ + Test the peer refuses to perform XID if the protocol does not support it. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + peer._state = AX25PeerState.CONNECTING + peer._protocol = version + + try: + peer._negotiate(lambda **kwa: None) + assert False, "Should not have worked" + except RuntimeError as e: + assert str(e) == "%s does not support negotiation" % (version.value) + + +@mark.parametrize("version", [AX25Version.AX25_22, AX25Version.UNKNOWN]) +def test_negotiate_supported(version): + """ + Test the peer refuses to perform XID if the protocol does not support it. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub XID transmission + count = dict(send_xid=0) + + def _send_xid(cr): + count["send_xid"] += 1 + + peer._send_xid = _send_xid + + peer._state = AX25PeerState.CONNECTING + peer._protocol = version + + peer._negotiate(lambda **kwa: None) + + # Check we actually did request a XID transmission + assert count == dict(send_xid=1) + + # Trigger the DM callback to abort time-outs + assert peer._dmframe_handler is not None + peer._dmframe_handler() + + +@mark.parametrize( + "version, response", + [ + (axver, res) + for axver in (AX25Version.AX25_22, AX25Version.UNKNOWN) + for res in ("frmr", "dm") + ], +) +def test_on_negotiate_result_unsupported(version, response): + """ + Test we handle a response that indicates an AX.25 2.0 or earlier station. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub XID functions + xid_params = set() + + def _set_xid_param(param, value): + xid_params.add(param) + + for param in ( + "cop", + "hdlcoptfunc", + "ifieldlenrx", + "winszrx", + "acktimer", + "retrycounter", + ): + setattr( + peer, "_process_xid_%s" % param, partial(_set_xid_param, param) + ) + + assert peer._negotiated == False + peer._modulo128 = True + peer._protocol = version + + peer._on_negotiate_result(response=response) + + # Should downgrade to AX.25 2.0 + assert peer._negotiated is True + assert peer._modulo128 is False + assert peer._protocol is AX25Version.AX25_20 + + # Should have inserted defaults for AX.25 2.0 + assert xid_params == set( + [ + "acktimer", + "cop", + "hdlcoptfunc", + "ifieldlenrx", + "retrycounter", + "winszrx", + ] + ) + + +@mark.parametrize( + "version, response", + [ + (axver, res) + for axver in (AX25Version.AX25_20, AX25Version.AX25_10) + for res in ("frmr", "dm") + ], +) +def test_on_negotiate_result_unsupported_old(version, response): + """ + Test we do not accidentally "upgrade" on FRMR/DM in response to XID. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub XID functions + xid_params = set() + + def _set_xid_param(param, value): + xid_params.add(param) + + for param in ( + "cop", + "hdlcoptfunc", + "ifieldlenrx", + "winszrx", + "acktimer", + "retrycounter", + ): + setattr( + peer, "_process_xid_%s" % param, partial(_set_xid_param, param) + ) + + assert peer._negotiated == False + peer._modulo128 = True + peer._protocol = version + + peer._on_negotiate_result(response=response) + + # Should leave this as is! + assert peer._protocol is version + + # Should disable AX.25 2.2 features + assert peer._negotiated is True + assert peer._modulo128 is False + + # Should have inserted defaults for AX.25 2.0 + assert xid_params == set( + [ + "acktimer", + "cop", + "hdlcoptfunc", + "ifieldlenrx", + "retrycounter", + "winszrx", + ] + ) + + +@mark.parametrize( + "version", + [ + AX25Version.UNKNOWN, + AX25Version.AX25_22, + AX25Version.AX25_20, + AX25Version.AX25_10, + ], +) +def test_on_negotiate_result_success(version): + """ + Test we upgrade to AX.25 2.2 if XID successful. + """ + station = DummyStation(AX25Address("VK4MSL", ssid=1)) + peer = TestingAX25Peer( + station=station, + address=AX25Address("VK4MSL"), + repeaters=AX25Path("VK4RZB"), + locked_path=True, + ) + + # Stub XID functions + xid_params = set() + + def _set_xid_param(param, value): + xid_params.add(param) + + for param in ( + "cop", + "hdlcoptfunc", + "ifieldlenrx", + "winszrx", + "acktimer", + "retrycounter", + ): + setattr( + peer, "_process_xid_%s" % param, partial(_set_xid_param, param) + ) + + assert peer._negotiated == False + peer._modulo128 = True + peer._protocol = version + + peer._on_negotiate_result(response="success") + + # Should bump to AX.25 2.2 + assert peer._protocol is AX25Version.AX25_22 + + # Should leave AX.25 2.2 features enabled + assert peer._negotiated is True + assert peer._modulo128 is True + + # Should not override XID parameters set by handler + assert xid_params == set([])