Inline the UTF-8 relabel at the body read
The helper had two call sites and one was guarding a case this stack
never produces: Puma sets PATH_INFO verbatim from REQUEST_PATH, still
percent-encoded, so a path-derived id is always ASCII. The body — forced
to binary by Rack::RewindableInput — is the one crossing, and one call
site is not a method.

Assisted-by: GLM-5.3 via pi
change ounmxwqnomnzxtqzvvyzssoxrkwmzknl
commit cc2f9572a1cdde05ac82ccd938b1270a6e69c544
author Alpha Chen <alpha@kejadlen.dev>
date
parent krvztmqz
diff --git a/AGENTS.md b/AGENTS.md
index 2646d17..a95c125 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -113,12 +113,15 @@ not a fixture; edit a `.vcf` to change what the replay serves.
   Never edit a migration that has run anywhere; add the next one.
 - Those tables are `STRICT`, so every string column needs `text: true`:
   Sequel's plain `String` is a varchar, which STRICT refuses. Strings
-  reach the store as UTF-8 by contract — `Web#utf8` relabels Rack's
-  binary (the request body, path segments carrying non-ASCII) where the
-  wire meets the route — and the adapter enforces it: the sqlite3 gem
-  encodes every bound value to UTF-8, so binary-flagged bytes above 7
-  bits raise at the bind, while bytes that are not UTF-8 at all are
-  refused by SQLite on the insert.
+  reach the store as UTF-8 by contract, and the adapter enforces it: the
+  sqlite3 gem encodes every bound value to UTF-8, so binary-flagged
+  bytes above 7 bits raise at the bind, while bytes that are not UTF-8
+  at all are refused by SQLite on the insert. The one binary input is
+  the request body — Rack requires input in ASCII-8BIT and
+  Rack::RewindableInput enforces it — relabelled to UTF-8 where it is
+  read, in `write_card`. Path-derived ids need no relabel: Puma hands
+  PATH_INFO over still percent-encoded (set verbatim from
+  REQUEST_PATH), so an id off the wire is ASCII.
 - The app is handed its store rather than reaching for one:
   `config.ru` builds it and sets `ProTacts::Web.store`, and a test does
   the same with a throwaway. There is no global `ProTacts.store`, so
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 7315b80..8b7366c 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -157,8 +157,8 @@ module ProTacts
     # to it: the sqlite3 gem encodes every bound value to UTF-8, so a
     # binary-flagged byte above 7 bits raises at the bind, and bytes
     # that are not UTF-8 at all stop at the insert, which SQLite refuses
-    # to store as text. Rack's binary is relabelled before this —
-    # Web#utf8, where the wire meets the route.
+    # to store as text. The body — the one binary input — is relabelled
+    # where it is read, in write_card.
     #: (String id, String vcard) -> Contact
     def put(id, vcard)
       contact = Contact.for(id:, vcard:)
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index 5d6bc0e..dd93a44 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -317,7 +317,7 @@ module ProTacts
           # Read on its own rather than through the collection: serving
           # one href has no reason to load every other contact first.
           r.get String do |filename|
-            contact = store.contact(utf8(filename.delete_suffix(".vcf")))
+            contact = store.contact(filename.delete_suffix(".vcf"))
 
             # No match falls through to the empty-body 404 that the
             # not_found handler fills in.
@@ -384,7 +384,13 @@ module ProTacts
     # CARDDAV:valid-address-data even when its If-Match is stale too.
     #: (String id) -> String
     def write_card(id)
-      vcard = utf8(request.body.read)
+      # The body is the one binary input: Rack requires input in
+      # ASCII-8BIT and Rack::RewindableInput enforces it again. Relabel
+      # rather than convert — the bytes are untouched, and the charset
+      # check below is what judges them. Paths need no counterpart:
+      # Puma hands PATH_INFO over still percent-encoded, so an id off
+      # the wire is ASCII.
+      vcard = request.body.read.force_encoding(Encoding::UTF_8)
 
       # CARDDAV:supported-address-data (RFC 6352 section 6.3.2.1): what
       # arrived must be a vCard, and text/vcard is the one media type
@@ -445,18 +451,6 @@ module ProTacts
       nil
     end
 
-    # Where the wire's bytes become the app's text. Rack hands the
-    # request body in as ASCII-8BIT, and path segments carrying non-ASCII
-    # likewise (its spec's rule for CGI values), while everything past
-    # the route — the store, the parser, the responses — speaks UTF-8.
-    # Relabelling is not converting: the bytes are untouched, and a body
-    # that is not UTF-8 at all is caught by the charset check in
-    # write_card rather than here.
-    #: (String wire) -> String
-    def utf8(wire)
-      wire.encoding == Encoding::BINARY ? wire.dup.force_encoding(Encoding::UTF_8) : wire
-    end
-
     # A 412 whose body names the CardDAV precondition that failed, in
     # the DAV:error form RFC 4918 section 16 defines; the element names
     # are RFC 6352 section 6.3.2.1's, qualified by the card: namespace