Skip to content

Instantly share code, notes, and snippets.

@kashiash
Created December 6, 2024 07:58
Show Gist options
  • Select an option

  • Save kashiash/22967bfef9783ba30c151cf999016051 to your computer and use it in GitHub Desktop.

Select an option

Save kashiash/22967bfef9783ba30c151cf999016051 to your computer and use it in GitHub Desktop.

Poniżej znajduje się przykład procedury dekompresji w języku Swift, która jest funkcjonalnie odpowiednikiem pokazanej wcześniej implementacji w C#. Kod ten zakłada istnienie klasy BitsReader analogicznej do tej z przykładu C#, a także identyczną logikę dekompresji danych NRV2E.

import Foundation

class BitsReader { private var data: [UInt8] private var dataPosition: Int private var currentByte: Int private var bitPosition: Int

init(_ data: [UInt8], startIdx: Int = 0) {
    self.data = data
    self.dataPosition = startIdx
    self.currentByte = 0
    self.bitPosition = 0
}

var isDataAvailable: Bool {
    return dataPosition < data.count
}

func readBit() throws -> UInt8 {
    guard isDataAvailable else { throw NSError(domain: "BitsReader", code: 1, userInfo: [NSLocalizedDescriptionKey: "No more data available!"]) }
    
    if bitPosition == 0 {
        guard isDataAvailable else { throw NSError(domain: "BitsReader", code: 1, userInfo: [NSLocalizedDescriptionKey: "No more data available!"]) }
        currentByte = Int(data[dataPosition] & 0xFF)
        dataPosition += 1
        bitPosition = 8
    }
    
    bitPosition -= 1
    return UInt8((currentByte >> bitPosition) & 1)
}

func readByte() throws -> UInt8 {
    guard isDataAvailable else { throw NSError(domain: "BitsReader", code: 1, userInfo: [NSLocalizedDescriptionKey: "No more data available!"]) }
    
    let val = data[dataPosition] & 0xFF
    dataPosition += 1
    return val
}

}

public class NRV2EDecompressor { public func Decompress(_ src: [UInt8]) -> [UInt8] { // Pierwsze 4 bajty to rozmiar danych wyjściowych if src.count < 4 { return [] }

    // Odczyt outputSize z 4 bajtów (little-endian)
    var tmp: UInt32 = 0
    tmp |= UInt32(src[0])
    tmp |= UInt32(src[1]) << 8
    tmp |= UInt32(src[2]) << 16
    tmp |= UInt32(src[3]) << 24
    let outputSize = Int32(bitPattern: tmp)
    
    if outputSize < 0 || outputSize > Int32.max {
        return [] // Nieprawidłowy rozmiar
    }
    
    let dstCount = Int(outputSize)
    var dst = [UInt8](repeating: 0, count: dstCount)
    
    guard let bitsReader = try? BitsReader(src, startIdx: 4) else {
        return []
    }
    
    var olen: UInt32 = 0
    var last_m_off: UInt32 = 1
    
