Skip to content

Commit be0a9c8

Browse files
danieleggertparkera
authored andcommitted
Implement NSHTTPURLResponse (#287)
1 parent 704ce5f commit be0a9c8

File tree

3 files changed

+438
-38
lines changed

3 files changed

+438
-38
lines changed

Foundation/NSURLResponse.swift

Lines changed: 198 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,19 +52,21 @@ public class NSURLResponse : NSObject, NSSecureCoding, NSCopying {
5252
@result The initialized NSURLResponse.
5353
@discussion This is the designated initializer for NSURLResponse.
5454
*/
55-
public init(URL: NSURL, MIMEType: String?, expectedContentLength length: Int, textEncodingName name: String?) {
56-
self.URL = URL
57-
self.MIMEType = MIMEType
55+
public init(url: NSURL, mimeType: String?, expectedContentLength length: Int, textEncodingName name: String?) {
56+
self.url = url
57+
self.mimeType = mimeType
5858
self.expectedContentLength = Int64(length)
5959
self.textEncodingName = name
60+
let c = url.lastPathComponent
61+
self.suggestedFilename = (c?.isEmpty ?? true) ? "Unknown" : c
6062
}
6163

6264
/*!
6365
@method URL
6466
@abstract Returns the URL of the receiver.
6567
@result The URL of the receiver.
6668
*/
67-
/*@NSCopying*/ public private(set) var URL: NSURL?
69+
/*@NSCopying*/ public private(set) var url: NSURL?
6870

6971

7072
/*!
@@ -78,7 +80,7 @@ public class NSURLResponse : NSObject, NSSecureCoding, NSCopying {
7880
be made if the origin source did not report any such information.
7981
@result The MIME type of the receiver.
8082
*/
81-
public private(set) var MIMEType: String?
83+
public private(set) var mimeType: String?
8284

8385
/*!
8486
@method expectedContentLength
@@ -120,7 +122,7 @@ public class NSURLResponse : NSObject, NSSecureCoding, NSCopying {
120122
This method always returns a valid filename.
121123
@result A suggested filename to use if saving the resource to disk.
122124
*/
123-
public var suggestedFilename: String? { NSUnimplemented() }
125+
public private(set) var suggestedFilename: String?
124126
}
125127

126128
/*!
@@ -143,7 +145,17 @@ public class NSHTTPURLResponse : NSURLResponse {
143145
@result the instance of the object, or NULL if an error occurred during initialization.
144146
@discussion This API was introduced in Mac OS X 10.7.2 and iOS 5.0 and is not available prior to those releases.
145147
*/
146-
public init?(URL url: NSURL, statusCode: Int, HTTPVersion: String?, headerFields: [String : String]?) { NSUnimplemented() }
148+
public init?(url: NSURL, statusCode: Int, httpVersion: String?, headerFields: [String : String]?) {
149+
self.statusCode = statusCode
150+
self.allHeaderFields = headerFields ?? [:]
151+
super.init(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil)
152+
expectedContentLength = getExpectedContentLength(fromHeaderFields: headerFields) ?? -1
153+
suggestedFilename = getSuggestedFilename(fromHeaderFields: headerFields) ?? "Unknown"
154+
if let type = ContentTypeComponents(headerFields: headerFields) {
155+
mimeType = type.mimeType.lowercased()
156+
textEncodingName = type.textEncoding?.lowercased()
157+
}
158+
}
147159

148160
public required init?(coder aDecoder: NSCoder) {
149161
NSUnimplemented()
@@ -154,28 +166,192 @@ public class NSHTTPURLResponse : NSURLResponse {
154166
@abstract Returns the HTTP status code of the receiver.
155167
@result The HTTP status code of the receiver.
156168
*/
157-
public var statusCode: Int { NSUnimplemented() }
169+
public let statusCode: Int
158170

159-
/*!
160-
@method allHeaderFields
161-
@abstract Returns a dictionary containing all the HTTP header fields
162-
of the receiver.
163-
@discussion By examining this header dictionary, clients can see
164-
the "raw" header information which was reported to the protocol
165-
implementation by the HTTP server. This may be of use to
166-
sophisticated or special-purpose HTTP clients.
167-
@result A dictionary containing all the HTTP header fields of the
168-
receiver.
169-
*/
170-
public var allHeaderFields: [NSObject : AnyObject] { NSUnimplemented() }
171+
/// Returns a dictionary containing all the HTTP header fields
172+
/// of the receiver.
173+
///
174+
/// By examining this header dictionary, clients can see
175+
/// the "raw" header information which was reported to the protocol
176+
/// implementation by the HTTP server. This may be of use to
177+
/// sophisticated or special-purpose HTTP clients.
178+
///
179+
/// - Returns: A dictionary containing all the HTTP header fields of the
180+
/// receiver.
181+
///
182+
/// - Important: This is an *experimental* change from the
183+
/// `[NSObject: AnyObject]` type that Darwin Foundation uses.
184+
public let allHeaderFields: [String: String]
171185

172-
/*!
186+
/*!
173187
@method localizedStringForStatusCode:
174188
@abstract Convenience method which returns a localized string
175189
corresponding to the status code for this response.
176190
@param the status code to use to produce a localized string.
177191
@result A localized string corresponding to the given status code.
178192
*/
179-
public class func localizedStringForStatusCode(_ statusCode: Int) -> String { NSUnimplemented() }
193+
public class func localizedString(forStatusCode statusCode: Int) -> String { NSUnimplemented() }
194+
}
195+
/// Parses the expected content length from the headers.
196+
///
197+
/// Note that the message content length is different from the message
198+
/// transfer length.
199+
/// The transfer length can only be derived when the Transfer-Encoding is identity (default).
200+
/// For compressed content (Content-Encoding other than identity), there is not way to derive the
201+
/// content length from the transfer length.
202+
private func getExpectedContentLength(fromHeaderFields headerFields: [String : String]?) -> Int64? {
203+
guard
204+
let f = headerFields,
205+
let contentLengthS = valueForCaseInsensitiveKey("content-length", fields: f),
206+
let contentLength = Int64(contentLengthS)
207+
else { return nil }
208+
return contentLength
209+
}
210+
/// Parses the suggested filename from the `Content-Disposition` header.
211+
///
212+
/// - SeeAlso: [RFC 2183](https://tools.ietf.org/html/rfc2183)
213+
private func getSuggestedFilename(fromHeaderFields headerFields: [String : String]?) -> String? {
214+
// Typical use looks like this:
215+
// Content-Disposition: attachment; filename="fname.ext"
216+
guard
217+
let f = headerFields,
218+
let contentDisposition = valueForCaseInsensitiveKey("content-disposition", fields: f),
219+
let field = contentDisposition.httpHeaderParts
220+
else { return nil }
221+
for part in field.parameters where part.attribute == "filename" {
222+
return part.value?.pathComponents.map{ $0 == "/" ? "" : $0}.joined(separator: "_")
223+
}
224+
return nil
225+
}
226+
/// Parts corresponding to the `Content-Type` header field in a HTTP message.
227+
private struct ContentTypeComponents {
228+
/// For `text/html; charset=ISO-8859-4` this would be `text/html`
229+
let mimeType: String
230+
/// For `text/html; charset=ISO-8859-4` this would be `ISO-8859-4`. Will be
231+
/// `nil` when no `charset` is specified.
232+
let textEncoding: String?
233+
}
234+
extension ContentTypeComponents {
235+
/// Parses the `Content-Type` header field
236+
///
237+
/// `Content-Type: text/html; charset=ISO-8859-4` would result in `("text/html", "ISO-8859-4")`, while
238+
/// `Content-Type: text/html` would result in `("text/html", nil)`.
239+
init?(headerFields: [String : String]?) {
240+
guard
241+
let f = headerFields,
242+
let contentType = valueForCaseInsensitiveKey("content-type", fields: f),
243+
let field = contentType.httpHeaderParts
244+
else { return nil }
245+
for parameter in field.parameters where parameter.attribute == "charset" {
246+
self.mimeType = field.value
247+
self.textEncoding = parameter.value
248+
return
249+
}
250+
self.mimeType = field.value
251+
self.textEncoding = nil
252+
}
253+
}
254+
255+
/// A type with paramteres
256+
///
257+
/// RFC 2616 specifies a few types that can have parameters, e.g. `Content-Type`.
258+
/// These are specified like so
259+
/// ```
260+
/// field = value *( ";" parameter )
261+
/// value = token
262+
/// ```
263+
/// where parameters are attribute/value as specified by
264+
/// ```
265+
/// parameter = attribute "=" value
266+
/// attribute = token
267+
/// value = token | quoted-string
268+
/// ```
269+
private struct ValueWithParameters {
270+
let value: String
271+
let parameters: [Parameter]
272+
struct Parameter {
273+
let attribute: String
274+
let value: String?
275+
}
180276
}
181277

278+
private extension String {
279+
/// Split the string at each ";", remove any quoting.
280+
///
281+
/// The trouble is if there's a
282+
/// ";" inside something that's quoted. And we can escape the separator and
283+
/// the quotes with a "\".
284+
var httpHeaderParts: ValueWithParameters? {
285+
var type: String?
286+
var parameters: [ValueWithParameters.Parameter] = []
287+
let ws = NSCharacterSet.whitespaceCharacterSet()
288+
func append(_ string: String) {
289+
if type == nil {
290+
type = string
291+
} else {
292+
if let r = string.rangeOfString("=") {
293+
let name = string[string.startIndex..<r.startIndex].stringByTrimmingCharactersInSet(ws)
294+
let value = string[r.endIndex..<string.endIndex].stringByTrimmingCharactersInSet(ws)
295+
parameters.append(ValueWithParameters.Parameter(attribute: name, value: value))
296+
} else {
297+
let name = string.stringByTrimmingCharactersInSet(ws)
298+
parameters.append(ValueWithParameters.Parameter(attribute: name, value: nil))
299+
}
300+
}
301+
}
302+
303+
let escape = UnicodeScalar(0x5c) // \
304+
let quote = UnicodeScalar(0x22) // "
305+
let separator = UnicodeScalar(0x3b) // ;
306+
enum State {
307+
case nonQuoted(String)
308+
case nonQuotedEscaped(String)
309+
case quoted(String)
310+
case quotedEscaped(String)
311+
}
312+
var state = State.nonQuoted("")
313+
for next in unicodeScalars {
314+
switch (state, next) {
315+
case (.nonQuoted(let s), separator):
316+
append(s)
317+
state = .nonQuoted("")
318+
case (.nonQuoted(let s), escape):
319+
state = .nonQuotedEscaped(s + String(next))
320+
case (.nonQuoted(let s), quote):
321+
state = .quoted(s)
322+
case (.nonQuoted(let s), _):
323+
state = .nonQuoted(s + String(next))
324+
325+
case (.nonQuotedEscaped(let s), _):
326+
state = .nonQuoted(s + String(next))
327+
328+
case (.quoted(let s), quote):
329+
state = .nonQuoted(s)
330+
case (.quoted(let s), escape):
331+
state = .quotedEscaped(s + String(next))
332+
case (.quoted(let s), _):
333+
state = .quoted(s + String(next))
334+
335+
case (.quotedEscaped(let s), _):
336+
state = .quoted(s + String(next))
337+
}
338+
}
339+
switch state {
340+
case .nonQuoted(let s): append(s)
341+
case .nonQuotedEscaped(let s): append(s)
342+
case .quoted(let s): append(s)
343+
case .quotedEscaped(let s): append(s)
344+
}
345+
guard let t = type else { return nil }
346+
return ValueWithParameters(value: t, parameters: parameters)
347+
}
348+
}
349+
private func valueForCaseInsensitiveKey(_ key: String, fields: [String: String]) -> String? {
350+
let kk = key.lowercased()
351+
for (k, v) in fields {
352+
if k.lowercased() == kk {
353+
return v
354+
}
355+
}
356+
return nil
357+
}

0 commit comments

Comments
 (0)