diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 2ad66ede..069b1dc5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -326,7 +326,9 @@ public typealias ActionBlock = () -> () let attributedString = NSMutableAttributedString(string: labelText, attributes: [NSAttributedString.Key.font: font.updateSize(standardFontSize), NSAttributedString.Key.foregroundColor: textColor as UIColor]) for attribute in attributes { - let range = NSRange(location: attribute.location, length: attribute.length) + guard let range = validateAttribute(range: NSRange(location: attribute.location, length: attribute.length) , in: attributedString, type: attribute.type) + else { continue } + switch attribute { case let underlineAtt as LabelAttributeUnderlineModel: attributedString.addAttribute(.underlineStyle, value: underlineAtt.underlineValue.rawValue, range: range) @@ -458,10 +460,9 @@ public typealias ActionBlock = () -> () for case let attribute as [String: Any] in attributes { guard let attributeType = attribute.optionalStringForKey(KeyType), let location = attribute["location"] as? Int, - let length = attribute["length"] as? Int - else { continue } - - let range = NSRange(location: location, length: length) + let length = attribute["length"] as? Int, + let range = validateAttribute(range: NSRange(location: location, length: length), in: attributedString, type: attributeType) + else { continue } switch attributeType { case "underline": @@ -833,8 +834,10 @@ extension Label { private func addActionAttributes(range: NSRange, string: NSMutableAttributedString?) { - guard let string = string else { return } - + guard let string = string, + let range = validateAttribute(range: range, in: string) + else { return } + string.addAttributes([NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue], range: range) } @@ -993,3 +996,26 @@ extension Label { return false } } + +//------------------------------------------------------ +// MARK: - Validations +//------------------------------------------------------ + +func validateAttribute(range: NSRange, in string: NSAttributedString, type: String = "") -> NSRange? { + guard range.location >= 0 && range.location <= string.length else { + if let loggingHandler = MVMCoreLoggingHandler.shared(), loggingHandler.responds(to: #selector(MVMCoreLoggingHandler.addError(toLog:))) { + loggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Attribute starting location \(range.lowerBound) is out of bounds for '\(string.string)'. Attribute is discarded.", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: "\(#file): \(#function)")!) + } + return nil + } + + if type != "image" && range.upperBound > string.length { + let newRange = NSRange(location: range.location, length: string.length - range.location) + if let loggingHandler = MVMCoreLoggingHandler.shared(), loggingHandler.responds(to: #selector(MVMCoreLoggingHandler.addError(toLog:))) { + loggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, messageToLog: "Attribute ending location \(range.upperBound) is out of bounds for '\(string)'. Adjusting to \(newRange.upperBound).", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: "\(#file): \(#function)")!) + } + return newRange + } + + return range +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift index 7396e749..d6ec1b74 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift @@ -35,6 +35,16 @@ class LabelAttributeImageModel: LabelAttributeModel { case name case URL } + + //-------------------------------------------------- + // MARK: - Validations + //-------------------------------------------------- + + public override func validateInRange(of text: String) throws { + guard location >= 0 && location <= text.count else { + throw MolecularError.validationError("Attribute starting location \(location) is out of bounds for '\(text)'.") + } + } //-------------------------------------------------- // MARK: - Codec diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift index 7df4097a..88192720 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift @@ -44,6 +44,21 @@ case length } + //-------------------------------------------------- + // MARK: - Validations + //-------------------------------------------------- + + public func validateInRange(of text: String) throws { + // Prevent invalid starting locations. + guard location >= 0 && location <= text.count else { + throw MolecularError.validationError("Attribute starting location \(location) is out of bounds for '\(text)'.") + } + // Prevent lengths extending beyond the bounds of the string. + guard length + location <= text.count else { + throw MolecularError.validationError("Attribute length \(length) starting at \(location) is out of bounds for '\(text)'.") + } + } + //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index 40740194..10e6a4e5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -62,6 +62,16 @@ self.text = text } + //-------------------------------------------------- + // MARK: - Validations + //-------------------------------------------------- + + public func validate(_ attributes: [LabelAttributeModel]) throws { + for attribute in attributes { + try attribute.validateInRange(of: text) + } + } + //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- @@ -82,6 +92,11 @@ makeWholeViewClickable = try typeContainer.decodeIfPresent(Bool.self, forKey: .makeWholeViewClickable) numberOfLines = try typeContainer.decodeIfPresent(Int.self, forKey: .numberOfLines) shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? false + + // Later make protocol based validate outside of decoding? + if let attributes = attributes { + try validate(attributes) + } } open func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index 1f1fdc1e..6845d44b 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -1,6 +1,7 @@ public enum MolecularError: Swift.Error { case error(String) + case validationError(String) case countImbalance(String) }