mirror of
https://github.com/andwati/andwati.com.git
synced 2026-11-07 01:42:14 +00:00
yolo
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 845 KiB |
@@ -0,0 +1,608 @@
|
||||
+++
|
||||
title = "node.example.com Is An IP Address"
|
||||
description = "node.example.com is an IP address."
|
||||
url = "posts/node-example-com-is-an-ip-address"
|
||||
date = 2020-12-28T06:13:05-08:00
|
||||
[taxonomies]
|
||||
tags = ["Python", "Testing", "Networking"]
|
||||
+++
|
||||
Hello! Welcome to the once-yearly blog post! This year I'd like to examine the
|
||||
most peculiar bug I encountered at work. To set the stage, let's start with a
|
||||
little background.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
When we write [URLs][URL] with a [non-standard][standard ports] [port] we
|
||||
specify the port after a `:`. With [hostnames][hostname] and [IPv4] addresses
|
||||
this is straightforward. Here's some [Python] code to show how easy it is.
|
||||
|
||||
```python
|
||||
>>> url = urllib.parse.urlparse("https://node.example.com:8000")
|
||||
>>> (url.hostname, url.port)
|
||||
('node.example.com', 8000)
|
||||
>>>
|
||||
>>> url = urllib.parse.urlparse("https://192.168.0.1:8000")
|
||||
>>> (url.hostname, url.port)
|
||||
('192.168.0.1', 8000)
|
||||
```
|
||||
|
||||
Unfortunately, when [IPv6] addresses are involved some ambiguity is introduced.
|
||||
|
||||
```python
|
||||
>>> url = urllib.parse.urlparse(
|
||||
... "https://fdc8:bf8b:e62c:abcd:1111:2222:3333:4444:8000"
|
||||
... )
|
||||
...
|
||||
>>> url.hostname
|
||||
'fdc8'
|
||||
>>> try:
|
||||
... url.port
|
||||
... except ValueError as error:
|
||||
... print(error)
|
||||
...
|
||||
Port could not be cast to integer value as 'bf8b:e62c:abcd:1111:2222:3333:4444:8000'
|
||||
```
|
||||
|
||||
Since IPv6 addresses use a "colon-hex" format with [hexadecimal] fields
|
||||
separated by `:` we can't tell a port apart from a normal field. Notice in the
|
||||
example above that the hostname is truncated after the first `:`, not the one
|
||||
just before `8000`.
|
||||
|
||||
Fortunately, the spec for URLs recognizes this ambiguity and gives us a way to
|
||||
handle it. [RFC 2732 (_Format for Literal IPv6 Addresses in URL's_)][RFC 2732]
|
||||
says
|
||||
|
||||
> To use a literal IPv6 address in a URL, the literal address should be
|
||||
> enclosed in "[" and "]" characters.
|
||||
|
||||
Update our example above to include `[` and `]` and voilà! It just works.
|
||||
|
||||
```python
|
||||
>>> url = urllib.parse.urlparse(
|
||||
... "https://[fdc8:bf8b:e62c:abcd:1111:2222:3333:4444]:8000"
|
||||
... )
|
||||
...
|
||||
>>> (url.hostname, url.port)
|
||||
('fdc8:bf8b:e62c:abcd:1111:2222:3333:4444', 8000)
|
||||
```
|
||||
|
||||
Armed with that knowledge we can dive into the problem. 🤿
|
||||
|
||||
## Works On My Machine
|
||||
|
||||
A few months ago a co-worker of mine wrote a seemingly innocuous function.
|
||||
|
||||
```python
|
||||
from ipaddress import ip_address
|
||||
|
||||
|
||||
def safe_host(host):
|
||||
"""Surround `host` with brackets if it is an IPv6 address."""
|
||||
try:
|
||||
if ip_address(host).version == 6:
|
||||
return "[{}]".format(host)
|
||||
except ValueError:
|
||||
pass
|
||||
return host
|
||||
```
|
||||
|
||||
Elsewhere in the code it was invoked something like this, so that hostnames,
|
||||
IPv4 addresses, and IPv6 addresses could all be safely interpolated.
|
||||
|
||||
```python
|
||||
url = "https://{host}:8000/some/path/".format(host=safe_host(host))
|
||||
```
|
||||
|
||||
Since my co-worker is awesome they wrote tests to validate their code. ✅
|
||||
|
||||
```python
|
||||
def test_safe_host_with_hostname():
|
||||
"""Hostnames should be unchanged."""
|
||||
assert safe_host("node.example.com") == "node.example.com"
|
||||
|
||||
|
||||
def test_safe_host_with_ipv4_address():
|
||||
"""IPv4 addresses should be unchanged."""
|
||||
assert safe_host("192.168.0.1") == "192.168.0.1"
|
||||
|
||||
|
||||
def test_safe_host_with_ipv6_address():
|
||||
"""IPv6 addresses should be surrounded by brackets."""
|
||||
assert (
|
||||
safe_host("fdc8:bf8b:e62c:abcd:1111:2222:3333:4444")
|
||||
== "[fdc8:bf8b:e62c:abcd:1111:2222:3333:4444]"
|
||||
)
|
||||
```
|
||||
|
||||
Thank goodness they did. The Python 2 tests failed ([don't look at me like
|
||||
that][drop-python2] 😒).
|
||||
|
||||
```ini
|
||||
✖ FAIL py27 in 1.83 seconds
|
||||
✔ OK py36 in 2.82 seconds
|
||||
✔ OK py37 in 2.621 seconds
|
||||
✔ OK py38 in 2.524 seconds
|
||||
✔ OK py39 in 2.461 seconds
|
||||
```
|
||||
|
||||
Both the hostname and IPv6 address tests failed. But _**why**_ did they fail?
|
||||
And why did the Python 3 tests pass? 🤔
|
||||
|
||||
We'll start with the hostname failure and try to isolate the bug.
|
||||
|
||||
```python
|
||||
E AssertionError: assert '[node.example.com]' == 'node.example.com'
|
||||
E - [node.example.com]
|
||||
E ? - -
|
||||
E + node.example.com
|
||||
```
|
||||
|
||||
The failure says `node.example.com` was surrounded by brackets, but that's
|
||||
only supposed to happen for IPv6 addresses! Let's crack open a Python 2
|
||||
interpreter for a quick sanity check.
|
||||
|
||||
```python
|
||||
>>> ipaddress.ip_address("node.example.com").version
|
||||
6
|
||||
```
|
||||
|
||||
<img src='confused-jeff-bridges.webp' alt='Confused Jeff Bridges' style='display: block; width: 100%;'>
|
||||
|
||||
## What On Htrae?
|
||||
|
||||
If, like Jeff Bridges, you were confused by that result, _relax_. We're
|
||||
probably not in a [Bizarro World] where `node.example.com` is a valid IPv6
|
||||
address. There must be an explanation for this behavior.
|
||||
|
||||
Things start to become a little more clear when we see the result of the
|
||||
[`ip_address()`][ip_address] function for ourselves.
|
||||
|
||||
```python
|
||||
>>> ipaddress.ip_address("node.example.com")
|
||||
IPv6Address(u'6e6f:6465:2e65:7861:6d70:6c65:2e63:6f6d')
|
||||
```
|
||||
|
||||
At first glance that looks like madness. Python 3 behaves in an entirely
|
||||
different manner.
|
||||
|
||||
```python
|
||||
>>> try:
|
||||
... ipaddress.ip_address("node.example.com")
|
||||
... except ValueError as error:
|
||||
... print(error)
|
||||
...
|
||||
'node.example.com' does not appear to be an IPv4 or IPv6 address
|
||||
```
|
||||
|
||||
Python 3 knows that's not an IPv6 address, so why doesn't Python 2? The answer
|
||||
is in how differently the two Python versions handle text.
|
||||
|
||||
## Text Is Hard
|
||||
|
||||
Computers don't operate on text as humans think of it. They operate on numbers.
|
||||
That's part of why we have IP addresses to begin with. In order to represent
|
||||
human-readable text with computers we had to assign meaning to the numbers.
|
||||
Thus, [ASCII] was born.
|
||||
|
||||
ASCII is a [character encoding], which means it specifies how to interpret
|
||||
[bytes][byte] as text we understand (provided you speak English). So, when your
|
||||
computer sees `01101110` in [binary] (`110` in [decimal]) you see `n` because
|
||||
that's what ASCII says it is.
|
||||
|
||||
You can see the number to text conversion in action right in the Python
|
||||
interpreter.
|
||||
|
||||
```python
|
||||
>>> ord("n")
|
||||
110
|
||||
>>> chr(110)
|
||||
'n'
|
||||
```
|
||||
|
||||
In fact, it doesn't matter what numbering system you use. If you specify
|
||||
binary, [octal], decimal, hexadecimal, whatever... If it can be understood as
|
||||
the right integer it will be displayed correctly.
|
||||
|
||||
```python
|
||||
>>> chr(0b01101110)
|
||||
'n'
|
||||
>>> chr(0o156)
|
||||
'n'
|
||||
>>> chr(110)
|
||||
'n'
|
||||
>>> chr(0x6e)
|
||||
'n'
|
||||
```
|
||||
|
||||
Neat, but what does that information do for us?
|
||||
|
||||
## It's Numbers All The Way Down
|
||||
|
||||
Just for giggles, humor me and let's look at the character-number translations
|
||||
for `node.example.com`. We'll leave out binary and octal, because they make
|
||||
this table uglier than it already is.
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">Character</th>
|
||||
<td>n</td>
|
||||
<td>o</td>
|
||||
<td>d</td>
|
||||
<td>e</td>
|
||||
<td>.</td>
|
||||
<td>e</td>
|
||||
<td>x</td>
|
||||
<td>a</td>
|
||||
<td>m</td>
|
||||
<td>p</td>
|
||||
<td>l</td>
|
||||
<td>e</td>
|
||||
<td>.</td>
|
||||
<td>c</td>
|
||||
<td>o</td>
|
||||
<td>m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Decimal</th>
|
||||
<td>110</td>
|
||||
<td>111</td>
|
||||
<td>100</td>
|
||||
<td>101</td>
|
||||
<td>46</td>
|
||||
<td>101</td>
|
||||
<td>120</td>
|
||||
<td>97</td>
|
||||
<td>109</td>
|
||||
<td>112</td>
|
||||
<td>108</td>
|
||||
<td>101</td>
|
||||
<td>46</td>
|
||||
<td>99</td>
|
||||
<td>111</td>
|
||||
<td>109</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Hexadecimal</th>
|
||||
<td>6e</td>
|
||||
<td>6f</td>
|
||||
<td>64</td>
|
||||
<td>65</td>
|
||||
<td>2e</td>
|
||||
<td>65</td>
|
||||
<td>78</td>
|
||||
<td>61</td>
|
||||
<td>6d</td>
|
||||
<td>70</td>
|
||||
<td>6c</td>
|
||||
<td>65</td>
|
||||
<td>2e</td>
|
||||
<td>63</td>
|
||||
<td>6f</td>
|
||||
<td>6d</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Hey, hold on a second... If you tilt your head sideways and squint that last
|
||||
row looks kinda like an IPv6 address, doesn't it?
|
||||
|
||||
We should verify, just to be absolutely certain. You've still got that Python 2
|
||||
interpreter open, right?
|
||||
|
||||
```python
|
||||
>>> # Convert the characters in the hostname to hexadecimal.
|
||||
>>> hostname = "node.example.com"
|
||||
>>> hostname_as_hexadecimal = "".join(hex(ord(c))[2:] for c in hostname)
|
||||
>>> hostname_as_hexadecimal
|
||||
'6e6f64652e6578616d706c652e636f6d'
|
||||
>>>
|
||||
>>> # Convert the "IP address" to text.
|
||||
>>> address = ipaddress.ip_address(hostname)
|
||||
>>> str(address)
|
||||
'6e6f:6465:2e65:7861:6d70:6c65:2e63:6f6d'
|
||||
>>>
|
||||
>>> # Remove the colons from that text.
|
||||
>>> address_without_colons = str(address).replace(":", "")
|
||||
>>> address_without_colons
|
||||
'6e6f64652e6578616d706c652e636f6d'
|
||||
>>>
|
||||
>>> # Compare the results and see they're equal.
|
||||
>>> hostname_as_hexadecimal == address_without_colons
|
||||
True
|
||||
```
|
||||
|
||||
Sure enough, when you boil them both down to numbers they're the same mess of
|
||||
hexadecimal.
|
||||
|
||||
## The Belly Of The Beast
|
||||
|
||||
If we dig into the source code for the Python 2 version of the
|
||||
[`ipaddress`][ipaddress2] module we ultimately come to a
|
||||
[curious set of lines].
|
||||
|
||||
```python
|
||||
# Constructing from a packed address
|
||||
if isinstance(address, bytes):
|
||||
self._check_packed_address(address, 16)
|
||||
bvs = _compat_bytes_to_byte_vals(address)
|
||||
self._ip = _compat_int_from_byte_vals(bvs, 'big')
|
||||
return
|
||||
```
|
||||
|
||||
It turns out that, under certain conditions, the `ipaddress` module can create
|
||||
IPv6 addresses from raw bytes. My assumption is that it offers this behavior as
|
||||
a convenient way to parse IP addresses from data fresh off the [wire].
|
||||
|
||||
Does `node.example.com` meet those certain conditions? You bet it does. Because
|
||||
we're using Python 2 it's just `bytes` and it happens to be 16 characters long.
|
||||
|
||||
```python
|
||||
>>> isinstance("node.example.com", bytes)
|
||||
True
|
||||
>>> # `self._check_packed_address` basically just checks how long it is.
|
||||
>>> len("node.example.com") == 16
|
||||
True
|
||||
```
|
||||
|
||||
The rest of the `ipaddress` lines say to interpret the sequence of bytes as a
|
||||
[big-endian][endianness] integer. That's [magic][struct unpacking] best left
|
||||
for another blog post, but the gist is that hexadecimal interpretation of
|
||||
`node.example.com` is condensed into a single, **huge** number.
|
||||
|
||||
```python
|
||||
>>> int("6e6f64652e6578616d706c652e636f6d", 16)
|
||||
146793460745001871434687145741037825901L
|
||||
```
|
||||
|
||||
That's an absolutely massive number, but not so massive it won't fit within the
|
||||
[IPv6 address space].
|
||||
|
||||
```python
|
||||
>>> ip_address(146793460745001871434687145741037825901L)
|
||||
IPv6Address(u'6e6f:6465:2e65:7861:6d70:6c65:2e63:6f6d')
|
||||
```
|
||||
|
||||
As it turns out, if you're liberal in your interpretation, `node.example.com`
|
||||
_can_ be an IPv6 address!
|
||||
|
||||
## You Will Be Reading Meanings
|
||||
|
||||
Obviously that's hogwash. Bizarro might be proud, but that's not what we wanted
|
||||
to happen.
|
||||
|
||||
There's a quote about numbers which is apocryphally attributed to [W.E.B. Du
|
||||
Bois], but that actually comes from [Harold Geneen]'s book,
|
||||
[_Managing_][Managing].
|
||||
|
||||
> When you have mastered the numbers, you will in fact no longer be reading
|
||||
> numbers, any more than you read words when reading a book. You will be
|
||||
> reading meanings.
|
||||
|
||||
Having not read the book I'm probably taking the quote way out of context, but
|
||||
I think it fits our situation well.
|
||||
|
||||
As we've seen above, we can freely convert characters to numbers and back
|
||||
again. The root of our problem is that when we use Python 2 it considers text
|
||||
to be bytes. There's not a deeper, inherent meaning. Maybe the bytes are meant
|
||||
to be ASCII, maybe they're meant to be a long number, maybe they're meant to be
|
||||
an IP address. The interpretation of those bytes is up to us.
|
||||
|
||||
Python 2 doesn't differentiate between bytes and text by default. In fact, the
|
||||
`bytes` type is just an [alias][bytes-str-alias] for `str`.
|
||||
|
||||
```python
|
||||
>>> bytes
|
||||
<type 'str'>
|
||||
>>> bytes is str
|
||||
True
|
||||
```
|
||||
|
||||
To make that even more concrete, see how Python 2 considers `n` to be the same
|
||||
as this sequence of raw bytes.
|
||||
|
||||
```python
|
||||
>>> "n" == b"\x6e"
|
||||
True
|
||||
```
|
||||
|
||||
Our Python 2 code doesn't work the way we want it to because raw bytes can have
|
||||
arbitrary meaning and we haven't told it to use our intended meaning.
|
||||
|
||||
So now we know why Python 2 interprets `node.example.com` as an IPv6 address,
|
||||
but why does Python 3 behave differently? More importantly, how can we
|
||||
reconcile the two?
|
||||
|
||||
|
||||
## 256 Characters Ought To Be Enough For Anybody
|
||||
|
||||
ASCII looked like a good idea in the 1960's. With decades of hindsight we
|
||||
know the 256 characters afforded to us by [Extended ASCII] are insufficient to
|
||||
handle all of the world's writing systems. Thus, [Unicode] was born.
|
||||
|
||||
There are scads of blog posts, Wikipedia articles, and technical documents that
|
||||
will do a better job than I can of explaining Unicode in detail. You should
|
||||
read them if you care to, but here's my gist.
|
||||
|
||||
Unicode is a set of character encodings. [UTF-8] is the dominant encoding.
|
||||
UTF-8 overlaps with ASCII, so ASCII characters are still just one byte. To
|
||||
handle the multitude of other characters, however, multiple bytes can express a
|
||||
single character.
|
||||
|
||||
```python
|
||||
>>> "n".encode("utf-8").hex() # 1 character (U+006E), 1 byte.
|
||||
'6e'
|
||||
>>> "🤿".encode("utf-8").hex() # 1 character (U+1F93F), 4 bytes.
|
||||
'f09fa4bf'
|
||||
>>> "悟り".encode("utf-8").hex() # 2 characters (U+609F, U+308A), 6 bytes.
|
||||
'e6829fe3828a'
|
||||
```
|
||||
|
||||
Every programming language I know of that respects the difference between
|
||||
raw bytes and Unicode text maintains a strict separation between the two
|
||||
datatypes.
|
||||
|
||||
In Python 3 this strict separation is enabled by default. Notice that it
|
||||
doesn't consider `n` and this sequence of raw bytes to be the same thing.
|
||||
|
||||
```python
|
||||
>>> "n" == b"\x6e"
|
||||
False
|
||||
```
|
||||
|
||||
Even better, it doesn't consider `str` and `bytes` to be the same type.
|
||||
|
||||
```python
|
||||
>>> bytes is str
|
||||
False
|
||||
>>> bytes
|
||||
<class 'bytes'>
|
||||
```
|
||||
|
||||
If we can get Python 2 to understand Unicode like Python 3 does, then we can
|
||||
probably fix our bug.
|
||||
|
||||
As an aside, if you want to learn more about how to handle Unicode in Python,
|
||||
check out [Ned Batchelder]'s talk on [_Pragmatic Unicode_][unipain].
|
||||
|
||||
## How Did We Fix It?
|
||||
|
||||
Python 2 does actually know about Unicode, but it considers Unicode text to
|
||||
be separate from "normal" text. At some point in Python 2 history the
|
||||
[`unicode`][python2-unicode] type was bolted onto the side of the language and
|
||||
not enabled by default. Hard to get excited about it, but it does the trick.
|
||||
At least they knew it's a pain to type `unicode()` all the time, so there's a
|
||||
handy literal syntax using a `u` prefix.
|
||||
|
||||
```python
|
||||
>>> unicode("node.example.com") == u"node.example.com"
|
||||
True
|
||||
```
|
||||
|
||||
This is _not_ the best fix, but it did in a pinch. We added a line converting
|
||||
the hostname to Unicode right off the bat. We also applied the same
|
||||
transformation to the line with brackets. This way we always process the
|
||||
hostname as Unicode and we always return a Unicode value.
|
||||
|
||||
```diff
|
||||
def safe_host(host):
|
||||
"""Surround `host` with brackets if it is an IPv6 address."""
|
||||
+ host = u"{}".format(host)
|
||||
try:
|
||||
if ip_address(host).version == 6:
|
||||
- return "[{}]".format(host)
|
||||
+ return u"[{}]".format(host)
|
||||
except ValueError:
|
||||
pass
|
||||
```
|
||||
|
||||
Luckily for us the `u` prefix also works in Python 3 whereas `unicode()` does not
|
||||
(because [all text is Unicode by default][python3-str-type], so the type has no
|
||||
business existing). In Python 3 the `u` is treated as a [no-op].
|
||||
|
||||
The Python 2 interpreter graciously understands the `unicode` type is not just
|
||||
raw `bytes`.
|
||||
|
||||
```python
|
||||
>>> isinstance(u"node.example.com", bytes)
|
||||
False
|
||||
```
|
||||
|
||||
When we use the `unicode` type the `ipaddress` module no longer tries to
|
||||
interpret `node.example.com` as `bytes` and convert those bytes to an IP
|
||||
address. We get just what we expect
|
||||
|
||||
```python
|
||||
>>> try:
|
||||
... ipaddress.ip_address(u"node.example.com")
|
||||
... except ValueError as error:
|
||||
... print(error)
|
||||
...
|
||||
u'node.example.com' does not appear to be an IPv4 or IPv6 address
|
||||
```
|
||||
|
||||
and our tests pass!
|
||||
|
||||
```ini
|
||||
✔ OK py27 in 1.728 seconds
|
||||
✔ OK py36 in 2.775 seconds
|
||||
✔ OK py37 in 2.717 seconds
|
||||
✔ OK py38 in 2.674 seconds
|
||||
✔ OK py39 in 2.506 seconds
|
||||
```
|
||||
|
||||
## Reflection
|
||||
|
||||
I mentioned above that our fix wasn't the best. Given more time, how can we do
|
||||
better?
|
||||
|
||||
<p id='drop-python2'>The first (and best) solution here is to
|
||||
<a href='https://python3statement.org/'>drop Python 2 support</a>. It's 2020
|
||||
now and Python 2 is officially no longer supported. The original code worked on
|
||||
Python 3. The best long-term decision is to migrate the code to run on Python 3
|
||||
<b>only</b> and avoid the hassle of Python 2 maintenance. Unfortunately many of
|
||||
the people running this code still depend on it working on Python 2, so we'll
|
||||
have to make that transition gracefully.</p>
|
||||
|
||||
If a migration away from Python 2 isn't possible in the near-term, the next
|
||||
best thing to do is update our code so that it uses a compatibility layer like
|
||||
[`future`][future] or [`six`][six]. Those libraries are designed to modernize
|
||||
Python 2 and help smooth over issues like this one.
|
||||
|
||||
It also wouldn't hurt for us to take a page from [Alexis King]'s
|
||||
[_Parse, don't validate_][parse-dont-validate] school of thought. When the
|
||||
hostname enters our program via user input it should **immediately** be
|
||||
converted to the `unicode` type (or maybe even an IP address type) so we don't
|
||||
end up solving this problem in several different places throughout the code.
|
||||
|
||||
Finally, though our program doesn't currently handle any hostnames in languages
|
||||
other than English, it's probably best to be thinking in Unicode anyway. Again,
|
||||
it's 2020 and [internationalized domain names] like [https://Яндекс.рф][yandex]
|
||||
are a thing.
|
||||
|
||||
If you made it this far, thanks for reading. It was fun to turn a brief
|
||||
debugging session with my co-worker into a treatise on the perils of Python 2
|
||||
and the value of Unicode. See you next year! 😂
|
||||
|
||||
[URL]: https://en.wikipedia.org/wiki/URL
|
||||
[standard ports]: https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
|
||||
[port]: https://en.wikipedia.org/wiki/Port_(computer_networking)
|
||||
[hostname]: https://en.wikipedia.org/wiki/Hostname
|
||||
[IPv4]: https://en.wikipedia.org/wiki/IPv4#Addressing
|
||||
[Python]: https://www.python.org/
|
||||
[IPv6]: https://en.wikipedia.org/wiki/IPv6#Addressing
|
||||
[hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal
|
||||
[RFC 2732]: https://www.ietf.org/rfc/rfc2732.txt
|
||||
[drop-python2]: /#drop-python2
|
||||
[Bizarro World]: https://en.wikipedia.org/wiki/Bizarro_World
|
||||
[ip_address]: https://github.com/python/cpython/blob/v3.9.0/Lib/ipaddress.py#L27-L54
|
||||
[ASCII]: https://en.wikipedia.org/wiki/ASCII
|
||||
[character encoding]: https://en.wikipedia.org/wiki/Character_encoding
|
||||
[byte]: https://en.wikipedia.org/wiki/Byte
|
||||
[binary]: https://en.wikipedia.org/wiki/Binary_number
|
||||
[decimal]: https://en.wikipedia.org/wiki/Decimal
|
||||
[octal]: https://en.wikipedia.org/wiki/Octal
|
||||
[W.E.B. Du Bois]: https://en.wikipedia.org/wiki/W._E._B._Du_Bois
|
||||
[Harold Geneen]: https://en.wikipedia.org/wiki/Harold_Geneen
|
||||
[Managing]: https://en.wikiquote.org/wiki/Harold_Geneen
|
||||
[bytes-str-alias]: https://docs.python.org/3/whatsnew/2.6.html#pep-3112-byte-literals
|
||||
[Extended ASCII]: https://en.wikipedia.org/wiki/ASCII#8-bit_codes
|
||||
[Unicode]: https://en.wikipedia.org/wiki/Unicode
|
||||
[UTF-8]: https://en.wikipedia.org/wiki/UTF-8
|
||||
[Ned Batchelder]: https://nedbatchelder.com/site/aboutned.html
|
||||
[unipain]: https://nedbatchelder.com/text/unipain.html
|
||||
[no-op]: https://en.wikipedia.org/wiki/NOP_(code)
|
||||
[ipaddress2]: https://github.com/phihag/ipaddress/blob/v1.0.23/ipaddress.py
|
||||
[curious set of lines]: https://github.com/phihag/ipaddress/blob/v1.0.23/ipaddress.py#L2026-L2031
|
||||
[wire]: https://en.wikipedia.org/wiki/Wire_data
|
||||
[endianness]: https://en.wikipedia.org/wiki/Endianness
|
||||
[struct unpacking]: https://docs.python.org/3.9/library/struct.html#struct.unpack
|
||||
[IPv6 address space]: https://en.wikipedia.org/wiki/IPv6#Larger_address_space
|
||||
[python2-unicode]: https://docs.python.org/2.7/library/functions.html#unicode
|
||||
[python3-str-type]: https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str
|
||||
[future]: https://python-future.org/
|
||||
[six]: https://six.readthedocs.io/
|
||||
[Alexis King]: https://lexi-lambda.github.io/about.html
|
||||
[parse-dont-validate]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
|
||||
[internationalized domain names]: https://en.wikipedia.org/wiki/Internationalized_domain_name
|
||||
[yandex]: https://Яндекс.рф
|
||||
Reference in New Issue
Block a user