TwitterImagePipeline/TIPFileUtils.m (233 lines of code) (raw):

// // TIPFileUtils.m // TwitterImagePipeline // // Created on 7/14/16. // Copyright © 2020 Twitter. All rights reserved. // #include <dirent.h> #include <objc/runtime.h> #include <sys/stat.h> #include <sys/xattr.h> #import "TIP_Project.h" #import "TIPFileUtils.h" NS_ASSUME_NONNULL_BEGIN #pragma mark - File Helpers NSArray<NSURL *> * __nullable TIPContentsAtPath(NSString *path, NSError * __nullable * __nullable outError) { NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES]; NSFileManager *fm = [NSFileManager defaultManager]; NSError *error = nil; NSArray<NSURL *> *paths = [fm contentsOfDirectoryAtURL:url includingPropertiesForKeys:@[NSURLFileSizeKey, NSURLContentModificationDateKey, NSURLIsDirectoryKey] options:NSDirectoryEnumerationSkipsSubdirectoryDescendants error:&error]; if (outError) { *outError = error; } return paths; } NSUInteger TIPFileSizeAtPath(NSString *path, NSError * __nullable * __nullable outError) { NSError *error = nil; NSUInteger size = 0; if (path.length > 0) { struct stat fileStatStruct; if (0 != stat(path.UTF8String, &fileStatStruct)) { error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]; } else if (fileStatStruct.st_size < 0) { error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EBADF userInfo:nil]; } else { size = (NSUInteger)fileStatStruct.st_size; } } if (error && outError) { *outError = error; } return size; } NSDate * __nullable TIPLastModifiedDateAtPath(NSString *path) { return TIPLastModifiedDateAtPathURL([NSURL fileURLWithPath:path]); } NSDate * __nullable TIPLastModifiedDateAtPathURL(NSURL *pathURL) { NSDate *date = nil; [pathURL getResourceValue:&date forKey:NSURLContentModificationDateKey error:NULL]; return date; } void TIPSetLastModifiedDateAtPath(NSString * path, NSDate *date) { TIPSetLastModifiedDateAtPathURL([NSURL fileURLWithPath:path], date); } void TIPSetLastModifiedDateAtPathURL(NSURL *pathURL, NSDate *date) { [pathURL setResourceValue:date forKey:NSURLContentModificationDateKey error:NULL]; } #pragma mark - File Extended Attribute Helpers NSArray<NSString *> * __nullable TIPListXAttributesForFile(NSString *filePath) { const char *cFilePath = filePath.fileSystemRepresentation; const ssize_t listStringSize = listxattr(cFilePath, NULL, 0, 0); if (listStringSize < 0) { return nil; } else if (listStringSize == 0) { return @[]; } char* listBuff = (char*)malloc((size_t)(listStringSize + 1)); listBuff[listStringSize] = '\0'; // make the buffer ARC controlled (freed when no longer used) NSData *listData = [NSData dataWithBytesNoCopy:listBuff length:(NSUInteger)(listStringSize + 1) freeWhenDone:YES]; if (listxattr(cFilePath, listBuff, (size_t)listStringSize, 0) < 0) { return nil; } NSMutableArray<NSString *> *attributeNames = [[NSMutableArray alloc] init]; char *head = listBuff; char *cur = listBuff; char *end = listBuff + listStringSize; for (; cur < end; cur++) { if (*cur == '\0') { NSString *attributeName = [[NSString alloc] initWithBytesNoCopy:head length:(NSUInteger)(cur - head) encoding:NSUTF8StringEncoding freeWhenDone:NO]; if (attributeName) { [attributeNames addObject:attributeName]; // Associate the NSString with the source NSData to keep the data properly ref counted static const char kAssociatedDataKey[] = "data_ref"; objc_setAssociatedObject(attributeName, &kAssociatedDataKey, listData, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } cur++; head = cur; } } return attributeNames; } NSDictionary *TIPGetXAttributesForFile(NSString *filePath, NSDictionary *keyKindMap) { const char *cFilePath = filePath.fileSystemRepresentation; NSMutableDictionary *xattrs = [NSMutableDictionary dictionaryWithCapacity:keyKindMap.count]; for (NSString *name in keyKindMap) { id value; const char *attrName = name.UTF8String; Class kind = keyKindMap[name]; if ([kind isSubclassOfClass:[NSNumber class]]) { value = TIPGetXAttributeNumberFromFile(attrName, cFilePath); } else if ([kind isSubclassOfClass:[NSString class]]) { value = TIPGetXAttributeStringFromFile(attrName, cFilePath); } else if ([kind isSubclassOfClass:[NSDate class]]) { value = TIPGetXAttributeDateFromFile(attrName, cFilePath); } else if ([kind isSubclassOfClass:[NSURL class]]) { value = TIPGetXAttributeURLFromFile(attrName, cFilePath); } if (value) { xattrs[name] = value; } } return xattrs; } NSUInteger TIPSetXAttributesForFile(NSDictionary *xattrs, NSString *filePath) { NSUInteger setCount = 0; const char *cFilePath = filePath.fileSystemRepresentation; for (NSString *name in xattrs) { int result = -1; const char *attrName = name.UTF8String; id value = xattrs[name]; if ([value isKindOfClass:[NSNumber class]]) { result = TIPSetXAttributeNumberForFile(attrName, value, cFilePath); } else if ([value isKindOfClass:[NSString class]]) { result = TIPSetXAttributeStringForFile(attrName, value, cFilePath); } else if ([value isKindOfClass:[NSDate class]]) { result = TIPSetXAttributeDateForFile(attrName, value, cFilePath); } else if ([value isKindOfClass:[NSURL class]]) { result = TIPSetXAttributeURLForFile(attrName, value, cFilePath); } if (0 != result) { int errorInt = errno; TIPLogWarning(@"Failed to setxattr '%@' on '%@': %i", name, filePath, errorInt); if (ENOENT == errorInt) { // The file doesn't exist, bail early break; } } else { setCount++; } } return setCount; } int TIPSetXAttributeDateForFile(const char *name, NSDate *date, const char *filePath) { NSTimeInterval ti = [date timeIntervalSinceReferenceDate]; return TIPSetXAttributeNumberForFile(name, @(ti), filePath); } int TIPSetXAttributeStringForFile(const char *name, NSString *string, const char *filePath) { const char *value = string.UTF8String; return setxattr(filePath, name, value, strlen(value), 0, 0); } int TIPSetXAttributeNumberForFile(const char *name, NSNumber *number, const char *filePath) { double value = number.doubleValue; return setxattr(filePath, name, &value, sizeof(double), 0, 0); } int TIPSetXAttributeURLForFile(const char *name, NSURL *URL, const char *filePath) { NSString *URLString = [URL absoluteString]; return TIPSetXAttributeStringForFile(name, URLString, filePath); } NSString * __nullable TIPGetXAttributeStringFromFile(const char *name, const char *filePath) { static const NSUInteger kStackBufferSize = 1024; char stackBuffer[kStackBufferSize]; ssize_t bytesRead = getxattr(filePath, name, &stackBuffer, kStackBufferSize, 0, 0); if (bytesRead > 0) { // copy to heap in NSString return [[NSString alloc] initWithBytes:stackBuffer length:(NSUInteger)bytesRead encoding:NSUTF8StringEncoding]; } else if (bytesRead == 0) { // no attribute return nil; } else if (errno != ERANGE) { // attribute access error is not recoverable return nil; } bytesRead = getxattr(filePath, name, NULL, 0, 0, 0); if (bytesRead <= 0) { // Failure to load attribute return nil; } char *buffer = (char *)malloc((size_t)bytesRead); if (getxattr(filePath, name, buffer, (size_t)bytesRead, 0, 0) <= 0) { // Failure to read attribute free(buffer); return nil; } // Return attribute as NSString on heap (use malloc'd buffer directly) return [[NSString alloc] initWithBytesNoCopy:buffer length:(NSUInteger)bytesRead encoding:NSUTF8StringEncoding freeWhenDone:YES]; } NSNumber * __nullable TIPGetXAttributeNumberFromFile(const char *name, const char *filePath) { double number; const ssize_t bufferLength = getxattr(filePath, name, &number, sizeof(double), 0, 0); if (bufferLength != sizeof(double)) { // failure to load attribute or failure to read attribute or incorrect format of attribute return nil; } return @(number); } NSDate * __nullable TIPGetXAttributeDateFromFile(const char *name, const char *filePath) { NSNumber *number = TIPGetXAttributeNumberFromFile(name, filePath); if (!number) { return nil; } return [NSDate dateWithTimeIntervalSinceReferenceDate:[number doubleValue]]; } NSURL * __nullable TIPGetXAttributeURLFromFile(const char *name, const char *filePath) { NSString *URLString = TIPGetXAttributeStringFromFile(name, filePath); NSURL *URL = nil; @try { URL = [NSURL URLWithString:URLString]; } @catch (NSException *) { } return URL; } NS_ASSUME_NONNULL_END