1// jcache.c
2
3#ifdef OPT_JAMFILE_CACHE_EXT
4
5#include <stdio.h>
6#include <stdlib.h>
7#include <string.h>
8
9#include "jam.h"
10#include "jcache.h"
11#include "filesys.h"
12#include "hash.h"
13#include "lists.h"
14#include "newstr.h"
15#include "pathsys.h"
16#include "parse.h"
17#include "rules.h"
18#include "search.h"
19#include "variable.h"
20
21///////////////
22// string_list
23//
24
25typedef struct string_list {
26	char**	strings;	// null terminated array of strings
27	int		count;		// number of strings in the array, not counting the
28						// terminating null
29	int		capacity;	// current capacity of the string array
30	int		block_size;	// granularity (number of entries) to be used for
31						// resizing the array
32} string_list;
33
34// string list prototypes
35static string_list* new_string_list(int block_size);
36static void delete_string_list(string_list* list);
37static int resize_string_list(string_list *list, int size);
38static int push_string(string_list *list, char *string);
39static char* pop_string(string_list *list);
40
41
42// file_read_line
43/*!	\brief Reads a line from the supplied file and writes it to the supplied
44		   buffer.
45
46	If the line ends in a LF, it is chopped off.
47
48	\param file The file.
49	\param value The pointer to where the read value shall be written.
50	\return \c ~0, if a number could be read, 0 otherwise.
51*/
52static
53int
54file_read_line(FILE *file, char *buffer, int bufferSize)
55{
56	int len;
57
58	if (!fgets(buffer, bufferSize, file))
59		return 0;
60
61	len = strlen(buffer);
62	if (len > 0 && buffer[len - 1] == '\n')
63		buffer[len - 1] = '\0';
64
65	return 1;
66}
67
68// file_read_line_long
69/*!	\brief Reads a line from the supplied file and interprets it as long value.
70
71	This is almost equivalent to \code fscanf(file, "%ld", value) \endcode,
72	save that fscanf() seems to eat all following LFs, while this function
73	only reads one.
74
75	\param file The file.
76	\param value The pointer to where the read value shall be written.
77	\return \c ~0, if a number could be read, 0 otherwise.
78*/
79static
80int
81file_read_line_long(FILE *file, long *value)
82{
83	char buffer[32];
84	int result;
85
86	result = file_read_line(file, buffer, sizeof(buffer));
87	if (!result)
88		return result;
89
90	if (sscanf(buffer, "%ld\n", value) != 1)
91		return 0;
92
93	return 1;
94}
95
96// new_string_list
97/*!	\brief Creates a new string_list.
98	\param block_size Granularity (number of entries) to be used for
99		   resizing the string array.
100	\return Pointer to the newly allocated string_list, or 0 when out of
101			memory.
102*/
103static
104string_list*
105new_string_list(int block_size)
106{
107	string_list *list = (string_list*)malloc(sizeof(string_list));
108	if (list) {
109		list->strings = 0;
110		list->count = 0;
111		list->capacity = 0;
112		if (block_size <= 0)
113			block_size = 5;
114		list->block_size = block_size;
115		if (!resize_string_list(list, 0)) {
116			free(list);
117			list = 0;
118		}
119	}
120	return list;
121}
122
123// delete_string_list
124/*!	\brief Deletes a string_list formerly allocated with new_string_list.
125
126	All strings the list contains are freed as well.
127
128	\param list The string_list to be deleted.
129*/
130static
131void
132delete_string_list(string_list* list)
133{
134	if (list) {
135		if (list->strings) {
136			int i = 0;
137			for (i = 0; i < list->count; i++) {
138				if (list->strings[i])
139					free(list->strings[i]);
140			}
141			free(list->strings);
142		}
143		free(list);
144	}
145}
146
147// resize_string_list
148/*!	\brief Resizes the string array of a string_list.
149
150	\note This functions is for internal use only.
151
152	\param list The string_list to be resized.
153	\param size Number of entries the list shall be able to contain.
154	\return \c !0, if everything went fine, 0, if an error occured (out of
155			memory).
156*/
157static
158int
159resize_string_list(string_list *list, int size)
160{
161	int result = 0;
162	if (list) {
163		// capacity must be at least size + 1 and a multiple of the block size
164		int newCapacity = (size + list->block_size)
165			/ list->block_size * list->block_size;
166		if (newCapacity == list->capacity)
167			result = !0;
168		else {
169			char** newStrings = (char**)realloc(list->strings,
170												newCapacity * sizeof(char*));
171			if (newStrings) {
172				result = !0;
173				list->strings = newStrings;
174				list->capacity = newCapacity;
175			}
176		}
177	}
178	return result;
179}
180
181// push_string
182/*!	\brief Appends a string to a string_list.
183
184	The list's string array is resized, if necessary and null terminated.
185
186	\param list The string_list.
187	\param string The string to be appended.
188	\return \c !0, if everything went fine, 0, if an error occured (out of
189			memory).
190*/
191static
192int
193push_string(string_list *list, char *string)
194{
195	int result = 0;
196	if (list) {
197		result = resize_string_list(list, list->count + 1);
198		if (result) {
199			list->strings[list->count] = string;
200			list->count++;
201			list->strings[list->count] = 0;	// null terminate
202		}
203	}
204	return result;
205}
206
207// pop_string
208/*!	\brief Removes the last string from a string_list.
209
210	The list's string array is resized, if necessary and null terminated.
211	The caller takes over ownership of the removed string and is responsible
212	for freeing it.
213
214	\param list The string_list.
215	\return The removed string, if everything went fine, 0 otherwise.
216*/
217static
218char*
219pop_string(string_list *list)
220{
221	char* string = 0;
222	if (list && list->count > 0) {
223		list->count--;
224		string = list->strings[list->count];
225		list->strings[list->count] = 0;
226		resize_string_list(list, list->count);
227	}
228	return string;
229}
230
231
232///////////////////
233// jamfile caching
234//
235
236// the jamfile cache
237typedef struct jamfile_cache {
238	struct hash*	entries;	// hash table of jcache_entrys
239	string_list*	filenames;	// entry filenames
240	char*			cache_file;	// name of the cache file
241} jamfile_cache;
242
243// a cache entry for an include file
244typedef struct jcache_entry {
245	char*			filename;	// name of the file
246	time_t			time;		// time stamp of the file
247	string_list*	strings;	// contents of the file
248	int				used;		// whether this cache entry has been used
249} jcache_entry;
250
251// pointer to the jamfile cache
252static jamfile_cache* jamfileCache = 0;
253
254// jamfile cache prototypes
255static jamfile_cache* new_jamfile_cache(void);
256static void delete_jamfile_cache(jamfile_cache* cache);
257static int init_jcache_entry(jcache_entry* entry, char *filename, time_t time,
258							 int used);
259static void cleanup_jcache_entry(jcache_entry* entry);
260static int add_jcache_entry(jamfile_cache* cache, jcache_entry* entry);
261static jcache_entry* find_jcache_entry(jamfile_cache* cache, char* filename);
262static string_list* read_file(const char *filename, string_list* list);
263static int read_jcache(jamfile_cache* cache, char* filename);
264static int write_jcache(jamfile_cache* cache);
265static char* jcache_name(void);
266static jamfile_cache* get_jcache(void);
267
268// new_jamfile_cache
269/*!	\brief Creates a new jamfile_cache.
270	\return A pointer to the newly allocated jamfile_cache, or 0, if out of
271			memory.
272*/
273static
274jamfile_cache*
275new_jamfile_cache(void)
276{
277	jamfile_cache *cache = (jamfile_cache*)malloc(sizeof(jamfile_cache));
278	if (cache) {
279		cache->entries = hashinit(sizeof(jcache_entry), "jcache");
280		cache->filenames = new_string_list(100);
281		cache->cache_file = 0;
282		if (!cache->entries || !cache->filenames) {
283			delete_jamfile_cache(cache);
284			cache = 0;
285		}
286	}
287	return cache;
288}
289
290// delete_jamfile_cache
291/*!	\brief Deletes a jamfile_cache formerly allocated with new_jamfile_cache.
292	\param cache The jamfile_cache to be deleted.
293*/
294static
295void
296delete_jamfile_cache(jamfile_cache* cache)
297{
298	if (cache) {
299		if (cache->entries)
300			hashdone(cache->entries);
301		delete_string_list(cache->filenames);
302	}
303}
304
305// init_jcache_entry
306/*!	\brief Initializes a pre-allocated jcache_entry.
307	\param entry The jcache_entry to be initialized.
308	\param filename The name of the include file to be associated with the
309		   entry.
310	\param time The time stamp of the include file to be associated with the
311		   entry.
312	\param used Whether or not the entry shall be marked used.
313	\return \c !0, if everything went fine, 0, if an error occured (out of
314			memory).
315*/
316static
317int
318init_jcache_entry(jcache_entry* entry, char *filename, time_t time, int used)
319{
320	int result = 0;
321	if (entry) {
322		result = !0;
323		entry->filename = (char*)malloc(strlen(filename) + 1);
324		if (entry->filename)
325			strcpy(entry->filename, filename);
326		entry->time = time;
327		entry->strings = new_string_list(100);
328		entry->used = used;
329		// cleanup on error
330		if (!entry->filename || !entry->strings) {
331			cleanup_jcache_entry(entry);
332			result = 0;
333		}
334	}
335	return result;
336}
337
338// cleanup_jcache_entry
339/*!	\brief De-initializes a jcache_entry.
340
341	All resources associated with the entry, save the memory for the entry
342	structure itself, are freed.
343
344	\param entry The jcache_entry to be de-initialized.
345*/
346static
347void
348cleanup_jcache_entry(jcache_entry* entry)
349{
350	if (entry) {
351		if (entry->filename)
352			free(entry->filename);
353		if (entry->strings)
354			delete_string_list(entry->strings);
355	}
356}
357
358// add_jcache_entry
359/*!	\brief Adds a jcache_entry to a jamfile_cache.
360	\param cache The jamfile_cache.
361	\param entry The jcache_entry to be added.
362	\return \c !0, if everything went fine, 0, if an error occured (out of
363			memory).
364*/
365static
366int
367add_jcache_entry(jamfile_cache* cache, jcache_entry* entry)
368{
369	int result = 0;
370	if (cache && entry) {
371		result = push_string(cache->filenames, entry->filename);
372		if (result) {
373			result = hashenter(cache->entries, (HASHDATA**)&entry);
374			if (!result)
375				pop_string(cache->filenames);
376		}
377	}
378	return result;
379}
380
381// find_jcache_entry
382/*!	\brief Looks up jcache_entry in a jamfile_cache.
383	\param cache The jamfile_cache.
384	\param filename The name of the include file for whose jcache_entry shall
385	 	   be retrieved.
386	\return A pointer to the found jcache_entry, or 0, if the cache does not
387			contain an entry for the specified filename.
388*/
389static
390jcache_entry*
391find_jcache_entry(jamfile_cache* cache, char* filename)
392{
393	jcache_entry _entry;
394	jcache_entry* entry = &_entry;
395	entry->filename = filename;
396	if (!hashcheck(cache->entries, (HASHDATA**)&entry))
397		entry = 0;
398	return entry;
399}
400
401// read_file
402/*!	\brief Reads in a text file and returns its contents as a string_list.
403
404	The function strips leading white spaces from each line and omits empty
405	or comment lines.
406
407	If a string_list is supplied via \a list the file's content is appended
408	to this list, otherwise a new string_list is allocated.
409
410	\param filename The name of the file to be read in.
411	\param list Pointer to a pre-allocated string_list. May be 0.
412	\return A pointer to the string_list containing the contents of the file,
413			or 0, if an error occured.
414*/
415static
416string_list*
417read_file(const char *filename, string_list* list)
418{
419	int result = 0;
420	FILE *file = 0;
421	string_list *allocatedList = 0;
422	// open file
423	if ((file = fopen(filename, "r")) != 0
424		&& (list || (list = allocatedList = new_string_list(100)) != 0)) {
425		// read the file
426		char buffer[513];
427		result = !0;
428		while (result && fgets(buffer, sizeof(buffer) - 1, file)) {
429			char* line = buffer;
430			int len = 0;
431			char *string = 0;
432			// skip leading white spaces
433			while (*line == ' ' || *line == '\t' || *line == '\n')
434				line++;
435			// make empty and comment lines simple new-line lines
436			if (!*line || *line == '#') {
437				line[0] = '\n';
438				line[1] = '\0';
439			}
440			len = strlen(line);
441			// make sure, the line ends in a LF
442			if (line[len - 1] != '\n') {
443				line[len] = '\n';
444				len++;
445				line[len] = '\0';
446			}
447			if ((size_t)len + 1 == sizeof(buffer)) {
448				fprintf(stderr, "error: %s:%d: line too long!\n", filename,
449					list->count + 1);
450				exit(1);
451			}
452			// copy it
453			string = (char*)malloc(len + 1);
454			if (string) {
455				strcpy(string, line);
456				result = push_string(list, string);
457			} else
458				result = 0;
459		}
460		// close the file
461		fclose(file);
462	} else
463		perror(filename);
464	// cleanup on error
465	if (!result) {
466		delete_string_list(allocatedList);
467		list = 0;
468	}
469	return list;
470}
471
472// read_jcache
473/*!	\brief Reads a jamfile_cache from a file.
474
475	Only cache entries for files, that don't have an entry in \a cache yet, are
476	added to it.
477
478	\param cache The jamfile_cache the cache stored in the file shall be added
479		   to.
480	\param filename The name of the file containing the cache to be read.
481	\return \c !0, if everything went fine, 0, if an error occured.
482*/
483static
484int
485read_jcache(jamfile_cache* cache, char* filename)
486{
487	int result = 0;
488	if (cache && filename) {
489		// open file
490		FILE *file = 0;
491		cache->cache_file = filename;
492		if ((file = fopen(filename, "r")) != 0) {
493			// read the file
494			char buffer[512];
495			long count = 0;
496			int i;
497			result = !0;
498			// read number of cache entries
499			result = file_read_line_long(file, &count);
500			// read the cache entries
501			for (i = 0; result && i < count; i++) {
502				char entryname[PATH_MAX];
503				long lineCount = 0;
504				time_t time = 0;
505				jcache_entry entry = { 0, 0, 0 };
506				// entry name, time and line count
507				if (file_read_line(file, entryname, sizeof(entryname))
508					&& strlen(entryname) > 0
509					&& file_read_line_long(file, &time)
510					&& file_read_line_long(file, &lineCount)
511					&& (init_jcache_entry(&entry, entryname, time, 0)) != 0) {
512					// read the lines
513					int j;
514					for (j = 0; result && j < lineCount; j++) {
515						if (fgets(buffer, sizeof(buffer), file)) {
516							char *string = (char*)malloc(strlen(buffer) + 1);
517							if (string) {
518								strcpy(string, buffer);
519								result = push_string(entry.strings, string);
520							} else
521								result = 0;
522						} else {
523							fprintf(stderr, "warning: Invalid jamfile cache: "
524								"Unexpected end of file.\n");
525							result = 0;
526						}
527					}
528				} else {
529					fprintf(stderr, "warning: Invalid jamfile cache: "
530						"Failed to read file info.\n");
531					result = 0;
532				}
533				if (result) {
534					// add only, if there's no entry for that file yet
535					if (find_jcache_entry(cache, entry.filename))
536						cleanup_jcache_entry(&entry);
537					else
538						result = add_jcache_entry(cache, &entry);
539				}
540				// cleanup on error
541				if (!result)
542					cleanup_jcache_entry(&entry);
543			}
544			// close the file
545			fclose(file);
546		} // else: Couldn't open cache file. Don't worry.
547	}
548	return result;
549}
550
551// write_jcache
552/*!	\brief Writes a jamfile_cache into a file.
553	\param cache The jamfile_cache that shall be stored in the file.
554	\param filename The name of the file the cache shall be stored in.
555	\return \c !0, if everything went fine, 0, if an error occured.
556*/
557static
558int
559write_jcache(jamfile_cache* cache)
560{
561	int result = 0;
562	if (cache && cache->cache_file) {
563		// open file
564		FILE *file = 0;
565		if ((file = fopen(cache->cache_file, "w")) != 0) {
566			int count = cache->filenames->count;
567			int i;
568			// write number of cache entries
569			result = (fprintf(file, "%d\n", count) > 0);
570			// write the cache entries
571			for (i = 0; result && i < count; i++) {
572				char* entryname = cache->filenames->strings[i];
573				jcache_entry* entry = find_jcache_entry(cache, entryname);
574				// entry name, time and line count
575				if (!entry) {
576					result = 0;
577				} else if (!entry->strings || !entry->used) {
578					// just skip the entry, if it is not loaded or not used
579				} else if (fprintf(file, "%s\n", entryname) > 0
580					&& (fprintf(file, "%ld\n", entry->time) > 0)
581					&& (fprintf(file, "%d\n", entry->strings->count) > 0)) {
582					int j;
583					// the lines
584					for (j = 0; result && j < entry->strings->count; j++) {
585						const char* string = entry->strings->strings[j];
586						result = (fwrite(string, strlen(string), 1, file) > 0);
587					}
588				} else
589					result = 0;
590			}
591			// close the file
592			fclose(file);
593		}
594	}
595	return result;
596}
597
598// jcache_name
599/*!	\brief Returns the name of the file containing the global jamfile_cache.
600
601	The returned filename is the path to the target stored in the jam variable
602	\c JCACHEFILE. The string does not need to be freed.
603
604	\return A pointer to the jamfile cache file, or 0, if the jam variable is
605			not set yet, or an error occured.
606*/
607static
608char*
609jcache_name(void)
610{
611	static char* name = 0;
612	if (!name) {
613		LIST *jcachevar = var_get("JCACHEFILE");
614
615		if (jcachevar) {
616			TARGET *t = bindtarget( jcachevar->string );
617
618			pushsettings( t->settings );
619			t->boundname = search( t->name, &t->time );
620			popsettings( t->settings );
621
622			if (t->boundname) {
623				name = (char*)copystr(t->boundname);
624			}
625		}
626	}
627	return name;
628}
629
630// get_jcache
631/*!	\brief Returns a pointer to the global jamfile_cache.
632
633	The cache is being lazy-allocated.
634
635	\return A pointer to the global jamfile_cache, or 0, if an error occured.
636*/
637static
638jamfile_cache*
639get_jcache(void)
640{
641	if (!jamfileCache)
642		jamfileCache = new_jamfile_cache();
643	if (jamfileCache && !jamfileCache->cache_file) {
644		char* filename = jcache_name();
645		if (filename) {
646			if (!read_jcache(jamfileCache, filename)) {
647				// An error occurred while reading the cache file. Remove all
648				// entries that we read in, assuming they might be corrupted.
649				// Since the hash doesn't support removing entries, we create
650				// a new one and copy over the entries we want to keep.
651				int count = jamfileCache->filenames->count;
652				int i;
653
654				jamfile_cache* newCache = new_jamfile_cache();
655				if (!newCache) {
656					fprintf(stderr, "Out of memory!\n");
657					exit(1);
658				}
659
660				for (i = 0; i < count; i++) {
661					char* entryname = jamfileCache->filenames->strings[i];
662					jcache_entry* entry = find_jcache_entry(jamfileCache,
663						entryname);
664					if (entry->used) {
665						jcache_entry newEntry;
666						if (!init_jcache_entry(&newEntry, entryname,
667								entry->time, entry->used)) {
668							fprintf(stderr, "Out of memory!\n");
669							exit(1);
670						}
671
672						delete_string_list(newEntry.strings);
673						newEntry.strings = entry->strings;
674						entry->strings = 0;
675
676						if (!add_jcache_entry(newCache, &newEntry)) {
677							fprintf(stderr, "Out of memory!\n");
678							exit(1);
679						}
680					}
681				}
682
683				delete_jamfile_cache(jamfileCache);
684				jamfileCache = newCache;
685				jamfileCache->cache_file = filename;
686			}
687		}
688	}
689	return jamfileCache;
690}
691
692// jcache_init
693/*!	\brief To be called before using the global jamfile_cache.
694
695	Does nothing currently. The global jamfile_cache is lazy-allocated by
696	get_jcache().
697*/
698void
699jcache_init(void)
700{
701}
702
703// jcache_done
704/*!	\brief To be called when done with the global jamfile_cache.
705
706	Writes the cache to the specified cache file.
707*/
708void
709jcache_done(void)
710{
711	jamfile_cache* cache = get_jcache();
712	if (cache) {
713		write_jcache(cache);
714		delete_jamfile_cache(cache);
715		jamfileCache = 0;
716	}
717}
718
719// jcache
720/*!	\brief Returns the contents of an include file as a null terminated string
721		   array.
722
723	If the file is cached and the respective entry is not obsolete, the cached
724	string array is returned, otherwise the file is read in.
725
726	The caller must not free the returned string array or any of the contained
727	strings.
728
729	\param filename The name of the include file.
730	\return A pointer to a null terminated string array representing the
731			contents of the specified file, or 0, if an error occured.
732*/
733char**
734jcache(char *_filename)
735{
736	char** strings = 0;
737	jamfile_cache* cache = get_jcache();
738	time_t time;
739	// normalize the filename
740    char _normalizedPath[PATH_MAX];
741    char *filename = normalize_path(_filename, _normalizedPath,
742    	sizeof(_normalizedPath));
743    if (!filename)
744    	filename = _filename;
745	// get file time
746	if (!cache)
747		return 0;
748	if (file_time(filename, &time) == 0) {
749		// lookup file in cache
750		jcache_entry* entry = find_jcache_entry(cache, filename);
751		if (entry) {
752			// in cache
753			entry->used = !0;
754			if (entry->time == time && entry->strings) {
755				// up to date
756				strings = entry->strings->strings;
757			} else {
758				// obsolete
759				delete_string_list(entry->strings);
760				entry->strings = read_file(filename, 0);
761				entry->time = time;
762				strings = entry->strings->strings;
763			}
764		} else {
765			// not in cache
766			jcache_entry newEntry;
767			entry = &newEntry;
768			init_jcache_entry(entry, filename, time, !0);
769			if (read_file(filename, entry->strings)) {
770				if (add_jcache_entry(cache, entry))
771					strings = entry->strings->strings;
772			}
773			// cleanup on error
774			if (!strings)
775				cleanup_jcache_entry(entry);
776		}
777	} else
778		perror(filename);
779	return strings;
780}
781
782#endif	// OPT_JAMFILE_CACHE_EXT
783