/
CKFTPReply.m
408 lines (307 loc) · 12 KB
/
CKFTPReply.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
//
// CKFTPResponse.m
// Connection
//
// Created by Mike on 24/03/2009.
// Copyright 2009 Karelia Software. All rights reserved.
//
#import "CKFTPReply.h"
@implementation CKFTPReply
#pragma mark init
+ (CKFTPReply *)replyWithCode:(NSUInteger)code text:(NSString *)text
{
return [[[self alloc] initWithReplyCode:code text:text] autorelease];
}
- (id)initWithReplyCode:(NSUInteger)code textLines:(NSArray *)lines
{
NSParameterAssert(code < 1000);
NSParameterAssert(lines);
NSParameterAssert([lines count] >= 1);
[super init];
_replyCode = code;
_textLines = [[NSArray alloc] initWithArray:lines copyItems:YES]; // deep copy
return self;
}
- (id)initWithReplyCode:(NSUInteger)code text:(NSString *)text
{
return [self initWithReplyCode:code textLines:[NSArray arrayWithObject:text]];
}
#pragma mark reply code
- (NSUInteger)replyCode { return _replyCode; }
- (NSString *)replyCodeString
{
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[formatter setNumberStyle:NSNumberFormatterNoStyle];
NSString *result = [formatter stringFromNumber:[NSNumber numberWithUnsignedInt:[self replyCode]]];
[formatter release];
return result;
}
- (CKFTPReplyType)replyType
{
// The first digit of -replyCode
CKFTPReplyType result = [self replyCode] / 100;
return result;
}
- (CKFTPReplyFunctionGroup)functionalGrouping
{
// The second digit or -replyCode
CKFTPReplyFunctionGroup result = ([self replyCode] - (100 * [self replyType])) / 10;
return result;
}
#pragma mark reply text
- (NSArray *)textLines { return _textLines; }
- (NSString *)quotedString
{
static NSString *quoteCharacter = @"\"";
static NSString *pairOfQuoteCharacters = @"\"\"";
NSString *text = [[self textLines] objectAtIndex:0];
NSScanner *scanner = [[NSScanner alloc] initWithString:text];
[scanner setCharactersToBeSkipped:nil];
NSString *result = nil;
// Find the start of the quote
[scanner scanUpToString:quoteCharacter intoString:NULL];
if (![scanner isAtEnd])
{
[scanner setScanLocation:([scanner scanLocation] + 1)];
if (![scanner isAtEnd])
{
// Find the first quote. The efficiency of parsing is fairly heavily weighted towards
// strings without a pair of quotes, as they are exception rather than the norm.
if (![scanner scanUpToString:quoteCharacter intoString:&result])
{
result = @""; // accounts for an empty quote
}
// The quote might actually be a pair, such as in this reply:
// 257 "/he said ""yo"" to me" created
while ([scanner scanString:pairOfQuoteCharacters intoString:NULL])
{
result = [result stringByAppendingString:quoteCharacter];
// Find the next quote
NSString *nextString;
if ([scanner scanUpToString:quoteCharacter intoString:&nextString])
{
result = [result stringByAppendingString:nextString];
}
}
// If we ever reach the end of the text, there was no closing quote, so it's invalid
if ([scanner isAtEnd]) result = nil;
}
}
[scanner release];
return result;
}
#pragma mark serialization
- (NSArray *)serializedTelnetLines
{
NSArray *lines = [self textLines];
// The last line always needs to be of the form "123 Text"
NSString *code = [self replyCodeString];
NSString *lastLine = [NSString stringWithFormat:@"%@ %@", code, [lines lastObject]];
// Handle simple replies
if ([lines count] <= 1)
{
return [NSArray arrayWithObject:lastLine];
}
// To get to here, it's a multiline response
NSMutableArray *result = [[lines mutableCopy] autorelease];
[result replaceObjectAtIndex:([result count] - 1) withObject:lastLine];
NSString *firstLine = [[NSString alloc] initWithFormat:@"%@-%@", code, [lines objectAtIndex:0]];
[result replaceObjectAtIndex:0 withObject:firstLine];
[firstLine release];
return result;
}
- (NSString *)description
{
return [[self serializedTelnetLines] componentsJoinedByString:@"\n"];
}
#pragma mark copy
- (id)copyWithZone:(NSZone *)zone
{
// Immutable object
return [self retain];
}
@end
#pragma mark -
@implementation CKStreamedFTPReply
#pragma mark init & dealloc
- (id)init
{
if (self = [self initWithReplyCode:000 text:@""])
{
_data = [[NSMutableData alloc] init];
}
return self;
}
- (void)dealloc
{
[_data release];
[_multilineReplyBuffer release];
[super dealloc];
}
#pragma mark copy
- (id)copyWithZone:(NSZone *)zone
{
id result = [[CKFTPReply allocWithZone:zone] initWithReplyCode:[self replyCode]
textLines:[self textLines]];
return result;
}
#pragma mark appending data
// Utility method to seek a particular character. Taken from -[NSData rangeOfData:range:]
+ (NSUInteger)locationOfCharacter:(char)character inData:(NSData *)data location:(NSUInteger)location
{
uint8_t *str = (uint8_t *)[data bytes];
while (location < [data length])
{
if (str[location] == character)
{
return location;
}
location++;
}
return NSNotFound;
}
- (NSData *)replyDidBecomeComplete
{
// Support method for when enough data has been received to complete the reply
// Copies any data out of the temporary ivars, and then resets them.
// Returns any leftover data from the buffer.
[_textLines release];
_textLines = _multilineReplyBuffer;
_multilineReplyBuffer = nil;
NSData *result = nil;
if (_scanLocation < [_data length])
{
result = [_data subdataWithRange:NSMakeRange(_scanLocation, [_data length] - _scanLocation)];
}
[_data release]; _data = nil;
return result;
}
/* The general strategy is to add received data to _data. Once a complete line has arrived,
* decode it into a string and increment scanLocation. Once the full reply has been received the
* buffer is no longer needed, and any remaining data in it is returned to the caller.
*/
- (BOOL)appendData:(NSData *)data nextData:(NSData **)excess
{
NSParameterAssert(data);
NSAssert(![self isComplete], @"Can't append data to streamed FTP reply, already complete");
// The code below assumes that some processing will take place and won't work if the empty data
// is allowed to enter the system
if ([data length] == 0) return 0;
// Add the data to the buffer (the majority of the time it should be valid)
BOOL result = YES;
NSUInteger oldScanLocation = oldScanLocation;
[_data appendData:data];
// To be a valid reply, the first 3 characters must all be numeric...
if (_scanLocation == 0)
{
NSString *replyStart = [[NSString alloc] initWithBytes:[_data bytes]
length:MAX(4, [_data length])
encoding:NSASCIIStringEncoding];
result = NO;
if (replyStart)
{
NSScanner *scanner = [[NSScanner alloc] initWithString:replyStart];
[scanner setCharactersToBeSkipped:nil];
int replyCode;
if ([scanner scanInt:&replyCode] && replyCode < 1000)
{
result = YES;
// ...and the 4th character must be a space or hyphen
if ([replyStart length] >= 4)
{
unichar spacerChar = [replyStart characterAtIndex:3];
switch (spacerChar)
{
case '-':
_multilineReplyBuffer = [[NSMutableArray alloc] init];
case ' ':
// Reaching this point means there is enough data for the full reply code.
_replyCode = replyCode;
_scanLocation = 4;
break;
default:
result = NO;
}
}
}
[scanner release];
[replyStart release];
}
}
// If we have a valid start to the reply, search for the end of it
// Already know that a valid reply code is present by this point, just have to decode the text
if (result)
{
// Multiline replies require us to wait until the final line is received
NSString *lastLineCode;
if (_multilineReplyBuffer) lastLineCode = [[self replyCodeString] stringByAppendingString:@" "];
// Split off text lines one at a time. They can end in <CR><LF> or <LF>
NSUInteger linebreakStart = [CKStreamedFTPReply locationOfCharacter:'\n'
inData:_data
location:_scanLocation];
while (linebreakStart != NSNotFound)
{
// What is the range of the actual text? Ignore the <LF>'s preceeding <CR> if it exists
NSRange lineRange = NSMakeRange(_scanLocation, linebreakStart - _scanLocation);
if (((uint8_t *)[_data bytes])[linebreakStart - 1] == '\r') lineRange.length--;
NSData *lineData = [_data subdataWithRange:lineRange];
_scanLocation = linebreakStart + 1;
// Unescape any <CR>s within the line
lineData = [CKStreamedFTPReply dataByUnescapingTelnetCarriageReturnsInData:lineData];
// Any valid data MUST now be ASCII
NSString *line = [[NSString alloc] initWithData:lineData encoding:NSASCIIStringEncoding];
if (line)
{
[line autorelease];
// We can easily use the line to finish a standard reply
if (!_multilineReplyBuffer)
{
*excess = [self replyDidBecomeComplete];
_textLines = [[NSArray alloc] initWithObjects:line, nil];
break;
}
// If this is the last line of a multiline reply, strip it of the code prefix
if ([line hasPrefix:lastLineCode])
{
line = [line substringFromIndex:4];
[_multilineReplyBuffer addObject:line];
*excess = [self replyDidBecomeComplete];
break;
}
[_multilineReplyBuffer addObject:line];
}
else
{
result = NO;
break;
}
// Read in the next line
linebreakStart = [CKStreamedFTPReply locationOfCharacter:'\n'
inData:_data
location:_scanLocation];
}
}
// If the data was invalid, undo the changes caused by it
if (!result)
{
[_data replaceBytesInRange:NSMakeRange([_data length] - [data length], [data length])
withBytes:NULL
length:0];
_scanLocation = oldScanLocation;
if (![self hasReplyCode]) _replyCode = 0;
}
return result;
}
+ (NSData *)dataByUnescapingTelnetCarriageReturnsInData:(NSData *)data
{
return data; // TODO: Actually do this!
}
#pragma mark accessors
- (BOOL)isComplete { return (_data == nil); }
- (BOOL)hasReplyCode
{
BOOL result = (_scanLocation >= 4);
return result;
}
@end