Skip to content

Commit 1e54272

Browse files
authored
Merge pull request #10 from ncmud/mxp-parity
MXP negotiation parity: C, Kotlin, and Swift client
2 parents ffbf0f7 + 97d683e commit 1e54272

11 files changed

Lines changed: 134 additions & 2 deletions

File tree

Sources/SwiftMTHClient/TelnetClientSession.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public final class TelnetClientSession {
2424
public private(set) var gmcpEnabled: Bool = false
2525
public private(set) var msdpEnabled: Bool = false
2626
public private(set) var msspEnabled: Bool = false
27+
public private(set) var mxpEnabled: Bool = false
2728

2829
// MARK: - Private State
2930

@@ -188,6 +189,10 @@ public final class TelnetClientSession {
188189
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.GMCP],
189190
handler: { s, src, i, n in s.processSbGmcp(src, at: i, srclen: n) }),
190191

192+
// MXP — accept the server's offer so it may send MXP markup.
193+
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.MXP],
194+
handler: { s, _, _, _ in s.processWillMxp(); return 3 }),
195+
191196
// MSDP
192197
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.MSDP],
193198
handler: { s, _, _, _ in s.processWillMsdp(); return 3 }),
@@ -314,6 +319,14 @@ public final class TelnetClientSession {
314319
delegate?.onGMCPNegotiated()
315320
}
316321

322+
// MARK: - Handler: MXP
323+
324+
private func processWillMxp() {
325+
mxpEnabled = true
326+
serverOptions.insert(TO.MXP)
327+
write([TC.IAC, TC.DO, TO.MXP])
328+
}
329+
317330
private func processSbGmcp(_ src: [UInt8], at offset: Int, srclen: Int) -> Int {
318331
let sbLen = skipSB(src, at: offset, srclen: srclen)
319332
if sbLen > srclen { return srclen + 1 }

Sources/cmth/mth.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ struct telnet_type telnet_table[] =
180180
{ "88", 0 },
181181
{ "89", 0 },
182182
{ "MSP", 0 },
183-
{ "MXP", 0 },
183+
{ "MXP", ANNOUNCE_WILL },
184184
{ "MSP2", 0 }, /* Unadopted */
185185
{ "ZMP", 0 }, /* Unadopted */
186186
{ "94", 0 },

Sources/cmth/mth.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ typedef struct descriptor_data DESCRIPTOR_DATA;
6262
#define COMM_FLAG_256COLORS BV06
6363
#define COMM_FLAG_UTF8 BV07
6464
#define COMM_FLAG_GMCP BV08
65+
#define COMM_FLAG_MXP BV09
6566

6667
#define MSDP_FLAG_COMMAND BV01
6768
#define MSDP_FLAG_LIST BV02

Sources/cmth/telopt.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ int process_do_msdp ( DESCRIPTOR_DATA *d, unsigned char *src, i
2121
int process_sb_msdp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
2222
int process_do_gmcp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
2323
int process_sb_gmcp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
24+
int process_do_mxp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
25+
int process_dont_mxp ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
2426
int process_do_mccp2 ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
2527
int process_dont_mccp2 ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
2628
int skip_sb ( DESCRIPTOR_DATA *d, unsigned char *src, int srclen );
@@ -67,6 +69,9 @@ const struct telopt_type telopt_table [] =
6769
{ 3, (unsigned char []) { IAC, DO, TELOPT_GMCP, 0 }, &process_do_gmcp},
6870
{ 3, (unsigned char []) { IAC, SB, TELOPT_GMCP, 0 }, &process_sb_gmcp},
6971

72+
{ 3, (unsigned char []) { IAC, DO, TELOPT_MXP, 0 }, &process_do_mxp},
73+
{ 3, (unsigned char []) { IAC, DONT, TELOPT_MXP, 0 }, &process_dont_mxp},
74+
7075
{ 3, (unsigned char []) { IAC, DO, TELOPT_MCCP2, 0 }, &process_do_mccp2},
7176
{ 3, (unsigned char []) { IAC, DONT, TELOPT_MCCP2, 0 }, &process_dont_mccp2},
7277

@@ -863,6 +868,32 @@ int process_sb_msdp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
863868
return i + 1;
864869
}
865870

871+
// MXP
872+
873+
int process_do_mxp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
874+
{
875+
if (HAS_BIT(d->mth->comm_flags, COMM_FLAG_MXP))
876+
{
877+
return 3;
878+
}
879+
880+
SET_BIT(d->mth->comm_flags, COMM_FLAG_MXP);
881+
882+
/* ESC[7z — Lock Locked: make "locked" the persistent default line mode, so
883+
normal output is never parsed as MXP markup. Links opt back in per-span. */
884+
descriptor_printf(d, "\033[7z");
885+
log_descriptor_printf(d, "INFO MXP ENABLED");
886+
887+
return 3;
888+
}
889+
890+
int process_dont_mxp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )
891+
{
892+
DEL_BIT(d->mth->comm_flags, COMM_FLAG_MXP);
893+
894+
return 3;
895+
}
896+
866897
// MSDP over GMCP
867898

