will-lumley / FaviconFinder

A small swift library for iOS & macOS to detect favicons used by a website.
MIT License
161 stars 33 forks source link

When trying to fetch Favicon of broken link, it causes a huge memory leak that grows more and more, up to 50MB and more #53

Closed yoshalevi3 closed 2 years ago

mergesort commented 3 months ago

I know this issue was closed, but I believe I'm experiencing the same thing. I see that some URLs will trigger my Heroku instance to run out of memory, here is example of a URL that does so. https://atlantic.ctvnews.ca/caught-me-by-surprise-rare-blue-frog-spotted-in-nova-scotia-1.7008979

For the sake of completeness, I can consistently replicate the issue with this code. (I don't believe the code for parsing out image sizes is part of the issue, but wanted to leave it in just in case.)

public struct ImageMetadata: Codable, Hashable {
    let url: URL
    let width: CGFloat?
    let height: CGFloat?
}

struct FaviconParser {
    static func fetchFaviconURL(from url: URL) async throws -> ImageMetadata? {
        let faviconFinder = FaviconFinder(url: url)
        let favicons = try await faviconFinder.fetchFaviconURLs()
        guard let favicon = favicons.largest() else { return nil }

        return ImageMetadata(
            url: favicon.source,
            width: favicon.width,
            height: favicon.height
        )
    }
}

// MARK: FaviconURL

private extension FaviconURL {
    var width: CGFloat? {
        guard let sizeTag else { return nil }

        if let width = self.parseDimensions(from: sizeTag)?.width {
            return CGFloat(width)
        } else {
            return nil
        }
    }

    var height: CGFloat? {
        guard let sizeTag else { return nil }

        if let height = self.parseDimensions(from: sizeTag)?.height {
            return CGFloat(height)
        } else {
            return nil
        }
    }

    private func parseDimensions(from input: String) -> (width: Double, height: Double)? {
        guard let components = self.sizeTag?.split(separator: "x") else { return nil }

        guard components.count == 2 else { return nil }

        guard let width = Double(components[0]), let height = Double(components[1]) else {
            return nil
        }

        return (width, height)
    }
}

// MARK: [FaviconURL]

private extension [FaviconURL] {
    func largest() -> FaviconURL? {
        self.max(by: { (first, second) -> Bool in
            let firstSize = (first.width ?? 0) * (first.height ?? 0)
            let secondSize = (second.width ?? 0) * (second.height ?? 0)

            // Prioritize HTML icons over other types of icons
            if firstSize == secondSize {
                return first.sourceType != .html && second.sourceType == .html
            } else {
                return firstSize < secondSize
            }
        })
    }
}