[FL-2831] Resources cleanup in updater (#1796)
* updater: remove files from existing resources Manifest file before deploying new resources * toolbox: tar: single file extraction API Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									e25b424188
								
							
						
					
					
						commit
						f8b532f063
					
				| @ -10,7 +10,7 @@ extern "C" { | |||||||
| #include <stdbool.h> | #include <stdbool.h> | ||||||
| #include <m-string.h> | #include <m-string.h> | ||||||
| 
 | 
 | ||||||
| #define UPDATE_DELAY_OPERATION_OK 300 | #define UPDATE_DELAY_OPERATION_OK 10 | ||||||
| #define UPDATE_DELAY_OPERATION_ERROR INT_MAX | #define UPDATE_DELAY_OPERATION_ERROR INT_MAX | ||||||
| 
 | 
 | ||||||
| typedef enum { | typedef enum { | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ | |||||||
| #include <update_util/dfu_file.h> | #include <update_util/dfu_file.h> | ||||||
| #include <update_util/lfs_backup.h> | #include <update_util/lfs_backup.h> | ||||||
| #include <update_util/update_operation.h> | #include <update_util/update_operation.h> | ||||||
|  | #include <update_util/resources/manifest.h> | ||||||
| #include <toolbox/tar/tar_archive.h> | #include <toolbox/tar/tar_archive.h> | ||||||
| #include <toolbox/crc32_calc.h> | #include <toolbox/crc32_calc.h> | ||||||
| 
 | 
 | ||||||
| @ -50,10 +51,46 @@ static bool update_task_resource_unpack_cb(const char* name, bool is_directory, | |||||||
|     update_task_set_progress( |     update_task_set_progress( | ||||||
|         unpack_progress->update_task, |         unpack_progress->update_task, | ||||||
|         UpdateTaskStageProgress, |         UpdateTaskStageProgress, | ||||||
|         unpack_progress->processed_files * 100 / (unpack_progress->total_files + 1)); |         /* For this stage, last 70% of progress = extraction */ | ||||||
|  |         30 + (unpack_progress->processed_files * 70) / (unpack_progress->total_files + 1)); | ||||||
|     return true; |     return true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | static void | ||||||
|  |     update_task_cleanup_resources(UpdateTask* update_task, uint32_t n_approx_file_entries) { | ||||||
|  |     ResourceManifestReader* manifest_reader = resource_manifest_reader_alloc(update_task->storage); | ||||||
|  |     do { | ||||||
|  |         FURI_LOG_I(TAG, "Cleaning up old manifest"); | ||||||
|  |         if(!resource_manifest_reader_open(manifest_reader, EXT_PATH("Manifest"))) { | ||||||
|  |             FURI_LOG_W(TAG, "No existing manifest"); | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /* We got # of entries in TAR file. Approx 1/4th is dir entries, we skip them */ | ||||||
|  |         n_approx_file_entries = n_approx_file_entries * 3 / 4 + 1; | ||||||
|  |         uint32_t n_processed_files = 0; | ||||||
|  | 
 | ||||||
|  |         ResourceManifestEntry* entry_ptr = NULL; | ||||||
|  |         while((entry_ptr = resource_manifest_reader_next(manifest_reader))) { | ||||||
|  |             if(entry_ptr->type == ResourceManifestEntryTypeFile) { | ||||||
|  |                 update_task_set_progress( | ||||||
|  |                     update_task, | ||||||
|  |                     UpdateTaskStageProgress, | ||||||
|  |                     /* For this stage, first 30% of progress = cleanup */ | ||||||
|  |                     (n_processed_files++ * 30) / (n_approx_file_entries + 1)); | ||||||
|  | 
 | ||||||
|  |                 string_t file_path; | ||||||
|  |                 string_init(file_path); | ||||||
|  |                 path_concat(STORAGE_EXT_PATH_PREFIX, string_get_cstr(entry_ptr->name), file_path); | ||||||
|  |                 FURI_LOG_D(TAG, "Removing %s", string_get_cstr(file_path)); | ||||||
|  |                 storage_simply_remove(update_task->storage, string_get_cstr(file_path)); | ||||||
|  |                 string_clear(file_path); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } while(false); | ||||||
|  |     resource_manifest_reader_free(manifest_reader); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static bool update_task_post_update(UpdateTask* update_task) { | static bool update_task_post_update(UpdateTask* update_task) { | ||||||
|     bool success = false; |     bool success = false; | ||||||
| 
 | 
 | ||||||
| @ -88,6 +125,8 @@ static bool update_task_post_update(UpdateTask* update_task) { | |||||||
| 
 | 
 | ||||||
|             progress.total_files = tar_archive_get_entries_count(archive); |             progress.total_files = tar_archive_get_entries_count(archive); | ||||||
|             if(progress.total_files > 0) { |             if(progress.total_files > 0) { | ||||||
|  |                 update_task_cleanup_resources(update_task, progress.total_files); | ||||||
|  | 
 | ||||||
|                 CHECK_RESULT(tar_archive_unpack_to(archive, STORAGE_EXT_PATH_PREFIX, NULL)); |                 CHECK_RESULT(tar_archive_unpack_to(archive, STORAGE_EXT_PATH_PREFIX, NULL)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -308,7 +308,7 @@ bool update_task_validate_optionbytes(UpdateTask* update_task) { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } else { |         } else { | ||||||
|             FURI_LOG_I( |             FURI_LOG_D( | ||||||
|                 TAG, |                 TAG, | ||||||
|                 "OB MATCH: #%d: real %08X == %08X (exp.)", |                 "OB MATCH: #%d: real %08X == %08X (exp.)", | ||||||
|                 idx, |                 idx, | ||||||
|  | |||||||
| @ -2269,6 +2269,7 @@ Function,+,tar_archive_get_entries_count,int32_t,TarArchive* | |||||||
| Function,+,tar_archive_open,_Bool,"TarArchive*, const char*, TarOpenMode" | Function,+,tar_archive_open,_Bool,"TarArchive*, const char*, TarOpenMode" | ||||||
| Function,+,tar_archive_set_file_callback,void,"TarArchive*, tar_unpack_file_cb, void*" | Function,+,tar_archive_set_file_callback,void,"TarArchive*, tar_unpack_file_cb, void*" | ||||||
| Function,+,tar_archive_store_data,_Bool,"TarArchive*, const char*, const uint8_t*, const int32_t" | Function,+,tar_archive_store_data,_Bool,"TarArchive*, const char*, const uint8_t*, const int32_t" | ||||||
|  | Function,+,tar_archive_unpack_file,_Bool,"TarArchive*, const char*, const char*" | ||||||
| Function,+,tar_archive_unpack_to,_Bool,"TarArchive*, const char*, Storage_name_converter" | Function,+,tar_archive_unpack_to,_Bool,"TarArchive*, const char*, Storage_name_converter" | ||||||
| Function,-,tempnam,char*,"const char*, const char*" | Function,-,tempnam,char*,"const char*, const char*" | ||||||
| Function,+,text_box_alloc,TextBox*, | Function,+,text_box_alloc,TextBox*, | ||||||
|  | |||||||
| 
 | 
| @ -6,6 +6,9 @@ void furi_hal_cortex_init_early() { | |||||||
|     CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; |     CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; | ||||||
|     DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; |     DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; | ||||||
|     DWT->CYCCNT = 0U; |     DWT->CYCCNT = 0U; | ||||||
|  | 
 | ||||||
|  |     /* Enable instruction prefetch */ | ||||||
|  |     SET_BIT(FLASH->ACR, FLASH_ACR_PRFTEN); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void furi_hal_cortex_delay_us(uint32_t microseconds) { | void furi_hal_cortex_delay_us(uint32_t microseconds) { | ||||||
|  | |||||||
| @ -168,7 +168,44 @@ typedef struct { | |||||||
|     Storage_name_converter converter; |     Storage_name_converter converter; | ||||||
| } TarArchiveDirectoryOpParams; | } TarArchiveDirectoryOpParams; | ||||||
| 
 | 
 | ||||||
|  | static bool archive_extract_current_file(TarArchive* archive, const char* dst_path) { | ||||||
|  |     mtar_t* tar = &archive->tar; | ||||||
|  |     File* out_file = storage_file_alloc(archive->storage); | ||||||
|  |     uint8_t* readbuf = malloc(FILE_BLOCK_SIZE); | ||||||
|  | 
 | ||||||
|  |     bool success = true; | ||||||
|  |     uint8_t n_tries = FILE_OPEN_NTRIES; | ||||||
|  |     do { | ||||||
|  |         while(n_tries-- > 0) { | ||||||
|  |             if(storage_file_open(out_file, dst_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             FURI_LOG_W(TAG, "Failed to open '%s', reties: %d", dst_path, n_tries); | ||||||
|  |             storage_file_close(out_file); | ||||||
|  |             furi_delay_ms(FILE_OPEN_RETRY_DELAY); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if(!storage_file_is_open(out_file)) { | ||||||
|  |             success = false; | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         while(!mtar_eof_data(tar)) { | ||||||
|  |             int32_t readcnt = mtar_read_data(tar, readbuf, FILE_BLOCK_SIZE); | ||||||
|  |             if(!readcnt || !storage_file_write(out_file, readbuf, readcnt)) { | ||||||
|  |                 success = false; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } while(false); | ||||||
|  |     storage_file_free(out_file); | ||||||
|  |     free(readbuf); | ||||||
|  | 
 | ||||||
|  |     return success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| static int archive_extract_foreach_cb(mtar_t* tar, const mtar_header_t* header, void* param) { | static int archive_extract_foreach_cb(mtar_t* tar, const mtar_header_t* header, void* param) { | ||||||
|  |     UNUSED(tar); | ||||||
|     TarArchiveDirectoryOpParams* op_params = param; |     TarArchiveDirectoryOpParams* op_params = param; | ||||||
|     TarArchive* archive = op_params->archive; |     TarArchive* archive = op_params->archive; | ||||||
| 
 | 
 | ||||||
| @ -199,58 +236,22 @@ static int archive_extract_foreach_cb(mtar_t* tar, const mtar_header_t* header, | |||||||
|         return 0; |         return 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     string_init(full_extracted_fname); |     FURI_LOG_D(TAG, "Extracting %d bytes to '%s'", header->size, header->name); | ||||||
| 
 | 
 | ||||||
|     string_t converted_fname; |     string_t converted_fname; | ||||||
|     string_init_set(converted_fname, header->name); |     string_init_set(converted_fname, header->name); | ||||||
|     if(op_params->converter) { |     if(op_params->converter) { | ||||||
|         op_params->converter(converted_fname); |         op_params->converter(converted_fname); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     string_init(full_extracted_fname); | ||||||
|     path_concat(op_params->work_dir, string_get_cstr(converted_fname), full_extracted_fname); |     path_concat(op_params->work_dir, string_get_cstr(converted_fname), full_extracted_fname); | ||||||
|  | 
 | ||||||
|  |     bool success = archive_extract_current_file(archive, string_get_cstr(full_extracted_fname)); | ||||||
|  | 
 | ||||||
|     string_clear(converted_fname); |     string_clear(converted_fname); | ||||||
| 
 |  | ||||||
|     FURI_LOG_D(TAG, "Extracting %d bytes to '%s'", header->size, header->name); |  | ||||||
|     File* out_file = storage_file_alloc(archive->storage); |  | ||||||
|     uint8_t* readbuf = malloc(FILE_BLOCK_SIZE); |  | ||||||
| 
 |  | ||||||
|     bool failed = false; |  | ||||||
|     uint8_t n_tries = FILE_OPEN_NTRIES; |  | ||||||
|     do { |  | ||||||
|         while(n_tries-- > 0) { |  | ||||||
|             if(storage_file_open( |  | ||||||
|                    out_file, |  | ||||||
|                    string_get_cstr(full_extracted_fname), |  | ||||||
|                    FSAM_WRITE, |  | ||||||
|                    FSOM_CREATE_ALWAYS)) { |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|             FURI_LOG_W( |  | ||||||
|                 TAG, |  | ||||||
|                 "Failed to open '%s', reties: %d", |  | ||||||
|                 string_get_cstr(full_extracted_fname), |  | ||||||
|                 n_tries); |  | ||||||
|             storage_file_close(out_file); |  | ||||||
|             furi_delay_ms(FILE_OPEN_RETRY_DELAY); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if(!storage_file_is_open(out_file)) { |  | ||||||
|             failed = true; |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         while(!mtar_eof_data(tar)) { |  | ||||||
|             int32_t readcnt = mtar_read_data(tar, readbuf, FILE_BLOCK_SIZE); |  | ||||||
|             if(!readcnt || !storage_file_write(out_file, readbuf, readcnt)) { |  | ||||||
|                 failed = true; |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } while(false); |  | ||||||
| 
 |  | ||||||
|     storage_file_free(out_file); |  | ||||||
|     free(readbuf); |  | ||||||
|     string_clear(full_extracted_fname); |     string_clear(full_extracted_fname); | ||||||
|     return failed ? -1 : 0; |     return success ? 0 : -1; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool tar_archive_unpack_to( | bool tar_archive_unpack_to( | ||||||
| @ -369,3 +370,16 @@ bool tar_archive_add_dir(TarArchive* archive, const char* fs_full_path, const ch | |||||||
|     storage_file_free(directory); |     storage_file_free(directory); | ||||||
|     return success; |     return success; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | bool tar_archive_unpack_file( | ||||||
|  |     TarArchive* archive, | ||||||
|  |     const char* archive_fname, | ||||||
|  |     const char* destination) { | ||||||
|  |     furi_assert(archive); | ||||||
|  |     furi_assert(archive_fname); | ||||||
|  |     furi_assert(destination); | ||||||
|  |     if(mtar_find(&archive->tar, archive_fname) != MTAR_ESUCCESS) { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     return archive_extract_current_file(archive, destination); | ||||||
|  | } | ||||||
| @ -41,6 +41,11 @@ bool tar_archive_add_dir(TarArchive* archive, const char* fs_full_path, const ch | |||||||
| 
 | 
 | ||||||
| int32_t tar_archive_get_entries_count(TarArchive* archive); | int32_t tar_archive_get_entries_count(TarArchive* archive); | ||||||
| 
 | 
 | ||||||
|  | bool tar_archive_unpack_file( | ||||||
|  |     TarArchive* archive, | ||||||
|  |     const char* archive_fname, | ||||||
|  |     const char* destination); | ||||||
|  | 
 | ||||||
| /* Optional per-entry callback on unpacking - return false to skip entry */ | /* Optional per-entry callback on unpacking - return false to skip entry */ | ||||||
| typedef bool (*tar_unpack_file_cb)(const char* name, bool is_directory, void* context); | typedef bool (*tar_unpack_file_cb)(const char* name, bool is_directory, void* context); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										115
									
								
								lib/update_util/resources/manifest.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								lib/update_util/resources/manifest.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,115 @@ | |||||||
|  | #include "manifest.h" | ||||||
|  | 
 | ||||||
|  | #include <toolbox/stream/buffered_file_stream.h> | ||||||
|  | #include <toolbox/hex.h> | ||||||
|  | 
 | ||||||
|  | struct ResourceManifestReader { | ||||||
|  |     Storage* storage; | ||||||
|  |     Stream* stream; | ||||||
|  |     string_t linebuf; | ||||||
|  |     ResourceManifestEntry entry; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ResourceManifestReader* resource_manifest_reader_alloc(Storage* storage) { | ||||||
|  |     ResourceManifestReader* resource_manifest = | ||||||
|  |         (ResourceManifestReader*)malloc(sizeof(ResourceManifestReader)); | ||||||
|  |     resource_manifest->storage = storage; | ||||||
|  |     resource_manifest->stream = buffered_file_stream_alloc(resource_manifest->storage); | ||||||
|  |     memset(&resource_manifest->entry, 0, sizeof(ResourceManifestEntry)); | ||||||
|  |     string_init(resource_manifest->entry.name); | ||||||
|  |     string_init(resource_manifest->linebuf); | ||||||
|  |     return resource_manifest; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void resource_manifest_reader_free(ResourceManifestReader* resource_manifest) { | ||||||
|  |     furi_assert(resource_manifest); | ||||||
|  | 
 | ||||||
|  |     string_clear(resource_manifest->linebuf); | ||||||
|  |     string_clear(resource_manifest->entry.name); | ||||||
|  |     buffered_file_stream_close(resource_manifest->stream); | ||||||
|  |     stream_free(resource_manifest->stream); | ||||||
|  |     free(resource_manifest); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool resource_manifest_reader_open(ResourceManifestReader* resource_manifest, const char* filename) { | ||||||
|  |     furi_assert(resource_manifest); | ||||||
|  | 
 | ||||||
|  |     return buffered_file_stream_open( | ||||||
|  |         resource_manifest->stream, filename, FSAM_READ, FSOM_OPEN_EXISTING); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Read entries in format of
 | ||||||
|  |  * F:<hash>:<size>:<name> | ||||||
|  |  * D:<name>  | ||||||
|  |  */ | ||||||
|  | ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* resource_manifest) { | ||||||
|  |     furi_assert(resource_manifest); | ||||||
|  | 
 | ||||||
|  |     string_reset(resource_manifest->entry.name); | ||||||
|  |     resource_manifest->entry.type = ResourceManifestEntryTypeUnknown; | ||||||
|  |     resource_manifest->entry.size = 0; | ||||||
|  |     memset(resource_manifest->entry.hash, 0, sizeof(resource_manifest->entry.hash)); | ||||||
|  | 
 | ||||||
|  |     do { | ||||||
|  |         if(!stream_read_line(resource_manifest->stream, resource_manifest->linebuf)) { | ||||||
|  |             return NULL; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /* Trim end of line */ | ||||||
|  |         string_strim(resource_manifest->linebuf); | ||||||
|  | 
 | ||||||
|  |         char type_code = string_get_char(resource_manifest->linebuf, 0); | ||||||
|  |         switch(type_code) { | ||||||
|  |         case 'F': | ||||||
|  |             resource_manifest->entry.type = ResourceManifestEntryTypeFile; | ||||||
|  |             break; | ||||||
|  |         case 'D': | ||||||
|  |             resource_manifest->entry.type = ResourceManifestEntryTypeDirectory; | ||||||
|  |             break; | ||||||
|  |         default: /* Skip other entries - version, timestamp, etc */ | ||||||
|  |             continue; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         if(resource_manifest->entry.type == ResourceManifestEntryTypeFile) { | ||||||
|  |             /* Parse file entry
 | ||||||
|  |               F:<hash>:<size>:<name> */ | ||||||
|  | 
 | ||||||
|  |             /* Remove entry type code */ | ||||||
|  |             string_right(resource_manifest->linebuf, 2); | ||||||
|  | 
 | ||||||
|  |             if(string_search_char(resource_manifest->linebuf, ':') != | ||||||
|  |                sizeof(resource_manifest->entry.hash) * 2) { | ||||||
|  |                 /* Invalid hash */ | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             /* Read hash */ | ||||||
|  |             hex_chars_to_uint8( | ||||||
|  |                 string_get_cstr(resource_manifest->linebuf), resource_manifest->entry.hash); | ||||||
|  | 
 | ||||||
|  |             /* Remove hash */ | ||||||
|  |             string_right( | ||||||
|  |                 resource_manifest->linebuf, sizeof(resource_manifest->entry.hash) * 2 + 1); | ||||||
|  | 
 | ||||||
|  |             resource_manifest->entry.size = atoi(string_get_cstr(resource_manifest->linebuf)); | ||||||
|  | 
 | ||||||
|  |             /* Remove size */ | ||||||
|  |             size_t offs = string_search_char(resource_manifest->linebuf, ':'); | ||||||
|  |             string_right(resource_manifest->linebuf, offs + 1); | ||||||
|  | 
 | ||||||
|  |             string_set(resource_manifest->entry.name, resource_manifest->linebuf); | ||||||
|  |         } else if(resource_manifest->entry.type == ResourceManifestEntryTypeDirectory) { | ||||||
|  |             /* Parse directory entry
 | ||||||
|  |                D:<name> */ | ||||||
|  | 
 | ||||||
|  |             /* Remove entry type code */ | ||||||
|  |             string_right(resource_manifest->linebuf, 2); | ||||||
|  | 
 | ||||||
|  |             string_set(resource_manifest->entry.name, resource_manifest->linebuf); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return &resource_manifest->entry; | ||||||
|  |     } while(true); | ||||||
|  | 
 | ||||||
|  |     return NULL; | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								lib/update_util/resources/manifest.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								lib/update_util/resources/manifest.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <storage/storage.h> | ||||||
|  | 
 | ||||||
|  | #include <m-string.h> | ||||||
|  | #include <stdbool.h> | ||||||
|  | #include <stdint.h> | ||||||
|  | 
 | ||||||
|  | #ifdef __cplusplus | ||||||
|  | extern "C" { | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | typedef enum { | ||||||
|  |     ResourceManifestEntryTypeUnknown = 0, | ||||||
|  |     ResourceManifestEntryTypeDirectory, | ||||||
|  |     ResourceManifestEntryTypeFile, | ||||||
|  | } ResourceManifestEntryType; | ||||||
|  | 
 | ||||||
|  | typedef struct { | ||||||
|  |     ResourceManifestEntryType type; | ||||||
|  |     string_t name; | ||||||
|  |     uint32_t size; | ||||||
|  |     uint8_t hash[16]; | ||||||
|  | } ResourceManifestEntry; | ||||||
|  | 
 | ||||||
|  | typedef struct ResourceManifestReader ResourceManifestReader; | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Initialize resource manifest reader | ||||||
|  |  * @param storage Storage API pointer | ||||||
|  |  * @return allocated object | ||||||
|  |  */ | ||||||
|  | ResourceManifestReader* resource_manifest_reader_alloc(Storage* storage); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Release resource manifest reader | ||||||
|  |  * @param resource_manifest allocated object | ||||||
|  |  */ | ||||||
|  | void resource_manifest_reader_free(ResourceManifestReader* resource_manifest); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Initialize resource manifest reader iteration | ||||||
|  |  * @param resource_manifest allocated object | ||||||
|  |  * @param filename manifest file name | ||||||
|  |  * @return true if file opened | ||||||
|  |  */ | ||||||
|  | bool resource_manifest_reader_open(ResourceManifestReader* resource_manifest, const char* filename); | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Read next file/dir entry from manifest | ||||||
|  |  * @param resource_manifest allocated object | ||||||
|  |  * @return entry or NULL if end of file | ||||||
|  |  */ | ||||||
|  | ResourceManifestEntry* resource_manifest_reader_next(ResourceManifestReader* resource_manifest); | ||||||
|  | 
 | ||||||
|  | #ifdef __cplusplus | ||||||
|  | } // extern "C"
 | ||||||
|  | #endif | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 hedger
						hedger