2013-07-23 22 views
6

Po wyszukaniu jak nadmuchać gzip skompresowane dane na iOS następujący sposób pojawia się w liczbie wyników:Czy to błąd w tej metodzie nadpisywania gzip?

- (NSData *)gzipInflate 
{ 
    if ([self length] == 0) return self; 

    unsigned full_length = [self length]; 
    unsigned half_length = [self length]/2; 

    NSMutableData *decompressed = [NSMutableData dataWithLength: full_length + half_length]; 
    BOOL done = NO; 
    int status; 

    z_stream strm; 
    strm.next_in = (Bytef *)[self bytes]; 
    strm.avail_in = [self length]; 
    strm.total_out = 0; 
    strm.zalloc = Z_NULL; 
    strm.zfree = Z_NULL; 

    if (inflateInit2(&strm, (15+32)) != Z_OK) return nil; 
    while (!done) 
    { 
     // Make sure we have enough room and reset the lengths. 
     if (strm.total_out >= [decompressed length]) 
      [decompressed increaseLengthBy: half_length]; 
     strm.next_out = [decompressed mutableBytes] + strm.total_out; 
     strm.avail_out = [decompressed length] - strm.total_out; 

     // Inflate another chunk. 
     status = inflate (&strm, Z_SYNC_FLUSH); 
     if (status == Z_STREAM_END) done = YES; 
     else if (status != Z_OK) break; 
    } 
    if (inflateEnd (&strm) != Z_OK) return nil; 

    // Set real length. 
    if (done) 
    { 
     [decompressed setLength: strm.total_out]; 
     return [NSData dataWithData: decompressed]; 
    } 
    else return nil; 
} 

Ale natrafiłem kilka przykładów danych (deflowanych na komputerze z systemem Linux z Pythona gzip module) że ta metoda działająca na iOS nie jest w stanie nadmuchać. Oto co się dzieje:

W ostatniej iteracji pętli while inflate() zwraca Z_BUF_ERROR, a pętla zostaje zakończona. Ale funkcja inflateEnd(), która jest wywoływana po pętli, zwraca Z_OK. Kod następnie zakłada, że ​​ponieważ inflate() nigdy nie zwróciło Z_STREAM_END, inflacja nie powiodła się i zwraca wartość null.

Zgodnie z tą stroną http://www.zlib.net/zlib_faq.html#faq05 Z_BUF_ERROR nie jest błędem krytycznym, a moje testy z ograniczonymi przykładami pokazują, że dane są skutecznie zawyżone, jeśli funkcja inflateEnd() zwraca Z_OK, mimo że ostatnie wywołanie metody inflate() nie powróciło Z_OK. Wygląda na to, że metoda inflateEnd() zakończyła pompowanie ostatniego fragmentu danych.

Nie wiem zbyt wiele na temat kompresji i działania gzip, więc nie zamierzam wprowadzać zmian w tym kodzie bez pełnego zrozumienia, co robi. Mam nadzieję, że ktoś o większej wiedzy na ten temat może rzucić nieco światła na tę potencjalną lukę logiczną w powyższym kodzie i zasugerować sposób na jej naprawienie.

Inną metodą, że Google okazuje się, że wydaje się cierpieć z tego samego problemu można znaleźć tutaj: https://github.com/nicklockwood/GZIP/blob/master/GZIP/NSData%2BGZIP.m

Edit:

Tak, to jest błąd! Teraz, jak to naprawić? Poniżej moja próba. Recenzja kodu, ktoś?

- (NSData *)gzipInflate 
{ 
    if ([self length] == 0) return self; 

    unsigned full_length = [self length]; 
    unsigned half_length = [self length]/2; 

    NSMutableData *decompressed = [NSMutableData dataWithLength: full_length + half_length]; 
    int status; 

    z_stream strm; 
    strm.next_in = (Bytef *)[self bytes]; 
    strm.avail_in = [self length]; 
    strm.total_out = 0; 
    strm.zalloc = Z_NULL; 
    strm.zfree = Z_NULL; 

    if (inflateInit2(&strm, (15+32)) != Z_OK) return nil; 

    do 
    { 
     // Make sure we have enough room and reset the lengths. 
     if (strm.total_out >= [decompressed length]) 
      [decompressed increaseLengthBy: half_length]; 
     strm.next_out = [decompressed mutableBytes] + strm.total_out; 
     strm.avail_out = [decompressed length] - strm.total_out; 

     // Inflate another chunk. 
     status = inflate (&strm, Z_SYNC_FLUSH); 

     switch (status) { 
      case Z_NEED_DICT: 
       status = Z_DATA_ERROR;  /* and fall through */ 
      case Z_DATA_ERROR: 
      case Z_MEM_ERROR: 
      case Z_STREAM_ERROR: 
       (void)inflateEnd(&strm); 
       return nil; 
     } 
    } while (status != Z_STREAM_END); 

    (void)inflateEnd (&strm); 

    // Set real length. 
    if (status == Z_STREAM_END) 
    { 
     [decompressed setLength: strm.total_out]; 
     return [NSData dataWithData: decompressed]; 
    } 
    else return nil; 
} 

