1 | /* |
2 | * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions |
6 | * are met: |
7 | * |
8 | * - Redistributions of source code must retain the above copyright |
9 | * notice, this list of conditions and the following disclaimer. |
10 | * |
11 | * - Redistributions in binary form must reproduce the above copyright |
12 | * notice, this list of conditions and the following disclaimer in the |
13 | * documentation and/or other materials provided with the distribution. |
14 | * |
15 | * - Neither the name of Oracle nor the names of its |
16 | * contributors may be used to endorse or promote products derived |
17 | * from this software without specific prior written permission. |
18 | * |
19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
20 | * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
21 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
22 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
23 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
24 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
25 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
26 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
27 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
28 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
29 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
30 | */ |
31 | |
32 | #include <assert.h> |
33 | #include <string.h> |
34 | #include <stdlib.h> |
35 | |
36 | #include "endian.hpp" |
37 | #include "imageDecompressor.hpp" |
38 | #include "imageFile.hpp" |
39 | #include "inttypes.hpp" |
40 | #include "jni.h" |
41 | #include "osSupport.hpp" |
42 | |
43 | // Map the full jimage, only with 64 bit addressing. |
44 | bool ImageFileReader::memory_map_image = sizeof(void *) == 8; |
45 | |
46 | #ifdef WIN32 |
47 | const char FileSeparator = '\\'; |
48 | #else |
49 | const char FileSeparator = '/'; |
50 | #endif |
51 | |
52 | // Image files are an alternate file format for storing classes and resources. The |
53 | // goal is to supply file access which is faster and smaller than the jar format. |
54 | // |
55 | // (More detailed nodes in the header.) |
56 | // |
57 | |
58 | // Compute the Perfect Hashing hash code for the supplied UTF-8 string. |
59 | s4 ImageStrings::hash_code(const char* string, s4 seed) { |
60 | assert(seed > 0 && "invariant" ); |
61 | // Access bytes as unsigned. |
62 | u1* bytes = (u1*)string; |
63 | u4 useed = (u4)seed; |
64 | // Compute hash code. |
65 | for (u1 byte = *bytes++; byte; byte = *bytes++) { |
66 | useed = (useed * HASH_MULTIPLIER) ^ byte; |
67 | } |
68 | // Ensure the result is not signed. |
69 | return (s4)(useed & 0x7FFFFFFF); |
70 | } |
71 | |
72 | // Match up a string in a perfect hash table. |
73 | // Returns the index where the name should be. |
74 | // Result still needs validation for precise match (false positive.) |
75 | s4 ImageStrings::find(Endian* endian, const char* name, s4* redirect, u4 length) { |
76 | // If the table is empty, then short cut. |
77 | if (!redirect || !length) { |
78 | return NOT_FOUND; |
79 | } |
80 | // Compute the basic perfect hash for name. |
81 | s4 hash_code = ImageStrings::hash_code(name); |
82 | // Modulo table size. |
83 | s4 index = hash_code % length; |
84 | // Get redirect entry. |
85 | // value == 0 then not found |
86 | // value < 0 then -1 - value is true index |
87 | // value > 0 then value is seed for recomputing hash. |
88 | s4 value = endian->get(redirect[index]); |
89 | // if recompute is required. |
90 | if (value > 0 ) { |
91 | // Entry collision value, need to recompute hash. |
92 | hash_code = ImageStrings::hash_code(name, value); |
93 | // Modulo table size. |
94 | return hash_code % length; |
95 | } else if (value < 0) { |
96 | // Compute direct index. |
97 | return -1 - value; |
98 | } |
99 | // No entry found. |
100 | return NOT_FOUND; |
101 | } |
102 | |
103 | // Test to see if UTF-8 string begins with the start UTF-8 string. If so, |
104 | // return non-NULL address of remaining portion of string. Otherwise, return |
105 | // NULL. Used to test sections of a path without copying from image string |
106 | // table. |
107 | const char* ImageStrings::starts_with(const char* string, const char* start) { |
108 | char ch1, ch2; |
109 | // Match up the strings the best we can. |
110 | while ((ch1 = *string) && (ch2 = *start)) { |
111 | if (ch1 != ch2) { |
112 | // Mismatch, return NULL. |
113 | return NULL; |
114 | } |
115 | // Next characters. |
116 | string++, start++; |
117 | } |
118 | // Return remainder of string. |
119 | return string; |
120 | } |
121 | |
122 | // Inflates the attribute stream into individual values stored in the long |
123 | // array _attributes. This allows an attribute value to be quickly accessed by |
124 | // direct indexing. Unspecified values default to zero (from constructor.) |
125 | void ImageLocation::set_data(u1* data) { |
126 | // Deflate the attribute stream into an array of attributes. |
127 | u1 byte; |
128 | // Repeat until end header is found. |
129 | while ((data != NULL) && (byte = *data)) { |
130 | // Extract kind from header byte. |
131 | u1 kind = attribute_kind(byte); |
132 | assert(kind < ATTRIBUTE_COUNT && "invalid image location attribute" ); |
133 | // Extract length of data (in bytes). |
134 | u1 n = attribute_length(byte); |
135 | // Read value (most significant first.) |
136 | _attributes[kind] = attribute_value(data + 1, n); |
137 | // Position to next attribute by skipping attribute header and data bytes. |
138 | data += n + 1; |
139 | } |
140 | } |
141 | |
142 | // Zero all attribute values. |
143 | void ImageLocation::clear_data() { |
144 | // Set defaults to zero. |
145 | memset(_attributes, 0, sizeof(_attributes)); |
146 | } |
147 | |
148 | // ImageModuleData constructor maps out sub-tables for faster access. |
149 | ImageModuleData::ImageModuleData(const ImageFileReader* image_file) : |
150 | _image_file(image_file), |
151 | _endian(image_file->endian()) { |
152 | } |
153 | |
154 | // Release module data resource. |
155 | ImageModuleData::~ImageModuleData() { |
156 | } |
157 | |
158 | |
159 | // Return the module in which a package resides. Returns NULL if not found. |
160 | const char* ImageModuleData::package_to_module(const char* package_name) { |
161 | // replace all '/' by '.' |
162 | char* replaced = new char[(int) strlen(package_name) + 1]; |
163 | assert(replaced != NULL && "allocation failed" ); |
164 | int i; |
165 | for (i = 0; package_name[i] != '\0'; i++) { |
166 | replaced[i] = package_name[i] == '/' ? '.' : package_name[i]; |
167 | } |
168 | replaced[i] = '\0'; |
169 | |
170 | // build path /packages/<package_name> |
171 | const char* radical = "/packages/" ; |
172 | char* path = new char[(int) strlen(radical) + (int) strlen(package_name) + 1]; |
173 | assert(path != NULL && "allocation failed" ); |
174 | strcpy(path, radical); |
175 | strcat(path, replaced); |
176 | delete[] replaced; |
177 | |
178 | // retrieve package location |
179 | ImageLocation location; |
180 | bool found = _image_file->find_location(path, location); |
181 | if (!found) { |
182 | delete[] path; |
183 | return NULL; |
184 | } |
185 | |
186 | // retrieve offsets to module name |
187 | int size = (int)location.get_attribute(ImageLocation::ATTRIBUTE_UNCOMPRESSED); |
188 | u1* content = new u1[size]; |
189 | assert(content != NULL && "allocation failed" ); |
190 | _image_file->get_resource(location, content); |
191 | u1* ptr = content; |
192 | // sequence of sizeof(8) isEmpty|offset. Use the first module that is not empty. |
193 | u4 offset = 0; |
194 | for (i = 0; i < size; i+=8) { |
195 | u4 isEmpty = _endian->get(*((u4*)ptr)); |
196 | ptr += 4; |
197 | if (!isEmpty) { |
198 | offset = _endian->get(*((u4*)ptr)); |
199 | break; |
200 | } |
201 | ptr += 4; |
202 | } |
203 | delete[] content; |
204 | return _image_file->get_strings().get(offset); |
205 | } |
206 | |
207 | // Manage a table of open image files. This table allows multiple access points |
208 | // to share an open image. |
209 | ImageFileReaderTable::ImageFileReaderTable() : _count(0), _max(_growth) { |
210 | _table = static_cast<ImageFileReader**>(calloc(_max, sizeof(ImageFileReader*))); |
211 | assert(_table != NULL && "allocation failed" ); |
212 | } |
213 | |
214 | ImageFileReaderTable::~ImageFileReaderTable() { |
215 | for (u4 i = 0; i < _count; i++) { |
216 | ImageFileReader* image = _table[i]; |
217 | |
218 | if (image != NULL) { |
219 | delete image; |
220 | } |
221 | } |
222 | free(_table); |
223 | } |
224 | |
225 | // Add a new image entry to the table. |
226 | void ImageFileReaderTable::add(ImageFileReader* image) { |
227 | if (_count == _max) { |
228 | _max += _growth; |
229 | _table = static_cast<ImageFileReader**>(realloc(_table, _max * sizeof(ImageFileReader*))); |
230 | } |
231 | _table[_count++] = image; |
232 | } |
233 | |
234 | // Remove an image entry from the table. |
235 | void ImageFileReaderTable::remove(ImageFileReader* image) { |
236 | for (u4 i = 0; i < _count; i++) { |
237 | if (_table[i] == image) { |
238 | // Swap the last element into the found slot |
239 | _table[i] = _table[--_count]; |
240 | break; |
241 | } |
242 | } |
243 | |
244 | if (_count != 0 && _count == _max - _growth) { |
245 | _max -= _growth; |
246 | _table = static_cast<ImageFileReader**>(realloc(_table, _max * sizeof(ImageFileReader*))); |
247 | } |
248 | } |
249 | |
250 | // Determine if image entry is in table. |
251 | bool ImageFileReaderTable::contains(ImageFileReader* image) { |
252 | for (u4 i = 0; i < _count; i++) { |
253 | if (_table[i] == image) { |
254 | return true; |
255 | } |
256 | } |
257 | return false; |
258 | } |
259 | |
260 | // Table to manage multiple opens of an image file. |
261 | ImageFileReaderTable ImageFileReader::_reader_table; |
262 | |
263 | SimpleCriticalSection _reader_table_lock; |
264 | |
265 | // Locate an image if file already open. |
266 | ImageFileReader* ImageFileReader::find_image(const char* name) { |
267 | // Lock out _reader_table. |
268 | SimpleCriticalSectionLock cs(&_reader_table_lock); |
269 | // Search for an exist image file. |
270 | for (u4 i = 0; i < _reader_table.count(); i++) { |
271 | // Retrieve table entry. |
272 | ImageFileReader* reader = _reader_table.get(i); |
273 | // If name matches, then reuse (bump up use count.) |
274 | assert(reader->name() != NULL && "reader->name must not be null" ); |
275 | if (strcmp(reader->name(), name) == 0) { |
276 | reader->inc_use(); |
277 | return reader; |
278 | } |
279 | } |
280 | |
281 | return NULL; |
282 | } |
283 | |
284 | // Open an image file, reuse structure if file already open. |
285 | ImageFileReader* ImageFileReader::open(const char* name, bool big_endian) { |
286 | ImageFileReader* reader = find_image(name); |
287 | if (reader != NULL) { |
288 | return reader; |
289 | } |
290 | |
291 | // Need a new image reader. |
292 | reader = new ImageFileReader(name, big_endian); |
293 | if (reader == NULL || !reader->open()) { |
294 | // Failed to open. |
295 | delete reader; |
296 | return NULL; |
297 | } |
298 | |
299 | // Lock to update |
300 | SimpleCriticalSectionLock cs(&_reader_table_lock); |
301 | // Search for an existing image file. |
302 | for (u4 i = 0; i < _reader_table.count(); i++) { |
303 | // Retrieve table entry. |
304 | ImageFileReader* existing_reader = _reader_table.get(i); |
305 | // If name matches, then reuse (bump up use count.) |
306 | assert(reader->name() != NULL && "reader->name still must not be null" ); |
307 | if (strcmp(existing_reader->name(), name) == 0) { |
308 | existing_reader->inc_use(); |
309 | reader->close(); |
310 | delete reader; |
311 | return existing_reader; |
312 | } |
313 | } |
314 | // Bump use count and add to table. |
315 | reader->inc_use(); |
316 | _reader_table.add(reader); |
317 | return reader; |
318 | } |
319 | |
320 | // Close an image file if the file is not in use elsewhere. |
321 | void ImageFileReader::close(ImageFileReader *reader) { |
322 | // Lock out _reader_table. |
323 | SimpleCriticalSectionLock cs(&_reader_table_lock); |
324 | // If last use then remove from table and then close. |
325 | if (reader->dec_use()) { |
326 | _reader_table.remove(reader); |
327 | delete reader; |
328 | } |
329 | } |
330 | |
331 | // Return an id for the specifed ImageFileReader. |
332 | u8 ImageFileReader::reader_to_ID(ImageFileReader *reader) { |
333 | // ID is just the cloaked reader address. |
334 | return (u8)reader; |
335 | } |
336 | |
337 | // Validate the image id. |
338 | bool ImageFileReader::id_check(u8 id) { |
339 | // Make sure the ID is a managed (_reader_table) reader. |
340 | SimpleCriticalSectionLock cs(&_reader_table_lock); |
341 | return _reader_table.contains((ImageFileReader*)id); |
342 | } |
343 | |
344 | // Return an id for the specifed ImageFileReader. |
345 | ImageFileReader* ImageFileReader::id_to_reader(u8 id) { |
346 | assert(id_check(id) && "invalid image id" ); |
347 | return (ImageFileReader*)id; |
348 | } |
349 | |
350 | // Constructor intializes to a closed state. |
351 | ImageFileReader::ImageFileReader(const char* name, bool big_endian) { |
352 | // Copy the image file name. |
353 | int len = (int) strlen(name) + 1; |
354 | _name = new char[len]; |
355 | assert(_name != NULL && "allocation failed" ); |
356 | strncpy(_name, name, len); |
357 | // Initialize for a closed file. |
358 | _fd = -1; |
359 | _endian = Endian::get_handler(big_endian); |
360 | _index_data = NULL; |
361 | } |
362 | |
363 | // Close image and free up data structures. |
364 | ImageFileReader::~ImageFileReader() { |
365 | // Ensure file is closed. |
366 | close(); |
367 | // Free up name. |
368 | if (_name) { |
369 | delete[] _name; |
370 | _name = NULL; |
371 | } |
372 | } |
373 | |
374 | // Open image file for read access. |
375 | bool ImageFileReader::open() { |
376 | // If file exists open for reading. |
377 | _fd = osSupport::openReadOnly(_name); |
378 | if (_fd == -1) { |
379 | return false; |
380 | } |
381 | // Retrieve the file size. |
382 | _file_size = osSupport::size(_name); |
383 | // Read image file header and verify it has a valid header. |
384 | size_t = sizeof(ImageHeader); |
385 | if (_file_size < header_size || |
386 | !read_at((u1*)&_header, header_size, 0) || |
387 | _header.magic(_endian) != IMAGE_MAGIC || |
388 | _header.major_version(_endian) != MAJOR_VERSION || |
389 | _header.minor_version(_endian) != MINOR_VERSION) { |
390 | close(); |
391 | return false; |
392 | } |
393 | // Size of image index. |
394 | _index_size = index_size(); |
395 | // Make sure file is large enough to contain the index. |
396 | if (_file_size < _index_size) { |
397 | return false; |
398 | } |
399 | // Memory map image (minimally the index.) |
400 | _index_data = (u1*)osSupport::map_memory(_fd, _name, 0, (size_t)map_size()); |
401 | assert(_index_data && "image file not memory mapped" ); |
402 | // Retrieve length of index perfect hash table. |
403 | u4 length = table_length(); |
404 | // Compute offset of the perfect hash table redirect table. |
405 | u4 redirect_table_offset = (u4)header_size; |
406 | // Compute offset of index attribute offsets. |
407 | u4 offsets_table_offset = redirect_table_offset + length * (u4)sizeof(s4); |
408 | // Compute offset of index location attribute data. |
409 | u4 location_bytes_offset = offsets_table_offset + length * (u4)sizeof(u4); |
410 | // Compute offset of index string table. |
411 | u4 string_bytes_offset = location_bytes_offset + locations_size(); |
412 | // Compute address of the perfect hash table redirect table. |
413 | _redirect_table = (s4*)(_index_data + redirect_table_offset); |
414 | // Compute address of index attribute offsets. |
415 | _offsets_table = (u4*)(_index_data + offsets_table_offset); |
416 | // Compute address of index location attribute data. |
417 | _location_bytes = _index_data + location_bytes_offset; |
418 | // Compute address of index string table. |
419 | _string_bytes = _index_data + string_bytes_offset; |
420 | |
421 | // Initialize the module data |
422 | module_data = new ImageModuleData(this); |
423 | // Successful open (if memory allocation succeeded). |
424 | return module_data != NULL; |
425 | } |
426 | |
427 | // Close image file. |
428 | void ImageFileReader::close() { |
429 | // Deallocate the index. |
430 | if (_index_data) { |
431 | osSupport::unmap_memory((char*)_index_data, (size_t)map_size()); |
432 | _index_data = NULL; |
433 | } |
434 | // Close file. |
435 | if (_fd != -1) { |
436 | osSupport::close(_fd); |
437 | _fd = -1; |
438 | } |
439 | } |
440 | |
441 | // Read directly from the file. |
442 | bool ImageFileReader::read_at(u1* data, u8 size, u8 offset) const { |
443 | return (u8)osSupport::read(_fd, (char*)data, size, offset) == size; |
444 | } |
445 | |
446 | // Find the location attributes associated with the path. Returns true if |
447 | // the location is found, false otherwise. |
448 | bool ImageFileReader::find_location(const char* path, ImageLocation& location) const { |
449 | // Locate the entry in the index perfect hash table. |
450 | s4 index = ImageStrings::find(_endian, path, _redirect_table, table_length()); |
451 | // If is found. |
452 | if (index != ImageStrings::NOT_FOUND) { |
453 | // Get address of first byte of location attribute stream. |
454 | u1* data = get_location_data(index); |
455 | // Expand location attributes. |
456 | location.set_data(data); |
457 | // Make sure result is not a false positive. |
458 | return verify_location(location, path); |
459 | } |
460 | return false; |
461 | } |
462 | |
463 | // Find the location index and size associated with the path. |
464 | // Returns the location index and size if the location is found, 0 otherwise. |
465 | u4 ImageFileReader::find_location_index(const char* path, u8 *size) const { |
466 | // Locate the entry in the index perfect hash table. |
467 | s4 index = ImageStrings::find(_endian, path, _redirect_table, table_length()); |
468 | // If found. |
469 | if (index != ImageStrings::NOT_FOUND) { |
470 | // Get address of first byte of location attribute stream. |
471 | u4 offset = get_location_offset(index); |
472 | u1* data = get_location_offset_data(offset); |
473 | // Expand location attributes. |
474 | ImageLocation location(data); |
475 | // Make sure result is not a false positive. |
476 | if (verify_location(location, path)) { |
477 | *size = (jlong)location.get_attribute(ImageLocation::ATTRIBUTE_UNCOMPRESSED); |
478 | return offset; |
479 | } |
480 | } |
481 | return 0; // not found |
482 | } |
483 | |
484 | // Verify that a found location matches the supplied path (without copying.) |
485 | bool ImageFileReader::verify_location(ImageLocation& location, const char* path) const { |
486 | // Manage the image string table. |
487 | ImageStrings strings(_string_bytes, _header.strings_size(_endian)); |
488 | // Position to first character of the path string. |
489 | const char* next = path; |
490 | // Get module name string. |
491 | const char* module = location.get_attribute(ImageLocation::ATTRIBUTE_MODULE, strings); |
492 | // If module string is not empty. |
493 | if (*module != '\0') { |
494 | // Compare '/module/' . |
495 | if (*next++ != '/') return false; |
496 | if (!(next = ImageStrings::starts_with(next, module))) return false; |
497 | if (*next++ != '/') return false; |
498 | } |
499 | // Get parent (package) string |
500 | const char* parent = location.get_attribute(ImageLocation::ATTRIBUTE_PARENT, strings); |
501 | // If parent string is not empty string. |
502 | if (*parent != '\0') { |
503 | // Compare 'parent/' . |
504 | if (!(next = ImageStrings::starts_with(next, parent))) return false; |
505 | if (*next++ != '/') return false; |
506 | } |
507 | // Get base name string. |
508 | const char* base = location.get_attribute(ImageLocation::ATTRIBUTE_BASE, strings); |
509 | // Compare with basne name. |
510 | if (!(next = ImageStrings::starts_with(next, base))) return false; |
511 | // Get extension string. |
512 | const char* extension = location.get_attribute(ImageLocation::ATTRIBUTE_EXTENSION, strings); |
513 | // If extension is not empty. |
514 | if (*extension != '\0') { |
515 | // Compare '.extension' . |
516 | if (*next++ != '.') return false; |
517 | if (!(next = ImageStrings::starts_with(next, extension))) return false; |
518 | } |
519 | // True only if complete match and no more characters. |
520 | return *next == '\0'; |
521 | } |
522 | |
523 | // Return the resource for the supplied location offset. |
524 | void ImageFileReader::get_resource(u4 offset, u1* uncompressed_data) const { |
525 | // Get address of first byte of location attribute stream. |
526 | u1* data = get_location_offset_data(offset); |
527 | // Expand location attributes. |
528 | ImageLocation location(data); |
529 | // Read the data |
530 | get_resource(location, uncompressed_data); |
531 | } |
532 | |
533 | // Return the resource for the supplied location. |
534 | void ImageFileReader::get_resource(ImageLocation& location, u1* uncompressed_data) const { |
535 | // Retrieve the byte offset and size of the resource. |
536 | u8 offset = location.get_attribute(ImageLocation::ATTRIBUTE_OFFSET); |
537 | u8 uncompressed_size = location.get_attribute(ImageLocation::ATTRIBUTE_UNCOMPRESSED); |
538 | u8 compressed_size = location.get_attribute(ImageLocation::ATTRIBUTE_COMPRESSED); |
539 | // If the resource is compressed. |
540 | if (compressed_size != 0) { |
541 | u1* compressed_data; |
542 | // If not memory mapped read in bytes. |
543 | if (!memory_map_image) { |
544 | // Allocate buffer for compression. |
545 | compressed_data = new u1[(size_t)compressed_size]; |
546 | assert(compressed_data != NULL && "allocation failed" ); |
547 | // Read bytes from offset beyond the image index. |
548 | bool is_read = read_at(compressed_data, compressed_size, _index_size + offset); |
549 | assert(is_read && "error reading from image or short read" ); |
550 | } else { |
551 | compressed_data = get_data_address() + offset; |
552 | } |
553 | // Get image string table. |
554 | const ImageStrings strings = get_strings(); |
555 | // Decompress resource. |
556 | ImageDecompressor::decompress_resource(compressed_data, uncompressed_data, uncompressed_size, |
557 | &strings, _endian); |
558 | // If not memory mapped then release temporary buffer. |
559 | if (!memory_map_image) { |
560 | delete[] compressed_data; |
561 | } |
562 | } else { |
563 | // Read bytes from offset beyond the image index. |
564 | bool is_read = read_at(uncompressed_data, uncompressed_size, _index_size + offset); |
565 | assert(is_read && "error reading from image or short read" ); |
566 | } |
567 | } |
568 | |
569 | // Return the ImageModuleData for this image |
570 | ImageModuleData * ImageFileReader::get_image_module_data() { |
571 | return module_data; |
572 | } |
573 | |