1 | /* |
2 | * Copyright © 2011 Red Hat, Inc |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library; if not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Author: Alexander Larsson <alexl@redhat.com> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include <glib.h> |
23 | #include <gstdio.h> |
24 | #include <gi18n.h> |
25 | #include <gioenums.h> |
26 | |
27 | #include <string.h> |
28 | #include <stdio.h> |
29 | #include <locale.h> |
30 | #include <errno.h> |
31 | #ifdef G_OS_UNIX |
32 | #include <unistd.h> |
33 | #endif |
34 | #ifdef G_OS_WIN32 |
35 | #include <io.h> |
36 | #endif |
37 | |
38 | #include <gio/gmemoryoutputstream.h> |
39 | #include <gio/gzlibcompressor.h> |
40 | #include <gio/gconverteroutputstream.h> |
41 | |
42 | #include <glib.h> |
43 | #include "gvdb/gvdb-builder.h" |
44 | |
45 | #include "gconstructor_as_data.h" |
46 | #include "glib/glib-private.h" |
47 | |
48 | typedef struct |
49 | { |
50 | char *filename; |
51 | char *content; |
52 | gsize content_size; |
53 | gsize size; |
54 | guint32 flags; |
55 | } FileData; |
56 | |
57 | typedef struct |
58 | { |
59 | GHashTable *table; /* resource path -> FileData */ |
60 | |
61 | gboolean collect_data; |
62 | |
63 | /* per gresource */ |
64 | char *prefix; |
65 | |
66 | /* per file */ |
67 | char *alias; |
68 | gboolean compressed; |
69 | char *preproc_options; |
70 | |
71 | GString *string; /* non-NULL when accepting text */ |
72 | } ParseState; |
73 | |
74 | static gchar **sourcedirs = NULL; |
75 | static gchar *xmllint = NULL; |
76 | static gchar *jsonformat = NULL; |
77 | static gchar *gdk_pixbuf_pixdata = NULL; |
78 | |
79 | static void |
80 | file_data_free (FileData *data) |
81 | { |
82 | g_free (mem: data->filename); |
83 | g_free (mem: data->content); |
84 | g_free (mem: data); |
85 | } |
86 | |
87 | static void |
88 | start_element (GMarkupParseContext *context, |
89 | const gchar *element_name, |
90 | const gchar **attribute_names, |
91 | const gchar **attribute_values, |
92 | gpointer user_data, |
93 | GError **error) |
94 | { |
95 | ParseState *state = user_data; |
96 | const GSList *element_stack; |
97 | const gchar *container; |
98 | |
99 | element_stack = g_markup_parse_context_get_element_stack (context); |
100 | container = element_stack->next ? element_stack->next->data : NULL; |
101 | |
102 | #define COLLECT(first, ...) \ |
103 | g_markup_collect_attributes (element_name, \ |
104 | attribute_names, attribute_values, error, \ |
105 | first, __VA_ARGS__, G_MARKUP_COLLECT_INVALID) |
106 | #define OPTIONAL G_MARKUP_COLLECT_OPTIONAL |
107 | #define STRDUP G_MARKUP_COLLECT_STRDUP |
108 | #define STRING G_MARKUP_COLLECT_STRING |
109 | #define BOOL G_MARKUP_COLLECT_BOOLEAN |
110 | #define NO_ATTRS() COLLECT (G_MARKUP_COLLECT_INVALID, NULL) |
111 | |
112 | if (container == NULL) |
113 | { |
114 | if (strcmp (s1: element_name, s2: "gresources" ) == 0) |
115 | return; |
116 | } |
117 | else if (strcmp (s1: container, s2: "gresources" ) == 0) |
118 | { |
119 | if (strcmp (s1: element_name, s2: "gresource" ) == 0) |
120 | { |
121 | COLLECT (OPTIONAL | STRDUP, |
122 | "prefix" , &state->prefix); |
123 | return; |
124 | } |
125 | } |
126 | else if (strcmp (s1: container, s2: "gresource" ) == 0) |
127 | { |
128 | if (strcmp (s1: element_name, s2: "file" ) == 0) |
129 | { |
130 | COLLECT (OPTIONAL | STRDUP, "alias" , &state->alias, |
131 | OPTIONAL | BOOL, "compressed" , &state->compressed, |
132 | OPTIONAL | STRDUP, "preprocess" , &state->preproc_options); |
133 | state->string = g_string_new (init: "" ); |
134 | return; |
135 | } |
136 | } |
137 | |
138 | if (container) |
139 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_UNKNOWN_ELEMENT, |
140 | _("Element <%s> not allowed inside <%s>" ), |
141 | element_name, container); |
142 | else |
143 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_UNKNOWN_ELEMENT, |
144 | _("Element <%s> not allowed at toplevel" ), element_name); |
145 | |
146 | } |
147 | |
148 | static GvdbItem * |
149 | get_parent (GHashTable *table, |
150 | gchar *key, |
151 | gint length) |
152 | { |
153 | GvdbItem *grandparent, *parent; |
154 | |
155 | if (length == 1) |
156 | return NULL; |
157 | |
158 | while (key[--length - 1] != '/'); |
159 | key[length] = '\0'; |
160 | |
161 | parent = g_hash_table_lookup (hash_table: table, key); |
162 | |
163 | if (parent == NULL) |
164 | { |
165 | parent = gvdb_hash_table_insert (table, key); |
166 | |
167 | grandparent = get_parent (table, key, length); |
168 | |
169 | if (grandparent != NULL) |
170 | gvdb_item_set_parent (item: parent, parent: grandparent); |
171 | } |
172 | |
173 | return parent; |
174 | } |
175 | |
176 | static gchar * |
177 | find_file (const gchar *filename) |
178 | { |
179 | guint i; |
180 | gchar *real_file; |
181 | gboolean exists; |
182 | |
183 | if (g_path_is_absolute (file_name: filename)) |
184 | return g_strdup (str: filename); |
185 | |
186 | /* search all the sourcedirs for the correct files in order */ |
187 | for (i = 0; sourcedirs[i] != NULL; i++) |
188 | { |
189 | real_file = g_build_path (separator: "/" , first_element: sourcedirs[i], filename, NULL); |
190 | exists = g_file_test (filename: real_file, test: G_FILE_TEST_EXISTS); |
191 | if (exists) |
192 | return real_file; |
193 | g_free (mem: real_file); |
194 | } |
195 | return NULL; |
196 | } |
197 | |
198 | static void |
199 | end_element (GMarkupParseContext *context, |
200 | const gchar *element_name, |
201 | gpointer user_data, |
202 | GError **error) |
203 | { |
204 | ParseState *state = user_data; |
205 | GError *my_error = NULL; |
206 | |
207 | if (strcmp (s1: element_name, s2: "gresource" ) == 0) |
208 | { |
209 | g_free (mem: state->prefix); |
210 | state->prefix = NULL; |
211 | } |
212 | |
213 | else if (strcmp (s1: element_name, s2: "file" ) == 0) |
214 | { |
215 | gchar *file; |
216 | gchar *real_file = NULL; |
217 | gchar *key; |
218 | FileData *data = NULL; |
219 | char *tmp_file = NULL; |
220 | |
221 | file = state->string->str; |
222 | key = file; |
223 | if (state->alias) |
224 | key = state->alias; |
225 | |
226 | if (state->prefix) |
227 | key = g_build_path (separator: "/" , first_element: "/" , state->prefix, key, NULL); |
228 | else |
229 | key = g_build_path (separator: "/" , first_element: "/" , key, NULL); |
230 | |
231 | if (g_hash_table_lookup (hash_table: state->table, key) != NULL) |
232 | { |
233 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_INVALID_CONTENT, |
234 | _("File %s appears multiple times in the resource" ), |
235 | key); |
236 | return; |
237 | } |
238 | |
239 | if (sourcedirs != NULL) |
240 | { |
241 | real_file = find_file (filename: file); |
242 | if (real_file == NULL && state->collect_data) |
243 | { |
244 | g_set_error (err: error, G_IO_ERROR, code: G_IO_ERROR_FAILED, |
245 | _("Failed to locate “%s” in any source directory" ), file); |
246 | return; |
247 | } |
248 | } |
249 | else |
250 | { |
251 | gboolean exists; |
252 | exists = g_file_test (filename: file, test: G_FILE_TEST_EXISTS); |
253 | if (!exists && state->collect_data) |
254 | { |
255 | g_set_error (err: error, G_IO_ERROR, code: G_IO_ERROR_FAILED, |
256 | _("Failed to locate “%s” in current directory" ), file); |
257 | return; |
258 | } |
259 | } |
260 | |
261 | if (real_file == NULL) |
262 | real_file = g_strdup (str: file); |
263 | |
264 | data = g_new0 (FileData, 1); |
265 | data->filename = g_strdup (str: real_file); |
266 | if (!state->collect_data) |
267 | goto done; |
268 | |
269 | if (state->preproc_options) |
270 | { |
271 | gchar **options; |
272 | guint i; |
273 | gboolean xml_stripblanks = FALSE; |
274 | gboolean json_stripblanks = FALSE; |
275 | gboolean to_pixdata = FALSE; |
276 | |
277 | options = g_strsplit (string: state->preproc_options, delimiter: "," , max_tokens: -1); |
278 | |
279 | for (i = 0; options[i]; i++) |
280 | { |
281 | if (!strcmp (s1: options[i], s2: "xml-stripblanks" )) |
282 | xml_stripblanks = TRUE; |
283 | else if (!strcmp (s1: options[i], s2: "to-pixdata" )) |
284 | to_pixdata = TRUE; |
285 | else if (!strcmp (s1: options[i], s2: "json-stripblanks" )) |
286 | json_stripblanks = TRUE; |
287 | else |
288 | { |
289 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_INVALID_CONTENT, |
290 | _("Unknown processing option “%s”" ), options[i]); |
291 | g_strfreev (str_array: options); |
292 | goto cleanup; |
293 | } |
294 | } |
295 | g_strfreev (str_array: options); |
296 | |
297 | if (xml_stripblanks) |
298 | { |
299 | /* This is not fatal: pretty-printed XML is still valid XML */ |
300 | if (xmllint == NULL) |
301 | { |
302 | static gboolean xmllint_warned = FALSE; |
303 | |
304 | if (!xmllint_warned) |
305 | { |
306 | /* Translators: the first %s is a gresource XML attribute, |
307 | * the second %s is an environment variable, and the third |
308 | * %s is a command line tool |
309 | */ |
310 | char *warn = g_strdup_printf (_("%s preprocessing requested, but %s is not set, and %s is not in PATH" ), |
311 | "xml-stripblanks" , |
312 | "XMLLINT" , |
313 | "xmllint" ); |
314 | g_printerr (format: "%s\n" , warn); |
315 | g_free (mem: warn); |
316 | |
317 | /* Only warn once */ |
318 | xmllint_warned = TRUE; |
319 | } |
320 | } |
321 | else |
322 | { |
323 | GSubprocess *proc; |
324 | int fd; |
325 | |
326 | fd = g_file_open_tmp (tmpl: "resource-XXXXXXXX" , name_used: &tmp_file, error); |
327 | if (fd < 0) |
328 | goto cleanup; |
329 | |
330 | close (fd: fd); |
331 | |
332 | proc = g_subprocess_new (flags: G_SUBPROCESS_FLAGS_STDOUT_SILENCE, error, |
333 | argv0: xmllint, "--nonet" , "--noblanks" , "--output" , tmp_file, real_file, NULL); |
334 | g_free (mem: real_file); |
335 | real_file = NULL; |
336 | |
337 | if (!proc) |
338 | goto cleanup; |
339 | |
340 | if (!g_subprocess_wait_check (subprocess: proc, NULL, error)) |
341 | { |
342 | g_object_unref (object: proc); |
343 | goto cleanup; |
344 | } |
345 | |
346 | g_object_unref (object: proc); |
347 | |
348 | real_file = g_strdup (str: tmp_file); |
349 | } |
350 | } |
351 | |
352 | if (json_stripblanks) |
353 | { |
354 | /* As above, this is not fatal: pretty-printed JSON is still |
355 | * valid JSON |
356 | */ |
357 | if (jsonformat == NULL) |
358 | { |
359 | static gboolean jsonformat_warned = FALSE; |
360 | |
361 | if (!jsonformat_warned) |
362 | { |
363 | /* Translators: the first %s is a gresource XML attribute, |
364 | * the second %s is an environment variable, and the third |
365 | * %s is a command line tool |
366 | */ |
367 | char *warn = g_strdup_printf (_("%s preprocessing requested, but %s is not set, and %s is not in PATH" ), |
368 | "json-stripblanks" , |
369 | "JSON_GLIB_FORMAT" , |
370 | "json-glib-format" ); |
371 | g_printerr (format: "%s\n" , warn); |
372 | g_free (mem: warn); |
373 | |
374 | /* Only warn once */ |
375 | jsonformat_warned = TRUE; |
376 | } |
377 | } |
378 | else |
379 | { |
380 | GSubprocess *proc; |
381 | int fd; |
382 | |
383 | fd = g_file_open_tmp (tmpl: "resource-XXXXXXXX" , name_used: &tmp_file, error); |
384 | if (fd < 0) |
385 | goto cleanup; |
386 | |
387 | close (fd: fd); |
388 | |
389 | proc = g_subprocess_new (flags: G_SUBPROCESS_FLAGS_STDOUT_SILENCE, error, |
390 | argv0: jsonformat, "--output" , tmp_file, real_file, NULL); |
391 | g_free (mem: real_file); |
392 | real_file = NULL; |
393 | |
394 | if (!proc) |
395 | goto cleanup; |
396 | |
397 | if (!g_subprocess_wait_check (subprocess: proc, NULL, error)) |
398 | { |
399 | g_object_unref (object: proc); |
400 | goto cleanup; |
401 | } |
402 | |
403 | g_object_unref (object: proc); |
404 | |
405 | real_file = g_strdup (str: tmp_file); |
406 | } |
407 | } |
408 | |
409 | if (to_pixdata) |
410 | { |
411 | GSubprocess *proc; |
412 | int fd; |
413 | |
414 | /* This is a fatal error: if to-pixdata is used it means that |
415 | * the code loading the GResource expects a specific data format |
416 | */ |
417 | if (gdk_pixbuf_pixdata == NULL) |
418 | { |
419 | /* Translators: the first %s is a gresource XML attribute, |
420 | * the second %s is an environment variable, and the third |
421 | * %s is a command line tool |
422 | */ |
423 | g_set_error (err: error, G_IO_ERROR, code: G_IO_ERROR_FAILED, |
424 | _("%s preprocessing requested, but %s is not set, and %s is not in PATH" ), |
425 | "to-pixdata" , |
426 | "GDK_PIXBUF_PIXDATA" , |
427 | "gdk-pixbuf-pixdata" ); |
428 | goto cleanup; |
429 | } |
430 | |
431 | fd = g_file_open_tmp (tmpl: "resource-XXXXXXXX" , name_used: &tmp_file, error); |
432 | if (fd < 0) |
433 | goto cleanup; |
434 | |
435 | close (fd: fd); |
436 | |
437 | proc = g_subprocess_new (flags: G_SUBPROCESS_FLAGS_STDOUT_SILENCE, error, |
438 | argv0: gdk_pixbuf_pixdata, real_file, tmp_file, NULL); |
439 | g_free (mem: real_file); |
440 | real_file = NULL; |
441 | |
442 | if (!g_subprocess_wait_check (subprocess: proc, NULL, error)) |
443 | { |
444 | g_object_unref (object: proc); |
445 | goto cleanup; |
446 | } |
447 | |
448 | g_object_unref (object: proc); |
449 | |
450 | real_file = g_strdup (str: tmp_file); |
451 | } |
452 | } |
453 | |
454 | if (!g_file_get_contents (filename: real_file, contents: &data->content, length: &data->size, error: &my_error)) |
455 | { |
456 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_INVALID_CONTENT, |
457 | _("Error reading file %s: %s" ), |
458 | real_file, my_error->message); |
459 | g_clear_error (err: &my_error); |
460 | goto cleanup; |
461 | } |
462 | /* Include zero termination in content_size for uncompressed files (but not in size) */ |
463 | data->content_size = data->size + 1; |
464 | |
465 | if (state->compressed) |
466 | { |
467 | GOutputStream *out = g_memory_output_stream_new (NULL, size: 0, realloc_function: g_realloc, destroy_function: g_free); |
468 | GZlibCompressor *compressor = |
469 | g_zlib_compressor_new (format: G_ZLIB_COMPRESSOR_FORMAT_ZLIB, level: 9); |
470 | GOutputStream *out2 = g_converter_output_stream_new (base_stream: out, G_CONVERTER (compressor)); |
471 | |
472 | if (!g_output_stream_write_all (stream: out2, buffer: data->content, count: data->size, |
473 | NULL, NULL, NULL) || |
474 | !g_output_stream_close (stream: out2, NULL, NULL)) |
475 | { |
476 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_INVALID_CONTENT, |
477 | _("Error compressing file %s" ), |
478 | real_file); |
479 | g_object_unref (object: compressor); |
480 | g_object_unref (object: out); |
481 | g_object_unref (object: out2); |
482 | goto cleanup; |
483 | } |
484 | |
485 | g_free (mem: data->content); |
486 | data->content_size = g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (out)); |
487 | data->content = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (out)); |
488 | |
489 | g_object_unref (object: compressor); |
490 | g_object_unref (object: out); |
491 | g_object_unref (object: out2); |
492 | |
493 | data->flags |= G_RESOURCE_FLAGS_COMPRESSED; |
494 | } |
495 | |
496 | done: |
497 | g_hash_table_insert (hash_table: state->table, key, value: data); |
498 | data = NULL; |
499 | |
500 | cleanup: |
501 | /* Cleanup */ |
502 | |
503 | g_free (mem: state->alias); |
504 | state->alias = NULL; |
505 | g_string_free (string: state->string, TRUE); |
506 | state->string = NULL; |
507 | g_free (mem: state->preproc_options); |
508 | state->preproc_options = NULL; |
509 | |
510 | g_free (mem: real_file); |
511 | |
512 | if (tmp_file) |
513 | { |
514 | unlink (name: tmp_file); |
515 | g_free (mem: tmp_file); |
516 | } |
517 | |
518 | if (data != NULL) |
519 | file_data_free (data); |
520 | } |
521 | } |
522 | |
523 | static void |
524 | text (GMarkupParseContext *context, |
525 | const gchar *text, |
526 | gsize text_len, |
527 | gpointer user_data, |
528 | GError **error) |
529 | { |
530 | ParseState *state = user_data; |
531 | gsize i; |
532 | |
533 | for (i = 0; i < text_len; i++) |
534 | if (!g_ascii_isspace (text[i])) |
535 | { |
536 | if (state->string) |
537 | g_string_append_len (string: state->string, val: text, len: text_len); |
538 | |
539 | else |
540 | g_set_error (err: error, G_MARKUP_ERROR, code: G_MARKUP_ERROR_INVALID_CONTENT, |
541 | _("text may not appear inside <%s>" ), |
542 | g_markup_parse_context_get_element (context)); |
543 | |
544 | break; |
545 | } |
546 | } |
547 | |
548 | static GHashTable * |
549 | parse_resource_file (const gchar *filename, |
550 | gboolean collect_data, |
551 | GHashTable *files) |
552 | { |
553 | GMarkupParser parser = { start_element, end_element, text, NULL, NULL }; |
554 | ParseState state = { 0, }; |
555 | GMarkupParseContext *context; |
556 | GError *error = NULL; |
557 | gchar *contents; |
558 | GHashTable *table = NULL; |
559 | gsize size; |
560 | |
561 | if (!g_file_get_contents (filename, contents: &contents, length: &size, error: &error)) |
562 | { |
563 | g_printerr (format: "%s\n" , error->message); |
564 | g_clear_error (err: &error); |
565 | return NULL; |
566 | } |
567 | |
568 | state.collect_data = collect_data; |
569 | state.table = g_hash_table_ref (hash_table: files); |
570 | |
571 | context = g_markup_parse_context_new (parser: &parser, |
572 | flags: G_MARKUP_TREAT_CDATA_AS_TEXT | |
573 | G_MARKUP_PREFIX_ERROR_POSITION, |
574 | user_data: &state, NULL); |
575 | |
576 | if (!g_markup_parse_context_parse (context, text: contents, text_len: size, error: &error) || |
577 | !g_markup_parse_context_end_parse (context, error: &error)) |
578 | { |
579 | g_printerr (format: "%s: %s.\n" , filename, error->message); |
580 | g_clear_error (err: &error); |
581 | } |
582 | else |
583 | { |
584 | GHashTableIter iter; |
585 | const char *key; |
586 | char *mykey; |
587 | gsize key_len; |
588 | FileData *data; |
589 | GVariant *v_data; |
590 | GVariantBuilder builder; |
591 | GvdbItem *item; |
592 | |
593 | table = gvdb_hash_table_new (NULL, NULL); |
594 | |
595 | g_hash_table_iter_init (iter: &iter, hash_table: state.table); |
596 | while (g_hash_table_iter_next (iter: &iter, key: (gpointer *)&key, value: (gpointer *)&data)) |
597 | { |
598 | key_len = strlen (s: key); |
599 | mykey = g_strdup (str: key); |
600 | |
601 | item = gvdb_hash_table_insert (table, key); |
602 | gvdb_item_set_parent (item, |
603 | parent: get_parent (table, key: mykey, length: key_len)); |
604 | |
605 | g_free (mem: mykey); |
606 | |
607 | g_variant_builder_init (builder: &builder, G_VARIANT_TYPE ("(uuay)" )); |
608 | |
609 | g_variant_builder_add (builder: &builder, format_string: "u" , data->size); /* Size */ |
610 | g_variant_builder_add (builder: &builder, format_string: "u" , data->flags); /* Flags */ |
611 | |
612 | v_data = g_variant_new_from_data (G_VARIANT_TYPE("ay" ), |
613 | data: data->content, size: data->content_size, TRUE, |
614 | notify: g_free, user_data: data->content); |
615 | g_variant_builder_add_value (builder: &builder, value: v_data); |
616 | data->content = NULL; /* Take ownership */ |
617 | |
618 | gvdb_item_set_value (item, |
619 | value: g_variant_builder_end (builder: &builder)); |
620 | } |
621 | } |
622 | |
623 | g_hash_table_unref (hash_table: state.table); |
624 | g_markup_parse_context_free (context); |
625 | g_free (mem: contents); |
626 | |
627 | return table; |
628 | } |
629 | |
630 | static gboolean |
631 | write_to_file (GHashTable *table, |
632 | const gchar *filename, |
633 | GError **error) |
634 | { |
635 | gboolean success; |
636 | |
637 | success = gvdb_table_write_contents (table, filename, |
638 | G_BYTE_ORDER != G_LITTLE_ENDIAN, |
639 | error); |
640 | |
641 | return success; |
642 | } |
643 | |
644 | static gboolean |
645 | extension_in_set (const char *str, |
646 | ...) |
647 | { |
648 | va_list list; |
649 | const char *ext, *value; |
650 | gboolean rv = FALSE; |
651 | |
652 | ext = strrchr (s: str, c: '.'); |
653 | if (ext == NULL) |
654 | return FALSE; |
655 | |
656 | ext++; |
657 | va_start (list, str); |
658 | while ((value = va_arg (list, const char *)) != NULL) |
659 | { |
660 | if (g_ascii_strcasecmp (s1: ext, s2: value) != 0) |
661 | continue; |
662 | |
663 | rv = TRUE; |
664 | break; |
665 | } |
666 | |
667 | va_end (list); |
668 | return rv; |
669 | } |
670 | |
671 | /* |
672 | * We must escape any characters that `make` finds significant. |
673 | * This is largely a duplicate of the logic in gcc's `mkdeps.c:munge()`. |
674 | */ |
675 | static char * |
676 | escape_makefile_string (const char *string) |
677 | { |
678 | GString *str; |
679 | const char *p, *q; |
680 | |
681 | str = g_string_sized_new (dfl_size: strlen (s: string) + 1); |
682 | for (p = string; *p != '\0'; ++p) |
683 | { |
684 | switch (*p) |
685 | { |
686 | case ' ': |
687 | case '\t': |
688 | /* GNU make uses a weird quoting scheme for white space. |
689 | A space or tab preceded by 2N+1 backslashes represents |
690 | N backslashes followed by space; a space or tab |
691 | preceded by 2N backslashes represents N backslashes at |
692 | the end of a file name; and backslashes in other |
693 | contexts should not be doubled. */ |
694 | for (q = p - 1; string <= q && *q == '\\'; q--) |
695 | g_string_append_c (str, '\\'); |
696 | g_string_append_c (str, '\\'); |
697 | break; |
698 | |
699 | case '$': |
700 | g_string_append_c (str, '$'); |
701 | break; |
702 | |
703 | case '#': |
704 | g_string_append_c (str, '\\'); |
705 | break; |
706 | } |
707 | g_string_append_c (str, *p); |
708 | } |
709 | |
710 | return g_string_free (string: str, FALSE); |
711 | } |
712 | |
713 | int |
714 | main (int argc, char **argv) |
715 | { |
716 | GError *error; |
717 | GHashTable *table; |
718 | GHashTable *files; |
719 | gchar *srcfile; |
720 | gboolean show_version_and_exit = FALSE; |
721 | gchar *target = NULL; |
722 | gchar *binary_target = NULL; |
723 | gboolean generate_automatic = FALSE; |
724 | gboolean generate_source = FALSE; |
725 | gboolean = FALSE; |
726 | gboolean manual_register = FALSE; |
727 | gboolean internal = FALSE; |
728 | gboolean external_data = FALSE; |
729 | gboolean generate_dependencies = FALSE; |
730 | gboolean generate_phony_targets = FALSE; |
731 | char *dependency_file = NULL; |
732 | char *c_name = NULL; |
733 | char *c_name_no_underscores; |
734 | const char *linkage = "extern" ; |
735 | GOptionContext *context; |
736 | GOptionEntry entries[] = { |
737 | { "version" , 0, 0, G_OPTION_ARG_NONE, &show_version_and_exit, N_("Show program version and exit" ), NULL }, |
738 | { "target" , 0, 0, G_OPTION_ARG_FILENAME, &target, N_("Name of the output file" ), N_("FILE" ) }, |
739 | { "sourcedir" , 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &sourcedirs, N_("The directories to load files referenced in FILE from (default: current directory)" ), N_("DIRECTORY" ) }, |
740 | { "generate" , 0, 0, G_OPTION_ARG_NONE, &generate_automatic, N_("Generate output in the format selected for by the target filename extension" ), NULL }, |
741 | { "generate-header" , 0, 0, G_OPTION_ARG_NONE, &generate_header, N_("Generate source header" ), NULL }, |
742 | { "generate-source" , 0, 0, G_OPTION_ARG_NONE, &generate_source, N_("Generate source code used to link in the resource file into your code" ), NULL }, |
743 | { "generate-dependencies" , 0, 0, G_OPTION_ARG_NONE, &generate_dependencies, N_("Generate dependency list" ), NULL }, |
744 | { "dependency-file" , 0, 0, G_OPTION_ARG_FILENAME, &dependency_file, N_("Name of the dependency file to generate" ), N_("FILE" ) }, |
745 | { "generate-phony-targets" , 0, 0, G_OPTION_ARG_NONE, &generate_phony_targets, N_("Include phony targets in the generated dependency file" ), NULL }, |
746 | { "manual-register" , 0, 0, G_OPTION_ARG_NONE, &manual_register, N_("Don’t automatically create and register resource" ), NULL }, |
747 | { "internal" , 0, 0, G_OPTION_ARG_NONE, &internal, N_("Don’t export functions; declare them G_GNUC_INTERNAL" ), NULL }, |
748 | { "external-data" , 0, 0, G_OPTION_ARG_NONE, &external_data, N_("Don’t embed resource data in the C file; assume it's linked externally instead" ), NULL }, |
749 | { "c-name" , 0, 0, G_OPTION_ARG_STRING, &c_name, N_("C identifier name used for the generated source code" ), NULL }, |
750 | { NULL } |
751 | }; |
752 | |
753 | #ifdef G_OS_WIN32 |
754 | gchar *tmp; |
755 | #endif |
756 | |
757 | setlocale (LC_ALL, GLIB_DEFAULT_LOCALE); |
758 | textdomain (GETTEXT_PACKAGE); |
759 | |
760 | #ifdef G_OS_WIN32 |
761 | tmp = _glib_get_locale_dir (); |
762 | bindtextdomain (GETTEXT_PACKAGE, tmp); |
763 | g_free (tmp); |
764 | #else |
765 | bindtextdomain (GETTEXT_PACKAGE, GLIB_LOCALE_DIR); |
766 | #endif |
767 | |
768 | #ifdef HAVE_BIND_TEXTDOMAIN_CODESET |
769 | bind_textdomain_codeset (GETTEXT_PACKAGE, codeset: "UTF-8" ); |
770 | #endif |
771 | |
772 | context = g_option_context_new (N_("FILE" )); |
773 | g_option_context_set_translation_domain (context, GETTEXT_PACKAGE); |
774 | g_option_context_set_summary (context, |
775 | N_("Compile a resource specification into a resource file.\n" |
776 | "Resource specification files have the extension .gresource.xml,\n" |
777 | "and the resource file have the extension called .gresource." )); |
778 | g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE); |
779 | |
780 | error = NULL; |
781 | if (!g_option_context_parse (context, argc: &argc, argv: &argv, error: &error)) |
782 | { |
783 | g_printerr (format: "%s\n" , error->message); |
784 | return 1; |
785 | } |
786 | |
787 | g_option_context_free (context); |
788 | |
789 | if (show_version_and_exit) |
790 | { |
791 | g_print (PACKAGE_VERSION "\n" ); |
792 | return 0; |
793 | } |
794 | |
795 | if (argc != 2) |
796 | { |
797 | g_printerr (_("You should give exactly one file name\n" )); |
798 | g_free (mem: c_name); |
799 | return 1; |
800 | } |
801 | |
802 | if (internal) |
803 | linkage = "G_GNUC_INTERNAL" ; |
804 | |
805 | srcfile = argv[1]; |
806 | |
807 | xmllint = g_strdup (str: g_getenv (variable: "XMLLINT" )); |
808 | if (xmllint == NULL) |
809 | xmllint = g_find_program_in_path (program: "xmllint" ); |
810 | |
811 | jsonformat = g_strdup (str: g_getenv (variable: "JSON_GLIB_FORMAT" )); |
812 | if (jsonformat == NULL) |
813 | jsonformat = g_find_program_in_path (program: "json-glib-format" ); |
814 | |
815 | gdk_pixbuf_pixdata = g_strdup (str: g_getenv (variable: "GDK_PIXBUF_PIXDATA" )); |
816 | if (gdk_pixbuf_pixdata == NULL) |
817 | gdk_pixbuf_pixdata = g_find_program_in_path (program: "gdk-pixbuf-pixdata" ); |
818 | |
819 | if (target == NULL) |
820 | { |
821 | char *dirname = g_path_get_dirname (file_name: srcfile); |
822 | char *base = g_path_get_basename (file_name: srcfile); |
823 | char *target_basename; |
824 | if (g_str_has_suffix (str: base, suffix: ".xml" )) |
825 | base[strlen(s: base) - strlen (s: ".xml" )] = 0; |
826 | |
827 | if (generate_source) |
828 | { |
829 | if (g_str_has_suffix (str: base, suffix: ".gresource" )) |
830 | base[strlen(s: base) - strlen (s: ".gresource" )] = 0; |
831 | target_basename = g_strconcat (string1: base, ".c" , NULL); |
832 | } |
833 | else if (generate_header) |
834 | { |
835 | if (g_str_has_suffix (str: base, suffix: ".gresource" )) |
836 | base[strlen(s: base) - strlen (s: ".gresource" )] = 0; |
837 | target_basename = g_strconcat (string1: base, ".h" , NULL); |
838 | } |
839 | else |
840 | { |
841 | if (g_str_has_suffix (str: base, suffix: ".gresource" )) |
842 | target_basename = g_strdup (str: base); |
843 | else |
844 | target_basename = g_strconcat (string1: base, ".gresource" , NULL); |
845 | } |
846 | |
847 | target = g_build_filename (first_element: dirname, target_basename, NULL); |
848 | g_free (mem: target_basename); |
849 | g_free (mem: dirname); |
850 | g_free (mem: base); |
851 | } |
852 | else if (generate_automatic) |
853 | { |
854 | if (extension_in_set (str: target, "c" , "cc" , "cpp" , "cxx" , "c++" , NULL)) |
855 | generate_source = TRUE; |
856 | else if (extension_in_set (str: target, "h" , "hh" , "hpp" , "hxx" , "h++" , NULL)) |
857 | generate_header = TRUE; |
858 | else if (extension_in_set (str: target, "gresource" , NULL)) |
859 | { } |
860 | } |
861 | |
862 | files = g_hash_table_new_full (hash_func: g_str_hash, key_equal_func: g_str_equal, key_destroy_func: g_free, value_destroy_func: (GDestroyNotify)file_data_free); |
863 | |
864 | if ((table = parse_resource_file (filename: srcfile, collect_data: !generate_dependencies, files)) == NULL) |
865 | { |
866 | g_free (mem: target); |
867 | g_free (mem: c_name); |
868 | g_hash_table_unref (hash_table: files); |
869 | return 1; |
870 | } |
871 | |
872 | /* This can be used in the same invocation |
873 | as other generate commands */ |
874 | if (dependency_file != NULL) |
875 | { |
876 | /* Generate a .d file that describes the dependencies for |
877 | * build tools, gcc -M -MF style */ |
878 | GString *dep_string; |
879 | GHashTableIter iter; |
880 | gpointer key, data; |
881 | FileData *file_data; |
882 | char *escaped; |
883 | |
884 | g_hash_table_iter_init (iter: &iter, hash_table: files); |
885 | |
886 | dep_string = g_string_new (NULL); |
887 | escaped = escape_makefile_string (string: srcfile); |
888 | g_string_printf (string: dep_string, format: "%s:" , escaped); |
889 | g_free (mem: escaped); |
890 | |
891 | /* First rule: foo.xml: resource1 resource2.. */ |
892 | while (g_hash_table_iter_next (iter: &iter, key: &key, value: &data)) |
893 | { |
894 | file_data = data; |
895 | if (!g_str_equal (v1: file_data->filename, v2: srcfile)) |
896 | { |
897 | escaped = escape_makefile_string (string: file_data->filename); |
898 | g_string_append_printf (string: dep_string, format: " %s" , escaped); |
899 | g_free (mem: escaped); |
900 | } |
901 | } |
902 | |
903 | g_string_append (string: dep_string, val: "\n" ); |
904 | |
905 | /* Optionally include phony targets as it silences `make` but |
906 | * isn't supported on `ninja` at the moment. See also: `gcc -MP` |
907 | */ |
908 | if (generate_phony_targets) |
909 | { |
910 | g_string_append (string: dep_string, val: "\n" ); |
911 | |
912 | /* One rule for every resource: resourceN: */ |
913 | g_hash_table_iter_init (iter: &iter, hash_table: files); |
914 | while (g_hash_table_iter_next (iter: &iter, key: &key, value: &data)) |
915 | { |
916 | file_data = data; |
917 | if (!g_str_equal (v1: file_data->filename, v2: srcfile)) |
918 | { |
919 | escaped = escape_makefile_string (string: file_data->filename); |
920 | g_string_append_printf (string: dep_string, format: "%s:\n\n" , escaped); |
921 | g_free (mem: escaped); |
922 | } |
923 | } |
924 | } |
925 | |
926 | if (g_str_equal (v1: dependency_file, v2: "-" )) |
927 | { |
928 | g_print (format: "%s\n" , dep_string->str); |
929 | } |
930 | else |
931 | { |
932 | if (!g_file_set_contents (filename: dependency_file, contents: dep_string->str, length: dep_string->len, error: &error)) |
933 | { |
934 | g_printerr (format: "Error writing dependency file: %s\n" , error->message); |
935 | g_string_free (string: dep_string, TRUE); |
936 | g_free (mem: dependency_file); |
937 | g_error_free (error); |
938 | g_hash_table_unref (hash_table: files); |
939 | return 1; |
940 | } |
941 | } |
942 | |
943 | g_string_free (string: dep_string, TRUE); |
944 | g_free (mem: dependency_file); |
945 | } |
946 | |
947 | if (generate_dependencies) |
948 | { |
949 | GHashTableIter iter; |
950 | gpointer key, data; |
951 | FileData *file_data; |
952 | |
953 | g_hash_table_iter_init (iter: &iter, hash_table: files); |
954 | |
955 | /* Generate list of files for direct use as dependencies in a Makefile */ |
956 | while (g_hash_table_iter_next (iter: &iter, key: &key, value: &data)) |
957 | { |
958 | file_data = data; |
959 | g_print (format: "%s\n" , file_data->filename); |
960 | } |
961 | } |
962 | else if (generate_source || generate_header) |
963 | { |
964 | if (generate_source) |
965 | { |
966 | int fd = g_file_open_tmp (NULL, name_used: &binary_target, NULL); |
967 | if (fd == -1) |
968 | { |
969 | g_printerr (format: "Can't open temp file\n" ); |
970 | g_free (mem: c_name); |
971 | g_hash_table_unref (hash_table: files); |
972 | return 1; |
973 | } |
974 | close (fd: fd); |
975 | } |
976 | |
977 | if (c_name == NULL) |
978 | { |
979 | char *base = g_path_get_basename (file_name: srcfile); |
980 | GString *s; |
981 | char *dot; |
982 | int i; |
983 | |
984 | /* Remove extensions */ |
985 | dot = strchr (s: base, c: '.'); |
986 | if (dot) |
987 | *dot = 0; |
988 | |
989 | s = g_string_new (init: "" ); |
990 | |
991 | for (i = 0; base[i] != 0; i++) |
992 | { |
993 | const char *first = G_CSET_A_2_Z G_CSET_a_2_z "_" ; |
994 | const char *rest = G_CSET_A_2_Z G_CSET_a_2_z G_CSET_DIGITS "_" ; |
995 | if (strchr (s: (s->len == 0) ? first : rest, c: base[i]) != NULL) |
996 | g_string_append_c (s, base[i]); |
997 | else if (base[i] == '-') |
998 | g_string_append_c (s, '_'); |
999 | |
1000 | } |
1001 | |
1002 | g_free (mem: base); |
1003 | c_name = g_string_free (string: s, FALSE); |
1004 | } |
1005 | } |
1006 | else |
1007 | binary_target = g_strdup (str: target); |
1008 | |
1009 | c_name_no_underscores = c_name; |
1010 | while (c_name_no_underscores && *c_name_no_underscores == '_') |
1011 | c_name_no_underscores++; |
1012 | |
1013 | if (binary_target != NULL && |
1014 | !write_to_file (table, filename: binary_target, error: &error)) |
1015 | { |
1016 | g_printerr (format: "%s\n" , error->message); |
1017 | g_free (mem: target); |
1018 | g_free (mem: c_name); |
1019 | g_hash_table_unref (hash_table: files); |
1020 | return 1; |
1021 | } |
1022 | |
1023 | if (generate_header) |
1024 | { |
1025 | FILE *file; |
1026 | |
1027 | file = fopen (filename: target, modes: "w" ); |
1028 | if (file == NULL) |
1029 | { |
1030 | g_printerr (format: "can't write to file %s" , target); |
1031 | g_free (mem: c_name); |
1032 | g_hash_table_unref (hash_table: files); |
1033 | return 1; |
1034 | } |
1035 | |
1036 | g_fprintf (file, |
1037 | format: "#ifndef __RESOURCE_%s_H__\n" |
1038 | "#define __RESOURCE_%s_H__\n" |
1039 | "\n" |
1040 | "#include <gio/gio.h>\n" |
1041 | "\n" |
1042 | "%s GResource *%s_get_resource (void);\n" , |
1043 | c_name, c_name, linkage, c_name); |
1044 | |
1045 | if (manual_register) |
1046 | g_fprintf (file, |
1047 | format: "\n" |
1048 | "%s void %s_register_resource (void);\n" |
1049 | "%s void %s_unregister_resource (void);\n" |
1050 | "\n" , |
1051 | linkage, c_name, linkage, c_name); |
1052 | |
1053 | g_fprintf (file, |
1054 | format: "#endif\n" ); |
1055 | |
1056 | fclose (stream: file); |
1057 | } |
1058 | else if (generate_source) |
1059 | { |
1060 | FILE *file; |
1061 | guint8 *data; |
1062 | gsize data_size; |
1063 | gsize i; |
1064 | const char *export = "G_MODULE_EXPORT" ; |
1065 | |
1066 | if (!g_file_get_contents (filename: binary_target, contents: (char **)&data, |
1067 | length: &data_size, NULL)) |
1068 | { |
1069 | g_printerr (format: "can't read back temporary file" ); |
1070 | g_free (mem: c_name); |
1071 | g_hash_table_unref (hash_table: files); |
1072 | return 1; |
1073 | } |
1074 | g_unlink (filename: binary_target); |
1075 | |
1076 | file = fopen (filename: target, modes: "w" ); |
1077 | if (file == NULL) |
1078 | { |
1079 | g_printerr (format: "can't write to file %s" , target); |
1080 | g_free (mem: c_name); |
1081 | g_hash_table_unref (hash_table: files); |
1082 | return 1; |
1083 | } |
1084 | |
1085 | if (internal) |
1086 | export = "G_GNUC_INTERNAL" ; |
1087 | |
1088 | g_fprintf (file, |
1089 | format: "#include <gio/gio.h>\n" |
1090 | "\n" |
1091 | "#if defined (__ELF__) && ( __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 6))\n" |
1092 | "# define SECTION __attribute__ ((section (\".gresource.%s\"), aligned (8)))\n" |
1093 | "#else\n" |
1094 | "# define SECTION\n" |
1095 | "#endif\n" |
1096 | "\n" , |
1097 | c_name_no_underscores); |
1098 | |
1099 | if (external_data) |
1100 | { |
1101 | g_fprintf (file, |
1102 | format: "extern const SECTION union { const guint8 data[%" G_GSIZE_FORMAT"]; const double alignment; void * const ptr;} %s_resource_data;" |
1103 | "\n" , |
1104 | data_size, c_name); |
1105 | } |
1106 | else |
1107 | { |
1108 | /* For Visual Studio builds: Avoid surpassing the 65535-character limit for a string, GitLab issue #1580 */ |
1109 | g_fprintf (file, format: "#ifdef _MSC_VER\n" ); |
1110 | g_fprintf (file, |
1111 | format: "static const SECTION union { const guint8 data[%" G_GSIZE_FORMAT"]; const double alignment; void * const ptr;} %s_resource_data = { {\n" , |
1112 | data_size + 1 /* nul terminator */, c_name); |
1113 | |
1114 | for (i = 0; i < data_size; i++) |
1115 | { |
1116 | if (i % 16 == 0) |
1117 | g_fprintf (file, format: " " ); |
1118 | g_fprintf (file, format: "0%3.3o" , (int)data[i]); |
1119 | if (i != data_size - 1) |
1120 | g_fprintf (file, format: ", " ); |
1121 | if (i % 16 == 15 || i == data_size - 1) |
1122 | g_fprintf (file, format: "\n" ); |
1123 | } |
1124 | |
1125 | g_fprintf (file, format: "} };\n" ); |
1126 | |
1127 | /* For other compilers, use the long string approach */ |
1128 | g_fprintf (file, format: "#else /* _MSC_VER */\n" ); |
1129 | g_fprintf (file, |
1130 | format: "static const SECTION union { const guint8 data[%" G_GSIZE_FORMAT"]; const double alignment; void * const ptr;} %s_resource_data = {\n \"" , |
1131 | data_size + 1 /* nul terminator */, c_name); |
1132 | |
1133 | for (i = 0; i < data_size; i++) |
1134 | { |
1135 | g_fprintf (file, format: "\\%3.3o" , (int)data[i]); |
1136 | if (i % 16 == 15) |
1137 | g_fprintf (file, format: "\"\n \"" ); |
1138 | } |
1139 | |
1140 | g_fprintf (file, format: "\" };\n" ); |
1141 | g_fprintf (file, format: "#endif /* !_MSC_VER */\n" ); |
1142 | } |
1143 | |
1144 | g_fprintf (file, |
1145 | format: "\n" |
1146 | "static GStaticResource static_resource = { %s_resource_data.data, sizeof (%s_resource_data.data)%s, NULL, NULL, NULL };\n" |
1147 | "\n" |
1148 | "%s\n" |
1149 | "GResource *%s_get_resource (void);\n" |
1150 | "GResource *%s_get_resource (void)\n" |
1151 | "{\n" |
1152 | " return g_static_resource_get_resource (&static_resource);\n" |
1153 | "}\n" , |
1154 | c_name, c_name, (external_data ? "" : " - 1 /* nul terminator */" ), |
1155 | export, c_name, c_name); |
1156 | |
1157 | |
1158 | if (manual_register) |
1159 | { |
1160 | g_fprintf (file, |
1161 | format: "\n" |
1162 | "%s\n" |
1163 | "void %s_unregister_resource (void);\n" |
1164 | "void %s_unregister_resource (void)\n" |
1165 | "{\n" |
1166 | " g_static_resource_fini (&static_resource);\n" |
1167 | "}\n" |
1168 | "\n" |
1169 | "%s\n" |
1170 | "void %s_register_resource (void);\n" |
1171 | "void %s_register_resource (void)\n" |
1172 | "{\n" |
1173 | " g_static_resource_init (&static_resource);\n" |
1174 | "}\n" , |
1175 | export, c_name, c_name, |
1176 | export, c_name, c_name); |
1177 | } |
1178 | else |
1179 | { |
1180 | g_fprintf (file, format: "%s" , gconstructor_code); |
1181 | g_fprintf (file, |
1182 | format: "\n" |
1183 | "#ifdef G_HAS_CONSTRUCTORS\n" |
1184 | "\n" |
1185 | "#ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA\n" |
1186 | "#pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(resource_constructor)\n" |
1187 | "#endif\n" |
1188 | "G_DEFINE_CONSTRUCTOR(resource_constructor)\n" |
1189 | "#ifdef G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA\n" |
1190 | "#pragma G_DEFINE_DESTRUCTOR_PRAGMA_ARGS(resource_destructor)\n" |
1191 | "#endif\n" |
1192 | "G_DEFINE_DESTRUCTOR(resource_destructor)\n" |
1193 | "\n" |
1194 | "#else\n" |
1195 | "#warning \"Constructor not supported on this compiler, linking in resources will not work\"\n" |
1196 | "#endif\n" |
1197 | "\n" |
1198 | "static void resource_constructor (void)\n" |
1199 | "{\n" |
1200 | " g_static_resource_init (&static_resource);\n" |
1201 | "}\n" |
1202 | "\n" |
1203 | "static void resource_destructor (void)\n" |
1204 | "{\n" |
1205 | " g_static_resource_fini (&static_resource);\n" |
1206 | "}\n" ); |
1207 | } |
1208 | |
1209 | fclose (stream: file); |
1210 | |
1211 | g_free (mem: data); |
1212 | } |
1213 | |
1214 | g_free (mem: binary_target); |
1215 | g_free (mem: target); |
1216 | g_hash_table_destroy (hash_table: table); |
1217 | g_free (mem: xmllint); |
1218 | g_free (mem: jsonformat); |
1219 | g_free (mem: c_name); |
1220 | g_hash_table_unref (hash_table: files); |
1221 | |
1222 | return 0; |
1223 | } |
1224 | |