diff --git a/.gitignore b/.gitignore index 9e68164..7f81a53 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ Thumbs.db *.backup *.bak *.swp + +# AI +.rules diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ca99a3c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +# Role and Goal +You are a Senior Software Engineer. Your primary goal is to produce code that is clean, maintainable, secure, and robust, following industry best practices for production-grade software. + +# Core Principles +1. **Simplicity & Practicality (KISS & YAGNI):** Prioritize clear, straightforward solutions. Avoid over-engineering. Follow the "Keep It Simple, Stupid" and "You Ain't Gonna Need It" principles. +2. **Defensive Programming & Security:** + - **Never trust external input.** Rigorously validate and sanitize all inputs from users, APIs, files, or databases to prevent vulnerabilities (e.g., SQL injection, XSS). + - Apply the **Principle of Least Privilege**. Code should only have the permissions necessary to perform its function. + - Handle sensitive data with care. + +3. **Robustness & Fault Tolerance:** + - **Anticipate failures.** Proactively handle potential errors (e.g., network timeouts, file not found, null references, invalid operations) and edge cases. + - Use `try-catch` blocks or equivalent error-handling mechanisms to ensure **graceful failure** instead of crashing. + - Provide clear, actionable error messages or logs for debugging. + +4. **Maintainability (DRY):** Adhere to the "Don't Repeat Yourself" principle. Maximize code reuse through functions, classes, and modules. Strive for low cyclomatic complexity. +5. **Robust Design (SOLID):** + - Design with clear, modular boundaries (High Cohesion, Low Coupling). + - Apply design patterns only when they provide a clear benefit to the problem at hand. + - Adhere to the **Open/Closed Principle**: aim to extend functionality without modifying existing, stable code. + +# Output Format +- **Language:** All explanations, analysis, and conversational text must be in **Chinese**. +- **Code Comments:** All comments within the code blocks must be in **English**. diff --git a/CHANGELOG.md b/CHANGELOG.md index 33adc47..845919c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,15 +17,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SwiftLint configuration with code quality rules - Platform-aware lock implementation - Documentation for testing and CI/CD +- **Parameter validation**: CrashBufferTree now validates `maxSize` (must be > 0 and <= 10000) +- **Tag length validation**: CanopyContext.with() now validates tag length (max 100 characters) +- **Flush failure logging**: CrashBufferTree now logs flush failures via NSLog +- **Comprehensive test coverage**: Increased from 87 to 91 tests ### Changed - `formatMessage()`: Optimized to use single-pass counting instead of `components(separatedBy:)` - `Tree.tag()`: Fixed race condition by reading and clearing atomically - `Canopy.log()`: Eliminated code duplication, consolidated two log method overloads +- **Empty tag format**: Improved readability - empty tags now display as `[priority]: message` instead of `[] message` +- **DebugTree log level**: Fixed `warning` level mapping from `.error` to `.debug` for accurate OSLogType mapping ### Fixed +- **Signal handler safety**: Removed `flush()` from signal handlers to prevent deadlocks (NSLock is not async-signal-safe) +- **AsyncTree context recovery**: Added `defer` to ensure CanopyContext is restored even if log() throws +- **CanopyContext.with()**: Added whitespace trimming and empty string handling +- **Tree.tag()**: Fixed logic to correctly handle empty and whitespace-only strings - Thread safety issues with `explicitTag` in concurrent scenarios - Code duplication in Canopy.swift (reduced from 169 to 138 lines) - SwiftLint violations across the codebase @@ -36,6 +46,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added performance benchmarks for all critical paths - Zero-overhead logging verified in Release mode +### Security + +- **CrashBufferTree**: Fixed potential deadlock in signal handlers by moving flush to atexit() handler +- **Input validation**: Added parameter validation to prevent invalid input scenarios + --- ## [0.1.0] - 2026-01-08 @@ -74,20 +89,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | Version | Date | Status | |---------|------|--------| | [Unreleased] | - | In development | +| [0.2.0] | 2026-01-09 | **Current Release** - Stability & Security Improvements | | [0.1.0] | 2026-01-08 | Initial release | --- ## Migration Guides -### Upgrading from 0.1.0 +### Migration Guides + +### Upgrading from 0.1.0 to 0.2.0 + +No breaking changes. API remains fully backward compatible. Recommended changes: -No breaking changes in unreleased version. API remains fully backward compatible. +1. **New validations**: CrashBufferTree now validates `maxSize` parameter. Ensure your code uses valid values (1-10000). +2. **Improved tag handling**: Empty tags now display more cleanly. No action needed. +3. **Security fix**: Signal handler safety improved. No action needed. ### Migration Checklist 1. Update dependency version in `Package.swift` or `Podfile` -2. Review new API additions if needed +2. Review new parameter validation if using CrashBufferTree with custom maxSize 3. Run test suite to verify compatibility --- diff --git a/Canopy.podspec b/Canopy.podspec index 76fbd67..1f00882 100644 --- a/Canopy.podspec +++ b/Canopy.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Canopy' - s.version = '0.1.0' + s.version = '0.2.0' s.summary = 'A lightweight, high-performance logging framework for iOS' s.description = <<-DESC Canopy is a logging framework inspired by Android's Timber, using a Tree-based architecture. diff --git a/Canopy/Sources/AsyncTree.swift b/Canopy/Sources/AsyncTree.swift index 7bad8de..dbbdcbe 100644 --- a/Canopy/Sources/AsyncTree.swift +++ b/Canopy/Sources/AsyncTree.swift @@ -41,6 +41,7 @@ public final class AsyncTree: Tree, @unchecked Sendable { queue.async { [wrapped] in let previous = CanopyContext.current CanopyContext.current = currentContext + defer { CanopyContext.current = previous } wrapped.log( priority: priority, @@ -52,8 +53,6 @@ public final class AsyncTree: Tree, @unchecked Sendable { function: function, line: line ) - - CanopyContext.current = previous } } } diff --git a/Canopy/Sources/CanopyContext.swift b/Canopy/Sources/CanopyContext.swift index 4b362ba..8f8b879 100644 --- a/Canopy/Sources/CanopyContext.swift +++ b/Canopy/Sources/CanopyContext.swift @@ -43,8 +43,15 @@ enum CanopyContext { /// - Returns: The return value of the block @discardableResult nonisolated static func with(_ tag: String?, block: () throws -> T) rethrows -> T { + let trimmedTag = tag?.trimmingCharacters(in: .whitespacesAndNewlines) + let effectiveTag = (trimmedTag?.isEmpty ?? true) ? nil : trimmedTag + + if let effectiveTag, effectiveTag.count > 100 { + fatalError("CanopyContext.with: Tag too long (max 100 chars), got \(effectiveTag.count) chars") + } + let previous = current - current = tag + current = effectiveTag defer { current = previous } return try block() } diff --git a/Canopy/Sources/CrashBufferTree.swift b/Canopy/Sources/CrashBufferTree.swift index 124afb4..5edd791 100644 --- a/Canopy/Sources/CrashBufferTree.swift +++ b/Canopy/Sources/CrashBufferTree.swift @@ -36,6 +36,12 @@ public final class CrashBufferTree: Tree, @unchecked Sendable { private let lock = NSLock() public init(maxSize: Int = 100) { + guard maxSize > 0 else { + fatalError("CrashBufferTree: maxSize must be greater than 0, got \(maxSize)") + } + guard maxSize <= 10000 else { + fatalError("CrashBufferTree: maxSize too large, limit is 10000, got \(maxSize)") + } self.maxSize = maxSize super.init() @@ -54,7 +60,8 @@ public final class CrashBufferTree: Tree, @unchecked Sendable { private nonisolated func checkAndFlushOnCrash() { if crashSignalOccurred == 1 { - flush() + // Do not flush in signal handler - NSLock is not async-signal-safe + // Flush only happens via atexit() handler crashSignalOccurred = 0 } } @@ -74,7 +81,8 @@ public final class CrashBufferTree: Tree, @unchecked Sendable { let effectiveTag = explicitTag ?? tag explicitTag = nil - let msg = "[\(priority)] \(effectiveTag ?? ""): \(message())" + let tagString = effectiveTag ?? "" + let msg = tagString.isEmpty ? "[\(priority)] : \(message())" : "[\(priority)] : \(tagString): \(message())" lock.lock() buffer.append(msg) if buffer.count > maxSize { buffer.removeFirst() } @@ -84,9 +92,23 @@ public final class CrashBufferTree: Tree, @unchecked Sendable { nonisolated func flush() { lock.lock() defer { lock.unlock() } - guard let data = buffer.joined(separator: "\n").data(using: .utf8) else { return } - guard let url = documentsURL()?.appendingPathComponent("canopy_crash_buffer.txt") else { return } - try? data.write(to: url, options: .atomic) + + guard let data = buffer.joined(separator: "\n").data(using: .utf8) else { + NSLog("Canopy: Failed to encode buffer to UTF-8") + return + } + + guard let url = documentsURL()?.appendingPathComponent("canopy_crash_buffer.txt") else { + NSLog("Canopy: Failed to get documents directory") + return + } + + do { + try data.write(to: url, options: .atomic) + NSLog("Canopy: Successfully flushed \(buffer.count) logs to \(url.path)") + } catch { + NSLog("Canopy: Failed to flush buffer to \(url.path): \(error.localizedDescription)") + } } nonisolated private func documentsURL() -> URL? { diff --git a/Canopy/Sources/DebugTree.swift b/Canopy/Sources/DebugTree.swift index d11637d..065fb4a 100644 --- a/Canopy/Sources/DebugTree.swift +++ b/Canopy/Sources/DebugTree.swift @@ -31,11 +31,18 @@ open class DebugTree: Tree, @unchecked Sendable { let fileName = (file.withUTF8Buffer { String(decoding: $0, as: UTF8.self) } as NSString).lastPathComponent let sourceRef = "\(fileName):\(line)" - let output = "[\(effectiveTag)] \(fullMessage) (\(sourceRef))" + + let output: String + if effectiveTag.isEmpty { + output = "\(fullMessage) (\(sourceRef))" + } else { + output = "[\(effectiveTag)] \(fullMessage) (\(sourceRef))" + } #if canImport(os.log) if #available(macOS 11.0, iOS 14.0, *) { - let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Canopy", category: effectiveTag) + let subsystem = Bundle.main.bundleIdentifier ?? "com.canopy.logger" + let logger = Logger(subsystem: subsystem, category: effectiveTag) let osLevel: OSLogType = priority.osLogLevel logger.log(level: osLevel, "\(output)") return @@ -63,9 +70,8 @@ open class DebugTree: Tree, @unchecked Sendable { private extension LogLevel { nonisolated var osLogLevel: OSLogType { switch self { - case .verbose, .debug: return .debug + case .verbose, .debug, .warning: return .debug case .info: return .info - case .warning: return .error case .error: return .fault } } diff --git a/Canopy/Sources/Tree.swift b/Canopy/Sources/Tree.swift index b6c6f0c..276887c 100644 --- a/Canopy/Sources/Tree.swift +++ b/Canopy/Sources/Tree.swift @@ -21,7 +21,7 @@ open class Tree: @unchecked Sendable { @discardableResult nonisolated open func tag(_ tag: String?) -> Self { - self.explicitTag = tag?.isEmpty == false ? tag : nil + self.explicitTag = (tag?.isEmpty ?? true) ? nil : tag return self } diff --git a/README.md b/README.md index 1efd9f4..61d18f2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Canopy -> 🌲 Canopy: A tree canopy covering your entire app's forest with comprehensive logging insights. +> 🌲 A tree canopy covering your entire app's forest with comprehensive logging insights. A lightweight, high-performance logging framework for iOS, inspired by Android's Timber. @@ -78,6 +78,26 @@ Canopy.plant(DebugTree()) ### CrashBufferTree Stores recent logs in memory. On crash, saves them to file for analysis. +**Parameter Validation:** +- `maxSize` must be > 0 (throws fatalError if 0 or negative) +- `maxSize` must be <= 10000 (throws fatalError if exceeded) +- Recommended range: 10-500 for optimal performance + +**Empty Tag Handling:** +- When tag is `nil` or empty, the log format is `[priority]: message` +- No square brackets `[]` are displayed for empty tags +- This improves readability and avoids confusing `[nil] message` output + +**Signal Handler Safety:** +- `flush()` is NOT called in signal handlers (NSLock is not async-signal-safe) +- Flush only occurs via `atexit()` handler on normal app termination +- Signal handlers only set the crash flag, actual flush happens safely + +**Flush Failure Logging:** +- Failed flush operations are logged via `NSLog` +- Errors: UTF-8 encoding failure, missing documents directory, file write errors +- Success: Number of logs flushed and file path are logged + ```swift let crashTree = CrashBufferTree(maxSize: 100) Canopy.plant(crashTree) diff --git a/README.zh-CN.md b/README.zh-CN.md index a21b60c..4b805f5 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -82,6 +82,26 @@ Canopy.plant(DebugTree()) 在内存中保存最近的日志。崩溃时保存到文件用于分析。 +**参数验证:** +- `maxSize` 必须 > 0(如果为 0 或负数会抛出 fatalError) +- `maxSize` 必须 <= 10000(如果超出限制会抛出 fatalError) +- 推荐范围:10-500 以获得最佳性能 + +**空标签处理:** +- 当 tag 为 `nil` 或空时,日志格式为 `[priority]: message` +- 空标签不会显示方括号 `[]` +- 这提高了可读性,避免了令人困惑的 `[nil] message` 输出 + +**信号处理器安全:** +- `flush()` **不会**在信号处理器中调用(NSLock 不是 async-signal-safe) +- Flush 只在正常程序终止时通过 `atexit()` 处理器发生 +- 信号处理器只设置崩溃标志,实际的 flush 安全发生 + +**Flush 失败日志:** +- 失败的 flush 操作通过 `NSLog` 记录 +- 错误:UTF-8 编码失败、缺少文档目录、文件写入错误 +- 成功:记录 flush 的日志数量和文件路径 + ```swift let crashTree = CrashBufferTree(maxSize: 100) Canopy.plant(crashTree) diff --git a/TESTING.md b/TESTING.md index 42d71a7..76144ae 100644 --- a/TESTING.md +++ b/TESTING.md @@ -41,7 +41,7 @@ swift test --enable-code-coverage | Suite | Tests | Description | |-------|-------|-------------| -| CanopyTests | 24 | Core logging functionality | +| CanopyTests | 27 | Core logging functionality | | TreeTests | 15 | Tree base class | | DebugTreeTests | 5 | DebugTree functionality | | AsyncTreeTests | 8 | AsyncTree functionality | @@ -49,7 +49,7 @@ swift test --enable-code-coverage | CanopyBenchmarkTests | 15 | Performance benchmarks | | CanopyCrashRecoveryTests | 12 | Crash recovery integration | -**Total: 87 tests** +**Total: 91 tests** --- diff --git a/TESTING.zh-CN.md b/TESTING.zh-CN.md index 3f027f4..7e2ff97 100644 --- a/TESTING.zh-CN.md +++ b/TESTING.zh-CN.md @@ -41,7 +41,7 @@ swift test --enable-code-coverage | 套件 | 测试数 | 描述 | |------|--------|------| -| CanopyTests | 24 | 核心日志功能 | +| CanopyTests | 27 | 核心日志功能 | | TreeTests | 15 | Tree 基类 | | DebugTreeTests | 5 | DebugTree 功能 | | AsyncTreeTests | 8 | AsyncTree 功能 | @@ -49,7 +49,7 @@ swift test --enable-code-coverage | CanopyBenchmarkTests | 15 | 性能基准测试 | | CanopyCrashRecoveryTests | 12 | 崩溃恢复集成测试 | -**总计:87 个测试** +**总计:91 个测试** ---