Signal-iOS/SignalServiceKit/Util/DataSource.m

491 lines
14 KiB
Objective-C

//
// Copyright 2017 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
#import "DataSource.h"
#import "OWSError.h"
#import "OWSFileSystem.h"
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark -
@interface DataSourceValue ()
@property (nonatomic) NSData *data;
@property (nonatomic) NSString *fileExtension;
@property (atomic) BOOL isConsumed;
// These properties is lazily-populated.
@property (nonatomic, nullable) NSURL *cachedFileUrl;
@property (nonatomic, nullable) ImageMetadata *cachedImageMetadata;
@end
#pragma mark -
@implementation DataSourceValue
- (void)dealloc
{
NSURL *_Nullable fileUrl = self.cachedFileUrl;
if (fileUrl != nil) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{ [OWSFileSystem deleteFileIfExists:fileUrl.path]; });
}
}
- (instancetype)initWithData:(NSData *)data fileExtension:(NSString *)fileExtension
{
self = [super init];
if (!self) {
return self;
}
_data = data;
_fileExtension = fileExtension;
_isConsumed = NO;
// Ensure that value is backed by file on disk.
__weak DataSourceValue *weakValue = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [weakValue dataUrl]; });
return self;
}
+ (_Nullable id<DataSource>)dataSourceWithData:(NSData *)data fileExtension:(NSString *)fileExtension
{
OWSAssertDebug(data);
if (!data) {
OWSFailDebug(@"data was unexpectedly nil");
return nil;
}
return [[self alloc] initWithData:data fileExtension:fileExtension];
}
+ (_Nullable id<DataSource>)dataSourceWithData:(NSData *)data utiType:(NSString *)utiType
{
NSString *fileExtension = [MimeTypeUtil fileExtensionForUtiType:utiType];
return [[self alloc] initWithData:data fileExtension:fileExtension];
}
+ (_Nullable id<DataSource>)dataSourceWithData:(NSData *)data mimeType:(NSString *)mimeType
{
NSString *fileExtension = [MimeTypeUtil fileExtensionForMimeType:mimeType];
if (fileExtension) {
return [[self alloc] initWithData:data fileExtension:fileExtension];
} else {
return nil;
}
}
+ (id<DataSource>)dataSourceWithOversizeText:(NSString *)text
{
NSData *data = [text.filterStringForDisplay dataUsingEncoding:NSUTF8StringEncoding];
return [[self alloc] initWithData:data fileExtension:MimeTypeUtil.oversizeTextAttachmentFileExtension];
}
+ (id<DataSource>)emptyDataSource
{
return [[self alloc] initWithData:[NSData new] fileExtension:@"bin"];
}
#pragma mark - DataSource
@synthesize sourceFilename = _sourceFilename;
- (void)setSourceFilename:(nullable NSString *)sourceFilename
{
OWSAssertDebug(!self.isConsumed);
_sourceFilename = sourceFilename.filterFilename;
}
- (nullable NSURL *)dataUrl
{
OWSAssertDebug(self.data);
OWSAssertDebug(!self.isConsumed);
@synchronized(self) {
if (!self.cachedFileUrl) {
NSURL *fileUrl = [OWSFileSystem temporaryFileUrlWithFileExtension:self.fileExtension
isAvailableWhileDeviceLocked:YES];
if ([self writeToUrl:fileUrl error:nil]) {
self.cachedFileUrl = fileUrl;
} else {
OWSFailDebug(@"Could not write data to disk.");
}
}
return self.cachedFileUrl;
}
}
- (NSUInteger)dataLength
{
OWSAssertDebug(self.data);
OWSAssertDebug(!self.isConsumed);
return self.data.length;
}
- (BOOL)writeToUrl:(NSURL *)dstUrl error:(NSError **)outError
{
OWSAssertDebug(self.data);
OWSAssertDebug(!self.isConsumed);
NSError *error = nil;
if (![self.data writeToURL:dstUrl options:NSDataWritingAtomic error:&error]) {
OWSFailDebug(@"Could not write data to disk: %@", error);
if (outError != NULL) {
*outError = error;
}
return NO;
}
return YES;
}
- (BOOL)moveToUrlAndConsume:(NSURL *)dstUrl error:(NSError **)outError
{
OWSAssertDebug(!self.isConsumed);
@synchronized(self) {
OWSAssertDebug(!NSThread.isMainThread);
// This method is meant to be fast. If _cachedFileUrl is nil,
// we'll still lazily generate it and this method will work,
// but it will be slower than expected.
OWSAssertDebug(self->_cachedFileUrl != nil);
NSURL *_Nullable srcUrl = self.dataUrl;
if (srcUrl == nil) {
if (outError != NULL) {
*outError = OWSErrorMakeAssertionError(@"Missing data URL.");
}
return NO;
}
self->_cachedFileUrl = nil;
self.isConsumed = YES;
NSError *error = nil;
if (![OWSFileSystem moveFileFrom:srcUrl to:dstUrl error:&error]) {
OWSFailDebug(@"Could not write data with error: %@", error);
if (outError != NULL) {
*outError = error;
}
return NO;
}
return YES;
}
}
- (BOOL)consumeAndDeleteWithError:(NSError **)outError
{
OWSAssertDebug(!self.isConsumed);
self.isConsumed = YES;
if (!self.cachedFileUrl) {
// Nothing to delete.
return YES;
}
return [OWSFileSystem deleteFileIfExistsWithUrl:self.cachedFileUrl error:outError];
}
- (BOOL)isValidImage
{
OWSAssertDebug(!self.isConsumed);
return [self.data ows_isValidImage];
}
- (BOOL)isValidVideo
{
OWSAssertDebug(!self.isConsumed);
if (![MimeTypeUtil isSupportedVideoFile:self.dataUrl.path]) {
return NO;
}
OWSFailDebug(@"Are we calling this anywhere? It seems quite inefficient.");
return [OWSMediaUtils isValidVideoWithPath:self.dataUrl.path];
}
- (nullable NSString *)mimeType
{
OWSAssertDebug(!self.isConsumed);
if (self.fileExtension == nil) {
OWSFailDebug(@"failure: fileExtension was unexpectedly nil");
return nil;
}
return [MimeTypeUtil mimeTypeForFileExtension:self.fileExtension];
}
- (BOOL)hasStickerLikeProperties
{
OWSAssertDebug(!self.isConsumed);
return [self.data ows_hasStickerLikeProperties];
}
- (ImageMetadata *)imageMetadata
{
OWSAssertDebug(!self.isConsumed);
@synchronized(self) {
if (self.cachedImageMetadata != nil) {
return self.cachedImageMetadata;
}
ImageMetadata *imageMetadata = [self.data imageMetadataWithPath:nil mimeType:self.mimeType ignoreFileSize:YES];
self.cachedImageMetadata = imageMetadata;
return imageMetadata;
}
}
@end
#pragma mark -
@interface DataSourcePath ()
@property (nonatomic) NSURL *fileUrl;
@property (nonatomic, readonly) BOOL shouldDeleteOnDeallocation;
@property (atomic) BOOL isConsumed;
// These properties is lazily-populated.
@property (nonatomic) NSData *cachedData;
@property (nonatomic, nullable) ImageMetadata *cachedImageMetadata;
@end
#pragma mark -
@implementation DataSourcePath
- (void)dealloc
{
if (self.shouldDeleteOnDeallocation && !self.isConsumed) {
NSURL *fileUrl = self.fileUrl;
if (!fileUrl) {
OWSFailDebug(@"fileUrl was unexpectedly nil");
return;
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error;
BOOL success = [[NSFileManager defaultManager] removeItemAtURL:fileUrl error:&error];
if (!success || error) {
OWSCFailDebug(@"DataSourcePath could not delete file: %@, %@", fileUrl, error);
}
});
}
}
- (nullable instancetype)initWithFileUrl:(NSURL *)fileUrl
shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation
error:(NSError **)error
{
if (!fileUrl || ![fileUrl isFileURL]) {
NSString *errorMsg = [NSString stringWithFormat:@"unexpected fileUrl: %@", fileUrl];
*error = OWSErrorMakeAssertionError(errorMsg);
return nil;
}
self = [super init];
if (!self) {
return self;
}
_fileUrl = fileUrl;
_shouldDeleteOnDeallocation = shouldDeleteOnDeallocation;
_isConsumed = NO;
return self;
}
+ (_Nullable id<DataSource>)dataSourceWithURL:(NSURL *)fileUrl
shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation
error:(NSError **)error
{
return [[self alloc] initWithFileUrl:fileUrl shouldDeleteOnDeallocation:shouldDeleteOnDeallocation error:error];
}
+ (_Nullable id<DataSource>)dataSourceWithFilePath:(NSString *)filePath
shouldDeleteOnDeallocation:(BOOL)shouldDeleteOnDeallocation
error:(NSError **)error
{
OWSAssertDebug(filePath);
if (!filePath) {
NSString *errorMsg = [NSString stringWithFormat:@"unexpected filePath: %@", filePath];
*error = OWSErrorMakeAssertionError(errorMsg);
return nil;
}
NSURL *fileUrl = [NSURL fileURLWithPath:filePath];
return [[self alloc] initWithFileUrl:fileUrl shouldDeleteOnDeallocation:shouldDeleteOnDeallocation error:error];
}
+ (_Nullable id<DataSource>)dataSourceWritingTempFileData:(NSData *)data
fileExtension:(NSString *)fileExtension
error:(NSError **)error
{
NSURL *fileUrl = [OWSFileSystem temporaryFileUrlWithFileExtension:fileExtension isAvailableWhileDeviceLocked:YES];
[data writeToURL:fileUrl options:NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication error:error];
if (*error != nil) {
return nil;
}
return [[self alloc] initWithFileUrl:fileUrl shouldDeleteOnDeallocation:YES error:error];
}
+ (_Nullable id<DataSource>)dataSourceWritingSyncMessageData:(NSData *)data error:(NSError **)error
{
return [self dataSourceWritingTempFileData:data fileExtension:MimeTypeUtil.syncMessageFileExtension error:error];
}
#pragma mark - DataSource
@synthesize sourceFilename = _sourceFilename;
- (void)setSourceFilename:(nullable NSString *)sourceFilename
{
OWSAssertDebug(!self.isConsumed);
_sourceFilename = sourceFilename.filterFilename;
}
- (NSData *)data
{
OWSAssertDebug(!self.isConsumed);
OWSAssertDebug(self.fileUrl);
@synchronized(self) {
if (!self.cachedData) {
self.cachedData = [NSData dataWithContentsOfFile:self.fileUrl.path];
}
if (!self.cachedData) {
OWSFailDebug(@"Could not read data from disk.");
self.cachedData = [NSData new];
}
return self.cachedData;
}
}
- (NSUInteger)dataLength
{
OWSAssertDebug(!self.isConsumed);
OWSAssertDebug(self.fileUrl);
NSNumber *fileSizeValue;
NSError *error;
[self.fileUrl getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&error];
if (error != nil) {
OWSFailDebug(@"Could not read data length from disk with error: %@", error);
return 0;
}
return fileSizeValue.unsignedIntegerValue;
}
- (nullable NSURL *)dataUrl
{
OWSAssertDebug(!self.isConsumed);
return self.fileUrl;
}
- (BOOL)isValidImage
{
OWSAssertDebug(!self.isConsumed);
return [NSData ows_isValidImageAtUrl:self.fileUrl mimeType:self.mimeType];
}
- (BOOL)isValidVideo
{
OWSAssertDebug(!self.isConsumed);
if (self.mimeType != nil) {
if (![MimeTypeUtil isSupportedVideoMimeType:self.mimeType]) {
return NO;
}
} else if (![MimeTypeUtil isSupportedVideoFile:self.dataUrl.path]) {
return NO;
}
return [OWSMediaUtils isValidVideoWithPath:self.dataUrl.path];
}
- (BOOL)hasStickerLikeProperties
{
OWSAssertDebug(!self.isConsumed);
return [NSData ows_hasStickerLikePropertiesWithPath:self.dataUrl.path];
}
- (ImageMetadata *)imageMetadata
{
OWSAssertDebug(!self.isConsumed);
@synchronized(self) {
if (self.cachedImageMetadata != nil) {
return self.cachedImageMetadata;
}
ImageMetadata *imageMetadata = [NSData imageMetadataWithPath:self.dataUrl.path
mimeType:self.mimeType
ignoreFileSize:YES];
self.cachedImageMetadata = imageMetadata;
return imageMetadata;
}
}
- (BOOL)writeToUrl:(NSURL *)dstUrl error:(NSError **)outError
{
OWSAssertDebug(!self.isConsumed);
OWSAssertDebug(self.fileUrl);
NSError *error = nil;
if (![NSFileManager.defaultManager copyItemAtURL:self.fileUrl toURL:dstUrl error:&error]) {
OWSFailDebug(@"Could not write data with error: %@", error);
if (outError != NULL) {
*outError = error;
}
return NO;
}
return YES;
}
- (BOOL)moveToUrlAndConsume:(NSURL *)dstUrl error:(NSError **)outError
{
OWSAssertDebug(!self.isConsumed);
OWSAssertDebug(self.fileUrl);
self.isConsumed = YES;
NSError *error = nil;
BOOL success = NO;
if ([[NSFileManager defaultManager] isWritableFileAtPath:self.fileUrl.path]) {
success = [OWSFileSystem moveFileFrom:self.fileUrl to:dstUrl error:&error];
} else {
OWSLogError(@"File was not writeable. Copying instead of moving.");
success = [NSFileManager.defaultManager copyItemAtURL:self.fileUrl toURL:dstUrl error:&error];
}
if (!success) {
if (outError != NULL) {
*outError = error;
}
OWSFailDebug(@"Could not write data with error: %@", error);
}
return success;
}
- (BOOL)consumeAndDeleteWithError:(NSError **)outError
{
OWSAssertDebug(!self.isConsumed);
self.isConsumed = YES;
return [OWSFileSystem deleteFileIfExistsWithUrl:self.fileUrl error:outError];
}
- (nullable NSString *)mimeType
{
OWSAssertDebug(!self.isConsumed);
NSString *_Nullable fileExtension = self.fileUrl.pathExtension;
return (fileExtension ? [MimeTypeUtil mimeTypeForFileExtension:fileExtension] : nil);
}
@end
NS_ASSUME_NONNULL_END