    decodeLoop: while true {
        var m_off: UInt32 = 1
        var m_len: UInt32
        
        // Sprawdź czy są dane
        if !bitsReader.isDataAvailable {
            return dst
        }
        
        // Odczyt literali
        do {
            while try bitsReader.readBit() == 1 {
                if olen >= UInt32(dstCount) { return dst }
                dst[Int(olen)] = try bitsReader.readByte()
                olen += 1
                if olen >= UInt32(dstCount) {
                    return dst
                }
            }
        } catch {
            return dst
        }
        
        // Wyliczanie m_off
        while true {
            if !bitsReader.isDataAvailable {
                return dst
            }
            do {
                m_off = m_off * 2 + (try bitsReader.readBit())
                if try bitsReader.readBit() == 1 {
                    break
                }
                m_off = (m_off - 1) * 2 + (try bitsReader.readBit())
            } catch {
                return dst
            }
        }
        
        do {
            if m_off == 2 {
                m_off = last_m_off
                m_len = try bitsReader.readBit()
            } else {
                let b = try bitsReader.readByte()
                m_off = (m_off - 3) * 256 + UInt32(b)
                if m_off == 1 {
                    // Koniec dekompresji
                    break decodeLoop
                }
                m_len = (m_off ^ 1) & 1
                m_off >>= 1
                last_m_off = m_off + 1
                m_off = last_m_off
            }
        } catch {
            return dst
        }
        
        if !bitsReader.isDataAvailable {
            return dst
        }
        
        // Wyliczanie długości m_len
        do {
            if m_len > 0 {
                // m_len = 1 + kolejny bit
                m_len = 1 + (try bitsReader.readBit())
            } else if try bitsReader.readBit() == 1 {
                // m_len = 3 + kolejny bit
                m_len = 3 + (try bitsReader.readBit())
            } else {
                m_len += 1
                repeat {
                    if !bitsReader.isDataAvailable { return dst }
                    m_len = m_len * 2 + (try bitsReader.readBit())
                    if !bitsReader.isDataAvailable { return dst }
                } while (try bitsReader.readBit() == 0)
                m_len += 3
            }
        } catch {
            return dst
        }
        
        if m_off > 0x500 {
            m_len += 1
        }
        
        let m_pos = Int(olen - m_off)
        if m_pos < 0 || m_pos >= dstCount {
            // Odwołanie wykracza poza aktualnie zdekodowane dane - dane mogą być uszkodzone
            return dst
        }
        
        if olen >= UInt32(dstCount) {
            return dst
        }
        
        // Kopiowanie sekwencji
        dst[Int(olen)] = dst[m_pos]
        olen += 1
        var m_copyPos = m_pos + 1
        var length = m_len
        while length > 0 {
            if olen >= UInt32(dstCount) {
                return dst
            }
            if m_copyPos >= dstCount {
                // Niewłaściwe dane wejściowe - wyjście poza zakres
                return dst
            }
            dst[Int(olen)] = dst[m_copyPos]
            olen += 1
            m_copyPos += 1
            length -= 1
        }
    }
    
    return dst
}

}

Komentarz do kodu

•	Kod został przeportowany z C# na Swift niemal 1:1, zachowując logikę i strukturę.
•	BitsReader w Swift działa analogicznie do wersji w C#: odczytuje pojedyncze bity i bajty z tablicy UInt8.
•	Metoda Decompress(_ src: [UInt8]) -> [UInt8] implementuje tę samą logikę dekompresji NRV2E, co oryginał w C#.
•	Zamiast rzutować wyjątków własnego typu, używamy tu NSError w BitsReader. Można to dostosować do własnych potrzeb (np. użyć throws z własnymi typami błędów).
•	Wszelkie operacje na indeksach i warunkach zostały zachowane, w tym pętle i warunki wyjścia.
•	Należy zwrócić uwagę, że do pełnej poprawności wymagane jest posiadanie poprawnych danych wejściowych. Niektóre ścieżki błędów wynikają z nierzetelnych lub uszkodzonych danych wejściowych.




import XCTest

// Zakładamy, że NRV2EDecompressor i BitsReader są dostępne w testowanym module // Jeżeli znajdują się w innym module, należy dodać import lub skopiować je do test targetu.

final class BitsReaderTests: XCTestCase {

func testReadBit() throws {
    // Dane: 0xAA = 10101010 w binarnym
    let data: [UInt8] = [0xAA]
    let reader = BitsReader(data, startIdx: 0)
    
    // Odczytujemy 8 bitów i sprawdzamy zgodność z 10101010
    XCTAssertEqual(try reader.readBit(), 1)
    XCTAssertEqual(try reader.readBit(), 0)
    XCTAssertEqual(try reader.readBit(), 1)
    XCTAssertEqual(try reader.readBit(), 0)
    XCTAssertEqual(try reader.readBit(), 1)
    XCTAssertEqual(try reader.readBit(), 0)
    XCTAssertEqual(try reader.readBit(), 1)
    XCTAssertEqual(try reader.readBit(), 0)
}

func testReadByte() throws {
    let data: [UInt8] = [0x12, 0x34, 0x56]
    let reader = BitsReader(data, startIdx: 0)
    
    XCTAssertEqual(try reader.readByte(), 0x12)
    XCTAssertEqual(try reader.readByte(), 0x34)
    XCTAssertEqual(try reader.readByte(), 0x56)
    XCTAssertFalse(reader.isDataAvailable)
}

func testNoDataThrowsError() {
    let data: [UInt8] = [0xFF]
    let reader = BitsReader(data, startIdx: 0)
    
    XCTAssertEqual(try? reader.readByte(), 0xFF)
    XCTAssertFalse(reader.isDataAvailable)
    
    // Próba odczytu kolejnych bajtów lub bitów powinna rzucić błąd
    XCTAssertThrowsError(try reader.readByte())
    XCTAssertThrowsError(try reader.readBit())
}

}

