Skip to content

Commit 1556d1d

Browse files
committed
Make MCCP conditional on zlib availability, add Windows CI.
1 parent 8b5d509 commit 1556d1d

6 files changed

Lines changed: 93 additions & 50 deletions

File tree

.github/workflows/swift.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ jobs:
1010
build:
1111
strategy:
1212
matrix:
13-
os: [macos-latest, ubuntu-latest]
13+
os: [macos-latest, ubuntu-latest, windows-latest]
1414
runs-on: ${{ matrix.os }}
1515
steps:
1616
- uses: actions/checkout@v4
17+
- name: Install Swift
18+
if: runner.os == 'Windows'
19+
uses: SwiftyLab/setup-swift@latest
1720
- name: Build
1821
run: swift build -v
1922
- name: Test

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ let package = Package(
3939
// New Swift targets
4040
.target(
4141
name: "MTH",
42-
dependencies: ["CZlib"],
42+
dependencies: [
43+
.target(name: "CZlib", condition: .when(platforms: [.macOS, .linux])),
44+
],
4345
path: "Sources/SwiftMTH"
4446
),
4547
.target(

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ let output = substituteColor("^RBold Red ^ggreen^x", depth: .trueColor)
8686

8787
## Platforms
8888

89-
macOS and Linux. Requires system zlib (present in macOS SDK and as a Swift toolchain dependency on Linux).
89+
macOS, Linux, and Windows. MCCP2/MCCP3 compression requires system zlib (present in macOS SDK and as a Swift toolchain dependency on Linux). On Windows, the library builds and runs without compression support — MCCP is compiled out and all other telnet options work normally.
9090

9191
## License
9292

Sources/SwiftMTH/Compression.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#if canImport(CZlib)
12
import CZlib
23

34
/// Streaming deflate compressor for MCCP2 (server → client compression).
@@ -188,3 +189,4 @@ public final class InflateStream {
188189
}
189190
}
190191
}
192+
#endif

