Finally getting the hand of how documentation comments work

This commit is contained in:
Greg Neagle
2024-09-10 16:47:34 -07:00
parent 4fbf1f0d4c
commit 605ac2eedf
2 changed files with 107 additions and 110 deletions

View File

@@ -57,29 +57,29 @@ func storeCachedChecksum(toPath path: String, hash: String? = nil) -> String? {
return nil
}
/// Verifies the integrity of the given software package.
///
/// The feature is controlled through the PackageVerificationMode key in
/// Munki's preferences. Following modes currently exist:
/// none: No integrity check is performed.
/// hash: Integrity check is performed by calculating a SHA-256 hash of
/// the given file and comparing it against the reference value in
/// catalog. Only applies for package plists that contain the
/// item_key; for packages without the item_key, verification always
/// returns true.
/// hash_strict: Same as hash, but returns false for package plists that
/// do not contain the item_key.
///
/// Args:
/// path: The file to check integrity on.
/// expectedHash: the sha256 hash expected.
/// alwaysHash: Boolean. Always check and return the hash even if not
/// necessary for this function.
///
/// Returns:
/// (true/false, sha256-hash)
/// true if the package integrity could be validated. Otherwise, false.
func verifySoftwarePackageIntegrity(_ path: String, expectedHash: String, alwaysHash: Bool = false) -> (Bool, String) {
/// Verifies the integrity of the given software package.
/// The feature is controlled through the PackageVerificationMode key in
/// Munki's preferences. Following modes currently exist:
/// none: No integrity check is performed.
/// hash: Integrity check is performed by calculating a SHA-256 hash of
/// the given file and comparing it against the reference value in
/// catalog. Only applies for package plists that contain the
/// item_key; for packages without the item_key, verification always
/// returns true.
/// hash_strict: Same as hash, but returns false for package plists that
/// do not contain the item_key.
///
/// Args:
/// path: The file to check integrity on.
/// expectedHash: the sha256 hash expected.
/// alwaysHash: Boolean. Always check and return the hash even if not
/// necessary for this function.
///
/// Returns:
/// (true/false, sha256-hash)
/// true if the package integrity could be validated. Otherwise, false.
let mode = pref("PackageVerificationMode") as? String ?? "hash"
let itemName = (path as NSString).lastPathComponent
var calculatedHash = ""
@@ -119,10 +119,10 @@ func verifySoftwarePackageIntegrity(_ path: String, expectedHash: String, always
return (false, calculatedHash)
}
/// Given a list of strings in http header format, return a dict.
/// A User-Agent header is added if none is present in the list.
/// If strList is nil, returns a dict with only the User-Agent header.
func headerDictFromList(_ strList: [String]?) -> [String: String] {
/// Given a list of strings in http header format, return a dict.
/// A User-Agent header is added if none is present in the list.
/// If strList is nil, returns a dict with only the User-Agent header.
var headerDict = [String: String]()
headerDict["User-Agent"] = DEFAULT_USER_AGENT
@@ -144,6 +144,17 @@ func runMiddleware(options: GurlOptions, pkginfo _: PlistDict?) -> GurlOptions {
return options
}
/// Gets an HTTP or HTTPS URL and stores it in
/// destination path. Returns a dictionary of headers, which includes
/// http_result_code and http_result_description.
/// Will throw FetchError.connection if Gurl has a connection error.
/// Will throw FetchError.http if HTTP Result code is not 2xx or 304.
/// Will throw FetchError.fileSystem if Gurl has a filesystem error.
/// If destinationpath already exists, you can set 'onlyifnewer' to true to
/// indicate you only want to download the file only if it's newer on the
/// server.
/// If you set resume to true, Gurl will attempt to resume an
/// interrupted download.
func getURL(
_ url: String,
destinationPath: String,
@@ -154,17 +165,6 @@ func getURL(
followRedirects: String = "none",
pkginfo: PlistDict? = nil
) throws -> [String: String] {
/// Gets an HTTP or HTTPS URL and stores it in
/// destination path. Returns a dictionary of headers, which includes
/// http_result_code and http_result_description.
/// Will throw FetchError.connection if Gurl has a connection error.
/// Will throw FetchError.http if HTTP Result code is not 2xx or 304.
/// Will throw FetchError.fileSystem if Gurl has a filesystem error.
/// If destinationpath already exists, you can set 'onlyifnewer' to true to
/// indicate you only want to download the file only if it's newer on the
/// server.
/// If you set resume to true, Gurl will attempt to resume an
/// interrupted download.
let tempDownloadPath = destinationPath + ".download"
if pathExists(tempDownloadPath), !resume {
try? FileManager.default.removeItem(atPath: tempDownloadPath)
@@ -286,6 +286,13 @@ func getURL(
throw FetchError.http(errorCode: session.status, description: statusDescription)
}
/// Gets file from HTTP URL, checking first to see if it has changed on the
/// server.
///
/// Returns True if a new download was required; False if the
/// item is already in the local cache.
///
/// Throws a FetchError if there is an error (.connection or .download)
func getHTTPfileIfChangedAtomically(
_ url: String,
destinationPath: String,
@@ -295,13 +302,6 @@ func getHTTPfileIfChangedAtomically(
followRedirects: String = "none",
pkginfo: PlistDict? = nil
) throws -> Bool {
/// Gets file from HTTP URL, checking first to see if it has changed on the
/// server.
///
/// Returns True if a new download was required; False if the
/// item is already in the local cache.
///
/// Throws a FetchError if there is an error (.connection or .download)
// TODO: etag support
// var eTag = ""
var getOnlyIfNewer = false
@@ -381,14 +381,14 @@ func getHTTPfileIfChangedAtomically(
return true
}
/// Gets file from path, checking first to see if it has changed on the
/// source.
///
/// Returns true if a new copy was required; false if the
/// item is already in the local cache.
///
/// Throws FetchError.fileSystem if there is an error.
func getFileIfChangedAtomically(_ path: String, destinationPath: String) throws -> Bool {
/// Gets file from path, checking first to see if it has changed on the
/// source.
///
/// Returns true if a new copy was required; false if the
/// item is already in the local cache.
///
/// Throws FetchError.fileSystem if there is an error.
let filemanager = FileManager.default
if !pathExists(path) {
throw FetchError.fileSystem("Source does not exist: \(path)")
@@ -445,6 +445,19 @@ func getFileIfChangedAtomically(_ path: String, destinationPath: String) throws
return true
}
/// Gets file from a URL.
/// Checks first if there is already a file with the necessary checksum.
/// Then checks if the file has changed on the server, resuming or
/// re-downloading as necessary.
///
/// If the file has changed verify the pkg hash if so configured.
///
/// Supported schemes are http, https, file.
///
/// Returns true if a new download was required; False if the
/// item is already in the local cache.
///
/// Throws a FetchError if there is an error.
func getResourceIfChangedAtomically(
_ url: String,
destinationPath: String,
@@ -456,20 +469,6 @@ func getResourceIfChangedAtomically(
followRedirects: String? = nil,
pkginfo: PlistDict? = nil
) throws -> Bool {
/// Gets file from a URL.
/// Checks first if there is already a file with the necessary checksum.
/// Then checks if the file has changed on the server, resuming or
/// re-downloading as necessary.
///
/// If the file has changed verify the pkg hash if so configured.
///
/// Supported schemes are http, https, file.
///
/// Returns true if a new download was required; False if the
/// item is already in the local cache.
///
/// Throws a FetchError if there is an error.
guard let resolvedURL = URL(string: url) else {
throw FetchError.connection(errorCode: -1, description: "Invalid URL: \(url)")
}
@@ -549,6 +548,19 @@ func getResourceIfChangedAtomically(
return changed
}
/// A high-level function for getting resources from the Munki repo.
/// Gets a given URL from the Munki server.
/// Adds any additional headers to the request if present
/// Throws a FetchError if there's an error
///
/// Add any additional headers specified in ManagedInstalls.plist.
/// AdditionalHttpHeaders must be an array of strings with valid HTTP
/// header format. For example:
/// <key>AdditionalHttpHeaders</key>
/// <array>
/// <string>Key-With-Optional-Dashes: Foo Value</string>
/// <string>another-custom-header: bar value</string>
/// </array>
func fetchMunkiResourceByURL(
_ url: String,
destinationPath: String,
@@ -558,19 +570,6 @@ func fetchMunkiResourceByURL(
verify: Bool = false,
pkginfo: PlistDict? = nil
) throws -> Bool {
/// A high-level function for getting resources from the Munki repo.
/// Gets a given URL from the Munki server.
/// Adds any additional headers to the request if present
/// Throws a FetchError if there's an error
///
/// Add any additional headers specified in ManagedInstalls.plist.
/// AdditionalHttpHeaders must be an array of strings with valid HTTP
/// header format. For example:
/// <key>AdditionalHttpHeaders</key>
/// <array>
/// <string>Key-With-Optional-Dashes: Foo Value</string>
/// <string>another-custom-header: bar value</string>
/// </array>
let customHeaders = pref(ADDITIONAL_HTTP_HEADERS_KEY) as? [String]
return try getResourceIfChangedAtomically(
@@ -593,6 +592,7 @@ enum MunkiResourceType: String {
case package = "pkgs"
}
/// An even higher-level function for getting resources from the Munki repo.
func fetchMunkiResource(
kind: MunkiResourceType,
name: String,
@@ -603,7 +603,6 @@ func fetchMunkiResource(
verify: Bool = false,
pkginfo: PlistDict? = nil
) throws -> Bool {
/// An even higher-level function for getting resources from the Munki repo.
guard let url = munkiRepoURL(kind.rawValue, resource: name) else {
throw FetchError.connection(
errorCode: -1,
@@ -621,13 +620,12 @@ func fetchMunkiResource(
)
}
/// Returns data from URL.
/// We use the existing fetchMunkiResource function so any custom
/// authentication/authorization headers are used
/// (including, eventually, middleware-generated headers)
/// May throw a FetchError
func getDataFromURL(_ url: String) throws -> Data? {
/// Returns data from URL.
/// We use the existing fetchMunkiResource function so any custom
/// authentication/authorization headers are used
/// (including, eventually, middleware-generated headers)
/// May throw a FetchError
guard let tmpDir = TempDir.shared.makeTempDir() else {
displayError("Could not create temporary directory")
return nil
@@ -640,12 +638,11 @@ func getDataFromURL(_ url: String) throws -> Data? {
return FileManager.default.contents(atPath: tempDataPath)
}
/// A function we can call to check to see if the server is
/// available before we kick off a full run. This can be fooled by
/// ISPs that return results for non-existent web servers...
/// Returns a tuple (exitCode, exitDescription)
func checkServer(_ urlString: String = "") -> (Int, String) {
/// A function we can call to check to see if the server is
/// available before we kick off a full run. This can be fooled by
/// ISPs that return results for non-existent web servers...
/// Returns a tuple (exitCode, exitDescription)
let serverURL: String = if !urlString.isEmpty {
urlString
} else {

View File

@@ -25,6 +25,7 @@ func defaultLogger(_ message: String) {
print(message)
}
/// Some options used by Gurl
struct GurlOptions {
var url: String
var destinationPath: String
@@ -41,10 +42,9 @@ struct GurlOptions {
var log: (String) -> Void = defaultLogger // logging function
}
/// A class for getting content from a URL
/// using NSURLSession and friends
class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
/// A class for getting content from a URL
/// using NSURLSession and friends
let GURL_XATTR = "com.googlecode.munki.downloadData"
var options: GurlOptions
@@ -67,8 +67,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
super.init()
}
/// Start the connection
func start() {
/// Start the connection
guard !options.destinationPath.isEmpty else {
options.log("No output file specified")
done = true
@@ -139,17 +139,17 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
}
/// Cancel the session
func cancel() {
/// Cancel the session
if let session {
session.invalidateAndCancel()
}
done = true
}
/// Check if the connection request is complete. As a side effect,
/// allow the delegates to work by letting the run loop run for a bit
func isDone() -> Bool {
/// Check if the connection request is complete. As a side effect,
/// allow the delegates to work by letting the run loop run for a bit
if done {
return true
}
@@ -158,8 +158,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
return done
}
/// Returns any stored headers for destinationPath
func getStoredHeaders() -> [String: String]? {
/// Returns any stored headers for destinationPath
if options.destinationPath.isEmpty {
displayDebug1("destination path is not defined")
return nil
@@ -182,8 +182,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
}
/// Store headers dictionary as an xattr for options.destinationPath
func storeHeaders(_ headers: [String: String]) {
/// Store headers dictionary as an xattr for options.destinationPath
guard let data = try? plistToData(headers) else {
options.log("header convert to plist data failure")
return
@@ -199,10 +199,10 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
}
/// Since HTTP header names are not case-sensitive, we normalize a
/// dictionary of HTTP headers by converting all the key names to
/// lower case
func normalizeHeaderDict(_ headers: [String: String]) -> [String: String] {
/// Since HTTP header names are not case-sensitive, we normalize a
/// dictionary of HTTP headers by converting all the key names to
/// lower case
var normalizedHeaders = [String: String]()
for (key, value) in headers {
normalizedHeaders[key.lowercased()] = value
@@ -210,8 +210,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
return normalizedHeaders
}
/// Record any error info from completed session
func recordError(_ error: NSError) {
/// Record any error info from completed session
self.error = error
// if this was an SSL error, try to extract the SSL error code
if let underlyingError = error.userInfo["NSUnderlyingError"] as? NSError,
@@ -224,8 +224,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
func removeExpectedSizeFromStoredHeaders() {
/// If a successful transfer, clear the expected size so we
/// don't attempt to resume the download next time
// If a successful transfer, clear the expected size so we
// don't attempt to resume the download next time
if String(status).hasPrefix("2"),
var headers = getStoredHeaders(),
headers.keys.contains("expected-length")
@@ -235,8 +235,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
}
/// URLSessionTaskDelegate method
@objc func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: (any Error)?) {
/// URLSessionTaskDelegate method
if task != task {
return
}
@@ -261,7 +261,7 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
func okToResume(downloadData: [String: String]) -> Bool {
/// returns a boolean
// returns a boolean
guard let storedData = getStoredHeaders() else {
options.log("No stored headers")
return false
@@ -297,13 +297,13 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
return true
}
/// URLSessionDataDelegate method
@objc func urlSession(
_: URLSession,
dataTask _: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping @Sendable (URLSession.ResponseDisposition) -> Void
) {
/// URLSessionDataDelegate method
// self.response = response // doesn't appear to be actually used
bytesReceived = 0
percentComplete = -1
@@ -372,6 +372,7 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
completionHandler(.allow)
}
/// URLSessionTaskDelegate method
@objc func urlSession(
_: URLSession,
task _: URLSessionTask,
@@ -379,7 +380,6 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
newRequest request: URLRequest,
completionHandler: @escaping @Sendable (URLRequest?) -> Void
) {
/// URLSessionTaskDelegate method
guard let newURL = request.url else {
// deny the redirect
completionHandler(nil)
@@ -403,13 +403,13 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
completionHandler(nil)
}
/// URLSessionTaskDelegate method
@objc func urlSession(
_: URLSession,
task _: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
/// URLSessionTaskDelegate method
// Handle an authentication challenge
let supportedAuthMethods = [
NSURLAuthenticationMethodDefault,
@@ -453,8 +453,8 @@ class Gurl: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionData
}
}
/// URLSessionDataDelegate method
@objc func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
/// URLSessionDataDelegate method
// Handle received data
if let destination {
destination.write(data)