Edit 2:

Oto przykładowy projekt Xcode, który ilustruje problem biegnę w deflate dzieje po stronie serwera, a dane jest base64 i url zakodowane przed przetransportowaniem przez. HTTP. Wbudowałem kodowany ciąg url base64 w ViewController.m. URL-i dekodowania base64-dekodowania jak wasze metody gzipInflate są w NSDataExtension.m

https://dl.dropboxusercontent.com/u/38893107/gzip/GZIPTEST.zip

Oto plik binarny jak deflowanej biblioteki python gzip:

https://dl.dropboxusercontent.com/u/38893107/gzip/binary.zip

Jest URL zakodowany ciągiem base64, który jest transportowany przez HTTP: https://dl.dropboxusercontent.com/u/38893107/gzip/urlEncodedBase64.txt

+0

Próba przechodzi w nieskończoną pętlę, jeśli strumień gzip nie jest kompletny. –

+0

Nawiasem mówiąc, "binary.zip" nie jest plikiem zip. To jest plik gzip. Nazwa powinna być "binary.gz". –

+0

Adres URL dekoduje plik binary.zip (który powinien być nazywany binary.gz), a kod podany w odpowiedzi prawidłowo rozpakowuje go do 221213 bajtowego pliku tekstowego. Nie patrzyłem na twój kod, żeby zobaczyć, co jest nie tak - to twoja praca. –

Odpowiedz

6

Tak, to błąd.

To prawda, że ​​jeśli inflate() nie zwróci Z_STREAM_END, to nie zakończyłeś inflacji. inflateEnd() powracający Z_OK tak naprawdę nie znaczy wiele - tylko, że otrzymał ważny stan i był w stanie uwolnić pamięć.

Tak więc inflate() musi ostatecznie wrócić Z_STREAM_END, zanim będzie można zadeklarować powodzenie. Jednak Z_BUF_ERROR nie jest powodem do rezygnacji. W takim przypadku wystarczy ponownie zadzwonić pod numer inflate(), wprowadzając więcej danych wejściowych lub większą przestrzeń wyjściową. Wtedy otrzymasz numer Z_STREAM_END.

Z dokumentacji w zlib.h:

/* ... 
Z_BUF_ERROR if no progress is possible or if there was not enough room in the 
output buffer when Z_FINISH is used. Note that Z_BUF_ERROR is not fatal, and 
inflate() can be called again with more input and more output space to 
continue decompressing. 
... */ 

Aktualizacja:

Ponieważ nie jest wadliwy kod krąży tam, poniżej przedstawiono prawidłowy kod wdrożyć odpowiednią metodę. Ten kod obsługuje niekompletne strumienie gzip, połączone strumienie gzip i bardzo duże strumienie gzip. W przypadku bardzo dużych strumieni gzip, unsigned długości w z_stream nie są wystarczająco duże, gdy są skompilowane jako pliki wykonywalne 64-bitowe. NSUInteger ma 64 bity, natomiast unsigned ma 32 bity. W takim przypadku musisz zapętlić dane wejściowe, aby przekazać je do inflate().

Ten przykład po prostu zwraca nil na dowolny błąd. Charakter błędu jest odnotowywany w komentarzu po każdym return nil;, w przypadku gdy pożądane jest bardziej zaawansowane zarządzanie błędami.