Sources/SwiftMTH/TelnetSession.swift

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,32 @@ public final class TelnetSession {
2828
/// The MSDP variable definitions to use when MSDP is initialized.
2929
public let msdpTable: [MSDPVariableDefinition]
3030

31+
#if canImport(CZlib)
3132
/// Whether MCCP2 compression is active for outbound data.
3233
public var isMCCP2Active: Bool { mccp2 != nil }
3334

3435
/// Whether MCCP3 decompression is active for inbound data.
3536
public var isMCCP3Active: Bool { mccp3 != nil }
37+
#else
38+
/// Whether MCCP2 compression is active for outbound data (unavailable without zlib).
39+
public var isMCCP2Active: Bool { false }
40+
41+
/// Whether MCCP3 decompression is active for inbound data (unavailable without zlib).
42+
public var isMCCP3Active: Bool { false }
43+
#endif
3644

3745
// MARK: - Private State
3846

3947
/// Buffer for incomplete telnet sequences (packet fragmentation).
4048
private var telbuf: [UInt8] = []
4149

50+
#if canImport(CZlib)
4251
/// MCCP2 deflate stream (server→client output compression).
4352
private var mccp2: DeflateStream?
4453

4554
/// MCCP3 inflate stream (client→server input decompression).
4655
private var mccp3: InflateStream?
56+
#endif
4757

4858
// MARK: - Init
4959

@@ -77,8 +87,10 @@ public final class TelnetSession {
7787

7888
/// Unannounce support (e.g. before copyover).
7989
public func unannounceSupport() {
90+
#if canImport(CZlib)
8091
endMCCP2()
8192
endMCCP3()
93+
#endif
8294
for i in 0..<min(telnetTable.count, 255) {
8395
let entry = telnetTable[i]
8496
if !entry.announce.isEmpty {
@@ -125,6 +137,7 @@ public final class TelnetSession {
125137
public func processInput(_ src: [UInt8]) -> [UInt8] {
126138
var input = src
127139

140+
#if canImport(CZlib)
128141
// MCCP3: decompress incoming data if active
129142
if let inflater = mccp3 {
130143
guard let result = inflater.decompress(input) else {
@@ -142,6 +155,7 @@ public final class TelnetSession {
142155
input = result.decompressed
143156
}
144157
}
158+
#endif
145159

146160
var out: [UInt8] = []
147161
out.reserveCapacity(input.count)
@@ -230,53 +244,61 @@ public final class TelnetSession {
230244
}
231245

232246
/// Lazily built telopt pattern table.
233-
private lazy var teloptPatterns: [TeloptPattern] = [
234-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.EOR],
235-
handler: { s, src, i, n in s.processDoEOR(); return 3 }),
236-
237-
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.TTYPE],
238-
handler: { s, src, i, n in s.processWillTtype(); return 3 }),
239-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.TTYPE, TS.ENV_IS],
240-
handler: { s, src, i, n in s.processSbTtypeIs(src, at: i, srclen: n) }),
241-
242-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.NAWS],
243-
handler: { s, src, i, n in s.processSbNaws(src, at: i, srclen: n) }),
244-
245-
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.NEW_ENVIRON],
246-
handler: { s, src, i, n in s.processWillNewEnviron(); return 3 }),
247-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.NEW_ENVIRON],
248-
handler: { s, src, i, n in s.processSbNewEnviron(src, at: i, srclen: n) }),
249-
250-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.CHARSET],
251-
handler: { s, src, i, n in s.processDoCharset(); return 3 }),
252-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.CHARSET],
253-
handler: { s, src, i, n in s.processSbCharset(src, at: i, srclen: n) }),
254-
255-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MSSP],
256-
handler: { s, src, i, n in s.processDoMssp(); return 3 }),
257-
258-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MSDP],
259-
handler: { s, src, i, n in s.processDoMsdp(); return 3 }),
260-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.MSDP],
261-
handler: { s, src, i, n in s.processSbMsdp(src, at: i, srclen: n) }),
262-
263-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.GMCP],
264-
handler: { s, src, i, n in s.processDoGmcp(); return 3 }),
265-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.GMCP],
266-
handler: { s, src, i, n in s.processSbGmcp(src, at: i, srclen: n) }),
267-
268-
// MCCP2
269-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MCCP2],
270-
handler: { s, src, i, n in s.processDoMccp2(); return 3 }),
271-
TeloptPattern(pattern: [TC.IAC, TC.DONT, TO.MCCP2],
272-
handler: { s, src, i, n in s.processDontMccp2(); return 3 }),
273-
274-
// MCCP3
275-
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MCCP3],
276-
handler: { s, src, i, n in return 3 }),
277-
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.MCCP3, TC.IAC, TC.SE],
278-
handler: { s, src, i, n in s.processSbMccp3(); return 5 }),
279-
]
247+
private lazy var teloptPatterns: [TeloptPattern] = buildTeloptPatterns()
248+
249+
private func buildTeloptPatterns() -> [TeloptPattern] {
250+
var patterns: [TeloptPattern] = [
251+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.EOR],
252+
handler: { s, src, i, n in s.processDoEOR(); return 3 }),
253+
254+
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.TTYPE],
255+
handler: { s, src, i, n in s.processWillTtype(); return 3 }),
256+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.TTYPE, TS.ENV_IS],
257+
handler: { s, src, i, n in s.processSbTtypeIs(src, at: i, srclen: n) }),
258+
259+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.NAWS],
260+
handler: { s, src, i, n in s.processSbNaws(src, at: i, srclen: n) }),
261+
262+
TeloptPattern(pattern: [TC.IAC, TC.WILL, TO.NEW_ENVIRON],
263+
handler: { s, src, i, n in s.processWillNewEnviron(); return 3 }),
264+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.NEW_ENVIRON],
265+
handler: { s, src, i, n in s.processSbNewEnviron(src, at: i, srclen: n) }),
266+
267+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.CHARSET],
268+
handler: { s, src, i, n in s.processDoCharset(); return 3 }),
269+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.CHARSET],
270+
handler: { s, src, i, n in s.processSbCharset(src, at: i, srclen: n) }),
271+
272+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MSSP],
273+
handler: { s, src, i, n in s.processDoMssp(); return 3 }),
274+
275+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MSDP],
276+
handler: { s, src, i, n in s.processDoMsdp(); return 3 }),
277+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.MSDP],
278+
handler: { s, src, i, n in s.processSbMsdp(src, at: i, srclen: n) }),
279+
280+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.GMCP],
281+
handler: { s, src, i, n in s.processDoGmcp(); return 3 }),
282+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.GMCP],
283+
handler: { s, src, i, n in s.processSbGmcp(src, at: i, srclen: n) }),
284+
]
285+
#if canImport(CZlib)
286+
patterns += [
287+
// MCCP2
288+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MCCP2],
289+
handler: { s, src, i, n in s.processDoMccp2(); return 3 }),
290+
TeloptPattern(pattern: [TC.IAC, TC.DONT, TO.MCCP2],
291+
handler: { s, src, i, n in s.processDontMccp2(); return 3 }),
292+
293+
// MCCP3
294+
TeloptPattern(pattern: [TC.IAC, TC.DO, TO.MCCP3],
295+
handler: { s, src, i, n in return 3 }),
296+
TeloptPattern(pattern: [TC.IAC, TC.SB, TO.MCCP3, TC.IAC, TC.SE],
297+
handler: { s, src, i, n in s.processSbMccp3(); return 5 }),
298+
]
299+
#endif
300+
return patterns
301+
}
280302

