Ben Copsey

Reorganise code to build URLS for S3 stuff - we now delay building an S3 url until the last minute

Add support for S3 over HTTPS
Added a couple of new constants for access policies
@@ -60,15 +60,12 @@ @@ -60,15 +60,12 @@
60 // Use for deleting buckets - they must be empty for this to succeed 60 // Use for deleting buckets - they must be empty for this to succeed
61 + (id)DELETERequestWithBucket:(NSString *)bucket; 61 + (id)DELETERequestWithBucket:(NSString *)bucket;
62 62
63 -//Builds a query string out of the list parameters we supplied 63 +@property (retain, nonatomic) NSString *bucket;
64 -- (void)createQueryString; 64 +@property (retain, nonatomic) NSString *subResource;
65 - 65 +@property (retain, nonatomic) NSString *prefix;
66 -@property (retain) NSString *bucket; 66 +@property (retain, nonatomic) NSString *marker;
67 -@property (retain) NSString *subResource; 67 +@property (assign, nonatomic) int maxResultCount;
68 -@property (retain) NSString *prefix; 68 +@property (retain, nonatomic) NSString *delimiter;
69 -@property (retain) NSString *marker;  
70 -@property (assign) int maxResultCount;  
71 -@property (retain) NSString *delimiter;  
72 @property (retain, readonly) NSMutableArray *objects; 69 @property (retain, readonly) NSMutableArray *objects;
73 @property (retain, readonly) NSMutableArray *commonPrefixes; 70 @property (retain, readonly) NSMutableArray *commonPrefixes;
74 @property (assign, readonly) BOOL isTruncated; 71 @property (assign, readonly) BOOL isTruncated;
@@ -30,20 +30,19 @@ @@ -30,20 +30,19 @@
30 30
31 + (id)requestWithBucket:(NSString *)bucket 31 + (id)requestWithBucket:(NSString *)bucket
32 { 32 {
33 - ASIS3BucketRequest *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com",bucket]]] autorelease]; 33 + ASIS3BucketRequest *request = [[[self alloc] initWithURL:nil] autorelease];
34 [request setBucket:bucket]; 34 [request setBucket:bucket];
35 return request; 35 return request;
36 } 36 }
37 37
38 + (id)requestWithBucket:(NSString *)bucket subResource:(NSString *)subResource 38 + (id)requestWithBucket:(NSString *)bucket subResource:(NSString *)subResource
39 { 39 {
40 - ASIS3BucketRequest *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com/?%@",bucket,subResource]]] autorelease]; 40 + ASIS3BucketRequest *request = [[[self alloc] initWithURL:nil] autorelease];
41 [request setBucket:bucket]; 41 [request setBucket:bucket];
42 [request setSubResource:subResource]; 42 [request setSubResource:subResource];
43 return request; 43 return request;
44 } 44 }
45 45
46 -  
47 + (id)PUTRequestWithBucket:(NSString *)bucket 46 + (id)PUTRequestWithBucket:(NSString *)bucket
48 { 47 {
49 ASIS3BucketRequest *request = [self requestWithBucket:bucket]; 48 ASIS3BucketRequest *request = [self requestWithBucket:bucket];
@@ -80,8 +79,14 @@ @@ -80,8 +79,14 @@
80 return [NSString stringWithFormat:@"/%@/",[self bucket]]; 79 return [NSString stringWithFormat:@"/%@/",[self bucket]];
81 } 80 }
82 81
83 -- (void)createQueryString 82 +- (void)buildURL
84 { 83 {
  84 + NSString *baseURL;
  85 + if ([self subResource]) {
  86 + baseURL = [NSString stringWithFormat:@"%@://%@.%@/?%@",[self requestScheme],[self bucket],[[self class] S3Host],[self subResource]];
  87 + } else {
  88 + baseURL = [NSString stringWithFormat:@"%@://%@.%@",[self requestScheme],[self bucket],[[self class] S3Host]];
  89 + }
85 NSMutableArray *queryParts = [[[NSMutableArray alloc] init] autorelease]; 90 NSMutableArray *queryParts = [[[NSMutableArray alloc] init] autorelease];
86 if ([self prefix]) { 91 if ([self prefix]) {
87 [queryParts addObject:[NSString stringWithFormat:@"prefix=%@",[[self prefix] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; 92 [queryParts addObject:[NSString stringWithFormat:@"prefix=%@",[[self prefix] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
@@ -95,20 +100,16 @@ @@ -95,20 +100,16 @@
95 if ([self maxResultCount] > 0) { 100 if ([self maxResultCount] > 0) {
96 [queryParts addObject:[NSString stringWithFormat:@"max-keys=%hi",[self maxResultCount]]]; 101 [queryParts addObject:[NSString stringWithFormat:@"max-keys=%hi",[self maxResultCount]]];
97 } 102 }
98 - if ([queryParts count]) 103 + if ([queryParts count]) {
99 - {  
100 NSString* template = @"%@?%@"; 104 NSString* template = @"%@?%@";
101 if ([[self subResource] length] > 0) { 105 if ([[self subResource] length] > 0) {
102 template = @"%@&%@"; 106 template = @"%@&%@";
103 } 107 }
104 - [self setURL:[NSURL URLWithString:[NSString stringWithFormat:template,[[self url] absoluteString],[queryParts componentsJoinedByString:@"&"]]]]; 108 + [self setURL:[NSURL URLWithString:[NSString stringWithFormat:template,baseURL,[queryParts componentsJoinedByString:@"&"]]]];
105 - } 109 + } else {
106 -} 110 + [self setURL:[NSURL URLWithString:baseURL]];
107 111
108 -- (void)main 112 + }
109 -{  
110 - [self createQueryString];  
111 - [super main];  
112 } 113 }
113 114
114 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict 115 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
@@ -146,7 +147,6 @@ @@ -146,7 +147,6 @@
146 } 147 }
147 } 148 }
148 149
149 -  
150 #pragma mark NSCopying 150 #pragma mark NSCopying
151 151
152 - (id)copyWithZone:(NSZone *)zone 152 - (id)copyWithZone:(NSZone *)zone
@@ -161,9 +161,6 @@ @@ -161,9 +161,6 @@
161 return newRequest; 161 return newRequest;
162 } 162 }
163 163
164 -  
165 -  
166 -  
167 @synthesize bucket; 164 @synthesize bucket;
168 @synthesize subResource; 165 @synthesize subResource;
169 @synthesize currentObject; 166 @synthesize currentObject;
@@ -60,11 +60,11 @@ @@ -60,11 +60,11 @@
60 // Creates a HEAD request for the object at path 60 // Creates a HEAD request for the object at path
61 + (id)HEADRequestWithBucket:(NSString *)bucket key:(NSString *)key; 61 + (id)HEADRequestWithBucket:(NSString *)bucket key:(NSString *)key;
62 62
63 -@property (retain) NSString *bucket; 63 +@property (retain, nonatomic) NSString *bucket;
64 -@property (retain) NSString *key; 64 +@property (retain, nonatomic) NSString *key;
65 -@property (retain) NSString *sourceBucket; 65 +@property (retain, nonatomic) NSString *sourceBucket;
66 -@property (retain) NSString *sourceKey; 66 +@property (retain, nonatomic) NSString *sourceKey;
67 @property (retain, nonatomic) NSString *mimeType; 67 @property (retain, nonatomic) NSString *mimeType;
68 -@property (retain) NSString *subResource; 68 +@property (retain, nonatomic) NSString *subResource;
69 69
70 @end 70 @end
@@ -21,8 +21,7 @@ @@ -21,8 +21,7 @@
21 21
22 + (id)requestWithBucket:(NSString *)bucket key:(NSString *)key 22 + (id)requestWithBucket:(NSString *)bucket key:(NSString *)key
23 { 23 {
24 - NSString *path = [ASIS3Request stringByURLEncodingForS3Path:key]; 24 + ASIS3ObjectRequest *request = [[[self alloc] initWithURL:nil] autorelease];
25 - ASIS3ObjectRequest *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@",bucket,path]]] autorelease];  
26 [request setBucket:bucket]; 25 [request setBucket:bucket];
27 [request setKey:key]; 26 [request setKey:key];
28 return request; 27 return request;
@@ -30,8 +29,7 @@ @@ -30,8 +29,7 @@
30 29
31 + (id)requestWithBucket:(NSString *)bucket key:(NSString *)key subResource:(NSString *)subResource 30 + (id)requestWithBucket:(NSString *)bucket key:(NSString *)key subResource:(NSString *)subResource
32 { 31 {
33 - NSString *path = [ASIS3Request stringByURLEncodingForS3Path:key]; 32 + ASIS3ObjectRequest *request = [[[self alloc] initWithURL:nil] autorelease];
34 - ASIS3ObjectRequest *request = [[[self alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://%@.s3.amazonaws.com%@?%@",bucket,path,subResource]]] autorelease];  
35 [request setSubResource:subResource]; 33 [request setSubResource:subResource];
36 [request setBucket:bucket]; 34 [request setBucket:bucket];
37 [request setKey:key]; 35 [request setKey:key];
@@ -79,8 +77,6 @@ @@ -79,8 +77,6 @@
79 return request; 77 return request;
80 } 78 }
81 79
82 -  
83 -  
84 - (id)copyWithZone:(NSZone *)zone 80 - (id)copyWithZone:(NSZone *)zone
85 { 81 {
86 ASIS3ObjectRequest *newRequest = [super copyWithZone:zone]; 82 ASIS3ObjectRequest *newRequest = [super copyWithZone:zone];
@@ -103,6 +99,15 @@ @@ -103,6 +99,15 @@
103 [super dealloc]; 99 [super dealloc];
104 } 100 }
105 101
  102 +- (void)buildURL
  103 +{
  104 + if ([self subResource]) {
  105 + [self setURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@://%@.%@%@?%@",[self requestScheme],[[self class] S3Host],[ASIS3Request stringByURLEncodingForS3Path:[self key]],[self subResource]]]];
  106 + } else {
  107 + [self setURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@://%@.%@%@",[self requestScheme],[self bucket],[[self class] S3Host],[ASIS3Request stringByURLEncodingForS3Path:[self key]]]]];
  108 + }
  109 +}
  110 +
106 - (NSString *)mimeType 111 - (NSString *)mimeType
107 { 112 {
108 if (mimeType) { 113 if (mimeType) {
@@ -18,15 +18,18 @@ @@ -18,15 +18,18 @@
18 // See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTAccessPolicy.html for what these mean 18 // See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?RESTAccessPolicy.html for what these mean
19 extern NSString *const ASIS3AccessPolicyPrivate; // This is the default in S3 when no access policy header is provided 19 extern NSString *const ASIS3AccessPolicyPrivate; // This is the default in S3 when no access policy header is provided
20 extern NSString *const ASIS3AccessPolicyPublicRead; 20 extern NSString *const ASIS3AccessPolicyPublicRead;
21 -extern NSString *const ASIS3AccessPolicyPublicReadWrote; 21 +extern NSString *const ASIS3AccessPolicyPublicReadWrite;
22 extern NSString *const ASIS3AccessPolicyAuthenticatedRead; 22 extern NSString *const ASIS3AccessPolicyAuthenticatedRead;
  23 +extern NSString *const ASIS3AccessPolicyBucketOwnerRead;
  24 +extern NSString *const ASIS3AccessPolicyBucketOwnerFullControl;
23 25
24 typedef enum _ASIS3ErrorType { 26 typedef enum _ASIS3ErrorType {
25 ASIS3ResponseParsingFailedType = 1, 27 ASIS3ResponseParsingFailedType = 1,
26 ASIS3ResponseErrorType = 2 28 ASIS3ResponseErrorType = 2
27 -  
28 } ASIS3ErrorType; 29 } ASIS3ErrorType;
29 30
  31 +extern NSString *const ASIS3RequestSchemeHTTP;
  32 +extern NSString *const ASIS3RequestSchemeHTTPS;
30 33
31 @interface ASIS3Request : ASIHTTPRequest <NSCopying, NSXMLParserDelegate> { 34 @interface ASIS3Request : ASIHTTPRequest <NSCopying, NSXMLParserDelegate> {
32 35
@@ -36,6 +39,9 @@ typedef enum _ASIS3ErrorType { @@ -36,6 +39,9 @@ typedef enum _ASIS3ErrorType {
36 // Your S3 secret access key. Set it on the request, or set it globally using [ASIS3Request setSharedSecretAccessKey:] 39 // Your S3 secret access key. Set it on the request, or set it globally using [ASIS3Request setSharedSecretAccessKey:]
37 NSString *secretAccessKey; 40 NSString *secretAccessKey;
38 41
  42 + // Set to ASIS3RequestSchemeHTTPS to send your requests via HTTPS (default is ASIS3RequestSchemeHTTP)
  43 + NSString *requestScheme;
  44 +
39 // The string that will be used in the HTTP date header. Generally you'll want to ignore this and let the class add the current date for you, but the accessor is used by the tests 45 // The string that will be used in the HTTP date header. Generally you'll want to ignore this and let the class add the current date for you, but the accessor is used by the tests
40 NSString *dateString; 46 NSString *dateString;
41 47
@@ -68,7 +74,7 @@ typedef enum _ASIS3ErrorType { @@ -68,7 +74,7 @@ typedef enum _ASIS3ErrorType {
68 + (void)setSharedAccessKey:(NSString *)newAccessKey; 74 + (void)setSharedAccessKey:(NSString *)newAccessKey;
69 + (NSString *)sharedSecretAccessKey; 75 + (NSString *)sharedSecretAccessKey;
70 + (void)setSharedSecretAccessKey:(NSString *)newAccessKey; 76 + (void)setSharedSecretAccessKey:(NSString *)newAccessKey;
71 - 77 +
72 # pragma mark helpers 78 # pragma mark helpers
73 79
74 // Returns a date formatter than can be used to parse a date from S3 80 // Returns a date formatter than can be used to parse a date from S3
@@ -82,7 +88,11 @@ typedef enum _ASIS3ErrorType { @@ -82,7 +88,11 @@ typedef enum _ASIS3ErrorType {
82 // You shouldn't normally need to use this yourself 88 // You shouldn't normally need to use this yourself
83 + (NSString *)stringByURLEncodingForS3Path:(NSString *)key; 89 + (NSString *)stringByURLEncodingForS3Path:(NSString *)key;
84 90
  91 +// Returns a string for the hostname used for S3 requests. You shouldn't ever need to change this.
  92 ++ (NSString *)S3Host;
85 93
  94 +// This is called automatically before the request starts to build the request URL (if one has not been manually set already)
  95 +- (void)buildURL;
86 96
87 @property (retain) NSString *dateString; 97 @property (retain) NSString *dateString;
88 @property (retain) NSString *accessKey; 98 @property (retain) NSString *accessKey;
@@ -90,4 +100,5 @@ typedef enum _ASIS3ErrorType { @@ -90,4 +100,5 @@ typedef enum _ASIS3ErrorType {
90 @property (retain) NSString *accessPolicy; 100 @property (retain) NSString *accessPolicy;
91 @property (retain) NSString *currentXMLElementContent; 101 @property (retain) NSString *currentXMLElementContent;
92 @property (retain) NSMutableArray *currentXMLElementStack; 102 @property (retain) NSMutableArray *currentXMLElementStack;
  103 +@property (retain) NSString *requestScheme;
93 @end 104 @end
@@ -9,10 +9,16 @@ @@ -9,10 +9,16 @@
9 #import "ASIS3Request.h" 9 #import "ASIS3Request.h"
10 #import <CommonCrypto/CommonHMAC.h> 10 #import <CommonCrypto/CommonHMAC.h>
11 11
12 -NSString* const ASIS3AccessPolicyPrivate = @"private"; 12 +NSString *const ASIS3AccessPolicyPrivate = @"private";
13 -NSString* const ASIS3AccessPolicyPublicRead = @"public-read"; 13 +NSString *const ASIS3AccessPolicyPublicRead = @"public-read";
14 -NSString* const ASIS3AccessPolicyPublicReadWrote = @"public-read-write"; 14 +NSString *const ASIS3AccessPolicyPublicReadWrite = @"public-read-write";
15 -NSString* const ASIS3AccessPolicyAuthenticatedRead = @"authenticated-read"; 15 +NSString *const ASIS3AccessPolicyAuthenticatedRead = @"authenticated-read";
  16 +NSString *const ASIS3AccessPolicyBucketOwnerRead = @"bucket-owner-read";
  17 +NSString *const ASIS3AccessPolicyBucketOwnerFullControl = @"bucket-owner-full-control";
  18 +
  19 +NSString *const ASIS3RequestSchemeHTTP = @"http";
  20 +NSString *const ASIS3RequestSchemeHTTPS = @"https";
  21 +
16 22
17 static NSString *sharedAccessKey = nil; 23 static NSString *sharedAccessKey = nil;
18 static NSString *sharedSecretAccessKey = nil; 24 static NSString *sharedSecretAccessKey = nil;
@@ -29,6 +35,7 @@ static NSString *sharedSecretAccessKey = nil; @@ -29,6 +35,7 @@ static NSString *sharedSecretAccessKey = nil;
29 self = [super initWithURL:newURL]; 35 self = [super initWithURL:newURL];
30 // After a bit of experimentation/guesswork, this number seems to reduce the chance of a 'RequestTimeout' error 36 // After a bit of experimentation/guesswork, this number seems to reduce the chance of a 'RequestTimeout' error
31 [self setPersistentConnectionTimeoutSeconds:20]; 37 [self setPersistentConnectionTimeoutSeconds:20];
  38 + [self setRequestScheme:ASIS3RequestSchemeHTTP];
32 return self; 39 return self;
33 } 40 }
34 41
@@ -41,6 +48,7 @@ static NSString *sharedSecretAccessKey = nil; @@ -41,6 +48,7 @@ static NSString *sharedSecretAccessKey = nil;
41 [accessKey release]; 48 [accessKey release];
42 [secretAccessKey release]; 49 [secretAccessKey release];
43 [accessPolicy release]; 50 [accessPolicy release];
  51 + [requestScheme release];
44 [super dealloc]; 52 [super dealloc];
45 } 53 }
46 54
@@ -67,6 +75,14 @@ static NSString *sharedSecretAccessKey = nil; @@ -67,6 +75,14 @@ static NSString *sharedSecretAccessKey = nil;
67 return headers; 75 return headers;
68 } 76 }
69 77
  78 +- (void)main
  79 +{
  80 + if (![self url]) {
  81 + [self buildURL];
  82 + }
  83 + [super main];
  84 +}
  85 +
70 - (NSString *)canonicalizedResource 86 - (NSString *)canonicalizedResource
71 { 87 {
72 return @"/"; 88 return @"/";
@@ -79,6 +95,9 @@ static NSString *sharedSecretAccessKey = nil; @@ -79,6 +95,9 @@ static NSString *sharedSecretAccessKey = nil;
79 95
80 - (void)buildRequestHeaders 96 - (void)buildRequestHeaders
81 { 97 {
  98 + if (![self url]) {
  99 + [self buildURL];
  100 + }
82 [super buildRequestHeaders]; 101 [super buildRequestHeaders];
83 102
84 // If an access key / secret access key haven't been set for this request, let's use the shared keys 103 // If an access key / secret access key haven't been set for this request, let's use the shared keys
@@ -274,6 +293,14 @@ static NSString *sharedSecretAccessKey = nil; @@ -274,6 +293,14 @@ static NSString *sharedSecretAccessKey = nil;
274 return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; 293 return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH];
275 } 294 }
276 295
  296 ++ (NSString *)S3Host
  297 +{
  298 + return @"s3.amazonaws.com";
  299 +}
  300 +
  301 +- (void)buildURL
  302 +{
  303 +}
277 304
278 @synthesize dateString; 305 @synthesize dateString;
279 @synthesize accessKey; 306 @synthesize accessKey;
@@ -281,4 +308,5 @@ static NSString *sharedSecretAccessKey = nil; @@ -281,4 +308,5 @@ static NSString *sharedSecretAccessKey = nil;
281 @synthesize currentXMLElementContent; 308 @synthesize currentXMLElementContent;
282 @synthesize currentXMLElementStack; 309 @synthesize currentXMLElementStack;
283 @synthesize accessPolicy; 310 @synthesize accessPolicy;
  311 +@synthesize requestScheme;
284 @end 312 @end
@@ -21,10 +21,10 @@ @@ -21,10 +21,10 @@
21 21
22 + (id)serviceRequest 22 + (id)serviceRequest
23 { 23 {
24 - return [[[self alloc] initWithURL:[NSURL URLWithString:@"http://s3.amazonaws.com"]] autorelease]; 24 + ASIS3ServiceRequest *request = [[[self alloc] initWithURL:nil] autorelease];
  25 + return request;
25 } 26 }
26 27
27 -  
28 - (id)initWithURL:(NSURL *)newURL 28 - (id)initWithURL:(NSURL *)newURL
29 { 29 {
30 self = [super initWithURL:newURL]; 30 self = [super initWithURL:newURL];
@@ -32,7 +32,6 @@ @@ -32,7 +32,6 @@
32 return self; 32 return self;
33 } 33 }
34 34
35 -  
36 - (void)dealloc 35 - (void)dealloc
37 { 36 {
38 [buckets release]; 37 [buckets release];
@@ -42,6 +41,11 @@ @@ -42,6 +41,11 @@
42 [super dealloc]; 41 [super dealloc];
43 } 42 }
44 43
  44 +- (void)buildURL
  45 +{
  46 + [self setURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@://%@",[self requestScheme],[[self class] S3Host]]]];
  47 +}
  48 +
45 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict 49 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
46 { 50 {
47 if ([elementName isEqualToString:@"Bucket"]) { 51 if ([elementName isEqualToString:@"Bucket"]) {
@@ -452,7 +452,7 @@ static NSString *bucket = @""; @@ -452,7 +452,7 @@ static NSString *bucket = @"";
452 [listRequest setPrefix:@"foo"]; 452 [listRequest setPrefix:@"foo"];
453 [listRequest setMarker:@"bar"]; 453 [listRequest setMarker:@"bar"];
454 [listRequest setMaxResultCount:5]; 454 [listRequest setMaxResultCount:5];
455 - [listRequest createQueryString]; 455 + [listRequest buildURL];
456 NSString *expectedURL = [NSString stringWithFormat:@"http://%@.s3.amazonaws.com/?acl&prefix=foo&marker=bar&delimiter=/&max-keys=5",bucket]; 456 NSString *expectedURL = [NSString stringWithFormat:@"http://%@.s3.amazonaws.com/?acl&prefix=foo&marker=bar&delimiter=/&max-keys=5",bucket];
457 success = ([[[listRequest url] absoluteString] isEqualToString:expectedURL]); 457 success = ([[[listRequest url] absoluteString] isEqualToString:expectedURL]);
458 GHAssertTrue(success,@"Generated the wrong url when requesting a subresource"); 458 GHAssertTrue(success,@"Generated the wrong url when requesting a subresource");
1 -Subproject commit 99621b30f837502875d3591479a647193af4c031 1 +Subproject commit ffb51729219514fb7948b9be29bbae3dfabb1f4a
This diff was suppressed by a .gitattributes entry.