summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/integration/harness.py5
-rw-r--r--tests/integration/test_accesslist.py4
-rw-r--r--tests/integration/test_dynamic_accesslist.py18
-rw-r--r--tests/integration/test_fullscrape.py14
-rw-r--r--tests/integration/test_keepalive.py16
-rw-r--r--tests/integration/test_livesync.py13
-rw-r--r--tests/integration/test_statefile.py41
-rw-r--r--tests/t_clean.c8
-rw-r--r--tests/t_urlencoded.c66
9 files changed, 131 insertions, 54 deletions
diff --git a/tests/integration/harness.py b/tests/integration/harness.py
index e5ec621..f3cd0fa 100644
--- a/tests/integration/harness.py
+++ b/tests/integration/harness.py
@@ -135,6 +135,11 @@ class Tracker:
135 self.log.seek(0) 135 self.log.seek(0)
136 return self.log.read().decode("utf-8", "replace") 136 return self.log.read().decode("utf-8", "replace")
137 137
138 def reload(self):
139 """SIGHUP makes the accesslist reader re-read its files, which also
140 empties the dynamic add/delete changesets."""
141 os.kill(self.process.pid, signal.SIGHUP)
142
138 def stop(self): 143 def stop(self):
139 """Returns anything the tracker complained about on the way out. 144 """Returns anything the tracker complained about on the way out.
140 145
diff --git a/tests/integration/test_accesslist.py b/tests/integration/test_accesslist.py
index 7d01264..3ed8ddf 100644
--- a/tests/integration/test_accesslist.py
+++ b/tests/integration/test_accesslist.py
@@ -7,6 +7,7 @@ produces: an empty file, and a last line with no newline after it.
7 7
8import os 8import os
9import struct 9import struct
10import time
10import unittest 11import unittest
11 12
12from harness import Tracker, TrackerTestCase 13from harness import Tracker, TrackerTestCase
@@ -109,9 +110,10 @@ class ListIsInPlaceBeforeTheStateFile(TrackerTestCase):
109 files = {"blacklist": listing(BLOCKED)} 110 files = {"blacklist": listing(BLOCKED)}
110 111
111 def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self): 112 def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self):
113 touched = int(time.time()) // 60 - 5
112 path = os.path.join(self.tracker.dir, "state") 114 path = os.path.join(self.tracker.dir, "state")
113 with open(path, "w") as handle: 115 with open(path, "w") as handle:
114 handle.write("%s:5:7\n%s:5:7\n" % (BLOCKED.hex(), ALLOWED.hex())) 116 handle.write("%s:%d:7\n%s:%d:7\n" % (BLOCKED.hex(), touched, ALLOWED.hex(), touched))
115 117
116 restarted = Tracker(self.config, self.files, extra_args=["-l", path]) 118 restarted = Tracker(self.config, self.files, extra_args=["-l", path])
117 try: 119 try:
diff --git a/tests/integration/test_dynamic_accesslist.py b/tests/integration/test_dynamic_accesslist.py
index 7fa5fde..2072f5e 100644
--- a/tests/integration/test_dynamic_accesslist.py
+++ b/tests/integration/test_dynamic_accesslist.py
@@ -40,6 +40,24 @@ class DynamicAccesslist(TrackerTestCase):
40 time.sleep(0.05) 40 time.sleep(0.05)
41 return False 41 return False
42 42
43 def test_a_hash_added_after_a_reload_is_blocked(self):
44 """A reload leaves the add changeset an empty list rather than an
45 absent one. Adding against it walked a null array; the hash must be
46 blocked and the tracker must stay up."""
47 OTHER = bytes(range(21, 41))
48 self.feed("add.fifo", BLOCKED)
49 self.assertTrue(self.wait_for(BLOCKED, blocked=True), "the first hash was never blocked")
50
51 # The reload empties the add changeset, which unblocks the first hash.
52 # Wait for that before adding the second, so it lands on the empty list
53 # the bug walked, not on the changeset before the reload has cleared it.
54 self.tracker.reload()
55 self.assertTrue(self.wait_for(BLOCKED, blocked=False), "the reload never emptied the changeset")
56
57 self.feed("add.fifo", OTHER)
58 self.assertTrue(self.wait_for(OTHER, blocked=True), "the hash added after the reload was never blocked")
59 # tearDown's stop() surfaces any sanitizer complaint on the way out.
60
43 def test_a_hash_can_be_blocked_and_released_while_running(self): 61 def test_a_hash_can_be_blocked_and_released_while_running(self):
44 self.assertNotIn(b"failure reason", self.tracker.announce(BLOCKED)) 62 self.assertNotIn(b"failure reason", self.tracker.announce(BLOCKED))
45 63
diff --git a/tests/integration/test_fullscrape.py b/tests/integration/test_fullscrape.py
index 3150612..45b3ffb 100644
--- a/tests/integration/test_fullscrape.py
+++ b/tests/integration/test_fullscrape.py
@@ -8,6 +8,7 @@ so these also exercise the chunked transfer the ordinary replies never take.
8 8
9import gzip 9import gzip
10import struct 10import struct
11import time
11import unittest 12import unittest
12 13
13from harness import TrackerTestCase, bdecode 14from harness import TrackerTestCase, bdecode
@@ -78,15 +79,14 @@ class FullScrape(TrackerTestCase):
78 escaped = line.rsplit(b":", 2)[0] 79 escaped = line.rsplit(b":", 2)[0]
79 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") 80 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes")
80 81
81 def test_the_state_dump_carries_an_age_rather_than_a_peer_count(self): 82 def test_the_state_dump_carries_a_stamp_rather_than_a_peer_count(self):
82 """What a restart reads back. The second column is how many minutes 83 """What a restart reads back. The second column is the minute the
83 ago the torrent was last touched, not a stamp and not a count of 84 torrent was last touched and not a count of anybody, by the wall
84 anybody: the clock counts from boot, so an absolute time would mean 85 clock, which is the only one two runs of the tracker agree on."""
85 nothing to the process that reads it."""
86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) 86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump"))
87 self.assertIn(self.seeding.hex().upper().encode(), entries) 87 self.assertIn(self.seeding.hex().upper().encode(), entries)
88 age, completed = entries[self.seeding.hex().upper().encode()] 88 stamp, completed = entries[self.seeding.hex().upper().encode()]
89 self.assertEqual(int(age), 0, "announced a moment ago") 89 self.assertAlmostEqual(int(stamp), int(time.time()) // 60, delta=1, msg="announced a moment ago")
90 self.assertEqual(int(completed), 0) 90 self.assertEqual(int(completed), 0)
91 91
92 # ------------------------------------------------------------ shape 92 # ------------------------------------------------------------ shape
diff --git a/tests/integration/test_keepalive.py b/tests/integration/test_keepalive.py
index 2c724fc..f9aa468 100644
--- a/tests/integration/test_keepalive.py
+++ b/tests/integration/test_keepalive.py
@@ -107,6 +107,22 @@ class KeepAlive(TrackerTestCase):
107 finally: 107 finally:
108 c.close() 108 c.close()
109 109
110 def test_a_tiny_split_request_does_not_overread(self):
111 """A request that arrives in pieces and completes as a buffer shorter
112 than the dispatch literals (\"GET /\" etc.) used to read past the
113 exactly sized buffer. It must be rejected without a sanitizer hit."""
114 c = self.tracker.connect()
115 try:
116 c.sock.sendall(b"\r\n")
117 time.sleep(0.05)
118 c.sock.sendall(b"\r\n") # completes as a 4-byte buffer
119 time.sleep(0.1)
120 finally:
121 c.close()
122 # the tracker must still answer; tearDown's stop() surfaces any sanitizer abort
123 answer = self.tracker.announce(self.hash_for("after-tiny"), port=6881, left=0)
124 self.assertNotIn(b"failure reason", answer, "the tracker stopped serving after a tiny request")
125
110 def test_a_split_request_answered_by_a_worker_does_not_spin(self): 126 def test_a_split_request_answered_by_a_worker_does_not_spin(self):
111 """A reply that a worker produces does not go through the send path, 127 """A reply that a worker produces does not go through the send path,
112 so the request stays in the buffer the read loop is reading. The loop 128 so the request stays in the buffer the read loop is reading. The loop
diff --git a/tests/integration/test_livesync.py b/tests/integration/test_livesync.py
index f4cb2db..e146d93 100644
--- a/tests/integration/test_livesync.py
+++ b/tests/integration/test_livesync.py
@@ -72,6 +72,19 @@ class LiveSyncIn(TrackerTestCase):
72 self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)), 72 self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)),
73 "scrape says %r" % (self.counts(info_hash),)) 73 "scrape says %r" % (self.counts(info_hash),))
74 74
75 def test_many_v6_announces_do_not_overflow_the_send_buffer(self):
76 """The outgoing peer buffer is filled by announces to a clustered
77 tracker. The flush watermark used sizeof(ot_peer) (== 1), so a v6
78 record could be appended past the 1480-byte buffer onto the adjacent
79 fill field, which then drove an oversized stack copy. Well past the
80 old overflow point (~37 v6 peers) the tracker must still be alive."""
81 last = None
82 for i in range(80):
83 last = self.tracker.announce(self.hash_for("flood-%d" % i),
84 ip="2001:db8::%x" % (i + 1), port=7000 + i, left=1)
85 self.assertIn(b"complete", last, "the tracker stopped answering mid-flood")
86 # tearDown's stop() surfaces any sanitizer complaint on the way out.
87
75 def test_one_packet_carries_several_torrents(self): 88 def test_one_packet_carries_several_torrents(self):
76 hashes = [self.hash_for("sync-many-%d" % i) for i in range(4)] 89 hashes = [self.hash_for("sync-many-%d" % i) for i in range(4)]
77 self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (5 + i), 6881, FLAG_SEEDING)) 90 self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (5 + i), 6881, FLAG_SEEDING))
diff --git a/tests/integration/test_statefile.py b/tests/integration/test_statefile.py
index 21eefae..a3c8d59 100644
--- a/tests/integration/test_statefile.py
+++ b/tests/integration/test_statefile.py
@@ -1,12 +1,12 @@
1"""The state dump and the -l switch that reads it back. 1"""The state dump and the -l switch that reads it back.
2 2
3Nothing exercised the load path before, and the dump's second column is now 3The second column is the minute a torrent was last touched, by the wall
4how long ago a torrent was last touched rather than when, because the clock 4clock, which is what the file has recorded for years and the only clock two
5counts from boot and an absolute stamp would mean nothing to the process 5runs of the tracker agree on.
6that reads the file.
7""" 6"""
8 7
9import os 8import os
9import time
10import unittest 10import unittest
11 11
12from harness import Tracker, TrackerTestCase 12from harness import Tracker, TrackerTestCase
@@ -54,23 +54,26 @@ class StateFileRoundTrip(TrackerTestCase):
54 restarted._cleanup() 54 restarted._cleanup()
55 55
56 56
57class StateFileWithNonsenseAges(TrackerTestCase): 57class StateFileWithNonsenseStamps(TrackerTestCase):
58 """The age column is read as unsigned and handed on as a signed ot_time. 58 """The column is read as unsigned and handed on as a signed ot_time. A
59 A value at or above 2^63 arrived negative, cleared the timeout check, and 59 value at or above 2^63 arrived negative, cleared the timeout check, and
60 put the torrent's base in the future, where the cleaner never reaches it. 60 put the torrent's base in the future, where the cleaner never reaches it.
61 At exactly 2^63 the subtraction itself overflowed.""" 61 At exactly 2^63 the subtraction itself overflowed."""
62 62
63 AGES = { 63 def stamps(self):
64 "overflow": 2**63, 64 now = int(time.time()) // 60
65 "wrapped": 2**63 + 60 * 24, 65 return {
66 "minus-one": 2**64 - 1, 66 "overflow": 2**63,
67 "int64-max": 2**63 - 1, 67 "minus-one": 2**64 - 1,
68 "past-timeout": 60 * 24 + 1, 68 "int64-max": 2**63 - 1,
69 } 69 "in-the-future": now + 60,
70 "past-timeout": now - (24 * 60 + 1),
71 }
70 72
71 def test_no_age_outside_the_timeout_builds_a_torrent(self): 73 def test_no_stamp_outside_the_timeout_builds_a_torrent(self):
72 lines = ["%s:%d:7" % (self.hash_for(name).hex(), age) for name, age in self.AGES.items()] 74 stamps = self.stamps()
73 lines.append("%s:5:7" % self.hash_for("honest").hex()) 75 lines = ["%s:%d:7" % (self.hash_for(name).hex(), stamp) for name, stamp in stamps.items()]
76 lines.append("%s:%d:7" % (self.hash_for("honest").hex(), int(time.time()) // 60 - 5))
74 77
75 path = os.path.join(self.tracker.dir, "nonsense") 78 path = os.path.join(self.tracker.dir, "nonsense")
76 with open(path, "w") as handle: 79 with open(path, "w") as handle:
@@ -78,9 +81,9 @@ class StateFileWithNonsenseAges(TrackerTestCase):
78 81
79 restarted = Tracker(extra_args=["-l", path]) 82 restarted = Tracker(extra_args=["-l", path])
80 try: 83 try:
81 for name in self.AGES: 84 for name, stamp in stamps.items():
82 self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"], 85 self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"],
83 "the line with age %d was loaded" % self.AGES[name]) 86 "the line stamped %d was loaded" % stamp)
84 self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"], 87 self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"],
85 "the state file was not read at all") 88 "the state file was not read at all")
86 self.assertEqual(restarted.torrent_count(), 1) 89 self.assertEqual(restarted.torrent_count(), 1)
diff --git a/tests/t_clean.c b/tests/t_clean.c
index 7eca9aa..2fadd03 100644
--- a/tests/t_clean.c
+++ b/tests/t_clean.c
@@ -11,7 +11,7 @@
11#include "trackerlogic.h" 11#include "trackerlogic.h"
12#include "test.h" 12#include "test.h"
13 13
14/* A clock stepped backwards used to leave peer_list->base in the future, so 14/* A clock stepped backwards used to leave the torrent's base in the future, so
15 the span the cleaner works from came out negative. It was then added to a 15 the span the cleaner works from came out negative. It was then added to a
16 peer's age and stored in the single byte that age lives in, where -1 reads 16 peer's age and stored in the single byte that age lives in, where -1 reads
17 as 255 and the next pass threw the whole swarm away. */ 17 as 255 and the next pass threw the whole swarm away. */
@@ -60,14 +60,12 @@ static ot_torrent *torrent_of(ot_hash *hash) {
60 60
61/* What a clock stepped back by `minutes` leaves behind */ 61/* What a clock stepped back by `minutes` leaves behind */
62static void step_clock_back(ot_torrent *torrent, int minutes) { 62static void step_clock_back(ot_torrent *torrent, int minutes) {
63 torrent->peer_list6->base += minutes; 63 torrent->base += minutes;
64 torrent->peer_list4->base += minutes;
65} 64}
66 65
67/* What the next minute of a well behaved clock looks like */ 66/* What the next minute of a well behaved clock looks like */
68static void age_by_a_minute(ot_torrent *torrent) { 67static void age_by_a_minute(ot_torrent *torrent) {
69 torrent->peer_list6->base -= 1; 68 torrent->base -= 1;
70 torrent->peer_list4->base -= 1;
71} 69}
72 70
73static void case_a_clock_that_went_back_keeps_the_swarm(void) { 71static void case_a_clock_that_went_back_keeps_the_swarm(void) {
diff --git a/tests/t_urlencoded.c b/tests/t_urlencoded.c
index 3b3e6c2..9935c1c 100644
--- a/tests/t_urlencoded.c
+++ b/tests/t_urlencoded.c
@@ -21,7 +21,7 @@ static void case_path(void) {
21 ssize_t n; 21 ssize_t n;
22 22
23 test_case("path stops at the question mark"); 23 test_case("path stops at the question mark");
24 n = urlencoded_parse(&p, dest, SCAN_PATH); 24 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_PATH);
25 CHECK_INT(n, 9); 25 CHECK_INT(n, 9);
26 dest[n] = 0; 26 dest[n] = 0;
27 CHECK_STR(dest, "/announce"); 27 CHECK_STR(dest, "/announce");
@@ -35,22 +35,22 @@ static void case_param_and_value(void) {
35 ssize_t n; 35 ssize_t n;
36 36
37 test_case("parameter stops at '=', value stops at '&'"); 37 test_case("parameter stops at '=', value stops at '&'");
38 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); 38 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_PARAM);
39 CHECK_INT(n, 9); 39 CHECK_INT(n, 9);
40 dest[n] = 0; 40 dest[n] = 0;
41 CHECK_STR(dest, "info_hash"); 41 CHECK_STR(dest, "info_hash");
42 42
43 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 43 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
44 CHECK_INT(n, 3); 44 CHECK_INT(n, 3);
45 dest[n] = 0; 45 dest[n] = 0;
46 CHECK_STR(dest, "abc"); 46 CHECK_STR(dest, "abc");
47 47
48 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); 48 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_PARAM);
49 CHECK_INT(n, 4); 49 CHECK_INT(n, 4);
50 dest[n] = 0; 50 dest[n] = 0;
51 CHECK_STR(dest, "port"); 51 CHECK_STR(dest, "port");
52 52
53 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 53 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
54 CHECK_INT(n, 2); 54 CHECK_INT(n, 2);
55 dest[n] = 0; 55 dest[n] = 0;
56 CHECK_STR(dest, "80"); 56 CHECK_STR(dest, "80");
@@ -63,7 +63,7 @@ static void case_percent_decoding(void) {
63 ssize_t n; 63 ssize_t n;
64 64
65 test_case("percent escapes decode"); 65 test_case("percent escapes decode");
66 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 66 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
67 CHECK_INT(n, 3); 67 CHECK_INT(n, 3);
68 CHECK_MEM(dest, "ABC", 3); 68 CHECK_MEM(dest, "ABC", 3);
69} 69}
@@ -77,7 +77,7 @@ static void case_binary_info_hash(void) {
77 ssize_t n; 77 ssize_t n;
78 78
79 test_case("twenty byte binary value survives decoding"); 79 test_case("twenty byte binary value survives decoding");
80 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 80 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
81 CHECK_INT(n, 20); 81 CHECK_INT(n, 20);
82 CHECK_MEM(dest, want, 20); 82 CHECK_MEM(dest, want, 20);
83} 83}
@@ -88,7 +88,7 @@ static void case_in_place(void) {
88 ssize_t n; 88 ssize_t n;
89 89
90 test_case("decoding in place over the source is safe"); 90 test_case("decoding in place over the source is safe");
91 n = urlencoded_parse(&p, query, SCAN_SEARCHPATH_VALUE); 91 n = urlencoded_parse(&p, query, query + sizeof(query), SCAN_SEARCHPATH_VALUE);
92 CHECK_INT(n, 2); 92 CHECK_INT(n, 2);
93 CHECK_MEM(query, "AB", 2); 93 CHECK_MEM(query, "AB", 2);
94} 94}
@@ -100,7 +100,7 @@ static void case_bad_escape(void) {
100 ssize_t n; 100 ssize_t n;
101 101
102 test_case("a non hexadecimal escape is a parse error"); 102 test_case("a non hexadecimal escape is a parse error");
103 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 103 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
104 CHECK_INT(n, -1); 104 CHECK_INT(n, -1);
105} 105}
106 106
@@ -111,7 +111,7 @@ static void case_truncated_escape(void) {
111 ssize_t n; 111 ssize_t n;
112 112
113 test_case("an escape cut short by the terminator is a parse error"); 113 test_case("an escape cut short by the terminator is a parse error");
114 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 114 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
115 CHECK_INT(n, -1); 115 CHECK_INT(n, -1);
116} 116}
117 117
@@ -123,11 +123,11 @@ static void case_terminator(void) {
123 ssize_t n; 123 ssize_t n;
124 124
125 test_case("a hard terminator before any output reports end of input"); 125 test_case("a hard terminator before any output reports end of input");
126 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); 126 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_PARAM);
127 CHECK_INT(n, -2); 127 CHECK_INT(n, -2);
128 128
129 p = space; 129 p = space;
130 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); 130 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_PARAM);
131 CHECK_INT(n, -2); 131 CHECK_INT(n, -2);
132} 132}
133 133
@@ -138,10 +138,10 @@ static void case_terminator_after_output(void) {
138 ssize_t n; 138 ssize_t n;
139 139
140 test_case("a hard terminator after output is left for the next call"); 140 test_case("a hard terminator after output is left for the next call");
141 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 141 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
142 CHECK_INT(n, 3); 142 CHECK_INT(n, 3);
143 CHECK_INT(*p, 0); 143 CHECK_INT(*p, 0);
144 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 144 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
145 CHECK_INT(n, -2); 145 CHECK_INT(n, -2);
146} 146}
147 147
@@ -157,14 +157,14 @@ static void case_separators_are_contextual(void) {
157 ssize_t n; 157 ssize_t n;
158 158
159 test_case("a question mark is literal inside a value"); 159 test_case("a question mark is literal inside a value");
160 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 160 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
161 CHECK_INT(n, 3); 161 CHECK_INT(n, 3);
162 dest[n] = 0; 162 dest[n] = 0;
163 CHECK_STR(dest, "a?b"); 163 CHECK_STR(dest, "a?b");
164 164
165 test_case("an equals sign is literal inside a value"); 165 test_case("an equals sign is literal inside a value");
166 p = equals; 166 p = equals;
167 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 167 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
168 CHECK_INT(n, 3); 168 CHECK_INT(n, 3);
169 dest[n] = 0; 169 dest[n] = 0;
170 CHECK_STR(dest, "a=b"); 170 CHECK_STR(dest, "a=b");
@@ -177,7 +177,7 @@ static void case_ampersand_in_path(void) {
177 ssize_t n; 177 ssize_t n;
178 178
179 test_case("an ampersand in a path is a parse error"); 179 test_case("an ampersand in a path is a parse error");
180 n = urlencoded_parse(&p, dest, SCAN_PATH); 180 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_PATH);
181 CHECK_INT(n, -1); 181 CHECK_INT(n, -1);
182} 182}
183 183
@@ -188,7 +188,7 @@ static void case_raw_high_byte(void) {
188 ssize_t n; 188 ssize_t n;
189 189
190 test_case("an undecoded byte above ASCII is a parse error"); 190 test_case("an undecoded byte above ASCII is a parse error");
191 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); 191 n = urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE);
192 CHECK_INT(n, -1); 192 CHECK_INT(n, -1);
193} 193}
194 194
@@ -215,15 +215,15 @@ static void case_find_keywords(void) {
215 char *p = match; 215 char *p = match;
216 216
217 test_case("a known keyword returns its value"); 217 test_case("a known keyword returns its value");
218 CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); 218 CHECK_INT(urlencoded_match_keyword(keywords, &p, match + sizeof(match), SCAN_SEARCHPATH_PARAM), 2);
219 219
220 test_case("an unknown keyword returns -3"); 220 test_case("an unknown keyword returns -3");
221 p = nomatch; 221 p = nomatch;
222 CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); 222 CHECK_INT(urlencoded_match_keyword(keywords, &p, nomatch + sizeof(nomatch), SCAN_SEARCHPATH_PARAM), -3);
223 223
224 test_case("a prefix of a keyword does not match it"); 224 test_case("a prefix of a keyword does not match it");
225 p = prefix; 225 p = prefix;
226 CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); 226 CHECK_INT(urlencoded_match_keyword(keywords, &p, prefix + sizeof(prefix), SCAN_SEARCHPATH_PARAM), -3);
227} 227}
228 228
229/* The encoding direction. Which bytes survive unescaped is fixed by what 229/* The encoding direction. Which bytes survive unescaped is fixed by what
@@ -313,10 +313,30 @@ static void case_fmt_decoder_disagrees_on_dollar(void) {
313 dest[n] = 0; 313 dest[n] = 0;
314 CHECK_STR(dest, "a$b"); 314 CHECK_STR(dest, "a$b");
315 315
316 got = urlencoded_parse(&p, scanned, SCAN_SEARCHPATH_VALUE); 316 got = urlencoded_parse(&p, scanned, scanned + sizeof(scanned), SCAN_SEARCHPATH_VALUE);
317 CHECK_INT(got, -1); 317 CHECK_INT(got, -1);
318} 318}
319 319
320static void case_value_stops_at_dest_end(void) {
321 /* A value that decodes to more than the destination holds is a parse error,
322 not a write past the buffer. (ip= used to overflow ws->outbuf this way.) */
323 char query[] = "AAAAAAAAAA&x=1"; /* ten A's then a terminator */
324 char dest[4];
325 char *p = query;
326 test_case("a value longer than the destination returns -1");
327 CHECK_INT((int)urlencoded_parse(&p, dest, dest + sizeof(dest), SCAN_SEARCHPATH_VALUE), -1);
328}
329
330static void case_keyword_with_decoded_nul_does_not_match(void) {
331 /* A percent-encoded NUL used to make a value match a shorter keyword and
332 then index the keyword past its end. */
333 static const ot_keywords keywords[] = {{"port", 1}, {NULL, -3}};
334 char query[] = "port%00xxxx=1";
335 char *p = query;
336 test_case("a keyword followed by a decoded NUL does not match the keyword");
337 CHECK_INT(urlencoded_match_keyword(keywords, &p, query + sizeof(query), SCAN_SEARCHPATH_PARAM), -3);
338}
339
320void test_urlencoded(void) { 340void test_urlencoded(void) {
321 case_path(); 341 case_path();
322 case_param_and_value(); 342 case_param_and_value();
@@ -332,6 +352,8 @@ void test_urlencoded(void) {
332 case_raw_high_byte(); 352 case_raw_high_byte();
333 case_skipvalue(); 353 case_skipvalue();
334 case_find_keywords(); 354 case_find_keywords();
355 case_value_stops_at_dest_end();
356 case_keyword_with_decoded_nul_does_not_match();
335 case_fmt_passes_safe_bytes(); 357 case_fmt_passes_safe_bytes();
336 case_fmt_escapes_the_rest(); 358 case_fmt_escapes_the_rest();
337 case_fmt_hex_is_lower_case(); 359 case_fmt_hex_is_lower_case();