Initial dmgutils implementation

This commit is contained in:
Greg Neagle
2024-06-30 19:30:54 -07:00
parent 97771e4528
commit 66c4f9a47c
3 changed files with 262 additions and 0 deletions
@@ -19,6 +19,9 @@
C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013644C2C30F5D8008DB215 /* RepoFactory.swift */; };
C013644F2C30F5D8008DB215 /* RepoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013644C2C30F5D8008DB215 /* RepoFactory.swift */; };
C01364522C311DFA008DB215 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C01364512C311DFA008DB215 /* ArgumentParser */; };
C01364542C321FE7008DB215 /* dmgutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364532C321FE7008DB215 /* dmgutils.swift */; };
C01364552C321FE7008DB215 /* dmgutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364532C321FE7008DB215 /* dmgutils.swift */; };
C01364562C321FE7008DB215 /* dmgutils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01364532C321FE7008DB215 /* dmgutils.swift */; };
C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */; };
C07A6FB02C2B22A400090743 /* prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FAF2C2B22A400090743 /* prefs.swift */; };
C07A6FB22C2B22D300090743 /* constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A6FB12C2B22D300090743 /* constants.swift */; };
@@ -75,6 +78,7 @@
C01364422C2DD1BA008DB215 /* plistutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = plistutils.swift; sourceTree = "<group>"; };
C01364482C2F8EFE008DB215 /* GitFileRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitFileRepo.swift; sourceTree = "<group>"; };
C013644C2C30F5D8008DB215 /* RepoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoFactory.swift; sourceTree = "<group>"; };
C01364532C321FE7008DB215 /* dmgutils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dmgutils.swift; sourceTree = "<group>"; };
C07A6FA52C2A82B400090743 /* managedsoftwareupdate */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = managedsoftwareupdate; sourceTree = BUILT_PRODUCTS_DIR; };
C07A6FA82C2A82B400090743 /* managedsoftwareupdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = managedsoftwareupdate.swift; sourceTree = "<group>"; };
C07A6FAF2C2B22A400090743 /* prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = prefs.swift; sourceTree = "<group>"; };
@@ -197,6 +201,7 @@
C07A6FD12C2B654300090743 /* utils.swift */,
C07A6FD92C2CF19600090743 /* cliutils.swift */,
C01364422C2DD1BA008DB215 /* plistutils.swift */,
C01364532C321FE7008DB215 /* dmgutils.swift */,
);
path = shared;
sourceTree = "<group>";
@@ -315,6 +320,7 @@
C07A6FB02C2B22A400090743 /* prefs.swift in Sources */,
C07A6FA92C2A82B400090743 /* managedsoftwareupdate.swift in Sources */,
C07A6FB22C2B22D300090743 /* constants.swift in Sources */,
C01364542C321FE7008DB215 /* dmgutils.swift in Sources */,
C07A6FD22C2B654300090743 /* utils.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -325,6 +331,7 @@
files = (
C01364442C2DD1BA008DB215 /* plistutils.swift in Sources */,
C01364462C2E051F008DB215 /* makecatalogslib.swift in Sources */,
C01364552C321FE7008DB215 /* dmgutils.swift in Sources */,
C07A6FDB2C2CF19600090743 /* cliutils.swift in Sources */,
C013644E2C30F5D8008DB215 /* RepoFactory.swift in Sources */,
C07A6FBE2C2B5AF000090743 /* prefs.swift in Sources */,
@@ -343,6 +350,7 @@
files = (
C07A6FDC2C2CF19600090743 /* cliutils.swift in Sources */,
C01364452C2DD1BA008DB215 /* plistutils.swift in Sources */,
C01364562C321FE7008DB215 /* dmgutils.swift in Sources */,
C07A6FCB2C2B5C3A00090743 /* prefs.swift in Sources */,
C013644F2C30F5D8008DB215 /* RepoFactory.swift in Sources */,
C07A6FC72C2B5C0700090743 /* makecatalogs.swift in Sources */,
+222
View File
@@ -0,0 +1,222 @@
//
// dmgutils.swift
// munki
//
// Created by Greg Neagle on 6/30/24.
//
import Foundation
func hdiutilData(arguments: [String], stdIn: String = "") -> PlistDict {
// runs an hdiutil <command> on a dmg and attempts to return a plist data structure
var hdiUtilArgs = arguments
if !hdiUtilArgs.contains("-plist") {
hdiUtilArgs.append("-plist")
}
let results = runCLI("/usr/bin/hdiutil", arguments: hdiUtilArgs, stdIn: stdIn)
if results.exitcode != 0 {
//TODO: implement Munki's display* methods
printStderr("hdiutil error \(results.error) with arguments \(arguments)")
}
let (plistStr, _) = parseFirstPlist(fromString: results.output)
if !plistStr.isEmpty {
do {
if let plist = try readPlistFromString(plistStr) as? PlistDict {
return plist
}
} catch {
return PlistDict()
}
}
return PlistDict()
}
func dmgImageInfo(_ dmgPath: String) -> PlistDict {
// runs hdiutil imageinfo on a dmg and returns a plist data structure
return hdiutilData(arguments: ["imageinfo", dmgPath])
}
func dmgIsWritable(_ dmgPath: String) -> Bool {
// Attempts to determine if the given disk image is writable
let imageInfo = dmgImageInfo(dmgPath)
if let format = imageInfo["Format"] as? String {
if ["UDSB", "UDSP", "UDRW", "RdWr"].contains(format) {
return true
}
}
return false
}
func dmgHasSLA(_ dmgPath: String) -> Bool {
// Returns true if dmg has a Software License Agreement.
// These dmgs normally cannot be attached without user intervention
let imageInfo = dmgImageInfo(dmgPath)
if let properties = imageInfo["Properties"] as? PlistDict {
if let hasSLA = properties["Software License Agreement"] as? Bool {
return hasSLA
}
}
return false
}
func hdiutilInfo() -> PlistDict {
// runs hdiutil info on a dmg and returns a plist data structure
return hdiutilData(arguments: ["info"])
}
func pathIsVolumeMountPoint(_ path: String) -> Bool {
// Returns a boolean to indicate if path is a mountpoint for a disk image
let info = hdiutilInfo()
if let images = info["images"] as? [PlistDict] {
// "images" is an array of dicts
for image in images {
if let systemEntities = image["system-entities"] as? [PlistDict] {
// "system-entities" is an array of dicts
for entity in systemEntities {
if let mountpoint = entity["mount-point"] as? String {
// there's a mount-point for this!
if path == mountpoint {
// our path is this mountpoint
return true
}
}
}
}
}
}
return false
}
func diskImageForMountPoint(_ path: String) -> String? {
// Attempts to find the path to a dmg for a given mount point
let info = hdiutilInfo()
if let images = info["images"] as? [PlistDict] {
// "images" is an array of dicts
for image in images {
if let imagePath = image["image-path"] as? String {
if let systemEntities = image["system-entities"] as? [PlistDict] {
// "system-entities" is an array of dicts
for entity in systemEntities {
if let mountpoint = entity["mount-point"] as? String {
// there's a mount-point for this!
if path == mountpoint {
// our path is this mountpoint
return imagePath
}
}
}
}
}
}
}
return nil
}
func mountPointsForDiskImage(_ dmgPath: String) -> [String] {
// returns a list of mountpoints for the given disk image
var mountpoints = [String]()
let info = hdiutilInfo()
if let images = info["images"] as? [PlistDict] {
// "images" is an array of dicts
for image in images {
if let imagePath = image["image-path"] as? String {
// "image-path" is path to dmg file
if imagePath == dmgPath {
// this is our disk image
if let systemEntities = image["system-entities"] as? [PlistDict] {
// "system-entities" is an array of dicts
for entity in systemEntities {
if let mountpoint = entity["mount-point"] as? String {
// there's a mount-point for this!
mountpoints.append(mountpoint)
}
}
}
}
}
}
}
return mountpoints
}
func diskImageIsMounted(_ dmgPath: String) -> Bool {
// Returns true if the given disk image is currently mounted
return !mountPointsForDiskImage(dmgPath).isEmpty
}
func mountdmg(
_ dmgPath: String,
useShadow: Bool = false,
useExistingMounts: Bool = false,
randomMountpoint: Bool = true,
skipVerification: Bool = false) -> [String]
{
// Attempts to mount the dmg at dmgpath
// and returns a list of mountpoints
// If use_shadow is true, mount image with shadow file
// If random_mountpoint, mount at random dir under /tmp
var mountpoints = [String]()
let dmgName = (dmgPath as NSString).lastPathComponent
if useExistingMounts {
if diskImageIsMounted(dmgPath) {
return mountPointsForDiskImage(dmgPath)
}
}
// attempt to mount the dmg
var stdIn = ""
if dmgHasSLA(dmgPath) {
stdIn = "Y\n"
// TODO: implement Munki display* methods
print("NOTE: \(dmgName) has embedded Software License Agreement")
}
var arguments = ["attach", dmgPath, "-nobrowse"]
if randomMountpoint {
arguments += ["-mountRandom", "/tmp"]
}
if useShadow {
arguments.append("-shadow")
}
if skipVerification {
arguments.append("-noverify")
}
let plistData = hdiutilData(arguments: arguments, stdIn: stdIn)
if let systemEntities = plistData["system-entities"] as? [PlistDict] {
for entity in systemEntities {
if let mountPoint = entity["mount-point"] as? String {
mountpoints.append(mountPoint)
}
}
} else {
// TODO: implement Munki display* methods
printStderr("Could not get mountpoint info from results of hdiutil attach \(dmgName)")
}
return mountpoints
}
func unmountdmg(_ mountpoint: String) {
// Unmounts the dmg at mountpoint
var arguments = ["detach", mountpoint]
let results = runCLI("/usr/bin/hdiutil", arguments: arguments)
if results.exitcode != 0 {
// regular unmount failed; try to force unmount
// TODO: implement Munki display* methods
printStderr("Polite unmount failed: \(results.error)")
printStderr("Attempting to force unmount \(mountpoint)")
arguments.append("-force")
let results = runCLI("/usr/bin/hdiutil", arguments: arguments)
if results.exitcode != 0 {
// TODO: implement Munki display* methods
print("WARNING: Failed to unmount \(mountpoint): \(results.error)")
}
}
}
+32
View File
@@ -12,3 +12,35 @@ func getVersion() -> String {
// or figure out a way to update this at build time
return "0.0.1"
}
func parseFirstPlist(fromString str: String) -> (String, String) {
// Parses a string, looking for the first thing that looks like a plist.
// Returns two strings. The first will be a string representaion of a plist (or empty)
// The second is any characters remaining after the found plist
let header = "<?xml version"
let footer = "</plist>"
let headerRange = (str as NSString).range(of: header)
if headerRange.location == NSNotFound {
// header not found
return ("", str)
}
let footerSearchIndex = headerRange.location + headerRange.length
let footerSearchRange = NSRange(
location: footerSearchIndex,
length: str.count - footerSearchIndex
)
let footerRange = (str as NSString).range(of: footer, range: footerSearchRange)
if footerRange.location == NSNotFound {
// footer not found
return ("", str)
}
let plistRange = NSRange(
location: headerRange.location,
length: footerRange.location + footerRange.length - headerRange.location
)
let plistStr = (str as NSString).substring(with: plistRange)
let remainderIndex = plistRange.location + plistRange.length
let remainder = (str as NSString).substring(from: remainderIndex)
return(plistStr, remainder)
}