281303
/// Handle generic telnet commands that don't match any specific pattern.
282304
private func handleGenericTelnet(_ src: [UInt8], at i: Int, remaining: Int, out: inout [UInt8]) -> Int {
@@ -322,13 +344,17 @@ public final class TelnetSession {
322344
// MARK: - Output
323345

324346
private func write(_ data: [UInt8]) {
347+
#if canImport(CZlib)
325348
if let mccp2 = mccp2 {
326349
if let compressed = mccp2.compress(data) {
327350
delegate?.telnetSession(self, write: compressed)
328351
}
329352
} else {
330353
delegate?.telnetSession(self, write: data)
331354
}
355+
#else
356+
delegate?.telnetSession(self, write: data)
357+
#endif
332358
}
333359

334360
/// Write data bypassing MCCP2 compression (used for the MCCP2 start marker).
@@ -685,6 +711,7 @@ public final class TelnetSession {
685711
return sbLen
686712
}
687713

714+
#if canImport(CZlib)
688715
// MARK: - Handler: MCCP2
689716

690717
private func processDoMccp2() {
@@ -746,4 +773,11 @@ public final class TelnetSession {
746773
log("MCCP3: COMPRESSION END")
747774
mccp3 = nil
748775
}
776+
#else
777+
/// No-op: MCCP2 unavailable without zlib.
778+
public func endMCCP2() {}
779+
780+
/// No-op: MCCP3 unavailable without zlib.
781+
public func endMCCP3() {}
782+
#endif
749783
}

Tests/MTHTests/TelnetSessionTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ private func makeSession() -> (TelnetSession, FakeDelegate) {
550550
#expect(!d.writtenChunks.isEmpty)
551551
}
552552

553+
#if canImport(CZlib)
553554
// MARK: - MCCP2 (Output Compression)
554555

555556
@Test func doMccp2StartsCompression() {
@@ -673,6 +674,7 @@ private func makeSession() -> (TelnetSession, FakeDelegate) {
673674
#expect(!s.isMCCP2Active)
674675
#expect(!s.isMCCP3Active)
675676
}
677+
#endif
676678

677679
// MARK: - Mixed Input
678680

0 commit comments

Comments
 (0)