image sizing

Validate image sizing
This commit is contained in:
Michael Kirk 2018-08-30 18:59:26 -06:00
parent 2c8cec183c
commit e715bf9ea2
12 changed files with 215 additions and 28 deletions

2
Pods

@ -1 +1 @@
Subproject commit 4c3935aa74dfe52f047d664197bbf3f9d7b50c86
Subproject commit 9a3f6876d4a6086d10501383b96fb2d9d47a75b6

View file

@ -12,6 +12,7 @@
#import <AssetsLibrary/AssetsLibrary.h>
#import <SignalMessaging/NSString+OWS.h>
#import <SignalMessaging/OWSUnreadIndicator.h>
#import <SignalServiceKit/NSData+Image.h>
#import <SignalServiceKit/OWSContact.h>
#import <SignalServiceKit/TSInteraction.h>
@ -482,8 +483,20 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if ([self.attachmentStream isAnimated]) {
self.messageCellType = OWSMessageCellType_AnimatedImage;
} else if ([self.attachmentStream isImage]) {
if (![self.attachmentStream isValidImage]) {
DDLogWarn(@"Treating invalid image as generic attachment.");
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
self.messageCellType = OWSMessageCellType_StillImage;
} else if ([self.attachmentStream isVideo]) {
if (![self.attachmentStream isValidVideo]) {
DDLogWarn(@"Treating invalid video as generic attachment.");
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
self.messageCellType = OWSMessageCellType_Video;
} else {
OWSFail(@"%@ unexpected attachment type.", self.logTag);

View file

@ -196,6 +196,11 @@ class GifPickerCell: UICollectionViewCell {
clearViewState()
return
}
guard NSData.ows_isValidImage(atPath: asset.filePath, mimeType: OWSMimeTypeImageGif) else {
owsFail("\(logTag) invalid asset.")
clearViewState()
return
}
guard let image = YYImage(contentsOfFile: asset.filePath) else {
owsFail("\(logTag) could not load asset.")
clearViewState()

View file

@ -207,6 +207,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
}
private func createImagePreview() {
guard attachment.isValidImage else {
createGenericPreview()
return
}
guard let image = attachment.image() else {
createGenericPreview()
return
@ -225,6 +229,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
}
private func createVideoPreview() {
guard attachment.isValidVideo else {
createGenericPreview()
return
}
guard let image = attachment.videoPreview() else {
createGenericPreview()
return

View file

@ -141,6 +141,11 @@ public class SignalAttachment: NSObject {
return dataSource.isValidImage()
}
@objc
public var isValidVideo: Bool {
return dataSource.isValidVideo()
}
// This flag should be set for text attachments that can be sent as text messages.
@objc
public var isConvertibleToTextMessage = false

View file

@ -198,7 +198,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
avatarImage = [self.cnContactAvatarCache objectForKey:contactId];
if (!avatarImage) {
NSData *_Nullable avatarData = [self avatarDataForCNContactId:contactId];
if (avatarData) {
if (avatarData && [avatarData ows_isValidImage]) {
avatarImage = [UIImage imageWithData:avatarData];
}
if (avatarImage) {

View file

@ -77,9 +77,10 @@ NS_ASSUME_NONNULL_BEGIN
// Non-nil for attachments which need "lazy backup restore."
- (nullable OWSBackupFragment *)lazyRestoreFragment;
#pragma mark - Image Validation
#pragma mark - Validation
- (BOOL)isValidImage;
- (BOOL)isValidVideo;
#pragma mark - Update With... Methods

View file

@ -14,6 +14,8 @@
NS_ASSUME_NONNULL_BEGIN
const CGFloat kMaxVideoStillSize = 1 * 1024;
@interface TSAttachmentStream ()
// We only want to generate the file path for this attachment once, so that
@ -315,14 +317,6 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Image Validation
- (BOOL)isValidImageWithData:(NSData *)data
{
OWSAssert(self.isImage || self.isAnimated);
OWSAssert(data);
return [data ows_isValidImageWithMimeType:self.contentType];
}
- (BOOL)isValidImage
{
OWSAssert(self.isImage || self.isAnimated);
@ -330,6 +324,13 @@ NS_ASSUME_NONNULL_BEGIN
return [NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType];
}
- (BOOL)isValidVideo
{
OWSAssert(self.isVideo);
return [NSData ows_isValidVideoAtURL:self.mediaURL];
}
#pragma mark -
- (nullable UIImage *)image
@ -341,11 +342,10 @@ NS_ASSUME_NONNULL_BEGIN
if (!mediaUrl) {
return nil;
}
NSData *data = [NSData dataWithContentsOfURL:mediaUrl];
if (![self isValidImageWithData:data]) {
if (![self isValidImage]) {
return nil;
}
return [UIImage imageWithData:data];
return [[UIImage alloc] initWithContentsOfFile:self.filePath];
} else {
return nil;
}
@ -362,17 +362,12 @@ NS_ASSUME_NONNULL_BEGIN
return nil;
}
NSURL *_Nullable mediaUrl = [self mediaURL];
if (!mediaUrl) {
if (![NSData ows_isValidImageAtPath:self.filePath mimeType:self.contentType]) {
OWSFail(@"%@ skipping invalid image", self.logTag);
return nil;
}
NSData *data = [NSData dataWithContentsOfURL:mediaUrl];
if (![self isValidImageWithData:data]) {
return nil;
}
return data;
return [NSData dataWithContentsOfFile:self.filePath];
}
+ (BOOL)hasThumbnailForMimeType:(NSString *)contentType
@ -462,6 +457,11 @@ NS_ASSUME_NONNULL_BEGIN
CGImageRelease(thumbnail);
} else if (self.isVideo) {
if (![self isValidVideo]) {
DDLogWarn(@"%@ skipping thumbnail for invalid video at path: %@", self.logTag, self.filePath);
return;
}
result = [self videoStillImageWithMaxSize:CGSizeMake(thumbnailSize, thumbnailSize)];
} else {
OWSFail(@"%@ trying to generate thumnail for unexpected attachment: %@ of type: %@",
@ -471,7 +471,7 @@ NS_ASSUME_NONNULL_BEGIN
}
if (result == nil) {
OWSFail(@"%@ Unable to build thumbnail for attachmentId: %@", self.logTag, self.uniqueId);
DDLogError(@"Unable to build thumbnail for attachmentId: %@", self.uniqueId);
return;
}
@ -484,12 +484,18 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable UIImage *)videoStillImage
{
if (![self isValidVideo]) {
return nil;
}
// Uses the assets intrinsic size by default
return [self videoStillImageWithMaxSize:CGSizeZero];
return [self videoStillImageWithMaxSize:CGSizeMake(kMaxVideoStillSize, kMaxVideoStillSize)];
}
- (nullable UIImage *)videoStillImageWithMaxSize:(CGSize)maxSize
{
maxSize.width = MIN(maxSize.width, kMaxVideoStillSize);
maxSize.height = MIN(maxSize.height, kMaxVideoStillSize);
NSURL *_Nullable mediaUrl = [self mediaURL];
if (!mediaUrl) {
return nil;
@ -502,6 +508,10 @@ NS_ASSUME_NONNULL_BEGIN
NSError *err = NULL;
CMTime time = CMTimeMake(1, 60);
CGImageRef imgRef = [generator copyCGImageAtTime:time actualTime:NULL error:&err];
if (imgRef == NULL) {
DDLogError(@"Could not generate video still: %@", self.filePath.pathExtension);
return nil;
}
return [[UIImage alloc] initWithCGImage:imgRef];
}
@ -531,6 +541,9 @@ NS_ASSUME_NONNULL_BEGIN
- (CGSize)calculateImageSize
{
if ([self isVideo]) {
if (![self isValidVideo]) {
return CGSizeZero;
}
return [self videoStillImage].size;
} else if ([self isImage] || [self isAnimated]) {
NSURL *_Nullable mediaUrl = [self mediaURL];

View file

@ -31,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)isValidImage;
- (BOOL)isValidVideo;
@end
#pragma mark -

View file

@ -73,6 +73,11 @@ NS_ASSUME_NONNULL_BEGIN
return [data ows_isValidImage];
}
- (BOOL)isValidVideo
{
return [NSData ows_isValidVideoAtURL:self.dataUrl];
}
- (void)setSourceFilename:(nullable NSString *)sourceFilename
{
_sourceFilename = sourceFilename.filterFilename;

View file

@ -11,4 +11,6 @@
- (BOOL)ows_isValidImage;
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType;
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url;
@end

View file

@ -4,6 +4,7 @@
#import "NSData+Image.h"
#import "MIMETypeUtil.h"
#import <AVFoundation/AVFoundation.h>
typedef NS_ENUM(NSInteger, ImageFormat) {
ImageFormat_Unknown,
@ -23,18 +24,127 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
- (BOOL)ows_isValidImage
{
return [self ows_isValidImageWithMimeType:nil];
if (![self ows_isValidImageWithMimeType:nil]) {
return NO;
}
if (![self ows_hasValidImageDimensions]) {
return NO;
}
return YES;
}
+ (BOOL)ows_isValidImageAtPath:(NSString *)filePath mimeType:(nullable NSString *)mimeType
{
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
if (error) {
NSData *_Nullable data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error];
if (!data || error) {
DDLogError(@"%@ could not read image data: %@", self.logTag, error);
return NO;
}
return [data ows_isValidImageWithMimeType:mimeType];
if (![data ows_isValidImageWithMimeType:mimeType]) {
return NO;
}
if (![self ows_hasValidImageDimensionsAtPath:filePath]) {
DDLogError(@"%@ image had invalid dimensions.", self.logTag);
return NO;
}
return YES;
}
- (BOOL)ows_hasValidImageDimensions
{
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self, NULL);
if (imageSource == NULL) {
return NO;
}
BOOL result = [NSData ows_hasValidImageDimensionWithImageSource:imageSource];
CFRelease(imageSource);
return result;
}
+ (BOOL)ows_hasValidImageDimensionsAtPath:(NSString *)path
{
NSURL *url = [NSURL fileURLWithPath:path];
if (!url) {
return NO;
}
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)url, NULL);
if (imageSource == NULL) {
return NO;
}
BOOL result = [self ows_hasValidImageDimensionWithImageSource:imageSource];
CFRelease(imageSource);
return result;
}
+ (BOOL)ows_hasValidImageDimensionWithImageSource:(CGImageSourceRef)imageSource
{
OWSAssert(imageSource);
NSDictionary *imageProperties
= (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (!imageProperties) {
return NO;
}
NSNumber *widthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelWidth];
if (!widthNumber) {
DDLogError(@"widthNumber was unexpectedly nil");
return NO;
}
CGFloat width = widthNumber.floatValue;
NSNumber *heightNumber = imageProperties[(__bridge NSString *)kCGImagePropertyPixelHeight];
if (!heightNumber) {
DDLogError(@"heightNumber was unexpectedly nil");
return NO;
}
CGFloat height = heightNumber.floatValue;
/* The number of bits in each color sample of each pixel. The value of this
* key is a CFNumberRef. */
NSNumber *depthNumber = imageProperties[(__bridge NSString *)kCGImagePropertyDepth];
if (!depthNumber) {
DDLogError(@"depthNumber was unexpectedly nil");
return NO;
}
NSUInteger depthBits = depthNumber.unsignedIntegerValue;
CGFloat depthBytes = (CGFloat)ceil(depthBits / 8.f);
/* The color model of the image such as "RGB", "CMYK", "Gray", or "Lab".
* The value of this key is CFStringRef. */
NSString *colorModel = imageProperties[(__bridge NSString *)kCGImagePropertyColorModel];
if (!colorModel) {
DDLogError(@"colorModel was unexpectedly nil");
return NO;
}
if (![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelRGB]
&& ![colorModel isEqualToString:(__bridge NSString *)kCGImagePropertyColorModelGray]) {
DDLogError(@"Invalid colorModel: %@", colorModel);
return NO;
}
// We only support (A)RGB and (A)Grayscale, so worst case is 4.
CGFloat kWorseCastComponentsPerPixel = 4;
CGFloat bytesPerPixel = kWorseCastComponentsPerPixel * depthBytes;
CGFloat kMaxDimension = 2 * 1024;
CGFloat kExpectedBytePerPixel = 4;
CGFloat kMaxBytes = kMaxDimension * kMaxDimension * kExpectedBytePerPixel;
CGFloat actualBytes = width * height * bytesPerPixel;
if (actualBytes > kMaxBytes) {
DDLogWarn(@"invalid dimensions width: %f, height %f, bytesPerPixel: %f", width, height, bytesPerPixel);
return NO;
}
return YES;
}
- (BOOL)ows_isValidImageWithMimeType:(nullable NSString *)mimeType
@ -151,4 +261,27 @@ typedef NS_ENUM(NSInteger, ImageFormat) {
return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize);
}
+ (BOOL)ows_isValidVideoAtURL:(NSURL *)url
{
OWSAssert(url);
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
CGSize maxSize = CGSizeZero;
for (AVAssetTrack *track in [asset tracksWithMediaType:AVMediaTypeVideo]) {
CGSize trackSize = track.naturalSize;
maxSize.width = MAX(maxSize.width, trackSize.width);
maxSize.height = MAX(maxSize.height, trackSize.height);
}
if (maxSize.width < 1.f || maxSize.height < 1.f) {
DDLogError(@"Invalid video size: %@", NSStringFromCGSize(maxSize));
return NO;
}
const CGFloat kMaxSize = 3 * 1024.f;
if (maxSize.width > kMaxSize || maxSize.height > kMaxSize) {
DDLogError(@"Invalid video dimensions: %@", NSStringFromCGSize(maxSize));
return NO;
}
return YES;
}
@end