Skip to content

Commit b20a152

Browse files
Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.
1 parent b670f55 commit b20a152

File tree

9 files changed

+1219
-917
lines changed

9 files changed

+1219
-917
lines changed

compressor/compress.go

Lines changed: 359 additions & 243 deletions
Large diffs are not rendered by default.

compressor/hfc/io.go

Lines changed: 198 additions & 652 deletions
Large diffs are not rendered by default.

compressor/lzw/io.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package lzw
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"file-compressor/constants"
12+
"file-compressor/utils"
13+
)
14+
15+
// Zip compresses a single file using LZW algorithm and writes to the output writer.
16+
// It writes metadata for the file (name length, name, data length, data).
17+
// The progressCallback expects (percentOfCurrentFile float64, messageFromZip string).
18+
func Zip(file utils.FileData, output io.Writer, progressCallback func(percentOfCurrentFile float64, messageFromZip string)) error {
19+
if progressCallback != nil {
20+
// Initial message for this specific file
21+
progressCallback(0.0, fmt.Sprintf("Starting LZW for %s", filepath.Base(file.Name)))
22+
}
23+
24+
if file.Reader == nil {
25+
if file.Size > 0 {
26+
utils.ColorPrint(utils.YELLOW, fmt.Sprintf("Warning: LZW Zip skipping '%s' due to nil reader.\n", file.Name))
27+
}
28+
// If reader is nil, we can't proceed.
29+
// Report 100% for this "skipped" file to allow overall progress to advance.
30+
if progressCallback != nil {
31+
progressCallback(1.0, fmt.Sprintf("Skipped %s (nil reader)", filepath.Base(file.Name)))
32+
}
33+
return nil
34+
}
35+
36+
fileReader := file.Reader
37+
38+
defer func(r io.Reader) {
39+
if c, ok := r.(io.Closer); ok {
40+
c.Close()
41+
}
42+
}(fileReader)
43+
44+
fileNameBytes := []byte(filepath.Base(file.Name))
45+
if err := binary.Write(output, binary.LittleEndian, uint32(len(fileNameBytes))); err != nil {
46+
return fmt.Errorf("LZW Zip: failed to write file name length for %s: %w", file.Name, err)
47+
}
48+
49+
if _, err := output.Write(fileNameBytes); err != nil {
50+
return fmt.Errorf("LZW Zip: failed to write file name for %s: %w", file.Name, err)
51+
}
52+
53+
tempCompressedData := utils.NewResizableBuffer()
54+
55+
var bytesReadForThisFile int64
56+
onProgressUpdate := func(readBytes int64, totalBytes int64) {
57+
bytesReadForThisFile = readBytes
58+
if progressCallback != nil && totalBytes > 0 {
59+
percent := float64(readBytes) / float64(totalBytes)
60+
if percent > 1.0 { percent = 1.0 } // Cap percent at 1.0
61+
progressCallback(percent, fmt.Sprintf("Processing %s", filepath.Base(file.Name)))
62+
}
63+
}
64+
65+
trackingReader := utils.NewProgressReader(fileReader, file.Size, onProgressUpdate)
66+
67+
errCompress := compressData(trackingReader, tempCompressedData)
68+
if errCompress != nil {
69+
return fmt.Errorf("LZW Zip: failed to compress data for %s: %w", file.Name, errCompress)
70+
}
71+
72+
// After compression, all bytes of this file should have been read (or an error occurred).
73+
// Ensure a final 100% progress for this file is signaled if no error.
74+
if progressCallback != nil && file.Size > 0 {
75+
// Check if the last report was already 100%
76+
// This might be redundant if compressData + ProgressTrackingReader already guarantees a final 100% call,
77+
// but it's a safeguard.
78+
currentProgress := 0.0
79+
if file.Size > 0 { // Avoid division by zero if file.Size is 0
80+
currentProgress = float64(bytesReadForThisFile) / float64(file.Size)
81+
}
82+
if currentProgress < 1.0 {
83+
progressCallback(1.0, fmt.Sprintf("Finalizing %s", filepath.Base(file.Name)))
84+
}
85+
}
86+
87+
88+
compressedSize := uint64(tempCompressedData.Len())
89+
if err := binary.Write(output, binary.LittleEndian, compressedSize); err != nil {
90+
return fmt.Errorf("LZW Zip: failed to write compressed data length for %s: %w", file.Name, err)
91+
}
92+
93+
if _, err := io.Copy(output, tempCompressedData.Reader()); err != nil {
94+
return fmt.Errorf("LZW Zip: failed to write compressed data for %s: %w", file.Name, err)
95+
}
96+
97+
if progressCallback != nil {
98+
// Final confirmation for this specific file being done.
99+
progressCallback(1.0, fmt.Sprintf("Finished LZW for %s", filepath.Base(file.Name)))
100+
}
101+
return nil
102+
}
103+
104+
// Unzip decompresses data using LZW algorithm from the input reader and writes to outputDir.
105+
func Unzip(input io.Reader, outputDir string, progressCallback utils.ProgressCallback) ([]string, error) {
106+
if progressCallback != nil {
107+
// This is a general progress callback, not the fine-grained one.
108+
// We'd need to adapt Unzip similarly to provide detailed progress.
109+
// For now, just initial and final messages based on the old system.
110+
// Example of how it might be adapted later:
111+
// utils.UpdateProgress(utils.ProgressInfo{OriginalMessage: "Starting LZW decompression..."}, progressCallback)
112+
progressCallback(0.0, "Starting LZW decompression...")
113+
}
114+
var decompressedFiles []string
115+
fileIndex := 0
116+
117+
for {
118+
fileIndex++
119+
var fileNameLen uint32
120+
err := binary.Read(input, binary.LittleEndian, &fileNameLen)
121+
if err != nil {
122+
if err == io.EOF {
123+
break
124+
}
125+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to read file name length (file #%d): %w", fileIndex, err)
126+
}
127+
128+
if fileNameLen == 0 || fileNameLen > 1024*1024 {
129+
return decompressedFiles, fmt.Errorf("LZW Unzip: invalid file name length %d (file #%d), archive might be corrupted", fileNameLen, fileIndex)
130+
}
131+
132+
fileNameBytes := make([]byte, fileNameLen)
133+
if _, err := io.ReadFull(input, fileNameBytes); err != nil {
134+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to read file name (file #%d): %w", fileIndex, err)
135+
}
136+
fileName := string(fileNameBytes)
137+
cleanFileName := filepath.Clean(fileName)
138+
139+
if strings.HasPrefix(cleanFileName, ".."+string(filepath.Separator)) || cleanFileName == ".." || strings.HasPrefix(cleanFileName, string(filepath.Separator)) || strings.Contains(fileName, "\\") || strings.Contains(fileName, ":") {
140+
utils.ColorPrint(utils.RED, fmt.Sprintf("Warning: LZW Unzip potentially unsafe file path '%s' in archive. Skipping.\n", fileName))
141+
var compressedSizeToSkip uint64
142+
if errSize := binary.Read(input, binary.LittleEndian, &compressedSizeToSkip); errSize != nil {
143+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to read size for potentially unsafe file %s: %w", fileName, errSize)
144+
}
145+
if _, errSeek := io.CopyN(io.Discard, input, int64(compressedSizeToSkip)); errSeek != nil {
146+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to skip data for potentially unsafe file %s: %w", fileName, errSeek)
147+
}
148+
// TODO: Report progress for skipped file if possible, e.g.
149+
// if progressCallback != nil { utils.UpdateProgress(utils.ProgressInfo{ ... CurrentFileName: fileName, OriginalMessage: "Skipped unsafe file" ... }, progressCallback) }
150+
continue
151+
}
152+
filePath := filepath.Join(outputDir, cleanFileName)
153+
154+
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
155+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to create directory for %s: %w", filePath, err)
156+
}
157+
158+
var compressedSize uint64
159+
if err := binary.Read(input, binary.LittleEndian, &compressedSize); err != nil {
160+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to read compressed data length for %s: %w", fileName, err)
161+
}
162+
163+
outFile, createErr := os.Create(filePath)
164+
if createErr != nil {
165+
return decompressedFiles, fmt.Errorf("LZW Unzip: %s for %s: %w", constants.FILE_CREATE_ERROR, filePath, createErr)
166+
}
167+
168+
successfulDecompression := false
169+
defer func(file *os.File, path string) {
170+
file.Close()
171+
if !successfulDecompression {
172+
os.Remove(path)
173+
}
174+
}(outFile, filePath)
175+
176+
if compressedSize == 0 {
177+
successfulDecompression = true
178+
decompressedFiles = append(decompressedFiles, filePath)
179+
if progressCallback != nil {
180+
// This is an approximation of overall progress, not ideal for the new system
181+
progress := float64(fileIndex) / (float64(fileIndex) + 1.0)
182+
// Example: utils.UpdateProgress(utils.ProgressInfo{ ... CurrentFileName: fileName, OriginalMessage: "Decompressed empty file" ... }, progressCallback)
183+
progressCallback(progress, fmt.Sprintf("Decompressed %s (empty)", fileName))
184+
}
185+
continue
186+
}
187+
188+
limitedReader := io.LimitReader(input, int64(compressedSize))
189+
// TODO: Wrap limitedReader with utils.NewProgressReader for decompressData if we want byte-level progress for decompression
190+
errDecompress := decompressData(limitedReader, outFile)
191+
192+
if errDecompress != nil {
193+
return decompressedFiles, fmt.Errorf("LZW Unzip: failed to decompress data for %s: %w", fileName, errDecompress)
194+
}
195+
successfulDecompression = true
196+
197+
decompressedFiles = append(decompressedFiles, filePath)
198+
if progressCallback != nil {
199+
// Approximation
200+
progress := float64(fileIndex) / (float64(fileIndex) + 1.0)
201+
// Example: utils.UpdateProgress(utils.ProgressInfo{ ... CurrentFileName: fileName, OriginalMessage: "Decompressed file" ... }, progressCallback)
202+
progressCallback(progress, fmt.Sprintf("Decompressed %s", fileName))
203+
}
204+
}
205+
206+
if progressCallback != nil {
207+
// Example: utils.UpdateProgress(utils.ProgressInfo{OriginalMessage: "LZW decompression completed.", OverallProcessedBytes: totalArchiveSize, OverallTotalBytes: totalArchiveSize}, progressCallback)
208+
progressCallback(1.0, "LZW decompression completed.")
209+
}
210+
return decompressedFiles, nil
211+
}

0 commit comments

Comments
 (0)