Skip to content

Commit e65f581

Browse files
committed
Add support for Windows
1 parent b2ae845 commit e65f581

File tree

10 files changed

+261
-1
lines changed

10 files changed

+261
-1
lines changed

Sources/AsyncHTTPClient/ConnectionPool.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import Musl
2424
import Android
2525
#elseif os(Linux) || os(FreeBSD)
2626
import Glibc
27+
#elseif os(Windows)
28+
import ucrt
29+
import WinSDK
2730
#else
2831
#error("unsupported target operating system")
2932
#endif

Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import func Darwin.pow
2020
import func Musl.pow
2121
#elseif canImport(Android)
2222
import func Android.pow
23+
#elseif canImport(ucrt)
24+
import func ucrt.pow
2325
#else
2426
import func Glibc.pow
2527
#endif

Sources/AsyncHTTPClient/HTTPClient+HTTPCookie.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import Darwin
2828
import Musl
2929
#elseif canImport(Android)
3030
import Android
31+
#elseif os(Windows)
32+
import ucrt
33+
import WinSDK
3134
#elseif canImport(Glibc)
3235
import Glibc
3336
#endif
@@ -216,12 +219,19 @@ extension String.UTF8View.SubSequence {
216219
}
217220
}
218221

222+
#if !os(Windows)
219223
nonisolated(unsafe) private let posixLocale: UnsafeMutableRawPointer = {
220224
// All POSIX systems must provide a "POSIX" locale, and its date/time formats are US English.
221225
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_05
222226
let _posixLocale = newlocale(LC_TIME_MASK | LC_NUMERIC_MASK, "POSIX", nil)!
223227
return UnsafeMutableRawPointer(_posixLocale)
224228
}()
229+
#else
230+
nonisolated(unsafe) private let posixLocale: UnsafeMutableRawPointer = {
231+
// FIXME: This can be cleaner. But the Windows shim doesn't need a locale pointer
232+
return UnsafeMutableRawPointer(bitPattern: 0)!
233+
}()
234+
#endif
225235

226236
private func parseTimestamp(_ utf8: String.UTF8View.SubSequence, format: String) -> tm? {
227237
var timeComponents = tm()
@@ -251,6 +261,16 @@ private func parseCookieTime(_ timestampUTF8: String.UTF8View.SubSequence) -> In
251261
else {
252262
return nil
253263
}
264+
#if os(Windows)
265+
let timegm = _mkgmtime
266+
#endif
267+
254268
let timestamp = Int64(timegm(&timeComponents))
255-
return timestamp == -1 && errno == EOVERFLOW ? nil : timestamp
269+
270+
#if os(Windows)
271+
let err = GetLastError()
272+
#else
273+
let err = errno
274+
#endif
275+
return timestamp == -1 && err == EOVERFLOW ? nil : timestamp
256276
}

Sources/CAsyncHTTPClient/CAsyncHTTPClient.c

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,208 @@
2121
#include <stdbool.h>
2222
#include <time.h>
2323

