forked from makew0rld/amfora
/
page.go
158 lines (141 loc) · 4.4 KB
/
page.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package renderer
import (
"bytes"
"errors"
"io"
"mime"
"os"
"strings"
"time"
"github.com/Focus172/yuzu/structs"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/spf13/viper"
"golang.org/x/text/encoding/ianaindex"
)
var ErrTooLarge = errors.New("page content would be too large")
var ErrTimedOut = errors.New("page download timed out")
var ErrCantDisplay = errors.New("invalid content for a page")
var ErrBadEncoding = errors.New("unsupported encoding")
var ErrBadMediatype = errors.New("displayable mediatype is not handled in the code, implementation error")
// isUTF8 returns true for charsets that are compatible with UTF-8 and don't need to be decoded.
func isUTF8(charset string) bool {
utfCharsets := []string{"", "utf-8", "us-ascii"}
for _, s := range utfCharsets {
if charset == s || strings.ToLower(charset) == s {
return true
}
}
return false
}
// decodeMeta returns the output of mime.ParseMediaType, but handles the empty
// META which is equal to "text/gemini; charset=utf-8" according to the spec.
func decodeMeta(meta string) (string, map[string]string, error) {
if meta == "" {
return "text/gemini", make(map[string]string), nil
}
mediatype, params, err := mime.ParseMediaType(meta)
if mediatype != "" && err != nil {
// The mediatype was successfully decoded but there's some error with the params
// Ignore the params
return mediatype, make(map[string]string), nil
}
return mediatype, params, err
}
// CanDisplay returns true if the response is supported by Amfora
// for displaying on the screen.
// It also doubles as a function to detect whether something can be stored in a Page struct.
func CanDisplay(res *gemini.Response) bool {
if gemini.SimplifyStatus(res.Status) != 20 {
// No content
return false
}
mediatype, params, err := decodeMeta(res.Meta)
if err != nil {
return false
}
if !strings.HasPrefix(mediatype, "text/") {
// Amfora doesn't support other filetypes
return false
}
if isUTF8(params["charset"]) {
return true
}
enc, err := ianaindex.MIME.Encoding(params["charset"]) // Lowercasing is done inside
// Encoding sometimes returns nil, see #3 on this repo and golang/go#19421
return err == nil && enc != nil
}
// MakePage creates a formatted, rendered Page from the given network response and params.
// You must set the Page.Width value yourself.
func MakePage(url string, res *gemini.Response, width int, proxied bool) (*structs.Page, error) {
if !CanDisplay(res) {
return nil, ErrCantDisplay
}
buf := new(bytes.Buffer)
_, err := io.CopyN(buf, res.Body, viper.GetInt64("a-general.page_max_size")+1)
if err == nil {
// Content was larger than max size
return nil, ErrTooLarge
} else if err != io.EOF {
if os.IsTimeout(err) {
// I would use
// errors.Is(err, os.ErrDeadlineExceeded)
// but that isn't supported before Go 1.15.
return nil, ErrTimedOut
}
// Some other error
return nil, err
}
// Otherwise, the error is EOF, which is what we want.
mediatype, params, _ := decodeMeta(res.Meta)
// Convert content first
var utfText string
if isUTF8(params["charset"]) {
utfText = buf.String()
} else {
encoding, err := ianaindex.MIME.Encoding(params["charset"])
if encoding == nil || err != nil {
// Some encoding doesn't exist and wasn't caught in CanDisplay()
return nil, ErrBadEncoding
}
utfText, err = encoding.NewDecoder().String(buf.String())
if err != nil {
return nil, err
}
}
if mediatype == "text/gemini" {
rendered, links := RenderGemini(utfText, width, proxied)
return &structs.Page{
Mediatype: structs.TextGemini,
RawMediatype: mediatype,
URL: url,
Raw: utfText,
Content: rendered,
Links: links,
MadeAt: time.Now(),
}, nil
} else if strings.HasPrefix(mediatype, "text/") {
if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") || strings.HasSuffix(url, ".ansi") {
// ANSI
return &structs.Page{
Mediatype: structs.TextAnsi,
RawMediatype: mediatype,
URL: url,
Raw: utfText,
Content: RenderANSI(utfText),
Links: []string{},
MadeAt: time.Now(),
}, nil
}
// Treated as plaintext
return &structs.Page{
Mediatype: structs.TextPlain,
RawMediatype: mediatype,
URL: url,
Raw: utfText,
Content: RenderPlainText(utfText),
Links: []string{},
MadeAt: time.Now(),
}, nil
}
return nil, ErrBadMediatype
}