Skip to content

Commit c1e447c

Browse files
committed
Vendor http_sfv
1 parent 0aa9efe commit c1e447c

19 files changed

+818
-7
lines changed

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Synopsis
1616

1717
.. code-block:: python
1818
19-
from http_message_signatures import HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureKeyResolver, algorithms
20-
import requests, base64, hashlib, http_sfv
19+
from http_message_signatures import HTTPMessageSigner, HTTPMessageVerifier, HTTPSignatureKeyResolver, algorithms, http_sfv
20+
import requests, base64, hashlib
2121
2222
class MyHTTPSignatureKeyResolver(HTTPSignatureKeyResolver):
2323
keys = {"my-key": b"top-secret-key"}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
Structured HTTP Field Values
3+
4+
Copyright (c) 2018-2020 Mark Nottingham
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
THE SOFTWARE.
23+
"""
24+
25+
# Top-level structures
26+
from .dictionary import Dictionary
27+
from .item import InnerList, Item
28+
from .list import List
29+
30+
# Item type wrappers
31+
from .types import DisplayString, Token
32+
33+
structures = {"dictionary": Dictionary, "list": List, "item": Item}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Tuple
2+
3+
QUESTION = ord(b"?")
4+
ONE = ord(b"1")
5+
ZERO = ord(b"0")
6+
7+
_boolean_map = {ONE: (2, True), ZERO: (2, False)}
8+
9+
10+
def parse_boolean(data: bytes) -> Tuple[int, bool]:
11+
try:
12+
return _boolean_map[data[1]]
13+
except (KeyError, IndexError):
14+
pass
15+
raise ValueError("No Boolean value found")
16+
17+
18+
def ser_boolean(inval: bool) -> str:
19+
return f"?{inval and '1' or '0'}"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import base64
2+
import binascii
3+
from string import ascii_letters, digits
4+
from typing import Tuple
5+
6+
BYTE_DELIMIT = ord(b":")
7+
B64CONTENT = set((ascii_letters + digits + "+/=").encode("ascii"))
8+
9+
10+
def parse_byteseq(data: bytes) -> Tuple[int, bytes]:
11+
bytes_consumed = 1
12+
try:
13+
end_delimit = data[bytes_consumed:].index(BYTE_DELIMIT)
14+
except ValueError as why:
15+
raise ValueError("Binary Sequence didn't contain ending ':'") from why
16+
b64_content = data[bytes_consumed : bytes_consumed + end_delimit]
17+
bytes_consumed += end_delimit + 1
18+
if not all(c in B64CONTENT for c in b64_content):
19+
raise ValueError("Binary Sequence contained disallowed character")
20+
try:
21+
binary_content = base64.standard_b64decode(b64_content)
22+
except binascii.Error as why:
23+
raise ValueError("Binary Sequence failed to decode") from why
24+
return bytes_consumed, binary_content
25+
26+
27+
def ser_byteseq(byteseq: bytes) -> str:
28+
return f":{base64.standard_b64encode(byteseq).decode('ascii')}:"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from datetime import datetime
2+
from typing import Tuple
3+
4+
from .integer import parse_integer
5+
6+
7+
def parse_date(data: bytes) -> Tuple[int, datetime]:
8+
bytes_consumed, value = parse_integer(data[1:])
9+
if not isinstance(value, int):
10+
raise ValueError("Non-integer Date")
11+
return bytes_consumed + 1, datetime.fromtimestamp(value)
12+
13+
14+
def ser_date(inval: datetime) -> str:
15+
return f"@{int(inval.timestamp())}"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from decimal import Decimal
2+
from typing import Tuple, Union
3+
4+
from .integer import parse_number
5+
6+
INT_DIGITS = 12
7+
FRAC_DIGITS = 3
8+
PRECISION = Decimal(10) ** -FRAC_DIGITS
9+
10+
11+
def parse_decimal(data: bytes) -> Tuple[int, Decimal]:
12+
return parse_number(data) # type: ignore
13+
14+
15+
def ser_decimal(input_decimal: Union[Decimal, float]) -> str:
16+
if isinstance(input_decimal, float):
17+
input_decimal = Decimal(input_decimal)
18+
if not isinstance(input_decimal, Decimal):
19+
raise ValueError("decimal input is not decimal")
20+
input_decimal = round(input_decimal, FRAC_DIGITS)
21+
abs_decimal = input_decimal.copy_abs()
22+
integer_component_s = str(int(abs_decimal))
23+
if len(integer_component_s) > INT_DIGITS:
24+
raise ValueError(f"decimal with oversize integer component {integer_component_s}")
25+
fractional_component = abs_decimal.quantize(PRECISION).normalize() % 1
26+
return (
27+
f"{'-' if input_decimal < 0 else ''}{integer_component_s}."
28+
f"{str(fractional_component)[2:] if fractional_component else '0'}"
29+
)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from collections import UserDict
2+
3+
from .item import Item, InnerList, itemise, AllItemType
4+
from .list import parse_item_or_inner_list
5+
from .types import JsonDictType
6+
from .util import (
7+
StructuredFieldValue,
8+
discard_http_ows,
9+
ser_key,
10+
parse_key,
11+
)
12+
13+
EQUALS = ord(b"=")
14+
COMMA = ord(b",")
15+
16+
17+
class Dictionary(UserDict, StructuredFieldValue):
18+
def parse_content(self, data: bytes) -> int:
19+
bytes_consumed = 0
20+
data_len = len(data)
21+
try:
22+
while True:
23+
offset, this_key = parse_key(data[bytes_consumed:])
24+
bytes_consumed += offset
25+
try:
26+
is_equals = data[bytes_consumed] == EQUALS
27+
except IndexError:
28+
is_equals = False
29+
if is_equals:
30+
bytes_consumed += 1 # consume the "="
31+
offset, member = parse_item_or_inner_list(data[bytes_consumed:])
32+
bytes_consumed += offset
33+
else:
34+
member = Item()
35+
member.value = True
36+
bytes_consumed += member.params.parse(data[bytes_consumed:])
37+
self[this_key] = member
38+
bytes_consumed += discard_http_ows(data[bytes_consumed:])
39+
if bytes_consumed == data_len:
40+
return bytes_consumed
41+
if data[bytes_consumed] != COMMA:
42+
raise ValueError(f"Dictionary member '{this_key}' has trailing characters")
43+
bytes_consumed += 1
44+
bytes_consumed += discard_http_ows(data[bytes_consumed:])
45+
if bytes_consumed == data_len:
46+
raise ValueError("Dictionary has trailing comma")
47+
except Exception as why:
48+
self.clear()
49+
raise ValueError from why
50+
51+
def __setitem__(self, key: str, value: AllItemType) -> None:
52+
self.data[key] = itemise(value)
53+
54+
def __str__(self) -> str:
55+
if len(self) == 0:
56+
raise ValueError("No contents; field should not be emitted")
57+
return ", ".join(
58+
[
59+
f"{ser_key(m)}"
60+
f"""{n.params if (isinstance(n, Item) and n.value is True) else f"={n}"}"""
61+
for m, n in self.items()
62+
]
63+
)
64+
65+
def to_json(self) -> JsonDictType:
66+
return [(key, val.to_json()) for (key, val) in self.items()]
67+
68+
def from_json(self, json_data: JsonDictType) -> None:
69+
for key, val in json_data:
70+
if isinstance(val[0], list):
71+
self[key] = InnerList()
72+
else:
73+
self[key] = Item()
74+
self[key].from_json(val)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Tuple
2+
3+
from .types import DisplayString
4+
5+
PERCENT = ord("%")
6+
DQUOTE = ord('"')
7+
8+
9+
def parse_display_string(data: bytes) -> Tuple[int, DisplayString]:
10+
output_array = bytearray([])
11+
if data[:2] != b'%"':
12+
raise ValueError('Display string does not start with %"')
13+
bytes_consumed = 2 # consume PERCENT DQUOTE
14+
while True:
15+
try:
16+
char = data[bytes_consumed]
17+
except IndexError as why:
18+
raise ValueError("Reached end of input without finding a closing DQUOTE") from why
19+
bytes_consumed += 1
20+
if char == PERCENT:
21+
try:
22+
next_chars = data[bytes_consumed : bytes_consumed + 2]
23+
except IndexError as why:
24+
raise ValueError("Incomplete percent encoding") from why
25+
bytes_consumed += 2
26+
if next_chars.lower() != next_chars:
27+
raise ValueError("Uppercase percent encoding")
28+
try:
29+
octet = int(next_chars, base=16)
30+
except ValueError as why:
31+
raise ValueError("Invalid percent encoding") from why
32+
output_array.append(octet)
33+
elif char == DQUOTE:
34+
try:
35+
output_string = output_array.decode("utf-8")
36+
except UnicodeDecodeError as why:
37+
raise ValueError("Invalid UTF-8") from why
38+
return bytes_consumed, DisplayString(output_string)
39+
elif 31 < char < 127:
40+
output_array.append(char)
41+
else:
42+
raise ValueError("String contains disallowed character")
43+
44+
45+
def ser_display_string(inval: DisplayString) -> str:
46+
byte_array = inval.encode("utf-8")
47+
escaped = []
48+
for byte in byte_array:
49+
if byte in [PERCENT, DQUOTE] or not 31 <= byte <= 127:
50+
escaped.append(f"%{byte:x}")
51+
else:
52+
escaped.append(chr(byte))
53+
return DisplayString(f'%"{"".join(escaped)}"')
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from decimal import Decimal
2+
from string import digits
3+
from typing import Tuple, Union
4+
5+
6+
MAX_INT = 999999999999999
7+
MIN_INT = -999999999999999
8+
9+
DIGITS = set(digits.encode("ascii"))
10+
NUMBER_START_CHARS = set((digits + "-").encode("ascii"))
11+
PERIOD = ord(b".")
12+
MINUS = ord(b"-")
13+
14+
15+
def parse_integer(data: bytes) -> Tuple[int, int]:
16+
return parse_number(data) # type: ignore
17+
18+
19+
def ser_integer(inval: int) -> str:
20+
if not MIN_INT <= inval <= MAX_INT:
21+
raise ValueError("Input is out of Integer range.")
22+
output = ""
23+
if inval < 0:
24+
output += "-"
25+
output += str(abs(inval))
26+
return output
27+
28+
29+
INTEGER = "integer"
30+
DECIMAL = "decimal"
31+
32+
33+
def parse_number(data: bytes) -> Tuple[int, Union[int, Decimal]]:
34+
_type = INTEGER
35+
_sign = 1
36+
bytes_consumed = 0
37+
num_start = 0
38+
decimal_index = 0
39+
num_length = 0
40+
if data[0] == MINUS:
41+
bytes_consumed += 1
42+
num_start += 1
43+
_sign = -1
44+
if not data[bytes_consumed:]:
45+
raise ValueError("Number input lacked a number")
46+
if not data[num_start] in DIGITS:
47+
raise ValueError("Number doesn't start with a DIGIT")
48+
while True:
49+
try:
50+
char = data[bytes_consumed]
51+
except IndexError:
52+
break
53+
bytes_consumed += 1
54+
num_length = bytes_consumed - num_start - 1
55+
if char in DIGITS:
56+
pass
57+
elif _type is INTEGER and char == PERIOD:
58+
if num_length > 12:
59+
raise ValueError("Decimal too long.")
60+
_type = DECIMAL
61+
decimal_index = bytes_consumed
62+
else:
63+
bytes_consumed -= 1
64+
break
65+
if _type == INTEGER:
66+
if num_length > 15:
67+
raise ValueError("Integer too long.")
68+
output_int = int(data[num_start:bytes_consumed]) * _sign
69+
if not MIN_INT <= output_int <= MAX_INT:
70+
raise ValueError("Integer outside allowed range")
71+
return bytes_consumed, output_int
72+
# Decimal
73+
if num_length > 16:
74+
raise ValueError("Decimal too long.")
75+
if data[bytes_consumed - 1] == MINUS:
76+
raise ValueError("Decimal ends in '.'")
77+
if bytes_consumed - decimal_index > 3:
78+
raise ValueError("Decimal fractional component too long")
79+
output_float = Decimal(data[num_start:bytes_consumed].decode("ascii")) * _sign
80+
return bytes_consumed, output_float

0 commit comments

Comments
 (0)