diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/harness.py | 5 | ||||
| -rw-r--r-- | tests/integration/test_accesslist.py | 4 | ||||
| -rw-r--r-- | tests/integration/test_dynamic_accesslist.py | 18 | ||||
| -rw-r--r-- | tests/integration/test_fullscrape.py | 14 | ||||
| -rw-r--r-- | tests/integration/test_keepalive.py | 16 | ||||
| -rw-r--r-- | tests/integration/test_livesync.py | 13 | ||||
| -rw-r--r-- | tests/integration/test_statefile.py | 41 | ||||
| -rw-r--r-- | tests/t_clean.c | 8 | ||||
| -rw-r--r-- | tests/t_urlencoded.c | 66 |
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 | ||
| 8 | import os | 8 | import os |
| 9 | import struct | 9 | import struct |
| 10 | import time | ||
| 10 | import unittest | 11 | import unittest |
| 11 | 12 | ||
| 12 | from harness import Tracker, TrackerTestCase | 13 | from 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 | ||
| 9 | import gzip | 9 | import gzip |
| 10 | import struct | 10 | import struct |
| 11 | import time | ||
| 11 | import unittest | 12 | import unittest |
| 12 | 13 | ||
| 13 | from harness import TrackerTestCase, bdecode | 14 | from 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 | ||
| 3 | Nothing exercised the load path before, and the dump's second column is now | 3 | The second column is the minute a torrent was last touched, by the wall |
| 4 | how long ago a torrent was last touched rather than when, because the clock | 4 | clock, which is what the file has recorded for years and the only clock two |
| 5 | counts from boot and an absolute stamp would mean nothing to the process | 5 | runs of the tracker agree on. |
| 6 | that reads the file. | ||
| 7 | """ | 6 | """ |
| 8 | 7 | ||
| 9 | import os | 8 | import os |
| 9 | import time | ||
| 10 | import unittest | 10 | import unittest |
| 11 | 11 | ||
| 12 | from harness import Tracker, TrackerTestCase | 12 | from harness import Tracker, TrackerTestCase |
| @@ -54,23 +54,26 @@ class StateFileRoundTrip(TrackerTestCase): | |||
| 54 | restarted._cleanup() | 54 | restarted._cleanup() |
| 55 | 55 | ||
| 56 | 56 | ||
| 57 | class StateFileWithNonsenseAges(TrackerTestCase): | 57 | class 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 */ |
| 62 | static void step_clock_back(ot_torrent *torrent, int minutes) { | 62 | static 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 */ |
| 68 | static void age_by_a_minute(ot_torrent *torrent) { | 67 | static 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 | ||
| 73 | static void case_a_clock_that_went_back_keeps_the_swarm(void) { | 71 | static 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 | ||
| 320 | static 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 | |||
| 330 | static 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 | |||
| 320 | void test_urlencoded(void) { | 340 | void 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(); |