24+
#if defined(_WIN32)
25+
#include <string.h>
26+
#include <ctype.h>
27+
// Windows does not provide strptime/strptime_l. Implement a tiny parser that
28+
// supports the three date formats used for cookie parsing in this package:
29+
// 1) "%a, %d %b %Y %H:%M:%S"
30+
// 2) "%a, %d-%b-%y %H:%M:%S"
31+
// 3) "%a %b %d %H:%M:%S %Y"
32+
33+
static int month_from_abbrev(const char *p) {
34+
// Return 0-11 for Jan..Dec, or -1 on failure.
35+
if (!p) return -1;
36+
switch (p[0]) {
37+
case 'J':
38+
if (p[1] == 'a' && p[2] == 'n') return 0; // Jan
39+
if (p[1] == 'u' && p[2] == 'n') return 5; // Jun
40+
if (p[1] == 'u' && p[2] == 'l') return 6; // Jul
41+
break;
42+
case 'F':
43+
if (p[1] == 'e' && p[2] == 'b') return 1; // Feb
44+
break;
45+
case 'M':
46+
if (p[1] == 'a' && p[2] == 'r') return 2; // Mar
47+
if (p[1] == 'a' && p[2] == 'y') return 4; // May
48+
break;
49+
case 'A':
50+
if (p[1] == 'p' && p[2] == 'r') return 3; // Apr
51+
if (p[1] == 'u' && p[2] == 'g') return 7; // Aug
52+
break;
53+
case 'S':
54+
if (p[1] == 'e' && p[2] == 'p') return 8; // Sep
55+
break;
56+
case 'O':
57+
if (p[1] == 'c' && p[2] == 't') return 9; // Oct
58+
break;
59+
case 'N':
60+
if (p[1] == 'o' && p[2] == 'v') return 10; // Nov
61+
break;
62+
case 'D':
63+
if (p[1] == 'e' && p[2] == 'c') return 11; // Dec
64+
break;
65+
}
66+
return -1;
67+
}
68+
69+
static int is_wkday_abbrev(const char *p) {
70+
// Check for valid weekday abbreviation (Mon..Sun)
71+
// Expect exactly 3 ASCII letters.
72+
if (!p) return 0;
73+
char a = p[0], b = p[1], c = p[2];
74+
if (!isalpha((unsigned char)a) || !isalpha((unsigned char)b) || !isalpha((unsigned char)c)) return 0;
75+
// Accept common English abbreviations, case-sensitive as typically emitted.
76+
return (a=='M'&&b=='o'&&c=='n')||(a=='T'&&b=='u'&&c=='e')||(a=='W'&&b=='e'&&c=='d')||
77+
(a=='T'&&b=='h'&&c=='u')||(a=='F'&&b=='r'&&c=='i')||(a=='S'&&b=='a'&&c=='t')||
78+
(a=='S'&&b=='u'&&c=='n');
79+
}
80+
81+
static int parse_1to2_digits(const char **pp) {
82+
const char *p = *pp;
83+
if (!isdigit((unsigned char)p[0])) return -1;
84+
int val = p[0]-'0';
85+
p++;
86+
if (isdigit((unsigned char)p[0])) {
87+
val = val*10 + (p[0]-'0');
88+
p++;
89+
}
90+
*pp = p;
91+
return val;
92+
}
93+
94+
static int parse_fixed2(const char **pp) {
95+
const char *p = *pp;
96+
if (!isdigit((unsigned char)p[0]) || !isdigit((unsigned char)p[1])) return -1;
97+
int val = (p[0]-'0')*10 + (p[1]-'0');
98+
p += 2;
99+
*pp = p;
100+
return val;
101+
}
102+
103+
static int parse_fixed4(const char **pp) {
104+
const char *p = *pp;
105+
for (int i = 0; i < 4; i++) {
106+
if (!isdigit((unsigned char)p[i])) return -1;
107+
}
108+
int val = (p[0]-'0')*1000 + (p[1]-'0')*100 + (p[2]-'0')*10 + (p[3]-'0');
109+
p += 4;
110+
*pp = p;
111+
return val;
112+
}
113+
114+
static int expect_char(const char **pp, char c) {
115+
if (**pp != c) return 0;
116+
(*pp)++;
117+
return 1;
118+
}
119+
120+
static int expect_space(const char **pp) {
121+
if (**pp != ' ') return 0;
122+
(*pp)++;
123+
return 1;
124+
}
125+
126+
static int parse_time_hms(const char **pp, int *h, int *m, int *s) {
127+
int hh = parse_fixed2(pp); if (hh < 0) return 0;
128+
if (!expect_char(pp, ':')) return 0;
129+
int mm = parse_fixed2(pp); if (mm < 0) return 0;
130+
if (!expect_char(pp, ':')) return 0;
131+
int ss = parse_fixed2(pp); if (ss < 0) return 0;
132+
if (hh > 23 || mm > 59 || ss > 60) return 0; // allow leap second 60
133+
*h = hh; *m = mm; *s = ss;
134+
return 1;
135+
}
136+
137+
static void init_tm_utc(struct tm *out) {
138+
memset(out, 0, sizeof(*out));
139+
out->tm_isdst = 0;
140+
}
141+
142+
static bool parse_cookie_format1(const char *p, struct tm *out) {
143+
// "%a, %d %b %Y %H:%M:%S"
144+
if (!is_wkday_abbrev(p)) return false;
145+
p += 3;
146+
if (!expect_char(&p, ',')) return false;
147+
if (!expect_space(&p)) return false;
148+
int mday = parse_1to2_digits(&p); if (mday < 1 || mday > 31) return false;
149+
if (!expect_space(&p)) return false;
150+
int mon = month_from_abbrev(p); if (mon < 0) return false; p += 3;
151+
if (!expect_space(&p)) return false;
152+
int year = parse_fixed4(&p); if (year < 1601) return false;
153+
if (!expect_space(&p)) return false;
154+
int hh, mm, ss; if (!parse_time_hms(&p, &hh, &mm, &ss)) return false;
155+
if (*p != '\0') return false;
156+
init_tm_utc(out);
157+
out->tm_mday = mday;
158+
out->tm_mon = mon;
159+
out->tm_year = year - 1900;
160+
out->tm_hour = hh; out->tm_min = mm; out->tm_sec = ss;
161+
return true;
162+
}
163+
164+
static bool parse_cookie_format2(const char *p, struct tm *out) {
165+
// "%a, %d-%b-%y %H:%M:%S"
166+
if (!is_wkday_abbrev(p)) return false;
167+
p += 3;
168+
if (!expect_char(&p, ',')) return false;
169+
if (!expect_space(&p)) return false;
170+
int mday = parse_1to2_digits(&p); if (mday < 1 || mday > 31) return false;
171+
if (!expect_char(&p, '-')) return false;
172+
int mon = month_from_abbrev(p); if (mon < 0) return false; p += 3;
173+
if (!expect_char(&p, '-')) return false;
174+
int y2 = parse_fixed2(&p); if (y2 < 0) return false;
175+
int year = (y2 >= 70) ? (1900 + y2) : (2000 + y2);
176+
if (!expect_space(&p)) return false;
177+
int hh, mm, ss; if (!parse_time_hms(&p, &hh, &mm, &ss)) return false;
178+
if (*p != '\0') return false;
179+
init_tm_utc(out);
180+
out->tm_mday = mday;
181+
out->tm_mon = mon;
182+
out->tm_year = year - 1900;
183+
out->tm_hour = hh; out->tm_min = mm; out->tm_sec = ss;
184+
return true;
185+
}
186+
187+
static bool parse_cookie_format3(const char *p, struct tm *out) {
188+
// "%a %b %d %H:%M:%S %Y"
189+
if (!is_wkday_abbrev(p)) return false;
190+
p += 3;
191+
if (!expect_space(&p)) return false;
192+
int mon = month_from_abbrev(p); if (mon < 0) return false; p += 3;
193+
if (!expect_space(&p)) return false;
194+
int mday = parse_1to2_digits(&p); if (mday < 1 || mday > 31) return false;
195+
if (!expect_space(&p)) return false;
196+
int hh, mm, ss; if (!parse_time_hms(&p, &hh, &mm, &ss)) return false;
197+
if (!expect_space(&p)) return false;
198+
int year = parse_fixed4(&p); if (year < 1601) return false;
199+
if (*p != '\0') return false;
200+
init_tm_utc(out);
201+
out->tm_mday = mday;
202+
out->tm_mon = mon;
203+
out->tm_year = year - 1900;
204+
out->tm_hour = hh; out->tm_min = mm; out->tm_sec = ss;
205+
return true;
206+
}
207+
208+
static bool parse_cookie_timestamp_windows(const char *string, const char *format, struct tm *result) {
209+
(void)format; // format ignored: we try the three known patterns regardless.
210+
return parse_cookie_format1(string, result) ||
211+
parse_cookie_format2(string, result) ||
212+
parse_cookie_format3(string, result);
213+
}
214+
215+
bool swiftahc_cshims_strptime(const char * string, const char * format, struct tm * result) {
216+
return parse_cookie_timestamp_windows(string, format, result);
217+
}
218+
219+
bool swiftahc_cshims_strptime_l(const char * string, const char * format, struct tm * result, void * locale) {
220+
(void)locale; // locale is ignored on Windows; we always use POSIX month/weekday names.
221+
return parse_cookie_timestamp_windows(string, format, result);
222+
}
223+
#endif // _WIN32
224+
225+
#if !defined(_WIN32)
24226
bool swiftahc_cshims_strptime(const char * string, const char * format, struct tm * result) {
25227
const char * firstNonProcessed = strptime(string, format, result);
26228
if (firstNonProcessed) {
@@ -41,3 +243,4 @@ bool swiftahc_cshims_strptime_l(const char * string, const char * format, struct
41243
}
42244
return false;
43245
}
246+
#endif // _WIN32

Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,11 +426,15 @@ class HTTP2ClientTests: XCTestCase {
426426
XCTAssertNoThrow(
427427
maybeServer = try ServerBootstrap(group: serverGroup)
428428
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
429+
#if !os(Windows)
429430
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1)
431+
#endif
430432
.childChannelInitializer { channel in
431433
channel.close()
432434
}
435+
#if !os(Windows)
433436
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
437+
#endif
434438
.bind(host: "127.0.0.1", port: serverPort)
435439
.wait()
436440
)

Tests/AsyncHTTPClientTests/HTTPClientCookieTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ class HTTPClientCookieTests: XCTestCase {
474474
XCTAssertEqual("abc\"", c?.value)
475475
}
476476

477+
#if !os(Windows)
477478
func testCookieExpiresDateParsingWithNonEnglishLocale() throws {
478479
try withCLocaleSetToGerman {
479480
// Check that we are using a German C locale.
@@ -500,4 +501,5 @@ class HTTPClientCookieTests: XCTestCase {
500501
XCTAssertNil(c?.expires)
501502
}
502503
}
504+
#endif
503505
}

Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import Musl
4343
import Android
4444
#elseif canImport(Glibc)
4545
import Glibc
46+
#elseif os(Windows)
47+
import WinSDK
4648
#endif
4749

4850
/// Are we testing NIO Transport services
@@ -69,14 +71,17 @@ let canBindIPv6Loopback: Bool = {
6971
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
7072
defer { try! elg.syncShutdownGracefully() }
7173
let serverChannel = try? ServerBootstrap(group: elg)
74+
#if !os(Windows)
7275
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
76+
#endif
7377
.bind(host: "::1", port: 0)
7478
.wait()
7579
let didBind = (serverChannel != nil)
7680
try! serverChannel?.close().wait()
7781
return didBind
7882
}()
7983

84+
#if !os(Windows)
8085
/// Runs the given block in the context of a non-English C locale (in this case, German).
8186
/// Throws an XCTSkip error if the locale is not supported by the system.
8287
func withCLocaleSetToGerman(_ body: () throws -> Void) throws {
@@ -94,6 +99,7 @@ func withCLocaleSetToGerman(_ body: () throws -> Void) throws {
9499
defer { _ = uselocale(oldLocale) }
95100
try body()
96101
}
102+
#endif
97103

98104
final class TestHTTPDelegate: HTTPClientResponseDelegate {
99105
typealias Response = Void
@@ -258,7 +264,13 @@ enum TemporaryFileHelpers {
258264
let templateBytesCount = templateBytes.count
259265
let fd = templateBytes.withUnsafeMutableBufferPointer { ptr in
260266
ptr.baseAddress!.withMemoryRebound(to: Int8.self, capacity: templateBytesCount) { ptr in
267+
#if os(Windows)
268+
// _mktemp_s is not great, as it's rumored to have limited randomness, but Windows doesn't have mkstemp
269+
// And this is a test utility only.
270+
_mktemp_s(ptr, templateBytesCount)
271+
#else
261272
mkstemp(ptr)
273+
#endif
262274
}
263275
}
264276
templateBytes.removeLast()
@@ -511,11 +523,13 @@ where
511523
let connectionIDAtomic = ManagedAtomic(0)
512524

513525
let serverChannel = try! ServerBootstrap(group: self.group)
526+
#if !os(Windows)
514527
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
515528
.serverChannelOption(
516529
ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT),
517530
value: reusePort ? 1 : 0
518531
)
532+
#endif
519533
.serverChannelInitializer { [activeConnCounterHandler] channel in
520534
channel.pipeline.addHandler(activeConnCounterHandler)
521535
}.childChannelInitializer { channel in

0 commit comments

Comments
 (0)