summaryrefslogtreecommitdiff
path: root/src/gopher.c
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-11-07 22:41:00 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-11-07 22:41:00 +0200
commit0e03a97ba89bb9e8fccf6978250addc14799c5c1 (patch)
treee840a7465101216d1d2e9b11aaed2b3a883b7466 /src/gopher.c
parentbf293d976fffcc80c4e4f26d553cda33746b95bc (diff)
Added support for Gopher
Needs more testing. Queries are not supported yet.
Diffstat (limited to 'src/gopher.c')
-rw-r--r--src/gopher.c206
1 files changed, 206 insertions, 0 deletions
diff --git a/src/gopher.c b/src/gopher.c
new file mode 100644
index 00000000..9a503442
--- /dev/null
+++ b/src/gopher.c
@@ -0,0 +1,206 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "gopher.h"
24
25#include <ctype.h>
26
27iDefineTypeConstruction(Gopher)
28
29iLocalDef iBool isLineTerminator_(const char *str) {
30 return str[0] == '\r' && str[1] == '\n';
31}
32
33static iBool isPreformatted_(iRangecc text) {
34 int numPunct = 0;
35 int numSpace = 0;
36 for (const char *ch = text.start; ch != text.end; ch++) {
37 if (*ch > 32 && ispunct(*ch)) {
38 if (++numPunct == 4)
39 return iTrue;
40 }
41 else {
42 numPunct = 0;
43 }
44 if (*ch == ' ' || *ch == '\n') {
45 if (++numSpace == 3) return iTrue;
46 }
47 else {
48 numSpace = 0;
49 }
50 }
51 return iFalse;
52}
53
54static void setPre_Gopher_(iGopher *d, iBool pre) {
55 if (pre && !d->isPre) {
56 appendCStr_Block(d->output, "```\n");
57 }
58 else if (!pre && d->isPre) {
59 appendCStr_Block(d->output, "```\n");
60 }
61 d->isPre = pre;
62}
63
64static iBool convertSource_Gopher_(iGopher *d) {
65 iBool converted = iFalse;
66 iRangecc body = range_Block(&d->source);
67 iRegExp *pattern = new_RegExp("(.)([^\t]*)\t([^\t]*)\t([^\t]*)\t([0-9]+)",
68 caseInsensitive_RegExpOption);
69 for (;;) {
70 /* Find the end of the line. */
71 iRangecc line = { body.start, body.start };
72 while (line.end < body.end - 1 && !isLineTerminator_(line.end)) {
73 line.end++;
74 }
75 if (line.end >= body.end - 1 || !isLineTerminator_(line.end)) {
76 /* Not a complete line. */
77 break;
78 }
79 body.start = line.end + 2;
80 iRegExpMatch m;
81 init_RegExpMatch(&m);
82 if (matchRange_RegExp(pattern, line, &m)) {
83 const char lineType = *capturedRange_RegExpMatch(&m, 1).start;
84 const iRangecc text = capturedRange_RegExpMatch(&m, 2);
85 const iRangecc path = capturedRange_RegExpMatch(&m, 3);
86 const iRangecc domain = capturedRange_RegExpMatch(&m, 4);
87 const iRangecc port = capturedRange_RegExpMatch(&m, 5);
88 iString *buf = new_String();
89 switch (lineType) {
90 case 'i':
91 case '3': {
92 setPre_Gopher_(d, isPreformatted_(text));
93 appendData_Block(d->output, text.start, size_Range(&text));
94 appendCStr_Block(d->output, "\n");
95 break;
96 }
97 case '0':
98 case '1':
99 case '7':
100 case '4':
101 case '5':
102 case '9':
103 case 'g':
104 case 'I':
105 case 's': {
106 iBeginCollect();
107 setPre_Gopher_(d, iFalse);
108 format_String(buf,
109 "=> gopher://%s:%s/%c%s %s\n",
110 cstr_Rangecc(domain),
111 cstr_Rangecc(port),
112 lineType,
113 cstr_Rangecc(path),
114 cstr_Rangecc(text));
115 appendData_Block(d->output, constBegin_String(buf), size_String(buf));
116 iEndCollect();
117 break;
118 }
119 default:
120 break; /* Ignore unknown types. */
121 }
122 delete_String(buf);
123 }
124 }
125 iRelease(pattern);
126 remove_Block(&d->source, 0, body.start - constBegin_Block(&d->source));
127 return converted;
128}
129
130void init_Gopher(iGopher *d) {
131 d->socket = NULL;
132 d->type = 0;
133 init_Block(&d->source, 0);
134 d->isPre = iFalse;
135 d->meta = NULL;
136 d->output = NULL;
137}
138
139void deinit_Gopher(iGopher *d) {
140 deinit_Block(&d->source);
141 iReleasePtr(&d->socket);
142}
143
144void open_Gopher(iGopher *d, const iString *url) {
145 open_Socket(d->socket);
146 iUrl parts;
147 init_Url(&parts, url);
148 d->type = '1';
149 if (!isEmpty_Range(&parts.path)) {
150 if (*parts.path.start == '/') {
151 parts.path.start++;
152 }
153 if (parts.path.start < parts.path.end) {
154 d->type = *parts.path.start;
155 parts.path.start++;
156 }
157 }
158 /* MIME type determined by the URI. */
159 switch (d->type) {
160 case '0':
161 setCStr_String(d->meta, "text/plain");
162 break;
163 case '1':
164 setCStr_String(d->meta, "text/gemini");
165 break;
166 case '4':
167 setCStr_String(d->meta, "application/mac-binhex");
168 break;
169 case 'g':
170 setCStr_String(d->meta, "image/gif");
171 break;
172 case 'I':
173 setCStr_String(d->meta, "image/generic");
174 break;
175 case 's':
176 setCStr_String(d->meta, "audio/wave");
177 break;
178 default:
179 setCStr_String(d->meta, "application/octet-stream");
180 break;
181 }
182 d->isPre = iFalse;
183 writeData_Socket(d->socket, parts.path.start, size_Range(&parts.path));
184 writeData_Socket(d->socket, "\r\n", 2);
185}
186
187void cancel_Gopher(iGopher *d) {
188 if (d->socket) {
189 close_Socket(d->socket);
190 }
191}
192
193iBool processResponse_Gopher(iGopher *d, const iBlock *data) {
194 iBool changed = iFalse;
195 if (d->type == '1') {
196 append_Block(&d->source, data);
197 if (convertSource_Gopher_(d)) {
198 changed = iTrue;
199 }
200 }
201 else {
202 append_Block(d->output, data);
203 changed = iTrue;
204 }
205 return changed;
206}