tep 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
@@ -0,0 +1,168 @@
1
+ # Tep::Parallel -- grosser/parallel-shaped process fan-out.
2
+ #
3
+ # Why
4
+ # ---
5
+ # Spinel doesn't ship Ractors, doesn't expose the GVL'd threading
6
+ # story, and the `parallel` gem (heavy use of `Marshal`,
7
+ # `IO.pipe`, dynamic `Proc` invocation) doesn't lower. Fork is
8
+ # however a perfectly cheap C call here, so the smallest useful
9
+ # slice of `parallel` -- "run this worker over a list of items,
10
+ # one child per item, collect the results" -- is implementable
11
+ # directly on top of sphttp's `sphttp_fork` + a tiny file-based
12
+ # IPC channel.
13
+ #
14
+ # API
15
+ # ---
16
+ # results = Tep::Parallel.map_processes(items, worker)
17
+ # #=> [String, String, ...] -- one entry per input, in order
18
+ #
19
+ # `worker.run(item)` must return a String. Each child runs the
20
+ # worker once, writes its return value to a per-index file under
21
+ # /tmp, exits; the parent reaps everyone and reads the files
22
+ # back. The String constraint exists because passing structured
23
+ # data across fork would need Marshal, which spinel doesn't
24
+ # emit -- and HTTP-shaped APIs (the dashboard) round-trip
25
+ # strings naturally.
26
+ #
27
+ # Fire-and-forget shape:
28
+ #
29
+ # Tep::Parallel.each_process(items, worker)
30
+ #
31
+ # Forks one child per item, doesn't capture results.
32
+ #
33
+ # Scope (v1)
34
+ # ----------
35
+ # * One child per item -- no fixed-size pool. Fine up to a few
36
+ # dozen items; for larger fan-outs the caller should chunk
37
+ # beforehand or write the round-trip into Tep::Job.
38
+ # * String return values only.
39
+ # * No thread mode -- spinel doesn't lower MRI's Thread reliably.
40
+ #
41
+ # Closeness to grosser/parallel
42
+ # -----------------------------
43
+ # `parallel`'s top-level API is
44
+ #
45
+ # Parallel.map(items, in_processes: N) { |x| ... }
46
+ #
47
+ # spinel can't take a block as a value, so we lift the body into
48
+ # a Worker class instead. Spinel also can't auto-cast subclass
49
+ # pointers at cmeth call sites (#429-shaped), which means cmeth
50
+ # args typed as a worker base class widen to poly at the call
51
+ # site and the C compile fails. The fix: store the worker in an
52
+ # instance field of `Tep::Parallel` -- typed-slot imeth dispatch
53
+ # works the same way `@before_filter.before(req, res)` does for
54
+ # `Tep::Filter`. Resulting shape:
55
+ #
56
+ # p = Tep::Parallel.new(MyWorker.new)
57
+ # results = p.map_processes(items)
58
+ #
59
+ # Worker base class
60
+ # -----------------
61
+ # Real workers subclass `Tep::ParallelWorker` and override `run(item)`.
62
+ # Two spinel landings made this name viable: matz/spinel#531 (270eceb)
63
+ # narrowed the poly-receiver dispatch table by ivar observed-class set
64
+ # (so `Tep::Server#run` no longer leaks into `@worker.run`'s switch),
65
+ # and matz/spinel#549 (1d561ad) collapsed the dispatch result to a
66
+ # scalar when all reachable arms agree on the return type (so the
67
+ # result lands as `const char *` instead of sp_RbVal).
68
+ module Tep
69
+ # Base class for Tep::Parallel workers. Override `run(item)` in
70
+ # subclasses; the default emits "" so a base-class instance used
71
+ # for seeding stays type-safe.
72
+ class ParallelWorker
73
+ def run(item)
74
+ ""
75
+ end
76
+ end
77
+
78
+ class Parallel
79
+ attr_accessor :worker
80
+
81
+ def initialize(worker)
82
+ @worker = worker
83
+ end
84
+
85
+ # Result-collecting fan-out. Returns an Array of Strings in
86
+ # input order; one fork per item. See module doc for the
87
+ # constraints (Strings only, no fixed pool).
88
+ def map_processes(items)
89
+ job_dir = Parallel.scratch_dir
90
+ Tep::Shell.run("mkdir -p " + job_dir)
91
+
92
+ n = items.length
93
+ i = 0
94
+ while i < n
95
+ # Pull each fork into its own stack frame -- spinel's
96
+ # codegen for the in-line fork-and-exec pattern was
97
+ # observed to share locals across the parent loop and
98
+ # the child body, so all children ended up processing
99
+ # the same (last) item. Method-call boundary gives each
100
+ # child a clean local snapshot.
101
+ spawn_one(items[i], i, job_dir)
102
+ i += 1
103
+ end
104
+
105
+ reaped = 0
106
+ while reaped < n
107
+ Sock.sphttp_wait_any
108
+ reaped += 1
109
+ end
110
+
111
+ out = [""]
112
+ out.delete_at(0)
113
+ k = 0
114
+ while k < n
115
+ out.push(Tep::Shell.read(job_dir + "/" + k.to_s))
116
+ k += 1
117
+ end
118
+ Tep::Shell.run("rm -rf " + job_dir)
119
+ out
120
+ end
121
+
122
+ # Fork one child to process `item`. When `job_dir` is non-empty,
123
+ # the child writes the worker's String result to `job_dir/idx`
124
+ # (consumed by map_processes); otherwise the result is discarded
125
+ # (fire-and-forget shape used by each_process). Returns the child
126
+ # pid in the parent; the child never returns (exits when done).
127
+ #
128
+ # The method-call boundary is load-bearing: an inline fork-and-
129
+ # exec loop body shared locals across iterations under spinel's
130
+ # codegen, so every child processed the same (last) item. A
131
+ # separate def gives each fork a clean local frame.
132
+ def spawn_one(item, idx, job_dir)
133
+ pid = Sock.sphttp_fork
134
+ if pid == 0
135
+ result = @worker.run(item)
136
+ if job_dir.length > 0
137
+ path = job_dir + "/" + idx.to_s
138
+ File.write(path, result)
139
+ end
140
+ Sock.sphttp_exit(0)
141
+ end
142
+ pid
143
+ end
144
+
145
+ # Fire-and-forget version. Returns 0 once every child exits.
146
+ def each_process(items)
147
+ n = items.length
148
+ i = 0
149
+ while i < n
150
+ spawn_one(items[i], 0, "")
151
+ i += 1
152
+ end
153
+ reaped = 0
154
+ while reaped < n
155
+ Sock.sphttp_wait_any
156
+ reaped += 1
157
+ end
158
+ 0
159
+ end
160
+
161
+ # Per-invocation scratch directory. Uses pid + monotonic
162
+ # timestamp so concurrent map_processes calls in different
163
+ # workers don't trample each other.
164
+ def self.scratch_dir
165
+ "/tmp/tep_par_" + Sock.sphttp_getpid.to_s + "_" + Time.now.to_i.to_s
166
+ end
167
+ end
168
+ end
data/lib/tep/parser.rb ADDED
@@ -0,0 +1,81 @@
1
+ # HTTP/1.x request parser. Produces a Tep::Request from the raw
2
+ # byte blob the C helper read off the wire (headers, possibly a
3
+ # prefix of the body).
4
+ module Tep
5
+ class Parser
6
+ # Returns a fully-populated Request, or nil if the blob is malformed.
7
+ def self.parse(blob)
8
+ # Note: Spinel's String#index returns -1 (not nil) when not found.
9
+ end_of_headers = Tep.str_find(blob, "\r\n\r\n", 0)
10
+ if end_of_headers < 0
11
+ return nil
12
+ end
13
+ headers_blob = blob[0, end_of_headers]
14
+ lines = headers_blob.split("\r\n")
15
+ if lines.length == 0
16
+ return nil
17
+ end
18
+
19
+ first = lines[0]
20
+ first_parts = first.split(" ")
21
+ if first_parts.length < 3
22
+ return nil
23
+ end
24
+
25
+ req = Request.new
26
+ req.verb = first_parts[0]
27
+ req.raw_path = first_parts[1]
28
+ req.http_version = first_parts[2]
29
+
30
+ qmark = Tep.str_find(req.raw_path, "?", 0)
31
+ if qmark < 0
32
+ req.path = req.raw_path
33
+ else
34
+ req.path = req.raw_path[0, qmark]
35
+ qstring = req.raw_path[qmark + 1, req.raw_path.length - qmark - 1]
36
+ req.query = Url.parse_query(qstring)
37
+ end
38
+
39
+ i = 1
40
+ while i < lines.length
41
+ line = lines[i]
42
+ colon = Tep.str_find(line, ":", 0)
43
+ if colon >= 0
44
+ name = line[0, colon].downcase
45
+ value = line[colon + 1, line.length - colon - 1].strip
46
+ req.req_headers[name] = value
47
+ end
48
+ i += 1
49
+ end
50
+
51
+ # Pre-merge query into params; path captures will be folded in
52
+ # by the router on a successful match.
53
+ req.query.each do |k, v|
54
+ req.params[k] = v
55
+ end
56
+
57
+ # Parse Cookie header into req.cookies. Format: "k=v; k2=v2; ...".
58
+ # Whitespace around `;` is allowed and stripped.
59
+ cookie_blob = req.req_headers["cookie"]
60
+ if cookie_blob.length > 0
61
+ cookie_blob.split(";").each do |pair|
62
+ eq = Tep.str_find(pair, "=", 0)
63
+ if eq > 0
64
+ cname = pair[0, eq].strip
65
+ cvalue = pair[eq + 1, pair.length - eq - 1].strip
66
+ req.cookies[cname] = Url.unescape(cvalue)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Carry over any body bytes already in the blob (the C helper
72
+ # may have read more than just the headers in one recv()).
73
+ body_start = end_of_headers + 4
74
+ if body_start < blob.length
75
+ req.raw_body = blob[body_start, blob.length - body_start]
76
+ end
77
+
78
+ req
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,102 @@
1
+ # Tep::Password -- password hashing for spinel-AOT'd apps.
2
+ #
3
+ # Uses PBKDF2-HMAC-SHA256 with a 16-byte CSPRNG salt and a default
4
+ # of 200,000 iterations -- sits in the OWASP-recommended ballpark
5
+ # (200k for SHA256 as of 2023). Backed by a small C helper in
6
+ # tep_crypto.c; no libcrypt / OpenSSL / bcrypt-gem dependency.
7
+ #
8
+ # Why PBKDF2 instead of bcrypt?
9
+ # -----------------------------
10
+ # Bcrypt is the textbook choice but its canonical impls are either
11
+ # the system `crypt(3)` (not portable: macOS doesn't ship $2b$, Linux
12
+ # needs libxcrypt) or the bcrypt-ruby gem (a CRuby C extension that
13
+ # spinel can't load). PBKDF2-SHA256 is in NIST SP 800-132 and OWASP
14
+ # acceptable, builds on the HMAC-SHA256 we already ship for the
15
+ # session store, and adds zero new system dependencies.
16
+ #
17
+ # scrypt / argon2 would be stronger but require linking libsodium or
18
+ # vendoring ~2k lines of C. Defer until callers need them.
19
+ #
20
+ # Format
21
+ # ------
22
+ # Stored hash is `pbkdf2-sha256$<iters>$<salt_b64>$<derived_b64>`.
23
+ # All segments are base64url, no padding. Self-describing so a
24
+ # future rotation to higher iter counts (or a different scheme) can
25
+ # coexist with old hashes -- `verify` honours the embedded iter
26
+ # count.
27
+ #
28
+ # Usage
29
+ # -----
30
+ #
31
+ # stored = Tep::Password.hash("user-input")
32
+ # # store `stored` in the DB
33
+ #
34
+ # # On login:
35
+ # if Tep::Password.verify("user-input", stored)
36
+ # # session.set("uid", row_id)
37
+ # end
38
+ module Tep
39
+ class Password
40
+ DEFAULT_ITERS = 200000
41
+ SALT_BYTES = 16
42
+
43
+ # Derive a stored hash from a plain password. Generates a
44
+ # fresh CSPRNG salt and runs PBKDF2-SHA256 at the default
45
+ # iter count. Returns the self-describing storage string.
46
+ # Named `hash` to match the bcrypt-gem-style factory shape.
47
+ def self.hash(plain)
48
+ salt = Crypto.sp_crypto_random_b64url(SALT_BYTES)
49
+ derived = Crypto.sp_crypto_pbkdf2_sha256_b64url(plain, salt, DEFAULT_ITERS)
50
+ "pbkdf2-sha256$" + DEFAULT_ITERS.to_s + "$" + salt + "$" + derived
51
+ end
52
+
53
+ # Verify `plain` against a stored hash. Re-runs PBKDF2 with the
54
+ # same salt + iter count embedded in the stored string and
55
+ # constant-time compares. Rejects malformed stored hashes by
56
+ # returning false.
57
+ def self.verify(plain, stored)
58
+ parts = Password.split4(stored)
59
+ if parts[0] != "pbkdf2-sha256"
60
+ return false
61
+ end
62
+ iters_s = parts[1]
63
+ salt = parts[2]
64
+ derived = parts[3]
65
+ if iters_s.length == 0 || salt.length == 0 || derived.length == 0
66
+ return false
67
+ end
68
+ iters = iters_s.to_i
69
+ if iters < 1
70
+ return false
71
+ end
72
+ candidate = Crypto.sp_crypto_pbkdf2_sha256_b64url(plain, salt, iters)
73
+ Tep::Jwt.timing_safe_eq(candidate, derived)
74
+ end
75
+
76
+ # Split a 4-segment "$"-delimited stored hash into its four
77
+ # parts. spinel's `String#split` exists but its behaviour on
78
+ # complex inputs has tripped us before; the explicit walker
79
+ # is small and obviously correct.
80
+ def self.split4(s)
81
+ out = ["", "", "", ""]
82
+ n = s.length
83
+ seg = 0
84
+ start = 0
85
+ i = 0
86
+ while i < n
87
+ if s[i] == "$"
88
+ if seg < 4
89
+ out[seg] = s[start, i - start]
90
+ end
91
+ seg += 1
92
+ start = i + 1
93
+ end
94
+ i += 1
95
+ end
96
+ if seg < 4
97
+ out[seg] = s[start, n - start]
98
+ end
99
+ out
100
+ end
101
+ end
102
+ end