Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * compression.c
4 : : *
5 : : * Shared code for compression methods and specifications.
6 : : *
7 : : * A compression specification specifies the parameters that should be used
8 : : * when performing compression with a specific algorithm. The simplest
9 : : * possible compression specification is an integer, which sets the
10 : : * compression level.
11 : : *
12 : : * Otherwise, a compression specification is a comma-separated list of items,
13 : : * each having the form keyword or keyword=value.
14 : : *
15 : : * Currently, the supported keywords are "level", "long", and "workers".
16 : : *
17 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
18 : : *
19 : : * IDENTIFICATION
20 : : * src/common/compression.c
21 : : *-------------------------------------------------------------------------
22 : : */
23 : :
24 : : #ifndef FRONTEND
25 : : #include "postgres.h"
26 : : #else
27 : : #include "postgres_fe.h"
28 : : #endif
29 : :
30 : : #ifdef USE_ZSTD
31 : : #include <zstd.h>
32 : : #endif
33 : : #ifdef HAVE_LIBZ
34 : : #include <zlib.h>
35 : : #endif
36 : :
37 : : #include "common/compression.h"
38 : :
39 : : static int expect_integer_value(char *keyword, char *value,
40 : : pg_compress_specification *result);
41 : : static bool expect_boolean_value(char *keyword, char *value,
42 : : pg_compress_specification *result);
43 : :
44 : : /*
45 : : * Look up a compression algorithm by archive file extension. Sets *algorithm
46 : : * and returns the length of the non-extension portion of the filename, or -1
47 : : * if the filename does not end with a recognized tar extension.
48 : : */
49 : : int
50 : 386 : parse_tar_compress_algorithm(const char *fname, pg_compress_algorithm *algorithm)
51 : : {
52 : 386 : int fname_len = strlen(fname);
53 : :
54 [ + + ]: 386 : if (fname_len >= 4 &&
55 [ + + ]: 385 : strcmp(fname + fname_len - 4, ".tar") == 0)
56 : : {
57 : 279 : *algorithm = PG_COMPRESSION_NONE;
58 : 279 : return fname_len - 4;
59 : : }
60 [ + + ]: 107 : else if (fname_len >= 4 &&
61 [ - + ]: 106 : strcmp(fname + fname_len - 4, ".tgz") == 0)
62 : : {
63 : 0 : *algorithm = PG_COMPRESSION_GZIP;
64 : 0 : return fname_len - 4;
65 : : }
66 [ + + ]: 107 : else if (fname_len >= 7 &&
67 [ + + ]: 106 : strcmp(fname + fname_len - 7, ".tar.gz") == 0)
68 : : {
69 : 22 : *algorithm = PG_COMPRESSION_GZIP;
70 : 22 : return fname_len - 7;
71 : : }
72 [ + + ]: 85 : else if (fname_len >= 8 &&
73 [ + + ]: 84 : strcmp(fname + fname_len - 8, ".tar.lz4") == 0)
74 : : {
75 : 11 : *algorithm = PG_COMPRESSION_LZ4;
76 : 11 : return fname_len - 8;
77 : : }
78 [ + + ]: 74 : else if (fname_len >= 8 &&
79 [ - + ]: 73 : strcmp(fname + fname_len - 8, ".tar.zst") == 0)
80 : : {
81 : 0 : *algorithm = PG_COMPRESSION_ZSTD;
82 : 0 : return fname_len - 8;
83 : : }
84 : :
85 : 74 : return -1;
86 : : }
87 : :
88 : : /*
89 : : * Look up a compression algorithm by name. Returns true and sets *algorithm
90 : : * if the name is recognized. Otherwise returns false.
91 : : */
92 : : bool
93 : 449 : parse_compress_algorithm(char *name, pg_compress_algorithm *algorithm)
94 : : {
95 [ + + ]: 449 : if (strcmp(name, "none") == 0)
96 : 349 : *algorithm = PG_COMPRESSION_NONE;
97 [ + + ]: 100 : else if (strcmp(name, "gzip") == 0)
98 : 87 : *algorithm = PG_COMPRESSION_GZIP;
99 [ + + ]: 13 : else if (strcmp(name, "lz4") == 0)
100 : 9 : *algorithm = PG_COMPRESSION_LZ4;
101 [ - + ]: 4 : else if (strcmp(name, "zstd") == 0)
102 : 0 : *algorithm = PG_COMPRESSION_ZSTD;
103 : : else
104 : 4 : return false;
105 : 445 : return true;
106 : : }
107 : :
108 : : /*
109 : : * Get the human-readable name corresponding to a particular compression
110 : : * algorithm.
111 : : */
112 : : const char *
113 : 14 : get_compress_algorithm_name(pg_compress_algorithm algorithm)
114 : : {
115 [ + + + - : 14 : switch (algorithm)
- ]
116 : : {
117 : 3 : case PG_COMPRESSION_NONE:
118 : 3 : return "none";
119 : 10 : case PG_COMPRESSION_GZIP:
120 : 10 : return "gzip";
121 : 1 : case PG_COMPRESSION_LZ4:
122 : 1 : return "lz4";
123 : 0 : case PG_COMPRESSION_ZSTD:
124 : 0 : return "zstd";
125 : : /* no default, to provoke compiler warnings if values are added */
126 : : }
127 : : Assert(false);
128 : 0 : return "???"; /* placate compiler */
129 : : }
130 : :
131 : : /*
132 : : * Parse a compression specification for a specified algorithm.
133 : : *
134 : : * See the file header comments for a brief description of what a compression
135 : : * specification is expected to look like.
136 : : *
137 : : * On return, all fields of the result object will be initialized.
138 : : * In particular, result->parse_error will be NULL if no errors occurred
139 : : * during parsing, and will otherwise contain an appropriate error message.
140 : : * The caller may free this error message string using pfree, if desired.
141 : : * Note, however, even if there's no parse error, the string might not make
142 : : * sense: e.g. for gzip, level=12 is not sensible, but it does parse OK.
143 : : *
144 : : * The compression level is assigned by default if not directly specified
145 : : * by the specification.
146 : : *
147 : : * Use validate_compress_specification() to find out whether a compression
148 : : * specification is semantically sensible.
149 : : */
150 : : void
151 : 443 : parse_compress_specification(pg_compress_algorithm algorithm, char *specification,
152 : : pg_compress_specification *result)
153 : : {
154 : : int bare_level;
155 : : char *bare_level_endp;
156 : :
157 : : /* Initial setup of result object. */
158 : 443 : result->algorithm = algorithm;
159 : 443 : result->options = 0;
160 : 443 : result->parse_error = NULL;
161 : :
162 : : /*
163 : : * Assign a default level depending on the compression method. This may
164 : : * be enforced later.
165 : : */
166 [ + + - + : 443 : switch (result->algorithm)
- ]
167 : : {
168 : 347 : case PG_COMPRESSION_NONE:
169 : 347 : result->level = 0;
170 : 347 : break;
171 : 9 : case PG_COMPRESSION_LZ4:
172 : : #ifdef USE_LZ4
173 : 9 : result->level = 0; /* fast compression mode */
174 : : #else
175 : : result->parse_error =
176 : : psprintf(_("this build does not support compression with %s"),
177 : : "LZ4");
178 : : #endif
179 : 9 : break;
180 : 0 : case PG_COMPRESSION_ZSTD:
181 : : #ifdef USE_ZSTD
182 : : result->level = ZSTD_CLEVEL_DEFAULT;
183 : : #else
184 : 0 : result->parse_error =
185 : 0 : psprintf(_("this build does not support compression with %s"),
186 : : "ZSTD");
187 : : #endif
188 : 0 : break;
189 : 87 : case PG_COMPRESSION_GZIP:
190 : : #ifdef HAVE_LIBZ
191 : 87 : result->level = Z_DEFAULT_COMPRESSION;
192 : : #else
193 : : result->parse_error =
194 : : psprintf(_("this build does not support compression with %s"),
195 : : "gzip");
196 : : #endif
197 : 87 : break;
198 : : }
199 : :
200 : : /* If there is no specification, we're done already. */
201 [ + + ]: 443 : if (specification == NULL)
202 : 424 : return;
203 : :
204 : : /* As a special case, the specification can be a bare integer. */
205 : 38 : bare_level = strtol(specification, &bare_level_endp, 10);
206 [ + + - + ]: 38 : if (specification != bare_level_endp && *bare_level_endp == '\0')
207 : : {
208 : 19 : result->level = bare_level;
209 : 19 : return;
210 : : }
211 : :
212 : : /* Look for comma-separated keyword or keyword=value entries. */
213 : : while (1)
214 : 2 : {
215 : : char *kwstart;
216 : : char *kwend;
217 : : char *vstart;
218 : : char *vend;
219 : : int kwlen;
220 : : int vlen;
221 : : bool has_value;
222 : : char *keyword;
223 : : char *value;
224 : :
225 : : /* Figure start, end, and length of next keyword and any value. */
226 : 21 : kwstart = kwend = specification;
227 [ + + + - : 109 : while (*kwend != '\0' && *kwend != ',' && *kwend != '=')
+ + ]
228 : 88 : ++kwend;
229 : 21 : kwlen = kwend - kwstart;
230 [ + + ]: 21 : if (*kwend != '=')
231 : : {
232 : 11 : vstart = vend = NULL;
233 : 11 : vlen = 0;
234 : 11 : has_value = false;
235 : : }
236 : : else
237 : : {
238 : 10 : vstart = vend = kwend + 1;
239 [ + + + + ]: 28 : while (*vend != '\0' && *vend != ',')
240 : 18 : ++vend;
241 : 10 : vlen = vend - vstart;
242 : 10 : has_value = true;
243 : : }
244 : :
245 : : /* Reject empty keyword. */
246 [ + + ]: 21 : if (kwlen == 0)
247 : : {
248 : 4 : result->parse_error =
249 : 4 : pstrdup(_("found empty string where a compression option was expected"));
250 : 4 : break;
251 : : }
252 : :
253 : : /* Extract keyword and value as separate C strings. */
254 : 17 : keyword = palloc(kwlen + 1);
255 : 17 : memcpy(keyword, kwstart, kwlen);
256 : 17 : keyword[kwlen] = '\0';
257 [ + + ]: 17 : if (!has_value)
258 : 7 : value = NULL;
259 : : else
260 : : {
261 : 10 : value = palloc(vlen + 1);
262 : 10 : memcpy(value, vstart, vlen);
263 : 10 : value[vlen] = '\0';
264 : : }
265 : :
266 : : /* Handle whatever keyword we found. */
267 [ + + ]: 17 : if (strcmp(keyword, "level") == 0)
268 : : {
269 : 10 : result->level = expect_integer_value(keyword, value, result);
270 : :
271 : : /*
272 : : * No need to set a flag in "options", there is a default level
273 : : * set at least thanks to the logic above.
274 : : */
275 : : }
276 [ + + ]: 7 : else if (strcmp(keyword, "workers") == 0)
277 : : {
278 : 2 : result->workers = expect_integer_value(keyword, value, result);
279 : 2 : result->options |= PG_COMPRESSION_OPTION_WORKERS;
280 : : }
281 [ + + ]: 5 : else if (strcmp(keyword, "long") == 0)
282 : : {
283 : 2 : result->long_distance = expect_boolean_value(keyword, value, result);
284 : 2 : result->options |= PG_COMPRESSION_OPTION_LONG_DISTANCE;
285 : : }
286 : : else
287 : 3 : result->parse_error =
288 : 3 : psprintf(_("unrecognized compression option: \"%s\""), keyword);
289 : :
290 : : /* Release memory, just to be tidy. */
291 : 17 : pfree(keyword);
292 [ + + ]: 17 : if (value != NULL)
293 : 10 : pfree(value);
294 : :
295 : : /*
296 : : * If we got an error or have reached the end of the string, stop.
297 : : *
298 : : * If there is no value, then the end of the keyword might have been
299 : : * the end of the string. If there is a value, then the end of the
300 : : * keyword cannot have been the end of the string, but the end of the
301 : : * value might have been.
302 : : */
303 [ + + + + ]: 17 : if (result->parse_error != NULL ||
304 [ - + + + ]: 8 : (vend == NULL ? *kwend == '\0' : *vend == '\0'))
305 : : break;
306 : :
307 : : /* Advance to next entry and loop around. */
308 [ - + ]: 2 : specification = vend == NULL ? kwend + 1 : vend + 1;
309 : : }
310 : : }
311 : :
312 : : /*
313 : : * Parse 'value' as an integer and return the result.
314 : : *
315 : : * If parsing fails, set result->parse_error to an appropriate message
316 : : * and return -1.
317 : : */
318 : : static int
319 : 12 : expect_integer_value(char *keyword, char *value, pg_compress_specification *result)
320 : : {
321 : : int ivalue;
322 : : char *ivalue_endp;
323 : :
324 [ + + ]: 12 : if (value == NULL)
325 : : {
326 : 2 : result->parse_error =
327 : 2 : psprintf(_("compression option \"%s\" requires a value"),
328 : : keyword);
329 : 2 : return -1;
330 : : }
331 : :
332 : 10 : ivalue = strtol(value, &ivalue_endp, 10);
333 [ + + - + ]: 10 : if (ivalue_endp == value || *ivalue_endp != '\0')
334 : : {
335 : 4 : result->parse_error =
336 : 4 : psprintf(_("value for compression option \"%s\" must be an integer"),
337 : : keyword);
338 : 4 : return -1;
339 : : }
340 : 6 : return ivalue;
341 : : }
342 : :
343 : : /*
344 : : * Parse 'value' as a boolean and return the result.
345 : : *
346 : : * If parsing fails, set result->parse_error to an appropriate message
347 : : * and return -1. The caller must check result->parse_error to determine if
348 : : * the call was successful.
349 : : *
350 : : * Valid values are: yes, no, on, off, 1, 0.
351 : : *
352 : : * Inspired by ParseVariableBool().
353 : : */
354 : : static bool
355 : 2 : expect_boolean_value(char *keyword, char *value, pg_compress_specification *result)
356 : : {
357 [ + - ]: 2 : if (value == NULL)
358 : 2 : return true;
359 : :
360 [ # # ]: 0 : if (pg_strcasecmp(value, "yes") == 0)
361 : 0 : return true;
362 [ # # ]: 0 : if (pg_strcasecmp(value, "on") == 0)
363 : 0 : return true;
364 [ # # ]: 0 : if (pg_strcasecmp(value, "1") == 0)
365 : 0 : return true;
366 : :
367 [ # # ]: 0 : if (pg_strcasecmp(value, "no") == 0)
368 : 0 : return false;
369 [ # # ]: 0 : if (pg_strcasecmp(value, "off") == 0)
370 : 0 : return false;
371 [ # # ]: 0 : if (pg_strcasecmp(value, "0") == 0)
372 : 0 : return false;
373 : :
374 : 0 : result->parse_error =
375 : 0 : psprintf(_("value for compression option \"%s\" must be a Boolean value"),
376 : : keyword);
377 : 0 : return false;
378 : : }
379 : :
380 : : /*
381 : : * Returns NULL if the compression specification string was syntactically
382 : : * valid and semantically sensible. Otherwise, returns an error message.
383 : : *
384 : : * Does not test whether this build of PostgreSQL supports the requested
385 : : * compression method.
386 : : */
387 : : char *
388 : 443 : validate_compress_specification(pg_compress_specification *spec)
389 : : {
390 : 443 : int min_level = 1;
391 : 443 : int max_level = 1;
392 : 443 : int default_level = 0;
393 : :
394 : : /* If it didn't even parse OK, it's definitely no good. */
395 [ + + ]: 443 : if (spec->parse_error != NULL)
396 : 13 : return spec->parse_error;
397 : :
398 : : /*
399 : : * Check that the algorithm expects a compression level and it is within
400 : : * the legal range for the algorithm.
401 : : */
402 [ + + - + : 430 : switch (spec->algorithm)
- ]
403 : : {
404 : 74 : case PG_COMPRESSION_GZIP:
405 : 74 : max_level = 9;
406 : : #ifdef HAVE_LIBZ
407 : 74 : default_level = Z_DEFAULT_COMPRESSION;
408 : : #endif
409 : 74 : break;
410 : 9 : case PG_COMPRESSION_LZ4:
411 : 9 : max_level = 12;
412 : 9 : default_level = 0; /* fast mode */
413 : 9 : break;
414 : 0 : case PG_COMPRESSION_ZSTD:
415 : : #ifdef USE_ZSTD
416 : : max_level = ZSTD_maxCLevel();
417 : : min_level = ZSTD_minCLevel();
418 : : default_level = ZSTD_CLEVEL_DEFAULT;
419 : : #endif
420 : 0 : break;
421 : 347 : case PG_COMPRESSION_NONE:
422 [ + + ]: 347 : if (spec->level != 0)
423 : 3 : return psprintf(_("compression algorithm \"%s\" does not accept a compression level"),
424 : : get_compress_algorithm_name(spec->algorithm));
425 : 344 : break;
426 : : }
427 : :
428 [ + + + + ]: 427 : if ((spec->level < min_level || spec->level > max_level) &&
429 [ + + ]: 412 : spec->level != default_level)
430 : 3 : return psprintf(_("compression algorithm \"%s\" expects a compression level between %d and %d (default at %d)"),
431 : : get_compress_algorithm_name(spec->algorithm),
432 : : min_level, max_level, default_level);
433 : :
434 : : /*
435 : : * Of the compression algorithms that we currently support, only zstd
436 : : * allows parallel workers.
437 : : */
438 [ + + ]: 424 : if ((spec->options & PG_COMPRESSION_OPTION_WORKERS) != 0 &&
439 [ + - ]: 2 : (spec->algorithm != PG_COMPRESSION_ZSTD))
440 : : {
441 : 2 : return psprintf(_("compression algorithm \"%s\" does not accept a worker count"),
442 : : get_compress_algorithm_name(spec->algorithm));
443 : : }
444 : :
445 : : /*
446 : : * Of the compression algorithms that we currently support, only zstd
447 : : * supports long-distance mode.
448 : : */
449 [ + + ]: 422 : if ((spec->options & PG_COMPRESSION_OPTION_LONG_DISTANCE) != 0 &&
450 [ + - ]: 2 : (spec->algorithm != PG_COMPRESSION_ZSTD))
451 : : {
452 : 2 : return psprintf(_("compression algorithm \"%s\" does not support long-distance mode"),
453 : : get_compress_algorithm_name(spec->algorithm));
454 : : }
455 : :
456 : 420 : return NULL;
457 : : }
458 : :
459 : : #ifdef FRONTEND
460 : :
461 : : /*
462 : : * Basic parsing of a value specified through a command-line option, commonly
463 : : * -Z/--compress.
464 : : *
465 : : * The parsing consists of a METHOD:DETAIL string fed later to
466 : : * parse_compress_specification(). This only extracts METHOD and DETAIL.
467 : : * If only an integer is found, the method is implied by the value specified.
468 : : */
469 : : void
470 : 48 : parse_compress_options(const char *option, char **algorithm, char **detail)
471 : : {
472 : : const char *sep;
473 : : char *endp;
474 : : long result;
475 : :
476 : : /*
477 : : * Check whether the compression specification consists of a bare integer.
478 : : *
479 : : * For backward-compatibility, assume "none" if the integer found is zero
480 : : * and "gzip" otherwise.
481 : : */
482 : 48 : result = strtol(option, &endp, 10);
483 [ + + ]: 48 : if (*endp == '\0')
484 : : {
485 [ - + ]: 6 : if (result == 0)
486 : : {
487 : 0 : *algorithm = pstrdup("none");
488 : 0 : *detail = NULL;
489 : : }
490 : : else
491 : : {
492 : 6 : *algorithm = pstrdup("gzip");
493 : 6 : *detail = pstrdup(option);
494 : : }
495 : 6 : return;
496 : : }
497 : :
498 : : /*
499 : : * Check whether there is a compression detail following the algorithm
500 : : * name.
501 : : */
502 : 42 : sep = strchr(option, ':');
503 [ + + ]: 42 : if (sep == NULL)
504 : : {
505 : 10 : *algorithm = pstrdup(option);
506 : 10 : *detail = NULL;
507 : : }
508 : : else
509 : : {
510 : : char *alg;
511 : :
512 : 32 : alg = palloc((sep - option) + 1);
513 : 32 : memcpy(alg, option, sep - option);
514 : 32 : alg[sep - option] = '\0';
515 : :
516 : 32 : *algorithm = alg;
517 : 32 : *detail = pstrdup(sep + 1);
518 : : }
519 : : }
520 : : #endif /* FRONTEND */
|