Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable {
self.fileManager = fileManager
}

@MainActor
public func getVersion() async -> String {
public static func libdigidocppVersion() -> String {
return DigiDocContainerWrapper.libdigidocppVersion()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import CommonsLib

/// @mockable
public protocol ContainerWrapperProtocol: Sendable {
func getVersion() async -> String
func getSignatures() async -> [SignatureWrapper]
func getDataFiles() async -> [DataFileWrapper]
func getMimetype() async -> String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,11 @@ struct ContainerWrapperTests {
}
}

@Test
func libdigidocppVersion_isNonEmpty() {
#expect(!ContainerWrapper.libdigidocppVersion().isEmpty)
}

@discardableResult
private func createSampleContainer(dataFileURLs: [URL?]) async throws -> SignedContainerProtocol {
let dataFilesUrls: [URL] = dataFileURLs.compactMap { $0 }
Expand Down
4 changes: 0 additions & 4 deletions Modules/UtilsLib/Sources/UtilsLib/DI/UtilsLibContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,4 @@ extension Container {
public var notificationUtil: Factory<NotificationUtilProtocol> {
self { @MainActor in NotificationUtil() }
}

public var userAgentUtil: Factory<UserAgentUtilProtocol> {
self { UserAgentUtil() }
}
}
13 changes: 13 additions & 0 deletions Modules/UtilsLib/Sources/UtilsLib/System/SystemUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,17 @@ public struct SystemUtil: Loggable {
logger().info("Operating system version: \(versionString, privacy: .public)")
return versionString
}

public static func getDeviceModelIdentifier() -> String {
if let simulatorModel = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] {
return simulatorModel.lowercased()
}

var systemInfo = utsname()
uname(&systemInfo)
let identifier = withUnsafeBytes(of: &systemInfo.machine) { raw in
String(cString: raw.bindMemory(to: CChar.self).baseAddress!)
}
return identifier.lowercased()
}
}
36 changes: 36 additions & 0 deletions Modules/UtilsLib/Sources/UtilsLib/User-Agent/DeviceCategory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2017 - 2026 Riigi Infosüsteemi Amet
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/

import Foundation

enum DeviceCategory: String {
case mobile
case tablet

init(modelIdentifier: String) {
self = modelIdentifier.hasPrefix("ipad") ? .tablet : .mobile
}

var osName: String {
switch self {
case .mobile: "iOS"
case .tablet: "iPadOS"
}
}
}
91 changes: 62 additions & 29 deletions Modules/UtilsLib/Sources/UtilsLib/User-Agent/UserAgentUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,56 +23,89 @@ import ExternalAccessory

public struct UserAgentUtil: UserAgentUtilProtocol {

private static let schemaVersion = 1

private let libdigidocppVersion: String

public init(libdigidocppVersion: String = "") {
self.libdigidocppVersion = libdigidocppVersion
}

public func userAgent(
diagnostics: UserAgentDiagnostics = .none,
language: String
) -> String {
var components: [String] = [
appIdentifier(),
osInfo(),
languageInfo(language: language)
]

switch diagnostics {
case .none:
break

case .devices:
if let devicesInfo = devicesInfo() {
components.append(devicesInfo)
}
let info = appInfo(diagnostics: diagnostics, language: language)

case .nfc:
components.append("NFC: true")
}

return components.joined(separator: " ")
guard !libdigidocppVersion.isEmpty else { return "APP \(info)" }
return "LIB libdigidocpp/\(libdigidocppVersion) (\(architecture())) APP \(info)"
}

private func appIdentifier() -> String {
"riadigidoc/\(appVersion())"
public func appInfo(
diagnostics: UserAgentDiagnostics = .none,
language: String
) -> String {
let metadata = metadataFields(diagnostics: diagnostics, language: language)
.joined(separator: "; ")
return "\(appIdentifier()) (\(metadata))"
}

private func osInfo() -> String {
"(iOS \(SystemUtil.getOSVersion()))"
private func metadataFields(
diagnostics: UserAgentDiagnostics,
language: String
) -> [String] {
let model = SystemUtil.getDeviceModelIdentifier()
let category = DeviceCategory(modelIdentifier: model)

let diagnosticsField: String? = switch diagnostics {
case .none: nil
case .devices: devicesInfo().map { "devices=\($0)" }
case .nfc: "nfc=true"
}

return [
"schema=\(Self.schemaVersion)",
"os=\(category.osName) \(SystemUtil.getOSVersion())",
"lang=\(sanitizeField(language))",
"devicetype=\(category.rawValue)/\(sanitizeField(model))",
diagnosticsField
].compactMap { $0 }
}

private func languageInfo(language: String) -> String {
return "Lang: \(language)"
private func appIdentifier() -> String {
"riadigidoc/\(BundleUtil.getAppVersion())"
}

private func appVersion() -> String {
return BundleUtil.getAppVersion()
private func architecture() -> String {
#if arch(arm64)
return "arm64"
#elseif arch(x86_64)
return "x86_64"
#elseif arch(arm)
return "arm"
#elseif arch(i386)
return "i386"
#else
return "unknown"
#endif
}

private func devicesInfo() -> String? {
let devices = EAAccessoryManager.shared()
.connectedAccessories
.map {
"\($0.manufacturer) \($0.name) (\($0.modelNumber))"
sanitizeField("\($0.manufacturer) \($0.name) \($0.modelNumber)")
}
.filter { !$0.isEmpty }

guard !devices.isEmpty else { return nil }
return "Devices: \(devices.joined(separator: ", "))"
return devices.joined(separator: ", ")
}

// Remove delimiters and line breaks so a field can't break the header structure.
private func sanitizeField(_ value: String) -> String {
let forbidden = CharacterSet(charactersIn: ";()\u{2028}\u{2029}").union(.controlCharacters)
let cleaned = String.UnicodeScalarView(value.unicodeScalars.filter { !forbidden.contains($0) })
return String(cleaned).trimmingCharacters(in: .whitespacesAndNewlines)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ public protocol UserAgentUtilProtocol: Sendable {
diagnostics: UserAgentDiagnostics,
language: String
) -> String

func appInfo(
diagnostics: UserAgentDiagnostics,
language: String
) -> String
}
19 changes: 19 additions & 0 deletions Modules/UtilsLib/Tests/UtilsLibTests/System/SystemUtilTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,23 @@ struct SystemUtilTests {
#expect(match != nil)
}
}

@Test
func getDeviceModelIdentifier_isNonEmptyAndLowercased() {
let model = SystemUtil.getDeviceModelIdentifier()

#expect(!model.isEmpty)
#expect(model == model.lowercased())
#expect(!model.contains(" "))
#expect(!model.contains("\0"))
}

@Test
func getDeviceModelIdentifier_matchesSimulatorEnvironmentWhenPresent() {
guard let simulatorModel = ProcessInfo.processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] else {
return
}

#expect(SystemUtil.getDeviceModelIdentifier() == simulatorModel.lowercased())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2017 - 2026 Riigi Infosüsteemi Amet
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/

import Foundation
import Testing

@testable import UtilsLib

struct DeviceCategoryTests {

@Test(
"iPad models map to tablet/iPadOS",
arguments: ["ipad13,4", "ipad8,12", "ipad14,1", "ipad"]
)
func tabletModels(model: String) {
let category = DeviceCategory(modelIdentifier: model)

#expect(category == .tablet)
#expect(category.rawValue == "tablet")
#expect(category.osName == "iPadOS")
}

@Test(
"Non-iPad models map to mobile/iOS",
arguments: ["iphone15,2", "iphone16,2", "ipod9,1", "", "x86_64"]
)
func mobileModels(model: String) {
let category = DeviceCategory(modelIdentifier: model)

#expect(category == .mobile)
#expect(category.rawValue == "mobile")
#expect(category.osName == "iOS")
}
}
Loading
Loading