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