868899
int process_do_gmcp( DESCRIPTOR_DATA *d, unsigned char *src, int srclen )

Tests/MTHTests/TelnetClientSessionTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ struct TelnetClientSessionTests {
5757
#expect(out == input)
5858
}
5959

60+
@Test func mxpWillIsAcceptedWithDo() {
61+
let (s, d) = makeSession()
62+
#expect(!s.mxpEnabled)
63+
_ = s.processInput([TC.IAC, TC.WILL, TO.MXP])
64+
#expect(s.mxpEnabled)
65+
#expect(d.allWrittenBytes.containsSequence([TC.IAC, TC.DO, TO.MXP]))
66+
}
67+
6068
@Test func emptyInput() {
6169
let (s, _) = makeSession()
6270
let out = s.processInput([])

kotlin/mth-core/src/main/kotlin/mth/core/AnnounceFlags.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ val defaultTelnetTable: List<TelnetOptionEntry> = buildList {
3232
this[86] = TelnetOptionEntry("MCCP2", AnnounceFlags.WILL)
3333
this[87] = TelnetOptionEntry("MCCP3", AnnounceFlags.WILL)
3434
this[90] = TelnetOptionEntry("MSP", AnnounceFlags.WILL)
35-
this[91] = TelnetOptionEntry("MXP")
35+
this[91] = TelnetOptionEntry("MXP", AnnounceFlags.WILL)
3636
this[201] = TelnetOptionEntry("GMCP", AnnounceFlags.WILL)
3737
}

kotlin/mth-core/src/main/kotlin/mth/core/CommFlags.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ value class CommFlags(val rawValue: Int = 0) {
1616
val COLORS_256 = CommFlags(1 shl 5)
1717
val UTF8 = CommFlags(1 shl 6)
1818
val GMCP = CommFlags(1 shl 7)
19+
val MXP = CommFlags(1 shl 8)
1920
}
2021
}

