mime: basic MIME type management

This commit is contained in:
dece 2021-03-14 00:04:55 +01:00
parent 2767402d9f
commit dd4a4196a8
4 changed files with 99 additions and 15 deletions

View file

@ -131,10 +131,10 @@ class Browser:
elif char == ord("l"): elif char == ord("l"):
self.scroll_page_horizontally(1) self.scroll_page_horizontally(1)
self.screen.nodelay(False) self.screen.nodelay(False)
# else:
ctrl_char = curses.unctrl(char) # unctrled = curses.unctrl(char)
if ctrl_char == "a": # if unctrled == b"^T":
self.set_status("yup!") # self.set_status("test!")
@property @property
def page_pad_size(self): def page_pad_size(self):
@ -281,13 +281,14 @@ class Browser:
return return
if response.code == 20: if response.code == 20:
# TODO handle MIME type; assume it's gemtext for now. handle_code = self.handle_response_content(response)
text = response.content.decode("utf-8", errors="replace") if handle_code == 0:
self.load_page(Page.from_gemtext(text)) if self.current_url and history:
if self.current_url and history: self.history.push(self.current_url)
self.history.push(self.current_url) self.current_url = url
self.current_url = url self.set_status(url)
self.set_status(url) elif handle_code == 1:
self.set_status(f"Downloaded {url}.")
elif response.generic_code == 30 and response.meta: elif response.generic_code == 30 and response.meta:
self.open_url(response.meta, base_url=url, redirects=redirects + 1) self.open_url(response.meta, base_url=url, redirects=redirects + 1)
elif response.generic_code in (40, 50): elif response.generic_code in (40, 50):
@ -299,6 +300,41 @@ class Browser:
error = f"Unhandled response code {response.code}" error = f"Unhandled response code {response.code}"
self.set_status_error(error) self.set_status_error(error)
def handle_response_content(self, response: Response) -> int:
"""Handle a response's content from a Gemini server.
According to the MIME type received or inferred, render or download the
response's content.
Currently only text/gemini content is rendered.
Arguments:
- response: a successful Response.
Returns:
An error code: 0 means a page has been loaded, so any book-keeping such
as history management can be applied; 1 means a content has been
successfully retrieved but has not been displayed (e.g. non-text
content) nor saved as a page; 2 means that the content could not be
handled, either due to bogus MIME type or MIME parameters.
"""
mime_type = response.get_mime_type()
if mime_type.main_type == "text":
if mime_type.sub_type == "gemini":
encoding = mime_type.charset
try:
text = response.content.decode(encoding, errors="replace")
except LookupError:
self.set_status_error("Unknown encoding {encoding}.")
return 2
self.load_page(Page.from_gemtext(text))
return 0
else:
pass # TODO
else:
pass # TODO
return 1
def load_page(self, page: Page): def load_page(self, page: Page):
"""Load Gemtext data as the current page.""" """Load Gemtext data as the current page."""
old_pad_height = self.page_pad.dim[0] old_pad_height = self.page_pad.dim[0]

View file

@ -47,7 +47,8 @@ class CommandLine:
command = "" command = ""
except TerminateCommandInterrupt as exc: except TerminateCommandInterrupt as exc:
command = exc.command command = exc.command
command = command[1:].rstrip() else:
command = command[1:].rstrip()
curses.curs_set(0) curses.curs_set(0)
self.clear() self.clear()
return command return command

40
bebop/mime.py Normal file
View file

@ -0,0 +1,40 @@
"""Basic MIME utilities (RFC 2046)."""
from dataclasses import dataclass
from typing import Optional
DEFAULT_CHARSET = "utf-8"
@dataclass
class MimeType:
main_type: str
sub_type: str
parameters: dict
@property
def charset(self):
return self.parameters.get("charset", DEFAULT_CHARSET)
@staticmethod
def from_str(mime_string) -> Optional["MimeType"]:
"""Parse a MIME string into a MimeType instance, or None on error."""
if ";" in mime_string:
type_str, *parameters = mime_string.split(";")
parameters = {}
for param in map(lambda s: s.strip().lower(), parameters):
if param.count("=") != 1:
return None
param_name, param_value = param.split("=")
parameters[param_name] = param_value
else:
type_str = mime_string.strip()
parameters = {}
if type_str.count("/") != 1:
return None
main_type, sub_type = type_str.split("/")
return MimeType(main_type, sub_type, parameters)
DEFAULT_MIME_TYPE = MimeType("text", "gemini", {"charset": DEFAULT_CHARSET})

View file

@ -5,7 +5,9 @@ import socket
import ssl import ssl
from dataclasses import dataclass from dataclasses import dataclass
from enum import IntEnum from enum import IntEnum
from typing import Optional
from bebop.mime import DEFAULT_MIME_TYPE, MimeType
from bebop.tofu import CertStatus, CERT_STATUS_INVALID, validate_cert from bebop.tofu import CertStatus, CERT_STATUS_INVALID, validate_cert
@ -190,11 +192,16 @@ class Response:
MAX_META_LEN = 1024 MAX_META_LEN = 1024
@property @property
def generic_code(self): def generic_code(self) -> int:
"""See `Response.get_generic_code`."""
return Response.get_generic_code(self.code) return Response.get_generic_code(self.code)
def get_mime_type(self) -> MimeType:
"""Return the MIME type if possible, else the default MIME type."""
return MimeType.from_str(self.meta) or DEFAULT_MIME_TYPE
@staticmethod @staticmethod
def parse(data): def parse(data: bytes) -> Optional["Response"]:
"""Parse a received response.""" """Parse a received response."""
try: try:
response_header_len = data.index(LINE_TERM) response_header_len = data.index(LINE_TERM)
@ -216,6 +223,6 @@ class Response:
return response return response
@staticmethod @staticmethod
def get_generic_code(code): def get_generic_code(code) -> int:
"""Return the generic version (x0) of this code.""" """Return the generic version (x0) of this code."""
return code - (code % 10) return code - (code % 10)