From 611b3c435dbcbaf325abe159883399d41186e26f Mon Sep 17 00:00:00 2001 From: Ethan Arbuckle Date: Mon, 6 Jan 2025 00:27:28 -0800 Subject: [PATCH] initial --- .gitignore | 3 + README.md | 32 ++ deploy.py | 103 ++++ entitlements.xml | 15 + oslo.xcodeproj/project.pbxproj | 399 +++++++++++++++ oslo/LoggingSupport.h | 92 ++++ oslo/grouping.h | 16 + oslo/grouping.m | 92 ++++ oslo/kat/hashtable.c | 737 +++++++++++++++++++++++++++ oslo/kat/hashtable.h | 98 ++++ oslo/kat/highlight.c | 887 +++++++++++++++++++++++++++++++++ oslo/kat/highlight.h | 205 ++++++++ oslo/main.m | 175 +++++++ oslo/output.h | 18 + oslo/output.m | 215 ++++++++ 15 files changed, 3087 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 deploy.py create mode 100644 entitlements.xml create mode 100644 oslo.xcodeproj/project.pbxproj create mode 100644 oslo/LoggingSupport.h create mode 100644 oslo/grouping.h create mode 100644 oslo/grouping.m create mode 100644 oslo/kat/hashtable.c create mode 100644 oslo/kat/hashtable.h create mode 100644 oslo/kat/highlight.c create mode 100644 oslo/kat/highlight.h create mode 100644 oslo/main.m create mode 100644 oslo/output.h create mode 100644 oslo/output.m diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebaf577 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +xcuserdata +*.xcworkspacedata +xcshareddata \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0431c1 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# oslo + +An `os_log` viewer for iOS that supports streaming both real-time logs and archived logs. + +## Features +- View real-time logs +- View archived logs (even if they weren't streamed live) + - Does not include logs prior to the last reboot +- Unredacts `` values in log messages +- Process name filtering +- Syntax highlighting by [kat](https://github.com/Theldus/kat/tree/master) +- PID-based grouping + last-log filtering + - "for each recent crash of ``, show the last couple of logs that were printed before it terminated, grouped by PID and ordered by last-event-in-group timestamp" + + +## Usage + +``` +oslo [-l] [-s] [-g] [ProcessName] + +Options: + -l Live logs (default) + -s Stored/archived logs + -g Group logs by PID (requires -s) + ProcessName Filter by process name +``` + + +[![asciicast](https://asciinema.org/a/tet26ugcwutH0CIwjKeS99C1P.svg?poster=npt:07)](https://asciinema.org/a/tet26ugcwutH0CIwjKeS99C1P?poster=npt:07) + + +Tested on iOS 14.0 - 18.2 \ No newline at end of file diff --git a/deploy.py b/deploy.py new file mode 100644 index 0000000..17b2fda --- /dev/null +++ b/deploy.py @@ -0,0 +1,103 @@ +import os +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +DEVICE_SSH_PORT = "2222" +DEVICE_SSH_IP = "localhost" +LOCAL_LDID2_PATH = "/opt/homebrew/bin/ldid2" + + +@dataclass +class BinaryInstallInformation: + # The on-device path to copy the binary to + on_device_path: str + # An entitlements file to sign the local binary with before copying to the device. + # If no file is specified, the binary will be signed without explicit entitlements + entitlements_file: Optional[str] = None + + +BINARY_DEPLOY_INFO = { + "oslo": BinaryInstallInformation("/var/jb/usr/bin/oslo", "entitlements.xml"), +} + + +def run_command_on_device(command: str) -> bytes: + return subprocess.check_output( + f'ssh -oStricthostkeychecking=no -oUserknownhostsfile=/dev/null -p {DEVICE_SSH_PORT} root@{DEVICE_SSH_IP} "{command}"', + shell=True, + ) + + +def copy_file_to_device(local: str, remote: str) -> None: + subprocess.check_output( + f'scp -O -oStricthostkeychecking=no -oUserknownhostsfile=/dev/null -P {DEVICE_SSH_PORT} "{local}" root@{DEVICE_SSH_IP}:"{remote}"', + shell=True, + ) + + +def deploy_to_device(local_path: Path, binary_deploy_info: BinaryInstallInformation) -> None: + # Sign the local binary + if not Path(LOCAL_LDID2_PATH).exists(): + raise Exception(f"Ldid2 path does not exist locally! {LOCAL_LDID2_PATH}") + + ldid_cmd_args = [LOCAL_LDID2_PATH] + if binary_deploy_info.entitlements_file: + ldid_cmd_args.append(f"-S{binary_deploy_info.entitlements_file}") + else: + ldid_cmd_args.append("-S") + ldid_cmd_args.append(local_path.as_posix()) + print(ldid_cmd_args) +# print(subprocess.check_output(ldid_cmd_args)) + + # Delete existing binary on-device if it exists + try: + run_command_on_device(f"/var/jb/usr/bin/rm {binary_deploy_info.on_device_path}") + except: + print(f"failed to delete on-device binary {binary_deploy_info.on_device_path}") + pass + + # Ensure the target install directory exists on-device + try: + on_device_destination_parent_dir = Path(binary_deploy_info.on_device_path).parent + run_command_on_device(f"mkdir -p {on_device_destination_parent_dir.as_posix()}") + except: + # Dir already exists? + pass + + # Copy local signed binary to device + try: + copy_file_to_device(local_path, binary_deploy_info.on_device_path) + except Exception as e: + raise Exception(f"Failed to copy {binary_deploy_info.on_device_path} to device with error: {e}") + + try: + copy_file_to_device(binary_deploy_info.entitlements_file, "/tmp/entitlements.xml") + except Exception as e: + print(f"Failed to copy entitlements file to device with error: {e}") + pass + + run_command_on_device(f"/var/jb/usr/bin/ldid -S/tmp/entitlements.xml {binary_deploy_info.on_device_path}") + + +if __name__ == "__main__": + print("deploying binaries device") + + if "BUILT_PRODUCTS_DIR" not in os.environ: + raise Exception("BUILT_PRODUCTS_DIR not found") + + BUILT_PRODUCTS_DIR = Path(os.environ["BUILT_PRODUCTS_DIR"]) + if not BUILT_PRODUCTS_DIR.exists(): + raise Exception("BUILT_PRODUCTS_DIR var exists but directory does not") + + for framework_path in BUILT_PRODUCTS_DIR.glob("*.framework"): + fw_binary_path = framework_path / framework_path.stem + if not fw_binary_path.exists(): + raise Exception(f"file does not exist: {fw_binary_path}") + + if framework_path.stem not in BINARY_DEPLOY_INFO: + continue + + binary_deploy_info = BINARY_DEPLOY_INFO[framework_path.stem] + deploy_to_device(fw_binary_path, binary_deploy_info) diff --git a/entitlements.xml b/entitlements.xml new file mode 100644 index 0000000..36ac68c --- /dev/null +++ b/entitlements.xml @@ -0,0 +1,15 @@ + + + + + platform-application + + com.apple.private.logging.admin + + com.apple.private.logging.diagnostic + + com.apple.private.logging.stream + + + + diff --git a/oslo.xcodeproj/project.pbxproj b/oslo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..91f2665 --- /dev/null +++ b/oslo.xcodeproj/project.pbxproj @@ -0,0 +1,399 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 70; + objects = { + +/* Begin PBXBuildFile section */ + 5F03D5772D2B4D90005BF9ED /* LoggingSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 5F03D5762D2B4D90005BF9ED /* LoggingSupport.h */; }; + 5F03D5A52D2B6607005BF9ED /* grouping.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F03D5A42D2B6607005BF9ED /* grouping.m */; }; + 5F03D5A62D2B6607005BF9ED /* grouping.h in Headers */ = {isa = PBXBuildFile; fileRef = 5F03D5A32D2B6607005BF9ED /* grouping.h */; }; + 5F03D5A82D2B68D1005BF9ED /* output.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F03D5A72D2B68D1005BF9ED /* output.m */; }; + 5F03D5AA2D2B68ED005BF9ED /* output.h in Headers */ = {isa = PBXBuildFile; fileRef = 5F03D5A92D2B68ED005BF9ED /* output.h */; }; + 5F3715F92D09AAED003DF348 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F3715F82D09AAED003DF348 /* main.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 5F03D5762D2B4D90005BF9ED /* LoggingSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoggingSupport.h; sourceTree = ""; }; + 5F03D5A32D2B6607005BF9ED /* grouping.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = grouping.h; sourceTree = ""; }; + 5F03D5A42D2B6607005BF9ED /* grouping.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = grouping.m; sourceTree = ""; }; + 5F03D5A72D2B68D1005BF9ED /* output.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = output.m; sourceTree = ""; }; + 5F03D5A92D2B68ED005BF9ED /* output.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = output.h; sourceTree = ""; }; + 5F3715F82D09AAED003DF348 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 5FE020792BFA862500D131E0 /* deploy.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = deploy.py; sourceTree = ""; }; + 5FE0207B2BFA862B00D131E0 /* entitlements.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = entitlements.xml; sourceTree = ""; }; + 5FE020822BFA86C400D131E0 /* oslo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = oslo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 5F03D56D2D2B1906005BF9ED /* kat */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = kat; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5FE0207F2BFA86C400D131E0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5F85499F2AD9E03E006D8CCD = { + isa = PBXGroup; + children = ( + 5FE020792BFA862500D131E0 /* deploy.py */, + 5FE0207B2BFA862B00D131E0 /* entitlements.xml */, + 5FE020832BFA86C400D131E0 /* oslo */, + 5F8549A92AD9E03E006D8CCD /* Products */, + ); + sourceTree = ""; + }; + 5F8549A92AD9E03E006D8CCD /* Products */ = { + isa = PBXGroup; + children = ( + 5FE020822BFA86C400D131E0 /* oslo.framework */, + ); + name = Products; + sourceTree = ""; + }; + 5FE020832BFA86C400D131E0 /* oslo */ = { + isa = PBXGroup; + children = ( + 5F03D56D2D2B1906005BF9ED /* kat */, + 5F3715F82D09AAED003DF348 /* main.m */, + 5F03D5A32D2B6607005BF9ED /* grouping.h */, + 5F03D5A42D2B6607005BF9ED /* grouping.m */, + 5F03D5A92D2B68ED005BF9ED /* output.h */, + 5F03D5A72D2B68D1005BF9ED /* output.m */, + 5F03D5762D2B4D90005BF9ED /* LoggingSupport.h */, + ); + path = oslo; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 5FE0207D2BFA86C400D131E0 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F03D5AA2D2B68ED005BF9ED /* output.h in Headers */, + 5F03D5772D2B4D90005BF9ED /* LoggingSupport.h in Headers */, + 5F03D5A62D2B6607005BF9ED /* grouping.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 5FE020812BFA86C400D131E0 /* oslo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5FE020862BFA86C400D131E0 /* Build configuration list for PBXNativeTarget "oslo" */; + buildPhases = ( + 5FE0207D2BFA86C400D131E0 /* Headers */, + 5FE0207E2BFA86C400D131E0 /* Sources */, + 5FE0207F2BFA86C400D131E0 /* Frameworks */, + 5FE020802BFA86C400D131E0 /* Resources */, + 5FE0208B2BFA88A100D131E0 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 5F03D56D2D2B1906005BF9ED /* kat */, + ); + name = oslo; + productName = authtool; + productReference = 5FE020822BFA86C400D131E0 /* oslo.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5F8549A02AD9E03E006D8CCD /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1500; + TargetAttributes = { + 5FE020812BFA86C400D131E0 = { + CreatedOnToolsVersion = 15.0.1; + }; + }; + }; + buildConfigurationList = 5F8549A32AD9E03E006D8CCD /* Build configuration list for PBXProject "oslo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5F85499F2AD9E03E006D8CCD; + productRefGroup = 5F8549A92AD9E03E006D8CCD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5FE020812BFA86C400D131E0 /* oslo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5FE020802BFA86C400D131E0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 5FE0208B2BFA88A100D131E0 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npython3 deploy.py\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5FE0207E2BFA86C400D131E0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F03D5A82D2B68D1005BF9ED /* output.m in Sources */, + 5F3715F92D09AAED003DF348 /* main.m in Sources */, + 5F03D5A52D2B6607005BF9ED /* grouping.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 5F8549BF2AD9E040006D8CCD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 5F8549C02AD9E040006D8CCD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5FE020872BFA86C400D131E0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = J478CS4UY8; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACH_O_TYPE = mh_execute; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.ethanarbuckle.authtool; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 5FE020882BFA86C400D131E0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = J478CS4UY8; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACH_O_TYPE = mh_execute; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = com.ethanarbuckle.authtool; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5F8549A32AD9E03E006D8CCD /* Build configuration list for PBXProject "oslo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5F8549BF2AD9E040006D8CCD /* Debug */, + 5F8549C02AD9E040006D8CCD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5FE020862BFA86C400D131E0 /* Build configuration list for PBXNativeTarget "oslo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5FE020872BFA86C400D131E0 /* Debug */, + 5FE020882BFA86C400D131E0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5F8549A02AD9E03E006D8CCD /* Project object */; +} diff --git a/oslo/LoggingSupport.h b/oslo/LoggingSupport.h new file mode 100644 index 0000000..540387d --- /dev/null +++ b/oslo/LoggingSupport.h @@ -0,0 +1,92 @@ +// +// LoggingSupport.h +// oslo +// +// Created by Ethan Arbuckle on 1/5/25. +// + +#ifndef LoggingSupport_h +#define LoggingSupport_h + +@class OSLogEventProxy; + +@interface OSLogEventSource : NSObject +@end + +@interface OSLogEventStore : NSObject +- (void)setProgressHandler:(void (^)(id, id))handler; +- (void)prepareWithCompletionHandler:(void (^)(OSLogEventSource *, NSError *))handler; +@end + +@interface OSLogEventLocalStore : OSLogEventStore ++ (id)localStore; +@end + +@interface OSLogEventStream : NSObject +- (id)initWithSource:(id)source; +- (id)initWithLiveSource:(id)source; +- (void)setFilterPredicate:(NSPredicate *)predicate; +- (void)setEventHandler:(void (^)(OSLogEventProxy *))handler; +- (void)setInvalidationHandler:(void (^)(OSLogEventStream *, NSUInteger code, id info))handler; +- (void)invalidate; +- (void)setFlags:(NSUInteger)flags; +- (void)activateStreamFromDate:(NSDate *)date; +@end + +@interface OSLogEventProxy : NSObject +@property (readonly) NSString *processImagePath; +@property (readonly) NSString *senderImagePath; +- (NSString *)composedMessage; +- (NSDate *)date; +- (void)_setIncludeSensitive:(BOOL)a3; +- (int)processIdentifier; +- (int)logType; +@end + +@interface OSLogEventLiveSource : OSLogEventSource +@end + +@interface OSLogEventLiveStore : OSLogEventStore ++ (id)liveLocalStore; +- (void)prepareWithCompletionHandler:(void (^)(OSLogEventLiveSource *))handler; +@end + +@interface OSLogEventLiveStream : OSLogEventStream +- (void)activate; +@end + +@interface OSLogTermDumper : NSObject +- (id)initWithFd:(int)fd colorMode:(int)colorMode; +- (void)setBold:(BOOL)bold; +- (void)dump:(NSString *)string; +- (void)setFgColor:(int)fgColor; +- (void)setBgColor:(int)bgColor; +- (void)puts:(const char *)string; +- (void)resetStyle; +- (void)writeln; +- (BOOL)flush:(BOOL)force; +- (void)_resetAttrsForNewline; +- (void)pad:(int)a3 count:(uint64_t)a4; +@end + +typedef NS_OPTIONS(NSUInteger, OSLogStreamFlags) { + OSLogStreamFlagActivityTracing = 1 << 0, + OSLogStreamFlagTraceMessages = 1 << 1, + OSLogStreamFlagLogMessages = 1 << 2, + OSLogStreamFlagSignpostMessages = 1 << 5, + OSLogStreamFlagLossMessages = 1 << 6 +}; + +// Wrapper for OSLogEventProxy, because the type cannot be safely retained +@interface _OSLogEventCopy : NSObject +@property NSString *processImagePath; +@property NSString *senderImagePath; +@property NSString *composedMessage; +@property NSDate *date; +@property int logType; +@property int processIdentifier; +@property BOOL isError; +- (id)initWithProxyEvent:(OSLogEventProxy *)event; +@end + +#endif /* LoggingSupport_h */ diff --git a/oslo/grouping.h b/oslo/grouping.h new file mode 100644 index 0000000..b2e5d20 --- /dev/null +++ b/oslo/grouping.h @@ -0,0 +1,16 @@ +// +// grouping.h +// oslo +// +// Created by Ethan Arbuckle on 1/5/25. +// + +#ifndef grouping_h +#define grouping_h + +#import +#import "LoggingSupport.h" + +void handleLogEventWithGrouping(OSLogEventProxy *logProxyEvent); + +#endif /* grouping_h */ diff --git a/oslo/grouping.m b/oslo/grouping.m new file mode 100644 index 0000000..51a5964 --- /dev/null +++ b/oslo/grouping.m @@ -0,0 +1,92 @@ +// +// grouping.m +// oslo +// +// Created by Ethan Arbuckle on 1/5/25. +// + +#import "grouping.h" +#import "output.h" + +@interface _OSLogEventGroup : NSObject +@property (nonatomic, strong) NSMutableArray <_OSLogEventCopy *> *events; +@property (nonatomic, assign) int processId; +@property (nonatomic, assign) NSDate *lastEventDate; +@end + +@implementation _OSLogEventGroup + +- (id)init { + if (self = [super init]) { + _events = [[NSMutableArray alloc] init]; + } + return self; +} + +@end + +static NSMutableDictionary *logGroups = nil; + +static void printAllGroups(void) { + // Sort groups by date of the last event they contain + NSArray *sortedPIDs = [logGroups.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSNumber *pid1, NSNumber *pid2) { + _OSLogEventGroup *group1 = logGroups[pid1]; + _OSLogEventGroup *group2 = logGroups[pid2]; + return [group1.lastEventDate compare:group2.lastEventDate]; + }]; + + for (NSNumber *pid in sortedPIDs) { + // Skip empty groups + _OSLogEventGroup *group = logGroups[pid]; + if (group.events.count == 0) { + continue; + } + + // Write a header with PID and timestamp of the group's last event + [termDumper setBold:YES]; + [termDumper setFgColor:7]; + [termDumper puts:[NSString stringWithFormat:@"\nPID %d ", group.processId].UTF8String]; + [termDumper pad:'-' count:10]; + [termDumper puts:" "]; + [termDumper puts:[group.lastEventDate descriptionWithLocale:[NSLocale currentLocale]].UTF8String]; + [termDumper writeln]; + [termDumper resetStyle]; + + // Print all events in the group + for (_OSLogEventCopy *event in group.events) { + printLogEvent(event); + } + [termDumper writeln]; + } +} + +void handleLogEventWithGrouping(OSLogEventProxy *logProxyEvent) { + // Ignore logs from stuff in the dyld cache + NSString *senderImagePath = logProxyEvent.senderImagePath; + BOOL shouldIgnore = !senderImagePath || ![[NSFileManager defaultManager] fileExistsAtPath:senderImagePath]; + // Unless the log is an error + shouldIgnore = shouldIgnore && logProxyEvent.logType != 16; + if (shouldIgnore) { + return; + } + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (!logGroups) { + logGroups = [[NSMutableDictionary alloc] init]; + } + }); + + NSNumber *pidKey = @(logProxyEvent.processIdentifier); + _OSLogEventGroup *group = logGroups[pidKey]; + if (!group) { + group = [[_OSLogEventGroup alloc] init]; + group.processId = logProxyEvent.processIdentifier; + group.lastEventDate = logProxyEvent.date; + logGroups[pidKey] = group; + printAllGroups(); + } + + [group.events addObject:[[_OSLogEventCopy alloc] initWithProxyEvent:logProxyEvent]]; +} + diff --git a/oslo/kat/hashtable.c b/oslo/kat/hashtable.c new file mode 100644 index 0000000..497b8c1 --- /dev/null +++ b/oslo/kat/hashtable.c @@ -0,0 +1,737 @@ +/* + * MIT License + * + * Copyright (c) 2019 Davidson Francis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "hashtable.h" +#include +#include +#include +#include +#include + +/** + * @brief Initializes the hashtable. + * + * @param ht Hashtable structure pointer to be + * initialized. + * + * @return Returns 0 if success and a negative number + * otherwise. + */ +int hashtable_init( + struct hashtable **ht, + void (*setup_algorithm)(struct hashtable **ht) + ) +{ + struct hashtable *out; + out = calloc(1, sizeof(struct hashtable)); + if (out == NULL) + return (-1); + + out->capacity = HASHTABLE_DEFAULT_SIZE; + out->elements = 0; + out->bucket = calloc(out->capacity, sizeof(void *)); + if (out->bucket == NULL) + { + free(out); + return (-1); + } + + /* + * Setup algorith, comparator and key size. + * + * If NULL, uses splitmix64 as default. + */ + if (setup_algorithm != NULL) + setup_algorithm(&out); + else + hashtable_splitmix64_setup(&out); + + *ht = out; + return (0); +} + +/** + * @brief Deallocates the hashtable. + * + * @param ht Hashtable pointer. + * @param dealloc Deallocation indicator: if different from 0, also + * deallocates the value, if 0 not. + * + * @return Returns 0 if success. + */ +int hashtable_finish(struct hashtable **ht, int dealloc) +{ + struct hashtable *h; /* Hashtable. */ + struct list *l_ptr; /* List pointer. */ + + h = *ht; + + /* Invalid array. */ + if (h == NULL) + return (-1); + + /* + * For each bucket, deallocates each list. + */ + for (size_t i = 0; i < h->capacity; i++) + { + struct list *l_ptr_next; + + l_ptr = h->bucket[i]; + while (l_ptr != NULL) + { + l_ptr_next = l_ptr->next; + if (dealloc) + free(l_ptr->value); + + free(l_ptr); + l_ptr = l_ptr_next; + } + } + + free(h->bucket); + free(h); + return (0); +} + +/** + * @brief Calculates the bucket size accordingly with the + * current hash function set and the current hashtable + * capacity. + * + * @param ht Hashtable pointer. + * @param key Key to be hashed. + * + * @return Returns a value between [0,h->capacity] that + * should be used to address the target bucket for the + * hash table. + */ +inline static size_t hashtable_bucket_index(struct hashtable **ht, const void *key) +{ + struct hashtable *h; /* Hashtable. */ + uint64_t hash; /* Hash value. */ + size_t index; /* Bucket index. */ + + /* + * Its important to note that the hashtable->capacity + * will always be power of 2, thus, allowing the &-1 + * as a modulus operation. + */ + h = *ht; + hash = h->hash(key, h->key_size); + index = hash & (h->capacity - 1); + + return (index); +} + +/** + * @brief Adds the current @p key and @p value into the hashtable. + * + * @param ht Hashtable pointer. + * @param key Key to be added. + * @param value Value to be added. + * + * The hash table will grows twice whenever it reaches + * its threshold of 60% percent. + * + * @return Returns 0 if success and a negative number otherwise. + */ +int hashtable_add(struct hashtable **ht, void *key, void *value) +{ + struct hashtable *h; /* Hashtable. */ + struct list *l_entry; /* List entry. */ + struct list *l_ptr; /* List pointer. */ + struct list *l_ptr_nxt; /* List pointer. */ + size_t hash; /* Hash value. */ + + h = *ht; + + /* Hashtable exists?. */ + if (h == NULL) + return (-1); + + /* Asserts if we have space enough. */ + if (h->elements >= h->capacity*0.6) + { + size_t old_capacity; + struct list **new_buckets; + + /* Allocate new buckets. */ + new_buckets = calloc(h->capacity << 1, sizeof(void *)); + if (new_buckets == NULL) + return (-1); + + old_capacity = h->capacity; + h->capacity <<= 1; + +#if HASHTABLE_DEBUG + h->collisions = 0; +#endif + + /* Move elements to it. */ + for (size_t i = 0; i < old_capacity; i++) + { + l_ptr = h->bucket[i]; + while (l_ptr != NULL) + { + l_ptr_nxt = l_ptr->next; + + if (l_ptr->key != NULL) + { + /* Recalculates the hash for each key inside the bucket. */ + hash = hashtable_bucket_index(ht, l_ptr->key); + l_ptr->next = new_buckets[hash]; +#if HASHTABLE_DEBUG + if(new_buckets[hash] != NULL) + h->collisions++; +#endif + new_buckets[hash] = l_ptr; + } + + l_ptr = l_ptr_nxt; + } + } + + free(h->bucket); + h->bucket = new_buckets; + } + + /* Hash it. */ + hash = hashtable_bucket_index(ht, key); + + /* + * Loops through the list in order to see if the key already exists, + * if so, overwrite the value. + */ + l_ptr = h->bucket[hash]; + while (l_ptr != NULL) + { + if (l_ptr->key != NULL && h->cmp(l_ptr->key, key) == 0) + { + l_ptr->value = value; + return (0); + } + l_ptr = l_ptr->next; + } + + /* Allocate a new list and adds into the appropriate location. */ + l_entry = calloc(1, sizeof(struct list)); + if (l_entry == NULL) + return (-1); + + /* Fill the entry. */ + l_entry->key = key; + l_entry->value = value; + l_entry->hash = hash; + l_entry->next = h->bucket[hash]; + + /* Add into the top of the buckets list. */ +#if HASHTABLE_DEBUG + if (h->bucket[hash] != NULL) + h->collisions++; +#endif + + h->bucket[hash] = l_entry; + h->elements++; + + return (0); +} + +/** + * @brief Retrieves the value belonging to the parameter @p key. + * + * @param ht Hashtable pointer. + * @param key Corresponding key to be retrieved. + * + * @return Returns the value belonging to the @p key, or NULL + * if not found. + */ +void* hashtable_get(struct hashtable **ht, void *key) +{ + struct hashtable *h; /* Hashtable. */ + struct list *l_ptr; /* List pointer. */ + uint64_t hash; /* Hash value. */ + + h = *ht; + + /* Hashtable exists and has at least one element?. */ + if (h == NULL || h->elements < 1) + return (NULL); + + /* Hash it. */ + hash = hashtable_bucket_index(ht, key); + + /* + * Loops through the list in order to see if the key exists, + * if so, gets the value. + */ + l_ptr = h->bucket[hash]; + while (l_ptr != NULL) + { + if (l_ptr->key != NULL && h->cmp(l_ptr->key, key) == 0) + return (l_ptr->value); + + l_ptr = l_ptr->next; + } + + /* If not found, return NULL. */ + return (NULL); +} + +/*===========================================================================* + * -.- Hash functions -.- * + *===========================================================================*/ + +/** + * @brief Generic key comparator. + * + * Compares two given pointers and returns a negative, 0 or positive + * number if less than, equal or greater for the keys specified. + * + * @param key1 First key to be compared. + * @param key2 Second key to be compared. + * + * @returns Returns a negative, 0 or positive number if @p key1 + * is less than, equal or greater than @p key2. + */ +int hashtable_cmp_ptr(const void *key1, const void *key2) +{ + return ( (int)( ((uintptr_t)key1) - ((uintptr_t)key2) ) ); +} + +/** + * @Brief Generic string key comparator. + * + * Compares two given strings and returns a negative, 0 or positive + * number if less than, equal or greater for the keys specified. + * + * @param key1 First string key to be compared. + * @param key2 Second string key to be compared. + * + * @returns Returns a negative, 0 or positive number if @p key1 + * is less than, equal or greater than @p key2. + */ +int hashtable_cmp_string(const void *key1, const void *key2) +{ + return (strcmp(key1, key2)); +} + +/*---------------------------------------------------------------------------* + * sdbm Hash Functions * + *---------------------------------------------------------------------------*/ + +/** + * @brief sdbm algorithm. + * + * @param key String key to be hashed. + * + * @return Returns a 64-bit hashed number for the @p key argument. + * + * @note This algorithm shows a decent randomness, low collisions rate, + * fast and easy to implement. + * + * More information about this and others: + * https://softwareengineering.stackexchange.com/a/145633/ + */ +uint64_t hashtable_sdbm(const void *key, size_t size) +{ + unsigned char *str; /* String pointer. */ + uint64_t hash; /* Resulting hash. */ + int c; /* Current character. */ + ((void)size); + + str = (unsigned char *)key; + hash = 0; + + while ((c = *str++) != 0) + hash = c + (hash << 6) + (hash << 16) - hash; + + return (hash); +} + +/** + * @brief Setup for the sdbm hash function. + * + * @param ht Hashtable pointer. + */ +void hashtable_sdbm_setup(struct hashtable **ht) +{ + (*ht)->hash = hashtable_sdbm; + (*ht)->cmp = hashtable_cmp_string; + (*ht)->key_size = 1; +} + +/*----------------------------------------------------------------------------* + * Based in splitmix64, from Better Bit Mixing - Improving on MurmurHash3's * + * 64-bit Finalizer * + * * + * Link:http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html* + * ---- * + * I've found this particular interesting and the results showed that the * + * resulting hash is very close to MurMur3 ;-). * + *----------------------------------------------------------------------------*/ + +/** + * @brief splitmix64 based algorithm. + * + * This algorithm seems to be the 'Mix13' for the link above. + * + * @param key Key to be hashed. + * @param size Pointer size (unused here). + * + * @return Returns a hashed number for the @p key argument. + */ +uint64_t hashtable_splitmix64_based(const void *key, size_t size) +{ + uint64_t x; /* Hash key. */ + ((void)size); + + x = (uint64_t)key; + x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9; + x = (x ^ (x >> 27)) * 0x94d049bb133111eb; + x = x ^ (x >> 31); + return (x); +} + +/** + * @brief Setup for the splitmix64 hash function. + * + * @param ht Hashtable pointer. + */ +void hashtable_splitmix64_setup(struct hashtable **ht) +{ + (*ht)->hash = hashtable_splitmix64_based; + (*ht)->cmp = hashtable_cmp_ptr; + (*ht)->key_size = sizeof(void *); +} + +/*---------------------------------------------------------------------------* + * MurMur3 Hash Functions * + *---------------------------------------------------------------------------*/ + +/** + * @brief Setup for the MurMur3 hash function. + * + * @param ht Hashtable pointer. + */ +void hashtable_MurMur3_setup(struct hashtable **ht) +{ + (*ht)->hash = hashtable_MurMur3_hash; + (*ht)->cmp = hashtable_cmp_ptr; + (*ht)->key_size = sizeof(void *); +} + +/* ---------------- * + * MurMur Helpers * + * ---------------- */ + +/** + * @brief Left-rotate an unsigned 64-bit integer by + * @p r times. + * + * @param x Number to be rotated. + * @param r Number of times. + * + * @return Rotated number. + */ +static inline uint64_t rotl64(uint64_t x, int8_t r) +{ + return (x << r) | (x >> (64 - r)); +} + +/** + * @brief 64-bit finalizer of MurMur3 Hash. + * + * @param k Intermediate hash value. + * + * @return Returns a hash value with higher entropy. + */ +static inline uint64_t fmix64(uint64_t k) +{ + k ^= k >> 33; + k *= 0xff51afd7ed558ccd; + k ^= k >> 33; + k *= 0xc4ceb9fe1a85ec53; + k ^= k >> 33; + return (k); +} + +/** + * @brief MurMur3 algorithm highly based in the publicly code + * available in: https://github.com/aappleby/smhasher. + * + * This algorithm takes an input @p key and a @p size + * and returns a hashed value of the pointer itself. + * + * @return Returns a hashed value of the key @p key. + */ +uint64_t hashtable_MurMur3_hash(const void *key, size_t size) +{ + const int nblocks = size / 4; + + /* Seeds are nulled for now. */ + uint64_t h1 = 0; + uint64_t h2 = 0; + + const uint64_t c1 = 0x87c37b91114253d5; + const uint64_t c2 = 0x4cf5ad432745937f; + + /* Body. */ + for(int i = 0; i < nblocks; i++) + { + uint64_t k1 = ((uint64_t) key >> (2 * i)) & 0xff; + uint64_t k2 = rotl64(k1, 13); + + k1 *= c1; + k1 = rotl64(k1, 31); + k1 *= c2; + h1 ^= k1; + h1 = rotl64(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + + k2 *= c2; + k2 = rotl64(k2, 33); + k2 *= c1; + h2 ^= k2; + h2 = rotl64(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + /* + * Finalization + * Since its power of two, the tail is not needed. + */ + h1 ^= size; h2 ^= size; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + return (h1); +} + +/*===========================================================================* + * -.- Tests -.- * + *===========================================================================*/ + +/* Array size test. */ +#define ARRAY_SIZE 1024 + +/* Set to 1 to dump all buckets. */ +#define DUMP_BUCKET 0 + +/* Considers each element as an integer value. */ +#define BUCKET_AS_INTEGER 0 + +/** + * @brief Print some statistics regarding the bucket usage, collisions + * and etc, useful to know if the currently hash function used is + * performing as expected. + * + * @param ht Hashtable pointer. + */ +void hashtable_print_stats(struct hashtable **ht) +{ + struct hashtable *h; /* Hashtable. */ + size_t used_buckets; /* Used buckets. */ + struct list *l_ptr; /* List pointer. */ + size_t max_bucket_size; /* Max bucket size. */ + size_t one_element_buckets; /* One-element buckets. */ + double elements_per_bucket; /* Elements per bucket. */ + double variance; /* Variance. */ + double stdev; /* Standard Deviation. */ + double mean; /* Mean. */ + + /* Initialize. */ + max_bucket_size = 0; + one_element_buckets = 0; + elements_per_bucket = 0; + used_buckets = 0; + variance = 0.0; + stdev = 0.0; + mean = 0.0; + + h = *ht; + + /* If valid hashtable. */ + if (h == NULL && h->elements < 1) + return; + +#if DUMP_BUCKET + printf("Buckets:\n"); +#endif + for (size_t i = 0; i < h->capacity; i++) + { + size_t bucket_size; + bucket_size = 0; + +#if DUMP_BUCKET + printf("Bucket[%zu] = ", i); +#endif + l_ptr = h->bucket[i]; + if (l_ptr != NULL) + used_buckets++; + + while (l_ptr != NULL) + { + if (l_ptr->key != NULL && l_ptr->value != NULL) + { + elements_per_bucket++; +#if DUMP_BUCKET +#if BUCKET_AS_INTEGER + printf("%d ", *((int *)l_ptr->value) ); +#else + printf("0x%" PRIx64 " ", (uint64_t)l_ptr->value); +#endif +#endif + } + l_ptr = l_ptr->next; + bucket_size++; + } +#if DUMP_BUCKET + printf("\n"); +#endif + if (bucket_size > max_bucket_size) + max_bucket_size = bucket_size; + if (bucket_size == 1) + one_element_buckets++; + if (bucket_size > 0) + variance += (bucket_size * bucket_size); + } + + mean = elements_per_bucket / used_buckets; + variance = (variance - (used_buckets * (mean*mean))) / (used_buckets - 1); + stdev = sqrt(variance); + + printf("--------------------------- Stats ---------------------------\n"); + printf("Buckets available: %zu\n", h->capacity); + printf("Used buckets: %zu (%f%%)\n", used_buckets, + ((double)used_buckets/h->capacity)*100); + printf("Max bucket size: %zu\n", max_bucket_size); + printf("One element buckets: %zu (%f%%)\n", one_element_buckets, + ((double)one_element_buckets/used_buckets)*100); + printf("Mean elements per bucket: %f\n", mean); + printf(" Variance: %f\n", variance); + printf(" Standard Deviation: %f\n", stdev); +#if HASHTABLE_DEBUG + printf("Collisions: %zu, elements: %zu\n", h->collisions, h->elements); +#endif + printf("-------------------------------------------------------------\n"); +} + +#if 0 +/** + * @brief Hashtable integrity test. + * + * @param ht Hashtable pointer. + * + * @return Returns 0 if success and a negative number + * otherwise. + */ +static int hashtable_integritytest(struct hashtable *ht) +{ + int *numbers; /* Numbers pointer. */ + int *num; /* Number poniter. */ + + /* Allocate a ARRAY_SIZE elements vector. */ + numbers = malloc(sizeof(int) * ARRAY_SIZE); + if (numbers == NULL) + { + fprintf(stderr, "hashtable: failed to allocate an array\n"); + goto out2; + } + + /* Fill in with previsible values. */ + for (int i = 0; i < ARRAY_SIZE; i++) + { + numbers[i] = i*10; + if (hashtable_add(&ht, &numbers[i], &numbers[i])) + { + fprintf(stderr, "hashtable: unable to add key, number: %d, iter: %d\n", + numbers[i], i); + goto out1; + } + } + + /* Check if all elements are retriavable. */ + for (int i = 0; i < ARRAY_SIZE; i++) + { + /* Attempt to get the number. */ + if ( ( num = hashtable_get(&ht, &numbers[i]) ) == NULL ) + { + fprintf(stderr, "hashtable: unable to get key: %" PRIx64 ", iter: %d\n", + (uint64_t)&numbers[i], i); + goto out1; + } + + /* Make sure its as expected. */ + if (*num != i*10) + { + fprintf(stderr, "hashtable: error, num: %d / expected: %d, iter: %d\n", + *num, i*10, i); + goto out1; + } + } + + /* --------------------------- Some stats --------------------------- */ + hashtable_print_stats(&ht); + + free(numbers); + return (0); +out1: + free(numbers); + return (-1); +out2: + return (-1); +} + + +/** + * @brief Execute tests + */ +int main(void) +{ + struct hashtable *ht; + + /* Allocate hashtable. */ + if (hashtable_init(&ht, NULL)) + { + fprintf(stderr, "hashtable: error while allocating hashtable\n"); + exit(EXIT_FAILURE); + } + + /* Tests. */ + printf("Hashtable integrity test [%s]\n", + !hashtable_integritytest(ht) ? "PASSED" : "FAILED"); + + hashtable_finish(&ht, 0); +} +#endif diff --git a/oslo/kat/hashtable.h b/oslo/kat/hashtable.h new file mode 100644 index 0000000..535a2b0 --- /dev/null +++ b/oslo/kat/hashtable.h @@ -0,0 +1,98 @@ +/* + * MIT License + * + * Copyright (c) 2019 Davidson Francis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef HASHTABLE_H +#define HASHTABLE_H + +#include +#include +#include + +/** + * @brief Hashtable initial size. + */ +#define HASHTABLE_DEFAULT_SIZE 16 + +/** + * @brief Enable additional debug data, like collision counting. + */ +#define HASHTABLE_DEBUG 1 + +/** + * @brief Hashtable linked list structure. + */ +struct list +{ + void *key; /* Entry key. */ + void *value; /* Entry value. */ + uint64_t hash; /* Entry hash. */ + struct list *next; /* Entry next list. */ +}; + +/** + * @brief Hashtable structure. + */ +struct hashtable +{ + struct list **bucket; /* Bucket list. */ + size_t capacity; /* Current capacity. */ + size_t elements; /* Current elements. */ + ssize_t key_size; /* Hash key size. */ + +#if HASHTABLE_DEBUG + size_t collisions; /* Collisions count. */ +#endif + + /* Function pointers. */ + uint64_t (*hash) (const void *key, size_t size); + int (*cmp) (const void *key1, const void *key2); +}; + +/* ==================== External functions ==================== */ +extern int hashtable_init( + struct hashtable **ht, + void (*setup_algorithm)(struct hashtable **ht) + ); + +extern int hashtable_finish(struct hashtable **ht, int dealloc); +extern int hashtable_add(struct hashtable **ht, void *key, void *value); +extern void* hashtable_get(struct hashtable **ht, void *key); +extern void hashtable_print_stats(struct hashtable **ht); + +/* ==================== Hash functions ==================== */ +extern int hashtable_cmp_ptr(const void *key1, const void *key2); + +/* sdbm. */ +extern void hashtable_sdbm_setup(struct hashtable **ht); +extern uint64_t hashtable_sdbm(const void *key, size_t size); + +/* Splitmix64. */ +extern void hashtable_splitmix64_setup(struct hashtable **ht); +extern uint64_t hashtable_splitmix64_hash(const void *key, size_t size); + +/* MurMur3 Hash. */ +extern void hashtable_MurMur3_setup(struct hashtable **ht); +extern uint64_t hashtable_MurMur3_hash(const void *key, size_t size); + +#endif /* HASHTABLE_H */ diff --git a/oslo/kat/highlight.c b/oslo/kat/highlight.c new file mode 100644 index 0000000..8f23c2b --- /dev/null +++ b/oslo/kat/highlight.c @@ -0,0 +1,887 @@ +/* + * MIT License + * + * Copyright (c) 2020 Davidson Francis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * This is Kat: A very simple syntax highlighter made in C + * for C language. + * + * Despite its simplicity, it has some interesting features: + * - Can work as a standalone program or as a library + * with only one major function `highlight_line()`. + * + * - Fast! Kat manages to catch up with the classic `cat` or even surpass it + * for certain types of source code. Also, it is *much* faster than + * similar programs, such as bat (https://github.com/sharkdp/bat). + * + * - Bloat free!. Kat has no additional dependencies and the only thing you + * need is the standard libc. + * + * - Not so bad/decent syntax highlight. Although with some limitations, + * Kat manages to deliver a visually pleasing, and similar or superior + * output to that found in programs that use GtkSourceView, such as + * Mousepad, GEdit. + * + * - Themes. Kat makes available a simple configuration scheme file that allows + * users to define your own themes. + */ + +#define _POSIX_C_SOURCE 200809L +#include "highlight.h" + +/* States. */ +#define HL_DEFAULT 0 +#define HL_KEYWORD 1 +#define HL_NUMBER 3 +#define HL_CHAR 4 +#define HL_STRING 5 +#define HL_COMMENT_MULTI 6 +#define HL_PREPROCESSOR 7 +#define HL_PREPROCESSOR_INCLUDE 8 +#define HL_PREPROCESSOR_INCLUDE_STRING 9 + +/* Themes. */ +#define COLOR_8 0 /* 8-colors theme. */ +#define ELF_DEITY 8 /* Elf Deity theme. */ +#define USER_DEF 16 /* User-defined theme. */ + +/* Current theme. */ +int CURRENT_THEME = ELF_DEITY; + +/* Color map. */ +char *COLORS[] = { + /* + * 8 colors theme. + */ + "\033[31m", /* LIGHT_RED */ + "\033[32m", /* LIGHT_GREEN */ + "\033[33m", /* LIGHT_YELLOW */ + "\033[34m", /* LIGHT_BLUE */ + "\033[35m", /* LIGHT_PURPLE */ + "\033[36m", /* LIGHT_CYAN */ + "\033[37m", /* LIGHT_GRAY */ + "\033[39m", /* DEFAULT */ + + /* + * 256 Colors. + * + * Elf Deity Theme. + * + * I don't really like design stuff and so on, and if I were + * to name colors myself, I could get a maximum of 16 names. + * However, since we are dealing with 256 colors and we can + * have several shades of the same color, the names below + * have been extracted from the website + * (http://chir.ag/projects/name-that-color) from their RGB + * values corresponding. + */ + "\033[38;5;63m", /* CORNFLOWER_BLUE_256 */ + "\033[38;5;83m", /* SCREAMIN_GREEN_256 */ + "\033[38;5;227m", /* LASER_FLAMINGO_256 */ + "\033[38;5;214m", /* YELLOW_SEA_256 */ + "\033[38;5;207m", /* PINK_FLAMINGO_256 */ + "\033[38;5;102m", /* GRAY_256 */ + "\033[38;5;193m", /* REEF_256 */ + "\033[38;5;101m", /* CLAY_CREEK_256 */ + + /* Placeholder for external theme. */ + "", + "", + "", + "", + "", + "", + "", + "" +}; + +/* + * Length map. + * + * Its kinda waste of processing time, calculating + * the length of the colors everytime. Since its + * constant, makes perfect sense to get rid of + * the bunch of strlen calls and just use constant + * values instead. + */ +int LENGTHS[] = { + /* 8 color theme. */ + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + + /* Elf Deity. */ + 10, + 10, + 11, + 11, + 11, + 11, + 11, + 11, + + /* User defined theme placeholder. */ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 +}; + +/* Global state. */ +struct global_state +{ + int state; +} gs = { + .state = HL_DEFAULT +}; + +/* Keyword. */ +struct keyword +{ + char *keyword; + int color; +}; + +/* Keywords. */ +struct keyword keywords_list[] = { + /* C Types. */ + {"double", TYPES_COLOR}, {"int", TYPES_COLOR}, {"long", TYPES_COLOR}, + {"char", TYPES_COLOR}, {"float", TYPES_COLOR}, {"short", TYPES_COLOR}, + {"unsigned", TYPES_COLOR}, {"signed", TYPES_COLOR}, + + /* Common typedefs. */ + {"int8_t", TYPES_COLOR}, {"uint8_t", TYPES_COLOR}, + {"int16_t", TYPES_COLOR}, {"uint16_t", TYPES_COLOR}, + {"int32_t", TYPES_COLOR}, {"uint32_t", TYPES_COLOR}, + {"int64_t", TYPES_COLOR}, {"uint64_t", TYPES_COLOR}, + + {"size_t", TYPES_COLOR}, {"ssize_t", TYPES_COLOR}, {"off_t", TYPES_COLOR}, + {"int8_t", TYPES_COLOR}, {"NULL", NUMBER_COLOR}, {"null", NUMBER_COLOR}, {"nil", NUMBER_COLOR}, + + /* Other keywords. */ + {"auto", KWRDS_COLOR}, {"struct", KWRDS_COLOR}, {"break", KWRDS_COLOR}, + {"else", KWRDS_COLOR}, {"switch", KWRDS_COLOR}, {"case", KWRDS_COLOR}, + {"enum", KWRDS_COLOR}, {"register", KWRDS_COLOR}, {"typedef", KWRDS_COLOR}, + {"extern", KWRDS_COLOR}, {"return", KWRDS_COLOR}, {"union", KWRDS_COLOR}, + {"const", KWRDS_COLOR}, {"continue", KWRDS_COLOR}, {"for", KWRDS_COLOR}, + {"void", KWRDS_COLOR}, {"default", KWRDS_COLOR}, {"goto", KWRDS_COLOR}, + {"sizeof", KWRDS_COLOR}, {"volatile", KWRDS_COLOR}, {"do", KWRDS_COLOR}, + {"if", KWRDS_COLOR}, {"static", KWRDS_COLOR}, {"inline", KWRDS_COLOR}, + {"while", KWRDS_COLOR}, + + /* Errors and Exceptions */ + {"NSError", KWRDS_COLOR}, {"NSException", KWRDS_COLOR}, {"Exception", KWRDS_COLOR}, + {"Error", KWRDS_COLOR}, {"Crash", KWRDS_COLOR}, {"Fault", KWRDS_COLOR}, {"Fail", KWRDS_COLOR}, + {"Failure", KWRDS_COLOR}, {"Abort", KWRDS_COLOR}, {"Panic", KWRDS_COLOR}, {"Terminate", KWRDS_COLOR}, + {"SIG", KWRDS_COLOR}, {"Signal", KWRDS_COLOR}, + + /* true / false */ + {"true", SYMBOL_COLOR}, {"false", SYMBOL_COLOR}, {"YES", SYMBOL_COLOR}, {"NO", SYMBOL_COLOR}, +}; + +/* Hashtable keywords. */ +struct hashtable *ht_keywords = NULL; + +/* + * Allowed symbols table. + * + * This is a simple lookup table that contains all symbols that + * will be highlighted. + */ +unsigned char symbols_table[256] = +{ + ['['] = 1, [']'] = 1, ['('] = 1, [')'] = 1, ['{'] = 1, ['}'] = 1, + ['*'] = 1, [':'] = 1, ['='] = 1, [';'] = 1, ['-'] = 1, ['>'] = 1, + ['&'] = 1, ['+'] = 1, ['~'] = 1, ['!'] = 1, ['/'] = 1, ['%'] = 1, + ['<'] = 1, ['^'] = 1, ['|'] = 1, ['?'] = 1 +}; + +/** + * Allocates a new Highlighted Buffer line. + * + * A Highlighted Buffer Line is a facility that transparently allows + * the user to append chars and strings inside a buffer without having + * to worry about space, reallocs and so on, something similiar to + * SDS strings. + * + * @returns Return a pointer to the highlighted line. + */ +char* highlight_alloc_line(void) +{ + struct highlighted_line *hl; + hl = calloc(1, sizeof(struct highlighted_line) + (sizeof(char) * 80)); + hl->idx = 0; + hl->size = 80; + return ((char*)(hl+1)); +} + +/** + * Deallocate a Highlighted Line Buffer. + * + * @param line Highlighted Line Buffer to be deallocated. + */ +void highlight_free(char *line) +{ + struct highlighted_line *hl; + hl = ((struct highlighted_line *)line - 1); + free(hl); +} + +/** + * Checks if the given keyword @p key is one of the keywords + * allowed, if so, returns the structure that belongs to the + * given keyword, otherwise, returns NULL. + * + * @param key Keyword to be checked. + * @param size Keyword length. + * + * @return Returns a keyword structure, otherwise, returns NULL. + */ +static struct keyword* is_keyword(const char *key, size_t size) +{ + struct keyword *k; + + /* This should occur in most of the cases. */ + if (size < 80) + { + char nkey[80]; + memcpy(nkey, key, size); + nkey[size] = '\0'; + return (hashtable_get(&ht_keywords, nkey)); + } + + /* + * Otherwise, we should temporarily allocate a new string + * in order to use as parameter of hashtable. + * + * I hope the overhead copy (in order to use hashtable) will + * compensate the O(n) iterations through the list. + */ + char *nkey = malloc(sizeof(char) * (size+1)); + memcpy(nkey, key, size); + nkey[size] = '\0'; + + /* + * Check if the at the given range belongs to + * some keyword. + */ + k = hashtable_get(&ht_keywords, nkey); + free(nkey); + return (k); +} + +/** + * For a given line @p line and a (already) allocated + * highlighted line buffer @p hl, highlights the + * line and returns @p hl with the highlighted line. + * + * @param line Line (null terminated string) to be highlighted. + * @param hl Pre-allocated Highlighted Line buffer. + * @param str_size (Optional, if != 0) Line size. + * + * @return Returns a Highlighted Line Buffer. + */ +char *highlight_line(const char *line, char *hl, size_t str_size) +{ + struct highlighted_line *high_line; + size_t tok_size; + int keyword_start; + int keyword_end; + + /* Reset indexes. */ + if (hl != NULL) + { + high_line = ((struct highlighted_line *)hl - 1); + high_line->idx = 0; + } + else + hl = highlight_alloc_line(); + + keyword_start = 0; + keyword_end = 0; + + if (!str_size) + str_size = strlen(line); + + /* For each char, including the null terminated. */ + for (size_t i = 0; i < str_size+1; i++) + { + switch (gs.state) + { + /* Default state. */ + case HL_DEFAULT: + { + /* + * If potential keyword. + * + * A valid C keyword may contain numbers, but *not* + * as a suffix. + */ + if (is_char_keyword(line[i]) && !isdigit(line[i])) + { + keyword_start = i; + gs.state = HL_KEYWORD; + continue; + } + + /* If potential number. */ + else if (isdigit(line[i])) + { + keyword_start = i; + gs.state = HL_NUMBER; + continue; + } + + /* If potential char. */ + else if (line[i] == '\'') + { + keyword_start = i; + gs.state = HL_CHAR; + continue; + } + + /* If potential string. */ + else if (line[i] == '"') + { + keyword_start = i; + gs.state = HL_STRING; + continue; + } + + /* Line or multiline comment. */ + else if (line[i] == '/' && i+1 < str_size) + { + /* If one of them, skip next char and puts the color. */ + if (line[i+1] == '/') + { + /* + * Line comment is pretty 'hacky': + * After a line comment is identified correctly, all remaining + * characters (to the end of the line) *will* be comments for + * sure, so we can safely analyze this and abort the loop. + */ + tok_size = str_size - i; + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+COMMENT_COLOR], + LENGTHS[CURRENT_THEME+COMMENT_COLOR]); + hl = add_str_to_hl(hl, line+i, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + + /* string terminator \'0', =). */ + hl = add_char_to_hl(hl, '\0'); + + /* Abort loop. */ + i = str_size; + continue; + } + + /* Multiline comments should be handled as usual. */ + else if (line[i+1] == '*') + { + gs.state = HL_COMMENT_MULTI; + keyword_start = i; + i += 1; + continue; + } + + /* Something else, maybe a symbol?. */ + highlight_symbol(line[i], &hl); + continue; + } + + /* Preprocessor. */ + else if (line[i] == '#') + { + keyword_start = i; + gs.state = HL_PREPROCESSOR; + continue; + } + + /* If any symbol supported. */ + else if (highlight_symbol(line[i], &hl)) + continue; + + hl = add_char_to_hl(hl, line[i]); + } + break; + + /* Keyword state. */ + case HL_KEYWORD: + { + /* End of keyword, check if it really is a valid keyword. */ + if (!is_char_keyword(line[i])) + { + struct keyword *keyword; + keyword_end = i - 1; + tok_size = keyword_end - keyword_start + 1; + gs.state = HL_DEFAULT; + + /* If keyword, highlight. */ + if ( (keyword = is_keyword(line+keyword_start, tok_size)) != NULL ) + { + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+keyword->color], + LENGTHS[CURRENT_THEME+keyword->color]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + + /* Maybe we should highlight this remaining char. */ + if (!highlight_symbol(line[i], &hl)) + hl = add_char_to_hl(hl, line[i]); + continue; + } + + /* + * If not keyword, maybe its a function call. + * + * Important to note that this is hacky and will only work + * if there is no space between keyword and '('. + */ + if (line[i] == '(') + { + keyword_end = i; + tok_size = keyword_end - keyword_start; + gs.state = HL_DEFAULT; + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+FUNC_CALL_COLOR], + LENGTHS[CURRENT_THEME+FUNC_CALL_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + + /* Opening parenthesis will always be highlighted */ + highlight_symbol(line[i], &hl); + continue; + } + + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + + /* Maybe we should highlight this remaining char. */ + if (!highlight_symbol(line[i], &hl)) + hl = add_char_to_hl(hl, line[i]); + continue; + } + } + break; + + /* Number state. */ + case HL_NUMBER: + { + char c = tolower(line[i]); + + /* + * Should we end the state?. + * + * Very important observation: + * Although the number highlight works fine for most (if not all) + * of the possible cases, it also assumes that the code is written + * correctly and the source is able to compile, meaning that: + * + * Numbers like: 123, 0xABC123, 12.3e4f, 123ULL.... + * will be correctly identified and highlighted + * + * But, 'numbers' like: 123ABC, 0xxxxABCxx123, 123UUUUU.... + * will also be highlighted. + * + * It also assumes that no keyword will start with a number + * and everything starting with a number (except inside strings or + * comments) will be a number. + */ + if (!isdigit(c) && (c < 'a' || c > 'f') && c != 'b' && + c != 'x' && c != 'u' && c != 'l' && c != '.') + { + keyword_end = i - 1; + tok_size = keyword_end - keyword_start + 1; + gs.state = HL_DEFAULT; + + /* If not a valid char keyword: valid number. */ + if (!is_char_keyword(line[i])) + { + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+NUMBER_COLOR], + LENGTHS[CURRENT_THEME+NUMBER_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + + /* Maybe we should highlight this remaining char. */ + if (!highlight_symbol(line[i], &hl)) + hl = add_char_to_hl(hl, line[i]); + continue; + } + + /* Otherwise, something else. */ + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + + /* Maybe we should highlight this remaining char. */ + if (!highlight_symbol(line[i], &hl)) + hl = add_char_to_hl(hl, line[i]); + continue; + } + } + break; + + /* Char state. */ + case HL_CHAR: + { + /* Should we end char state?. */ + if (i == str_size || (line[i] == '\'' && line[i + 1] != '\'')) + { + keyword_end = i - 1; + tok_size = keyword_end - keyword_start + 1; + gs.state = HL_DEFAULT; + + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+STRING_COLOR], + LENGTHS[CURRENT_THEME+STRING_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_char_to_hl(hl, line[i]); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + continue; + } + } + break; + + /* String state. */ + case HL_STRING: + { + /* Should we end char state?. */ + if (i == str_size || (line[i] == '"' && line[i - 1] != '\\')) + { + if (i == str_size) + keyword_end = i - 1; + else + { + gs.state = HL_DEFAULT; + keyword_end = i; + } + + tok_size = keyword_end - keyword_start + 1; + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+STRING_COLOR], + LENGTHS[CURRENT_THEME+STRING_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + if (i == str_size) + hl = add_char_to_hl(hl, '\0'); + continue; + } + } + break; + + /* Multiline comment. */ + case HL_COMMENT_MULTI: + { + /* + * If we are at the end of line _or_ have identified + * an end of comment... + */ + if (i == str_size || (line[i] == '*' && i+1 < str_size && + line[i+1] == '/')) + { + if (i == str_size) + keyword_end = i - 1; + else + { + gs.state = HL_DEFAULT; + keyword_end = i + 1; + i += 1; + } + + tok_size = keyword_end - keyword_start + 1; + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+COMMENT_COLOR], + LENGTHS[CURRENT_THEME+COMMENT_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + if (i == str_size) + hl = add_char_to_hl(hl, '\0'); + continue; + } + } + break; + + /* Preprocessor. */ + case HL_PREPROCESSOR: + { + if (!isspace(line[i])) + { + char temp[7 + 1]; + + /* + * Maybe include? + * 6 = nclude, chars remaining. + */ + if (line[i] == 'i' && i+6 < str_size) + { + memcpy(temp, line+i, 7); + temp[7] = '\0'; + + /* If include, lets set or include state. */ + if (!strcmp(temp, "include")) + { + gs.state = HL_PREPROCESSOR_INCLUDE; + i += 6; + continue; + } + } + } + + if (i >= str_size-1) + { + gs.state = HL_DEFAULT; + keyword_end = i; + tok_size = keyword_end - keyword_start + 1; + + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+PREPROC_COLOR], + LENGTHS[CURRENT_THEME+PREPROC_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + } + } + break; + + /* + * Preprocessor/Preprocessor include + * + * This is a 'dumb' preprocessor highlighter: + * it highlights everything with the same color + * and if and only if an '#include' is detected + * the included header will be handled as string + * and thus, will have the same color as the string. + * + * In fact, it is somehow similar to what GtkSourceView + * does (Mousepad, Gedit...) but with one silly difference: + * single-line/multi-line comments will not be handled + * while inside the preprocessor state, meaning that + * comments will also have the same color as the remaining + * of the line, yeah, ugly. + */ + case HL_PREPROCESSOR_INCLUDE: + case HL_PREPROCESSOR_INCLUDE_STRING: + { + /* If start of string, it means that we advanced enough + * to now colorify the '#include' keyword. */ + if (gs.state == HL_PREPROCESSOR_INCLUDE) + { + if (line[i] == '<' || line[i] == '"' || i == str_size) + { + tok_size = i - keyword_start; + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+PREPROC_COLOR], + LENGTHS[CURRENT_THEME+PREPROC_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + keyword_start = i; + gs.state = HL_PREPROCESSOR_INCLUDE_STRING; + } + continue; + } + + /* End of string. */ + if (line[i] == '>' || line[i] == '"' || i == str_size) + { + keyword_end = i; + tok_size = keyword_end - keyword_start + 1; + gs.state = HL_DEFAULT; + + hl = add_str_to_hl(hl, COLORS[CURRENT_THEME+STRING_COLOR], + LENGTHS[CURRENT_THEME+STRING_COLOR]); + hl = add_str_to_hl(hl, line+keyword_start, tok_size); + hl = add_str_to_hl(hl, RESET_COLOR, 4); + continue; + } + } + break; + + default: + break; + } + } + return (hl); +} + +/** + * Safe string-to-int routine that takes into account: + * - Overflow and Underflow + * - No undefined behaviour + * + * Taken from https://stackoverflow.com/a/12923949/3594716 + * and slightly adapted: no error classification, because + * I dont need to know, error is error. + * + * @param out Pointer to integer. + * @param s String to be converted. + * + * @return Returns 0 if success and a negative number otherwise. + */ +int str2int(int *out, char *s) +{ + char *end; + if (s[0] == '\0' || isspace(s[0])) + return (-1); + errno = 0; + + long l = strtol(s, &end, 10); + + /* Both checks are needed because INT_MAX == LONG_MAX is possible. */ + if (l > INT_MAX || (errno == ERANGE && l == LONG_MAX)) + return (-1); + if (l < INT_MIN || (errno == ERANGE && l == LONG_MIN)) + return (-1); + if (*end != '\0') + return (-1); + + *out = l; + return (0); +} + +/** + * Initialize the syntax highlight engine. + * + * @param theme_file Theme file, if NULL, will use + * the internal theme. + * + * @return Returns 0 if success and 1 otherwise. + */ +int highlight_init(const char *theme_file) +{ + int num; /* Integer color. */ + int idx; /* Color vector index. */ + FILE *fp; /* File pointer. */ + char *file; /* File buffer. */ + char *str_num; /* String number buffer. */ + size_t kw_size; /* Keyword size. */ + long file_size; /* Theme file size. */ + + kw_size = sizeof(keywords_list)/sizeof(struct keyword); + + /* Configure themes. */ + if (theme_file != NULL) + { + char *p; /* strtok pointer. */ + idx = 0; + + fp = fopen(theme_file, "r"); + if (fp == NULL) + { + fprintf(stderr, "highlight: cannot open the theme file %s, is it" + " really exists?\n", theme_file); + return (-1); + } + + /* File size. */ + fseek(fp, 0, SEEK_END); + file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + /* Allocate and read the file. */ + file = malloc(sizeof(char) * (file_size+1)); + if (fread(file, file_size, 1, fp) != 1) + { + fprintf(stderr, "highlight: an error has ocurred while" + " trying to read %s!\n", theme_file); + + free(file); + fclose(fp); + return (-1); + } + + file[file_size] = '\0'; + p = file; + + /* Parse each number. */ + for (str_num = strtok(p, ", \t\n"); str_num != NULL && idx < 8; + str_num = strtok(NULL, ", \t\n"), idx++ ) + { + char *color; + + if (str2int(&num, str_num) < 0 || num < 0 || num > 255) + { + fprintf(stderr, "highlight: cannot proceed, invalid" + " number: %s, valid numbers must be between 0-255\n", str_num); + break; + } + + CURRENT_THEME = USER_DEF; + + /* + * Color string size: + * 12 = length of \033[38;5; + dddm + '\0' + * + * Moreover, we have ensured that num is at most 3 chars, i.e: 255, + * so there is no worries about sprintf =). + */ + color = malloc(sizeof(char) * 12); + sprintf(color, "\033[38;5;%dm", num); + + /* Copy to the list. */ + COLORS[CURRENT_THEME+idx] = color; + LENGTHS[CURRENT_THEME+idx] = strlen(color); + } + + /* If something goes wrong. */ + if (idx != 8) + { + /* Deallocate previously configured pointers. */ + for (int i = 0; i < idx; i++) + free(COLORS[CURRENT_THEME+i]); + + fprintf(stderr, "highlight: wrong theme, maybe a wrong number of colors? (%d/8)\n" + " colors should be exactly 8 and between 0-255!\n", idx); + + free(file); + fclose(fp); + return (-1); + } + + free(file); + fclose(fp); + } + + /* Initialize hashtable. */ + hashtable_init(&ht_keywords, hashtable_sdbm_setup); + + for (size_t i = 0; i < kw_size; i++) + hashtable_add(&ht_keywords, keywords_list[i].keyword, + &keywords_list[i]); + + return (0); +} + +/** + * Finishes the highlight 'engine'. + */ +void highlight_finish(void) +{ + /* Finish hashtable. */ + hashtable_finish(&ht_keywords, 0); + + /* If user-defined theme. */ + if (CURRENT_THEME == USER_DEF) + { + for (int i = 0; i < 8; i++) + free(COLORS[CURRENT_THEME+i]); + + CURRENT_THEME = ELF_DEITY; + } +} diff --git a/oslo/kat/highlight.h b/oslo/kat/highlight.h new file mode 100644 index 0000000..5f197b3 --- /dev/null +++ b/oslo/kat/highlight.h @@ -0,0 +1,205 @@ +/* + * MIT License + * + * Copyright (c) 2020 Davidson Francis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef HIGHLIGHT_H +#define HIGHLIGHT_H + +#include +#include +#include +#include +#include +#include "hashtable.h" + +/* External definitions. */ +extern char *COLORS[]; +extern int LENGTHS[]; +extern unsigned char symbols_table[]; +extern int CURRENT_THEME; + +/* Colors constants. */ +#define RESET_COLOR "\033[0m" +#define PREPROC_COLOR 0 +#define TYPES_COLOR 1 +#define KWRDS_COLOR 2 +#define NUMBER_COLOR 3 +#define STRING_COLOR 4 +#define COMMENT_COLOR 5 +#define FUNC_CALL_COLOR 6 +#define SYMBOL_COLOR 7 + +/* Always inline. */ +# if defined(__GNUC__) || defined(__GNUG__) || defined(__clang__) +# define INLINE __attribute__((always_inline)) inline +# else +# define INLINE inline +# endif + +/* Highlighted line. */ +struct highlighted_line +{ + size_t idx; + size_t size; +}; + +/** + * Allocates a new Highlighted Buffer line. + * + * A Highlighted Buffer Line is a facility that transparently allows + * the user to append chars and strings inside a buffer without having + * to worry about space, reallocs and so on, something similiar to + * SDS strings. + * + * @returns Return a pointer to the highlighted line. + */ +extern char* highlight_alloc_line(void); + +/** + * Deallocate a Highlighted Line Buffer. + * + * @param line Highlighted Line Buffer to be deallocated. + */ +extern void highlight_free(char *line); + +/** + * For a given line @p line and a (already) allocated + * highlighted line buffer @p hl, highlights the + * line and returns @p hl with the highlighted line. + * + * @param line Line (null terminated string) to be highlighted. + * @param hl Pre-allocated Highlighted Line buffer. + * + * @return Returns a Highlighted Line Buffer. + */ +extern char *highlight_line(const char *line, char *hl, size_t str_size); + +/** + * Initialize the syntax highlight engine. + * + * @param theme_file Theme file, if NULL, will use + * the internal theme. + * + * @return Returns 0 if success and 1 otherwise. + */ +extern int highlight_init(const char *theme_file); + +/** + * Finishes the highlight 'engine'. + */ +extern void highlight_finish(void); + +/* ----------------------- Inline functions. ----------------------- */ + +/** + * Appends a single char @p c into the highlighted buffer + * @p line. + * + * @param line Highlighted Buffer. + * @param c Char to be highlighted. + * + * @return Returns a pointer to the highlighted buffer containing + * the appended character, + */ +INLINE char* add_char_to_hl(char *line, char c) +{ + struct highlighted_line *hl; + hl = ((struct highlighted_line *)line - 1); + + if (hl->idx >= hl->size) + { + hl->size += 32; + hl = realloc(hl, sizeof(struct highlighted_line) + + (sizeof(char) * hl->size)); + } + line = (char*)(hl+1); + line[hl->idx++] = c; + return (line); +} + +/** + * Appends a new string @p str of size @p size into the + * highlighted buffer @p line. + * + * @param line Highlighted Buffer. + * @param str String to be appended. + * @param size Size of the given string @p str. + * + * @return Returns a pointer to the highlighted buffer containing + * the appended string. + */ +INLINE char* add_str_to_hl(char *line, const char *str, size_t size) +{ + struct highlighted_line *hl; + hl = ((struct highlighted_line *)line - 1); + + if (!size) + size = strlen(str); + + if ( (hl->size - hl->idx) < size ) + { + /* Make room for the string and adds 32 extra chars. */ + hl->size += (size - (hl->size - hl->idx)) + 32; + hl = realloc(hl, sizeof(struct highlighted_line) + + (sizeof(char) * hl->size)); + } + line = (char*)(hl+1); + memcpy(line+hl->idx, str, size); + hl->idx += size; + return (line); +} + +/** + * Checks if the given character belongs to a valid keyword + * or not. + * + * @param c Character to be checked. + * + * @return Returns 1 if the character belongs to a valid + * keyword, 0 otherwise. + */ +INLINE static int is_char_keyword(char c) +{ + return (isalpha(c) || isdigit(c) || c == '_'); +} + +/** + * Highlight (or not) a given symbol @p c. + * + * @param c Symbol to be highlighted. + * @param hl Highlighted Line Buffer. + */ +INLINE static int highlight_symbol(char c, char **hl) +{ + if (symbols_table[(unsigned char)c]) + { + *hl = add_str_to_hl(*hl, COLORS[CURRENT_THEME+SYMBOL_COLOR], + LENGTHS[CURRENT_THEME+SYMBOL_COLOR]); + *hl = add_char_to_hl(*hl, c); + *hl = add_str_to_hl(*hl, RESET_COLOR, 4); + return (1); + } + return (0); +} + +#endif /* HIGHLIGHT_H */ diff --git a/oslo/main.m b/oslo/main.m new file mode 100644 index 0000000..9dab60f --- /dev/null +++ b/oslo/main.m @@ -0,0 +1,175 @@ +// +// main.m +// oslo +// +// Created by Ethan Arbuckle on 01/05/25. +// + +#import +#import +#import +#import "output.h" +#import "grouping.h" +#import "highlight.h" + +#define VERSION "1.0.0" + +@implementation _OSLogEventCopy : NSObject +- (id)initWithProxyEvent:(OSLogEventProxy *)event { + if (self = [super init]) { + + // Include {private} fields in the composed message + [event _setIncludeSensitive:YES]; + _composedMessage = event.composedMessage; + _processImagePath = event.processImagePath; + _senderImagePath = event.senderImagePath; + _date = event.date; + _processIdentifier = event.processIdentifier; + _logType = event.logType; + _isError = event.logType == 16; + } + return self; +} +@end + +static void (^streamEventHandler)(OSLogEventProxy *) = ^void(OSLogEventProxy *logProxyEvent) { + if (!logProxyEvent) { + return; + } + printLogEvent([[_OSLogEventCopy alloc] initWithProxyEvent:logProxyEvent]); +}; + +static void (^streamInvalidationHandler)(OSLogEventStream *, NSUInteger, id) = ^void(OSLogEventStream *stream, NSUInteger code, id info) { + printf("Stream invalidated with code %lu\n", code); + exit(0); +}; + +static void startLiveStreamWithPredicate(NSPredicate *predicate) { + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + + __block OSLogEventLiveStream *stream = nil; + OSLogEventLiveStore *store = [objc_getClass("OSLogEventLiveStore") liveLocalStore]; + [store prepareWithCompletionHandler:^(OSLogEventLiveSource *liveEventSource) { + if (liveEventSource) { + stream = [[objc_getClass("OSLogEventLiveStream") alloc] initWithLiveSource:liveEventSource]; + if (stream) { + [stream setFlags:OSLogStreamFlagLogMessages]; + } + } + dispatch_semaphore_signal(sem); + }]; + + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC)); + if (!stream) { + printf("Failed to create live stream\n"); + return; + } + + [stream setFilterPredicate:predicate]; + [stream setEventHandler:streamEventHandler]; + [stream setInvalidationHandler:streamInvalidationHandler]; + [stream activate]; +} + +static void startStoredStreamWithPredicate(NSPredicate *predicate, BOOL group) { + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + + __block OSLogEventStream *stream; + OSLogEventLocalStore *store = [objc_getClass("OSLogEventLocalStore") localStore]; + [store prepareWithCompletionHandler:^(OSLogEventSource *eventSource, NSError *error) { + if (eventSource) { + stream = [[objc_getClass("OSLogEventStream") alloc] initWithSource:eventSource]; + [stream setFlags:OSLogStreamFlagLogMessages]; + } + dispatch_semaphore_signal(sem); + }]; + + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC)); + if (!stream) { + printf("Failed to create stored stream\n"); + return; + } + + [stream setFilterPredicate:predicate]; + if (!group) { + [stream setEventHandler:streamEventHandler]; + } + else { + // Grouping requires a different event handler + [stream setEventHandler:^(OSLogEventProxy *event) { + if (event) { + handleLogEventWithGrouping(event); + } + }]; + } + [stream activateStreamFromDate:[NSDate distantPast]]; +} + +static void printUsage(void) { + printf("oslo %s\n", VERSION); + printf("Usage: oslo [-s] [-g] [ProcessName]\n"); + printf("Options:\n"); + printf(" -l: Stream live logs\n"); + printf(" -s: Stream past logs\n"); + printf(" -g: Group by PID\n"); + printf(" ProcessName: Filter by process name\n"); + printf("\n"); +} + +int main(int argc, char *argv[]) { + @autoreleasepool { + dlopen("/System/Library/PrivateFrameworks/LoggingSupport.framework/LoggingSupport", RTLD_NOW); + termDumper = [[objc_getClass("OSLogTermDumper") alloc] initWithFd:STDOUT_FILENO colorMode:2]; + if (!termDumper || highlight_init(NULL) != KERN_SUCCESS) { + printf("Failed to initialize highlight\n"); + return 1; + } + + bool live = true; + bool group = false; + char *process = NULL; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-l") == 0) { + live = true; + } + else if (strcmp(argv[i], "-s") == 0) { + live = false; + } + else if (strcmp(argv[i], "-g") == 0) { + group = true; + } + else if (argv[i][0] != '-' && !process) { + process = argv[i]; + } + else { + printUsage(); + return 1; + } + } + + if (group) { + if (live || !process) { + printf("Grouping only works with stored logs and requires a process name\n"); + return 1; + } + } + + NSPredicate *predicate = [NSPredicate predicateWithValue:YES]; + if (process) { + printf("Filtering process image path with: %s\n", process); + predicate = [NSPredicate predicateWithFormat:@"processImagePath contains %@", [NSString stringWithUTF8String:process]]; + } + + if (live) { + startLiveStreamWithPredicate(predicate); + } + else { + startStoredStreamWithPredicate(predicate, group); + } + + dispatch_main(); + } + + return 0; +} diff --git a/oslo/output.h b/oslo/output.h new file mode 100644 index 0000000..a3417bc --- /dev/null +++ b/oslo/output.h @@ -0,0 +1,18 @@ +// +// output.h +// oslo +// +// Created by Ethan Arbuckle on 1/5/25. +// + +#ifndef output_h +#define output_h + +#import +#import "LoggingSupport.h" + +extern OSLogTermDumper *termDumper; + +void printLogEvent(_OSLogEventCopy *logProxyEvent); + +#endif /* output_h */ diff --git a/oslo/output.m b/oslo/output.m new file mode 100644 index 0000000..5e17978 --- /dev/null +++ b/oslo/output.m @@ -0,0 +1,215 @@ +// +// output.m +// oslo +// +// Created by Ethan Arbuckle on 1/5/25. +// + +#import +#import "kat/highlight.h" +#import "output.h" + +OSLogTermDumper *termDumper = nil; + +static int termWidth(void) { + static dispatch_once_t onceToken; + static int width = 0; + dispatch_once(&onceToken, ^{ + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + width = ws.ws_col; + }); + return width; +} + +static void removeNewlines(char *str) { + char *pos = str; + while (*str) { + if (*str != '\n' && *str != '\t') { + *pos++ = *str; + } + str++; + } + *pos = '\0'; +} + +static char *padLineBreaks(const char *str, int padding, int termWidth) { + if (!str || !*str || padding < 0 || termWidth <= 0) { + return NULL; + } + + int effectiveWidth = termWidth - padding; + if (effectiveWidth <= 0) { + return NULL; + } + + size_t len = strlen(str); + size_t maxSize = len + ((len / effectiveWidth) + 1) * (padding + 1) + 1; + if (maxSize < len) { + return NULL; + } + + char *result = calloc(1, maxSize); + if (!result) { + return NULL; + } + + size_t pos = 0; + size_t outPos = 0; + size_t lineStart = 0; + int column = 0; + while (pos < len) { + if (outPos >= maxSize - 1) { + free(result); + return NULL; + } + + if (str[pos] == '\n' || (column >= effectiveWidth - 3 && column > 0)) { + size_t segLen = pos - lineStart; + if (outPos + segLen >= maxSize - 1) { + free(result); + return NULL; + } + + memcpy(&result[outPos], &str[lineStart], segLen); + outPos += segLen; + result[outPos++] = '\n'; + + if (pos + 1 < len) { + if (outPos + padding >= maxSize - 1) { + free(result); + return NULL; + } + memset(&result[outPos], ' ', padding); + outPos += padding; + } + lineStart = pos + (str[pos] == '\n' ? 1 : 0); + column = 0; + } + else { + if (str[pos] == '\033') { + while (pos < len && str[pos] != 'm') { + pos++; + } + } + else { + column++; + } + } + pos++; + } + + if (lineStart < len) { + size_t remaining = len - lineStart; + if (outPos + remaining >= maxSize) { + free(result); + return NULL; + } + memcpy(&result[outPos], &str[lineStart], remaining); + outPos += remaining; + } + + result[outPos] = '\0'; + return result; +} + +void printLogEvent(_OSLogEventCopy *logProxyEvent) { + // The composed message is the fully formatted log + const char *composed_message = [logProxyEvent.composedMessage UTF8String]; + if (composed_message == NULL || strlen(composed_message) == 0) { + return; + } + + NSString *processName = logProxyEvent.processImagePath.lastPathComponent; + NSString *imageName = logProxyEvent.senderImagePath.lastPathComponent; + if (!processName || !imageName) { + return; + } + + // If the date is >=24 hours ago, show the full date + static NSDateFormatter *formatter = nil; + if (!formatter) { + formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"HH:mm:ss"]; + } + + if (fabs([logProxyEvent.date timeIntervalSinceNow]) >= 86400) { + [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + } + + BOOL processAndSenderMatch = [processName isEqualToString:imageName]; + if (!processAndSenderMatch && [imageName length] > 20) { + // Truncate it by snipping the middle and adding ellipsis + imageName = [NSString stringWithFormat:@"%@...%@", [imageName substringToIndex:10], [imageName substringFromIndex:[imageName length] - 10]]; + } + + // If process and image names are the same, only show it once + NSString *processAndImage = processAndSenderMatch ? processName : [NSString stringWithFormat:@"%@(%@)", processName, imageName]; + + NSString *timestamp = [formatter stringFromDate:logProxyEvent.date]; + NSString *prefix = [NSString stringWithFormat:@"%@ %@:%d ", timestamp, processAndImage, logProxyEvent.processIdentifier]; + int prefixLen = (int)[prefix lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + + // Log line prefix -- `12:15:01 Process(Image):PID ` + [termDumper setBold:YES]; + [termDumper setFgColor:6]; + [termDumper puts:timestamp.UTF8String]; + [termDumper puts:" "]; + // Process name + [termDumper setFgColor:2]; + [termDumper setBold:NO]; + [termDumper puts:processName.UTF8String]; + // image name in brackets + if (!processAndSenderMatch) { + [termDumper setFgColor:7]; + [termDumper puts:"("]; + [termDumper setFgColor:3]; + [termDumper puts:imageName.UTF8String]; + [termDumper setFgColor:7]; + [termDumper puts:")"]; + } + [termDumper setFgColor:7]; + [termDumper puts:":"]; + [termDumper setFgColor:5]; + [termDumper puts:[NSString stringWithFormat:@"%d", logProxyEvent.processIdentifier].UTF8String]; + [termDumper setFgColor:7]; + [termDumper puts:" "]; + [termDumper resetStyle]; + [termDumper flush:YES]; + + char *message_copy = strdup(composed_message); + if (message_copy == NULL) { + return; + } + + removeNewlines((char *)message_copy); + + if (logProxyEvent.isError) { + [termDumper setFgColor:1]; + char *padded = padLineBreaks(message_copy, prefixLen, termWidth()); + if (padded) { + [termDumper puts:padded]; + free(padded); + [termDumper writeln]; + } + } + else { + char *highlighted_log = highlight_line(message_copy, NULL, 0); + if (!highlighted_log) { + [termDumper resetStyle]; + [termDumper puts:message_copy]; + [termDumper writeln]; + } + else { + char *padded = padLineBreaks(highlighted_log, prefixLen, termWidth()); + if (padded) { + puts(padded); + free(padded); + } + highlight_free(highlighted_log); + } + } + + free(message_copy); + [termDumper _resetAttrsForNewline]; +}