kotlin/mth-core/src/main/kotlin/mth/core/client/TelnetClientSession.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class TelnetClientSession(
3939
var msspEnabled: Boolean = false
4040
private set
4141

42+
/** Whether MXP has been negotiated. */
43+
var mxpEnabled: Boolean = false
44+
private set
45+
4246
// -- Private State --
4347

4448
/** Buffer for incomplete telnet sequences (packet fragmentation). */
@@ -230,6 +234,10 @@ class TelnetClientSession(
230234
TeloptPattern(byteArrayOf(TC.IAC, TC.SB, TO.GMCP))
231235
{ s, src, i, n -> s.processSbGmcp(src, i, n) },
232236

237+
// Server offers MXP — accept so it may send MXP markup.
238+
TeloptPattern(byteArrayOf(TC.IAC, TC.WILL, TO.MXP))
239+
{ s, _, _, _ -> s.processWillMxp(); 3 },
240+
233241
// Server offers MCCP2
234242
TeloptPattern(byteArrayOf(TC.IAC, TC.WILL, TO.MCCP2))
235243
{ s, _, _, _ -> s.processWillMccp2(); 3 },
@@ -346,6 +354,12 @@ class TelnetClientSession(
346354
delegate?.onGMCPNegotiated()
347355
}
348356

357+
private fun processWillMxp() {
358+
mxpEnabled = true
359+
serverOptions.add(TO.MXP)
360+
write(byteArrayOf(TC.IAC, TC.DO, TO.MXP))
361+
}
362+
349363
private fun processSbGmcp(src: ByteArray, offset: Int, srclen: Int): Int {
350364
val sbLen = skipSB(src, offset, srclen)
351365
if (sbLen > srclen) return srclen + 1

kotlin/mth-core/src/main/kotlin/mth/core/server/TelnetSession.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ class TelnetSession(
113113
write(byteArrayOf(TC.IAC, TC.WILL, TO.GMCP))
114114
}
115115

116+
/** Whether the client negotiated MXP (telnet option 91). */
117+
val mxpEnabled: Boolean get() = CommFlags.MXP in commFlags
118+
119+
/** Re-assert the locked-default MXP line mode after a copyover restore. */
120+
fun reassertMXP() {
121+
if (CommFlags.MXP in commFlags) write(MXP_LOCKED_DEFAULT)
122+
}
123+
116124
/** Send echo-off (password mode). */
117125
fun sendEchoOff() {
118126
commFlags = commFlags.insert(CommFlags.PASSWORD)
@@ -342,6 +350,11 @@ class TelnetSession(
342350
TeloptPattern(byteArrayOf(TC.IAC, TC.SB, TO.GMCP))
343351
{ s, src, i, n -> s.processSbGmcp(src, i, n) },
344352

353+
TeloptPattern(byteArrayOf(TC.IAC, TC.DO, TO.MXP))
354+
{ s, _, _, _ -> s.processDoMxp(); 3 },
355+
TeloptPattern(byteArrayOf(TC.IAC, TC.DONT, TO.MXP))
356+
{ s, _, _, _ -> s.processDontMxp(); 3 },
357+
345358
// MCCP2
346359
TeloptPattern(byteArrayOf(TC.IAC, TC.DO, TO.MCCP2))
347360
{ s, _, _, _ -> s.processDoMccp2(); 3 },
@@ -728,6 +741,19 @@ class TelnetSession(
728741
return sbLen
729742
}
730743

744+
// -- Handler: MXP --
745+
746+
private fun processDoMxp() {
747+
if (CommFlags.MXP in commFlags) return
748+
commFlags = commFlags.insert(CommFlags.MXP)
749+
write(MXP_LOCKED_DEFAULT)
750+
log("INFO MXP ENABLED")
751+
}
752+
753+
private fun processDontMxp() {
754+
commFlags = commFlags.remove(CommFlags.MXP)
755+
}
756+
731757
// -- Handler: GMCP --
732758

733759
private fun processDoGmcp() {
@@ -857,4 +883,10 @@ class TelnetSession(
857883
log("MCCP3: COMPRESSION END")
858884
mccp3 = null
859885
}
886+
887+
private companion object {
888+
// ESC[7z — Lock Locked: makes "locked" the persistent default line mode so normal
889+
// output is never parsed as MXP markup; links opt back in per-span with ESC[1z…ESC[2z.
890+
val MXP_LOCKED_DEFAULT = byteArrayOf(0x1B, 0x5B, 0x37, 0x7A)
891+
}
860892
}

kotlin/mth-core/src/test/kotlin/mth/core/client/TelnetClientSessionTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ class TelnetClientSessionTest {
124124
assertContentEquals(byteArrayOf(IAC, DO, GMCP), d.allWrittenBytes)
125125
}
126126

127+
@Test fun serverWillMxpRespondsDoMxp() {
128+
val (s, d) = makeSession()
129+
assertFalse(s.mxpEnabled)
130+
s.processInput(bytes(0xFF, 0xFB, 91))
131+
assertTrue(s.mxpEnabled)
132+
assertContentEquals(bytes(0xFF, 0xFD, 91), d.allWrittenBytes)
133+
}
134+
127135
@Test fun serverSendsGmcpDataParsed() {
128136
val (s, d) = makeSession()
129137
s.processInput(byteArrayOf(IAC, WILL, GMCP))

0 commit comments

Comments
 (0)