diff --git a/Mini vMac.xcodeproj/project.pbxproj b/Mini vMac.xcodeproj/project.pbxproj index 9fcbaa7..c12835f 100644 --- a/Mini vMac.xcodeproj/project.pbxproj +++ b/Mini vMac.xcodeproj/project.pbxproj @@ -153,6 +153,7 @@ 28F6B4C21CF07F5C002D76D0 /* liblibres.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 28F6B4B61CF07F32002D76D0 /* liblibres.a */; }; 28F6B4CA1CF1FA7A002D76D0 /* about.plist in Resources */ = {isa = PBXBuildFile; fileRef = 28F6B4C91CF1FA7A002D76D0 /* about.plist */; }; 28F6B4CF1CF77099002D76D0 /* compat.m in Sources */ = {isa = PBXBuildFile; fileRef = 28F6B4CE1CF77099002D76D0 /* compat.m */; }; + CA06D3352BA25DC10019B7B7 /* HFSDiskImage.m in Sources */ = {isa = PBXBuildFile; fileRef = CA06D3332BA25DC00019B7B7 /* HFSDiskImage.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -371,6 +372,8 @@ 28F6B4CE1CF77099002D76D0 /* compat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = compat.m; sourceTree = ""; }; 28F875921D29402B001E99EB /* PlugIn-Capabilities.plist.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "PlugIn-Capabilities.plist.xml"; sourceTree = ""; }; CA06D3322BA255830019B7B7 /* CodeSigning.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = CodeSigning.xcconfig; sourceTree = ""; }; + CA06D3332BA25DC00019B7B7 /* HFSDiskImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HFSDiskImage.m; sourceTree = ""; }; + CA06D3342BA25DC00019B7B7 /* HFSDiskImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HFSDiskImage.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -617,6 +620,8 @@ 28848B611CDE97D600B86C45 /* InsertDiskViewController.m */, 28848B631CDE97E900B86C45 /* SettingsViewController.h */, 28848B641CDE97E900B86C45 /* SettingsViewController.m */, + CA06D3342BA25DC00019B7B7 /* HFSDiskImage.h */, + CA06D3332BA25DC00019B7B7 /* HFSDiskImage.m */, 28F676C91CD15E0B00FC6FA6 /* Main.storyboard */, 28F676CC1CD15E0B00FC6FA6 /* Assets.xcassets */, 28BA896E1CE7314500A98104 /* Keyboard */, @@ -1301,6 +1306,7 @@ 28D5A3FD1CD6868F001A33F6 /* TouchScreen.m in Sources */, 28F676C51CD15E0B00FC6FA6 /* AppDelegate.m in Sources */, 28BA89801CE7315400A98104 /* KBKeyboardView.m in Sources */, + CA06D3352BA25DC10019B7B7 /* HFSDiskImage.m in Sources */, 28F676C21CD15E0B00FC6FA6 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mini vMac/HFSDiskImage.h b/Mini vMac/HFSDiskImage.h new file mode 100644 index 0000000..9f3ec72 --- /dev/null +++ b/Mini vMac/HFSDiskImage.h @@ -0,0 +1,42 @@ +// +// HFSDiskImage.h +// Mini vMac +// +// Created by Lieven Dekeyser on 13/03/2024. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface HFSDiskImage : NSObject + +@property (nonatomic, readonly, copy) NSString * path; +@property (nonatomic, readonly, getter=isOpen) BOOL open; +@property (nonatomic, readonly, getter=isReadOnly) BOOL readOnly; + ++ (nullable HFSDiskImage *)createDiskImageWithName:(NSString *)name size:(size_t)volumeSize atPath:(NSString *)path; + +- (nullable instancetype)initWithPath:(NSString *)path; + +- (instancetype)init __unavailable; ++ (instancetype)new __unavailable; + +- (BOOL)openForReading; +- (BOOL)openForReadingAndWriting; + +- (BOOL)close; + +- (BOOL)addFile:(NSString *)sourceFile; + +@end // HFSDiskImage + + +@interface HFSDiskImage (Import) + ++ (nullable HFSDiskImage *)importFileIntoTemporaryDiskImage:(NSString *)sourceFile; + +@end // HFSDiskImage (Import) + + +NS_ASSUME_NONNULL_END diff --git a/Mini vMac/HFSDiskImage.m b/Mini vMac/HFSDiskImage.m new file mode 100644 index 0000000..2cd6df5 --- /dev/null +++ b/Mini vMac/HFSDiskImage.m @@ -0,0 +1,328 @@ +// +// HFSDiskImage.m +// Mini vMac +// +// Created by Lieven Dekeyser on 13/03/2024. +// + +#import "HFSDiskImage.h" +#include "libhfs.h" + +@interface NSString (HFSSafe) +- (nullable NSString *)hfsSafeFileName; +- (nullable NSString *)hfsSafeVolumeName; +@end // NSString (HFSSafe) + + +@interface HFSDiskImage () +@property (nonatomic, copy) NSString * path; +@end // HFSDiskImage() + +@implementation HFSDiskImage +{ + hfsvol * _volume; +} + ++ (nullable HFSDiskImage *)createDiskImageWithName:(NSString *)name size:(size_t)volumeSize atPath:(NSString *)path +{ + NSFileManager * fm = [NSFileManager defaultManager]; + if ([fm fileExistsAtPath:path]) + { + return nil; + } + + + int fileDescriptor = open(path.fileSystemRepresentation, O_CREAT | O_TRUNC | O_EXCL | O_WRONLY, 0644); + if (fileDescriptor == -1) + { + return nil; + } + + int error = 0; + if (ftruncate(fileDescriptor, volumeSize)) + { + error = errno; + } + else + { + const char * volumeName = [name cStringUsingEncoding:NSMacOSRomanStringEncoding]; + if (volumeName != nil) + { + error = hfs_format(path.fileSystemRepresentation, 0, 0, volumeName, 0, NULL); + } + else + { + error = EINVAL; + } + } + + close(fileDescriptor); + + if (error != 0) + { + [fm removeItemAtPath:path error:nil]; + return nil; + } + + return [[self alloc] initWithPath:path]; +} + +- (nullable instancetype)initWithPath:(NSString *)path +{ + if ((self = [super init])) + { + _readOnly = NO; + _volume = nil; + self.path = path; + } + return self; +} + +- (void)dealloc +{ + [self close]; +} + +- (BOOL)isOpen +{ + return _volume != NULL; +} + +- (BOOL)openForReading +{ + return [self _openWithMode:HFS_MODE_RDONLY]; +} + +- (BOOL)openForReadingAndWriting +{ + return [self _openWithMode:HFS_MODE_RDWR]; +} + +- (BOOL)_openWithMode:(int)mode +{ + if ([self isOpen]) + { + return YES; + } + + _volume = hfs_mount(self.path.fileSystemRepresentation, 0, mode); + _readOnly = (mode != HFS_MODE_RDWR); + + return [self isOpen]; +} + +- (BOOL)close +{ + if (_volume) + { + int error = hfs_umount(_volume); + if (error == 0) { + _volume = NULL; + } + } + + return ![self isOpen]; +} + +- (BOOL)addFile:(NSString *)sourceFilePath +{ + if (![self isOpen] || [self isReadOnly]) + { + return NO; + } + + NSString * fileName = [[sourceFilePath lastPathComponent] hfsSafeFileName]; + if (fileName == nil) + { + return NO; + } + + const char * targetPath = [[NSString stringWithFormat:@":%@", fileName] cStringUsingEncoding:NSMacOSRomanStringEncoding]; + if (targetPath == NULL) + { + return NO; + } + + + FILE * sourceFile = fopen(sourceFilePath.fileSystemRepresentation, "r"); + if (sourceFile == NULL) + { + return NO; + } + + BOOL error = NO; + hfsfile * file = hfs_create(_volume, targetPath, "SIT!", "SITx"); // FIXME: type and creator from extension + if (file) + { + const size_t bufferSize = HFS_BLOCKSZ; + uint8_t buffer[bufferSize] = { 0 }; + + size_t bytesRead = 0; + while ((bytesRead = fread(buffer, 1, bufferSize, sourceFile)) > 0) + { + unsigned long bytesWritten = hfs_write(file, buffer, bytesRead); + if (bytesWritten < bytesRead) + { + error = YES; + break; + } + } + + hfs_close(file); + file = NULL; + } + else + { + error = YES; + } + + fclose(sourceFile); + sourceFile = NULL; + + return error == NO; +} + +@end // HFSDiskImage + + +@implementation HFSDiskImage (Import) + ++ (HFSDiskImage *)importFileIntoTemporaryDiskImage:(NSString *)sourceFile +{ + NSFileManager * fm = [NSFileManager defaultManager]; + NSError * error = nil; + size_t fileSize = [fm attributesOfItemAtPath:sourceFile error:&error].fileSize; + if (fileSize == 0) + { + return nil; + } + + + NSString * tempFolder = NSTemporaryDirectory(); + + NSString * volumeName = [[[sourceFile lastPathComponent] stringByDeletingPathExtension] hfsSafeVolumeName]; + if (volumeName == nil) + { + return nil; + } + NSString * diskImagePath = [tempFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.img", volumeName]]; + int tries = 1; + while ([fm fileExistsAtPath:diskImagePath]) + { + ++tries; + diskImagePath = [tempFolder stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d.img", volumeName, tries]]; + } + + + HFSDiskImage * diskImage = [HFSDiskImage createDiskImageWithName:volumeName size:(fileSize + (512*1024)) atPath:diskImagePath]; + if (diskImage == NULL || ![diskImage openForReadingAndWriting]) + { + return nil; + } + + BOOL result = [diskImage addFile:sourceFile]; + + [diskImage close]; + + if (result) + { + return diskImage; + } + else + { + return nil; + } + +} + +@end // HFSDiskImage (Import) + + + +@implementation NSString (HFSSafe) + +- (NSString *)macOSRomanSafeStringGettingLength:(NSUInteger *)outLength +{ + NSData * converted = [self dataUsingEncoding:NSMacOSRomanStringEncoding allowLossyConversion:YES]; + if (converted) + { + if (outLength) + { + *outLength = [converted length]; + } + return [[NSString alloc] initWithData:converted encoding:NSMacOSRomanStringEncoding]; + } + else + { + return nil; + } +} + +- (NSString *)macOSRomanSafeStringWithMaxLength:(NSUInteger)maxLength +{ + NSData * converted = [self dataUsingEncoding:NSMacOSRomanStringEncoding allowLossyConversion:YES]; + if (converted) + { + NSUInteger convertedLength = [converted length]; + if (convertedLength > maxLength) + { + converted = [converted subdataWithRange:NSMakeRange(0, maxLength)]; + } + + return [[NSString alloc] initWithData:converted encoding:NSMacOSRomanStringEncoding]; + } + else + { + return nil; + } +} + +- (NSString *)hfsSafeFileName +{ + NSString * noColons = [self stringByReplacingOccurrencesOfString:@":" withString:@"_"]; + NSUInteger convertedStringLength = 0; + NSString * convertedString = [noColons macOSRomanSafeStringGettingLength:&convertedStringLength]; + if (convertedString) + { + if (convertedStringLength <= HFS_MAX_FLEN) + { + return convertedString; + } + else + { + // keep path extension if possible: + NSUInteger pathExtensionLength = 0; + NSString * pathExtension = [[self pathExtension] macOSRomanSafeStringGettingLength:&pathExtensionLength]; + if (pathExtension) + { + NSInteger remainingLength = HFS_MAX_FLEN - pathExtensionLength - 1; + if (remainingLength > 2) + { + NSString * trimmedName = [[self stringByDeletingPathExtension] macOSRomanSafeStringWithMaxLength:remainingLength]; + return [trimmedName stringByAppendingPathExtension:pathExtension]; + + } + else + { + return [[self stringByDeletingPathExtension] macOSRomanSafeStringWithMaxLength:HFS_MAX_FLEN]; + } + } + else + { + return [self macOSRomanSafeStringWithMaxLength:HFS_MAX_FLEN]; + } + } + } + else + { + return nil; + } +} + +- (NSString *)hfsSafeVolumeName +{ + NSString * noColons = [self stringByReplacingOccurrencesOfString:@":" withString:@"_"]; + return [noColons macOSRomanSafeStringWithMaxLength:HFS_MAX_VLEN]; +} + +@end // NSString (HFSSafe)