final class NRV2EDecompressorTests: XCTestCase {

func testDecompressEmpty() {
    // Pierwsze 4 bajty to rozmiar 0 - puste dane wyjściowe
    let input: [UInt8] = [0x00, 0x00, 0x00, 0x00]
    let decompressor = NRV2EDecompressor()
    let result = decompressor.Decompress(input)
    XCTAssertEqual(result.count, 0)
}

func testDecompressTruncatedData() {
    // Rozmiar wyjściowy = 10, ale brak danych poza nagłówkiem
    let input: [UInt8] = [0x0A, 0x00, 0x00, 0x00]
    let decompressor = NRV2EDecompressor()
    let result = decompressor.Decompress(input)
    
    // Nie powinno rzucać błędu. Może zwrócić pustą lub częściową tablicę (wynik zależy od implementacji)
    XCTAssertTrue(result.count <= 10)
}

func testDecompressKnownData() {
    // W prawdziwym teście należy użyć znanych danych skompresowanych
    // Tu przykład hipotetyczny:
    // Załóżmy, że "Hello" (ASCII: 0x48 0x65 0x6C 0x6C 0x6F) zostało skompresowane
    // i znamy gotowy strumień. Poniższy zestaw to fikcyjne dane przykładowe.
    
    let compressedData: [UInt8] = [
        // Rozmiar wyjścia = 5 (Hello)
        0x05, 0x00, 0x00, 0x00,
        // Tu wstawić prawdziwe dane skompresowane NRV2E
        // Poniżej fikcyjne wartości - w realnym użyciu wstawić poprawne
        0x90, 0xA2, 0xB3, 0x01
    ]
    
    let decompressor = NRV2EDecompressor()
    let result = decompressor.Decompress(compressedData)
    
    // Ponieważ dane są fikcyjne, ten test prawdopodobnie nie przejdzie.
    // W prawdziwym teście "expectedOutput" to oryginalny tekst "Hello"
    let expectedOutput = "Hello".data(using: .ascii)!.map { $0 }
    
    XCTAssertEqual(result, expectedOutput)
}

func testDecompressLiteralData() {
    // Test sprawdza czy dane wyjściowe są poprawnie zwracane, gdy strumień zawiera same literaly.
    // Rozmiar wyjścia = 3 (np. "ABC")
    // Konfigurujemy bity tak, aby trzy bajty zostały odczytane dosłownie:
    // Każdy literal wymaga bitu '1', potem bajtu danych.
    // Używamy 0xFF (11111111), co daje nam mnóstwo bitów '1'. Pierwsze trzy '1' wystarczą.
    
    let input: [UInt8] = [
        0x03, 0x00, 0x00, 0x00, // Rozmiar 3 bajty
        0xFF, // bity (1,1,1,...)
        0x41, // 'A'
        0x42, // 'B'
        0x43  // 'C'
    ]
    
    let decompressor = NRV2EDecompressor()
    let result = decompressor.Decompress(input)
    
    XCTAssertEqual(result, [0x41, 0x42, 0x43]) // "ABC"
}

func testDecompressInsufficientBits() {
    // Test sprawdza sytuację, gdy dane deklarują pewien rozmiar, ale kończą się wcześniej.
    let input: [UInt8] = [
        0x04, 0x00, 0x00, 0x00, // Rozmiar = 4
        0x80 // Tylko jeden bajt danych
    ]
    
    let decompressor = NRV2EDecompressor()
    let result = decompressor.Decompress(input)
    
    // Nie powinien wystąpić wyjątek, ale wynik może być pusty lub częściowy.
    XCTAssertNotNil(result)
    XCTAssertTrue(result.count <= 4)
}

}

Omówienie testów

•	**BitsReaderTests**:

Testujemy odczyt bitów i bajtów, brak danych oraz czy odpowiednio rzuca błędy w przypadku braku dostępnych danych. • NRV2EDecompressorTests: • testDecompressEmpty(): sprawdza dekompresję pustych danych. • testDecompressTruncatedData(): sprawdza sytuację, gdy deklarowany jest większy rozmiar wyjścia niż dostępne dane. • testDecompressKnownData(): przykładowy test, który w realnych warunkach używałby znanych testowych danych skompresowanych. • testDecompressLiteralData(): sprawdza, czy dane dosłowne (literalne) są odczytywane poprawnie. • testDecompressInsufficientBits(): testuje przypadek, w którym dane kończą się w trakcie dekompresji.

Powyższy zestaw można dowolnie rozszerzać, dodając kolejne scenariusze lub wykorzystując rzeczywiste dane testowe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment