core: fix /start remote host parser when iface name contains a space (#7025)

* core: fix /start remote host parser when iface name contains a space

The iface= field used jsonP (which calls takeByteString and strict-decodes
the entire remaining input as JSON). When port= followed iface=, the strict
decode failed on the trailing data and the text1P fallback stopped at the
first space inside the JSON-quoted interface name (e.g. "Ethernet 2"),
leaving unparseable junk and producing "Failed reading: empty".

Replace jsonP with a bounded quotedP that consumes only up to the closing
quote, leaving port=… for the next parser.

* plan: document fix for /start remote host iface-with-space parser bug
This commit is contained in:
Narasimha-sc
2026-05-30 06:33:10 +00:00
committed by GitHub
parent 68abd805d4
commit 5aace8401c
3 changed files with 24 additions and 2 deletions
@@ -0,0 +1,13 @@
# Fix `/start remote host` parser when iface name contains a space
## Problem
Picking a non-default interface (e.g. Windows `Ethernet 2`) on the "Link a mobile" screen and refreshing the QR code returns `error chat: error chat commandError Failed reading: empty`. The desktop UI sends `/start remote host new addr=… iface="Ethernet 2" port=…`; the chat backend rejects it as an unparseable command. Without a workaround the user can't pin a specific local interface for the mobile-link controller.
## Cause
`rcCtrlAddressP` parses the iface value with `jsonP <|> text1P` (`src/Simplex/Chat/Library/Commands.hs:5549`). `jsonP` calls `A.takeByteString`, consuming *all* remaining input, then runs `eitherDecodeStrict'`. When `port=…` follows `iface=…` the strict decode fails because the JSON value `"Ethernet 2"` has trailing junk after it, so attoparsec backtracks to `text1P` (`takeTill (== ' ')`). `text1P` stops at the first space — inside the JSON quotes — leaving `2" port=12345` which nothing downstream can consume, `A.endOfInput` fails, the whole `A.choice` exhausts and surfaces attoparsec's `empty` message. With an iface name that has no space (`"lo"`) the bug is invisible: text1P swallows the full quoted token and the rest parses, but the interface name is stored with literal quotes so the iface preference silently never matches a real adapter anyway.
## Fix
Replace `jsonP` with a bounded `quotedP` that consumes only the bytes between `"…"` and leaves trailing fields for the next parser. `text1P` is kept as the unquoted fallback. Two-line change in `Commands.hs` plus a pure regression test in `tests/RemoteTests.hs` that asserts `parseChatCommand` of `/start remote host new addr=192.168.1.5 iface="Ethernet 2" port=12345` produces `RCCtrlAddress _ "Ethernet 2"` with port `12345`.
+2 -1
View File
@@ -5526,7 +5526,8 @@ chatCommandP =
addressAA = AddressSettings False <$> (Just . AutoAccept <$> (" incognito=" *> onOffP <|> pure False)) <*> autoReply
businessAA = " business" *> (AddressSettings True (Just $ AutoAccept False) <$> autoReply)
autoReply = optional (A.space *> msgContentP)
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (quotedP <|> text1P))
quotedP = safeDecodeUtf8 <$> (A.char '"' *> A.takeTill (== '"') <* A.char '"')
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
char_ = optional . A.char
+9 -1
View File
@@ -16,7 +16,8 @@ import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.List (find, isPrefixOf)
import qualified Data.Map.Strict as M
import Simplex.Chat.Controller (ChatConfig (..), versionNumber)
import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), versionNumber)
import Simplex.Chat.Library.Commands (parseChatCommand)
import qualified Simplex.Chat.Controller as Controller
import Simplex.Chat.Mobile.File
import Simplex.Chat.Remote (remoteFilesFolder)
@@ -24,6 +25,7 @@ import Simplex.Chat.Remote.Types
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Util
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
import System.FilePath ((</>))
import Test.Hspec hiding (it)
import UnliftIO
@@ -32,6 +34,12 @@ import UnliftIO.Directory
remoteTests :: SpecWith TestParams
remoteTests = describe "Remote" $ do
describe "/start remote host parser" $ do
it "parses iface name with a space followed by port=" $ \_ ->
parseChatCommand "/start remote host new addr=192.168.1.5 iface=\"Ethernet 2\" port=12345"
`shouldSatisfy` \case
Right (StartRemoteHost Nothing (Just (RCCtrlAddress _ "Ethernet 2")) (Just 12345)) -> True
_ -> False
xdescribe "No compression" $ aroundWith (. ((False, False),)) runRemoteTests
xdescribe "Mobile offers compression" $ aroundWith (. ((True, False),)) runRemoteTests
xdescribe "Desktop offers compression" $ aroundWith (. ((False, True),)) runRemoteTests