- (NSData *) gzipInflate 
{ 
    z_stream strm; 

    // Initialize input 
    strm.next_in = (Bytef *)[self bytes]; 
    NSUInteger left = [self length];  // input left to decompress 
    if (left == 0) 
     return nil;       // incomplete gzip stream 

    // Create starting space for output (guess double the input size, will grow 
    // if needed -- in an extreme case, could end up needing more than 1000 
    // times the input size) 
    NSUInteger space = left << 1; 
    if (space < left) 
     space = NSUIntegerMax; 
    NSMutableData *decompressed = [NSMutableData dataWithLength: space]; 
    space = [decompressed length]; 

    // Initialize output 
    strm.next_out = (Bytef *)[decompressed mutableBytes]; 
    NSUInteger have = 0;     // output generated so far 

    // Set up for gzip decoding 
    strm.avail_in = 0; 
    strm.zalloc = Z_NULL; 
    strm.zfree = Z_NULL; 
    strm.opaque = Z_NULL; 
    int status = inflateInit2(&strm, (15+16)); 
    if (status != Z_OK) 
     return nil;       // out of memory 

    // Decompress all of self 
    do { 
     // Allow for concatenated gzip streams (per RFC 1952) 
     if (status == Z_STREAM_END) 
      (void)inflateReset(&strm); 

     // Provide input for inflate 
     if (strm.avail_in == 0) { 
      strm.avail_in = left > UINT_MAX ? UINT_MAX : (unsigned)left; 
      left -= strm.avail_in; 
     } 

     // Decompress the available input 
     do { 
      // Allocate more output space if none left 
      if (space == have) { 
       // Double space, handle overflow 
       space <<= 1; 
       if (space < have) { 
        space = NSUIntegerMax; 
        if (space == have) { 
         // space was already maxed out! 
         (void)inflateEnd(&strm); 
         return nil;   // output exceeds integer size 
        } 
       } 

       // Increase space 
       [decompressed setLength: space]; 
       space = [decompressed length]; 

       // Update output pointer (might have moved) 
       strm.next_out = (Bytef *)[decompressed mutableBytes] + have; 
      } 

      // Provide output space for inflate 
      strm.avail_out = space - have > UINT_MAX ? UINT_MAX : 
          (unsigned)(space - have); 
      have += strm.avail_out; 

      // Inflate and update the decompressed size 
      status = inflate (&strm, Z_SYNC_FLUSH); 
      have -= strm.avail_out; 

      // Bail out if any errors 
      if (status != Z_OK && status != Z_BUF_ERROR && 
       status != Z_STREAM_END) { 
       (void)inflateEnd(&strm); 
       return nil;     // invalid gzip stream 
      } 

      // Repeat until all output is generated from provided input (note 
      // that even if strm.avail_in is zero, there may still be pending 
      // output -- we're not done until the output buffer isn't filled) 
     } while (strm.avail_out == 0); 

     // Continue until all input consumed 
    } while (left || strm.avail_in); 

    // Free the memory allocated by inflateInit2() 
    (void)inflateEnd(&strm); 

    // Verify that the input is a valid gzip stream 
    if (status != Z_STREAM_END) 
     return nil;       // incomplete gzip stream 

    // Set the actual length and return the decompressed data 
    [decompressed setLength: have]; 
    return decompressed; 
} 
+0

Dzięki Mark. Nic takiego jak odpowiedź samego autora zlib! Próbowałem naprawić błąd (patrz pytanie powyżej, jeśli jest zainteresowane), wykonując pętlę do czasu zwrócenia Z_STREAM_END. Ale w adnotowanym przykładzie @ Joachim połączonym, wewnętrzna pętla jest uwarunkowana na strm.avail_out == 0, której nie rozumiem powodu. –

+0

Twoja poprawka nie zawsze działa. W szczególności przejdzie w nieskończoną pętlę, jeśli zostanie zasilony niekompletnym strumieniem gzip. Również to podejście w ogóle zadziała tylko wtedy, gdy skompresowane i nieskompresowane długości są wystarczająco małe, aby zmieścić się w niepodpisanym typie. I tylko wtedy, gdy łączna długość pasuje do typu bez znaku. Istnieją mocniejsze sposoby napisania tej metody, która nie zależy od tych założeń. Dodam poprawny kod do mojej odpowiedzi. –

+0

Zapętlenie na strm.avail_out == 0, lub równoważnie, oczekiwanie na strm.avail_out! = 0, czeka na wszystkie skompresowane dane, które można wygenerować z dostarczonego skompresowanego wejścia. Nie zostanie to zrobione, dopóki nie wypełni bufora wyjściowego. Trochę skompresowanych danych może czasami generować wiele nieskompresowanych danych, więc potrzebujesz pętli, aby to wszystko wyciągnąć. –

2

Tak, wygląda jak błąd. Według this annotated example from the zlib site, Z_BUF_ERROR jest tylko wskazówką, że nie ma więcej wyjścia, chyba że nadmuchanie() jest dostarczane z większą ilością danych wejściowych, a nie samo w sobie powodem do nienormalnego przerwania pętli nadmuchiwania.

W rzeczywistości połączona próbka wydaje się obsługiwać Z_BUF_ERROR dokładnie tak, jak Z_OK.

+0

Dzięki! Zmodyfikowałem to pytanie za pomocą mojej próby naprawy. –

+0

@ personality-c Tak długo, jak przekazujesz wszystkie dane wejściowe naraz, nie widzę problemu. Zewnętrzna pętla w próbce służy do "przesyłania strumieniowego", tzn. Jeśli avail_out ma wartość 0, próbuje ponownie napełnić bufor wejściowy i próbuje ponownie, aż zwrócone zostanie 'Z_STREAM_END'. Ponieważ przekazałeś wszystkie dane naraz i nie masz nic do uzupełnienia, nie widzę żadnej opcji, ale spróbuj ponownie, aż pojawi się poważny błąd lub 'Z_STREAM_END'. –

+0

dzięki za przejrzenie. –