PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
ZipExtractor.cpp
Go to the documentation of this file.
1#include "ZipExtractor.h"
2#include <zip.h>
3#include <sys/stat.h>
4#include <fstream>
5#include <cstring>
6#include <iostream>
7
8#ifdef _WIN32
9#include <direct.h>
10#define PATH_SEPARATOR '\\'
11#define mkdir(path, mode) _mkdir(path)
12#else
13#include <sys/types.h>
14#define PATH_SEPARATOR '/'
15#endif
16
17// Extract entire ZIP to destination
19 const std::string& destPath) {
20 int error = 0;
21 zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &error);
22
23 if (!archive) {
24 zip_error_t ziperror;
25 zip_error_init_with_code(&ziperror, error);
26 std::string msg = "Failed to open ZIP: " + std::string(zip_error_strerror(&ziperror));
27 zip_error_fini(&ziperror);
28 return Result(false, msg);
29 }
30
31 // Get number of files in archive
32 zip_int64_t numEntries = zip_get_num_entries(archive, 0);
33 if (numEntries < 0) {
34 zip_close(archive);
35 return Result(false, "Failed to get entry count");
36 }
37
38 // Create destination directory if it doesn't exist
39 if (!CreateDirectories(destPath)) {
40 zip_close(archive);
41 return Result(false, "Failed to create destination directory: " + destPath);
42 }
43
44 // Extract each file
45 for (zip_int64_t i = 0; i < numEntries; i++) {
46 // Get file stats
47 struct zip_stat st;
48 zip_stat_init(&st);
49 if (zip_stat_index(archive, i, 0, &st) != 0) {
50 zip_close(archive);
51 return Result(false, "Failed to stat file at index " + std::to_string(i));
52 }
53
54 std::string filename = st.name;
55
56 // Skip directories (they end with /)
57 if (filename.back() == '/') {
58 // Create directory
59 std::string dirPath = destPath + PATH_SEPARATOR + filename;
60 CreateDirectories(dirPath);
61 continue;
62 }
63
64 // Build destination path
65 std::string destFile = destPath + PATH_SEPARATOR + filename;
66
67 // Create parent directories if needed
68 size_t lastSlash = destFile.find_last_of("/\\");
69 if (lastSlash != std::string::npos) {
70 std::string dirPath = destFile.substr(0, lastSlash);
71 if (!CreateDirectories(dirPath)) {
72 zip_close(archive);
73 return Result(false, "Failed to create directory: " + dirPath);
74 }
75 }
76
77 // Open file in ZIP
78 zip_file_t* file = zip_fopen_index(archive, i, 0);
79 if (!file) {
80 zip_close(archive);
81 return Result(false, "Failed to open file in ZIP: " + filename);
82 }
83
84 // Read file contents
85 std::vector<char> buffer(st.size);
86 zip_int64_t bytesRead = zip_fread(file, buffer.data(), st.size);
87 zip_fclose(file);
88
89 if (bytesRead != static_cast<zip_int64_t>(st.size)) {
90 zip_close(archive);
91 return Result(false, "Failed to read file: " + filename);
92 }
93
94 // Write to destination
95 std::ofstream out(destFile, std::ios::binary);
96 if (!out) {
97 zip_close(archive);
98 return Result(false, "Failed to create file: " + destFile);
99 }
100 out.write(buffer.data(), bytesRead);
101 out.close();
102 }
103
104 zip_close(archive);
105 return Result(true);
106}
107
108// Extract single file from ZIP
110 const std::string& fileInZip,
111 const std::string& destPath) {
112 int error = 0;
113 zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &error);
114
115 if (!archive) {
116 return Result(false, "Failed to open ZIP");
117 }
118
119 // Find file by name
120 zip_int64_t index = zip_name_locate(archive, fileInZip.c_str(), 0);
121 if (index < 0) {
122 zip_close(archive);
123 return Result(false, "File not found in ZIP: " + fileInZip);
124 }
125
126 // Get file stats
127 struct zip_stat st;
128 zip_stat_init(&st);
129 if (zip_stat_index(archive, index, 0, &st) != 0) {
130 zip_close(archive);
131 return Result(false, "Failed to stat file");
132 }
133
134 // Open and read file
135 zip_file_t* file = zip_fopen_index(archive, index, 0);
136 if (!file) {
137 zip_close(archive);
138 return Result(false, "Failed to open file");
139 }
140
141 std::vector<char> buffer(st.size);
142 zip_int64_t bytesRead = zip_fread(file, buffer.data(), st.size);
143 zip_fclose(file);
144 zip_close(archive);
145
146 if (bytesRead != static_cast<zip_int64_t>(st.size)) {
147 return Result(false, "Failed to read file");
148 }
149
150 // Create parent directories for destination
151 size_t lastSlash = destPath.find_last_of("/\\");
152 if (lastSlash != std::string::npos) {
153 std::string dirPath = destPath.substr(0, lastSlash);
154 if (!CreateDirectories(dirPath)) {
155 return Result(false, "Failed to create directory: " + dirPath);
156 }
157 }
158
159 // Write to destination
160 std::ofstream out(destPath, std::ios::binary);
161 if (!out) {
162 return Result(false, "Failed to create file: " + destPath);
163 }
164 out.write(buffer.data(), bytesRead);
165 out.close();
166
167 return Result(true);
168}
169
170// Read single file from ZIP without extracting
172 const std::string& fileInZip,
173 std::string& outContents) {
174 int error = 0;
175 zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &error);
176
177 if (!archive) {
178 return Result(false, "Failed to open ZIP");
179 }
180
181 // Find file by name
182 zip_int64_t index = zip_name_locate(archive, fileInZip.c_str(), 0);
183 if (index < 0) {
184 zip_close(archive);
185 return Result(false, "File not found in ZIP: " + fileInZip);
186 }
187
188 // Get file stats
189 struct zip_stat st;
190 zip_stat_init(&st);
191 if (zip_stat_index(archive, index, 0, &st) != 0) {
192 zip_close(archive);
193 return Result(false, "Failed to stat file");
194 }
195
196 // Open and read file
197 zip_file_t* file = zip_fopen_index(archive, index, 0);
198 if (!file) {
199 zip_close(archive);
200 return Result(false, "Failed to open file");
201 }
202
203 std::vector<char> buffer(st.size + 1); // +1 for null terminator
204 zip_int64_t bytesRead = zip_fread(file, buffer.data(), st.size);
205 zip_fclose(file);
206 zip_close(archive);
207
208 if (bytesRead != static_cast<zip_int64_t>(st.size)) {
209 return Result(false, "Failed to read file");
210 }
211
212 buffer[st.size] = '\0';
213 outContents = std::string(buffer.data());
214
215 return Result(true);
216}
217
218// List all files in ZIP
220 std::vector<std::string>& outFiles) {
221 int error = 0;
222 zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &error);
223
224 if (!archive) {
225 return Result(false, "Failed to open ZIP");
226 }
227
228 zip_int64_t numEntries = zip_get_num_entries(archive, 0);
229 if (numEntries < 0) {
230 zip_close(archive);
231 return Result(false, "Failed to get entry count");
232 }
233
234 outFiles.clear();
235 for (zip_int64_t i = 0; i < numEntries; i++) {
236 const char* name = zip_get_name(archive, i, 0);
237 if (name) {
238 outFiles.push_back(name);
239 }
240 }
241
242 zip_close(archive);
243 return Result(true);
244}
245
246// Check if ZIP contains specific file
247bool ZipExtractor::ContainsFile(const std::string& zipPath,
248 const std::string& fileInZip) {
249 int error = 0;
250 zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY, &error);
251
252 if (!archive) {
253 return false;
254 }
255
256 zip_int64_t index = zip_name_locate(archive, fileInZip.c_str(), 0);
257 zip_close(archive);
258
259 return index >= 0;
260}
261
262// Validate ZIP file
263ZipExtractor::Result ZipExtractor::Validate(const std::string& zipPath) {
264 int error = 0;
265 zip_t* archive = zip_open(zipPath.c_str(), ZIP_RDONLY | ZIP_CHECKCONS, &error);
266
267 if (!archive) {
268 zip_error_t ziperror;
269 zip_error_init_with_code(&ziperror, error);
270 std::string msg = std::string(zip_error_strerror(&ziperror));
271 zip_error_fini(&ziperror);
272 return Result(false, "Invalid or corrupted ZIP: " + msg);
273 }
274
275 zip_close(archive);
276 return Result(true);
277}
278
279// Validate PEBL snapshot
281 // First validate as ZIP
282 Result validZip = Validate(zipPath);
283 if (!validZip.success) {
284 return validZip;
285 }
286
287 // Check for required files
288 if (!ContainsFile(zipPath, "study-info.json")) {
289 return Result(false, "Not a PEBL snapshot: missing study-info.json");
290 }
291
292 // Check for tests directory (at least one file starting with "tests/")
293 std::vector<std::string> files;
294 Result listResult = ListContents(zipPath, files);
295 if (!listResult.success) {
296 return listResult;
297 }
298
299 bool hasTests = false;
300 for (const auto& file : files) {
301 if (file.find("tests/") == 0) {
302 hasTests = true;
303 break;
304 }
305 }
306
307 if (!hasTests) {
308 return Result(false, "Not a PEBL snapshot: missing tests/ directory");
309 }
310
311 return Result(true);
312}
313
314// Helper: Create directories recursively
315bool ZipExtractor::CreateDirectories(const std::string& path) {
316 if (path.empty()) {
317 return false;
318 }
319
320 struct stat st;
321 if (stat(path.c_str(), &st) == 0) {
322 // Path exists - check if it's a directory
323 return S_ISDIR(st.st_mode);
324 }
325
326 // Find parent directory
327 size_t lastSlash = path.find_last_of("/\\");
328 if (lastSlash != std::string::npos && lastSlash > 0) {
329 std::string parent = path.substr(0, lastSlash);
330 if (!CreateDirectories(parent)) {
331 return false;
332 }
333 }
334
335 // Create this directory
336#ifdef _WIN32
337 return _mkdir(path.c_str()) == 0;
338#else
339 return mkdir(path.c_str(), 0755) == 0;
340#endif
341}
#define PATH_SEPARATOR
static bool ContainsFile(const std::string &zipPath, const std::string &fileInZip)
static Result ExtractAll(const std::string &zipPath, const std::string &destPath)
static Result ListContents(const std::string &zipPath, std::vector< std::string > &outFiles)
static Result ExtractFile(const std::string &zipPath, const std::string &fileInZip, const std::string &destPath)
static Result Validate(const std::string &zipPath)
static Result ValidateSnapshot(const std::string &zipPath)
static Result ReadFile(const std::string &zipPath, const std::string &fileInZip, std::string &outContents)