/* * MiniVHD Minimalist VHD implementation in C. * * This file is part of the MiniVHD Project. * * VHD management functions (open, close, read write etc) * * Version: @(#)manage.c 1.0.4 2021/04/16 * * Authors: Sherman Perry, * Fred N. van Kempen, * * Copyright 2019-2021 Sherman Perry. * Copyright 2021 Fred N. van Kempen. * * MIT License * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documenta- * tion files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom * the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall * be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF O R IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef _FILE_OFFSET_BITS # define _FILE_OFFSET_BITS 64 #endif #include #include #include #include #include #include #include "minivhd.h" #include "internal.h" #include "version.h" #include "cwalk.h" #include "xml2_encoding.h" struct MVHDPaths { char dir_path[MVHD_MAX_PATH_BYTES]; char file_name[MVHD_MAX_PATH_BYTES]; char w2ku_path[MVHD_MAX_PATH_BYTES]; char w2ru_path[MVHD_MAX_PATH_BYTES]; char joined_path[MVHD_MAX_PATH_BYTES]; uint16_t tmp_src_path[MVHD_MAX_PATH_CHARS]; }; int mvhd_errno = 0; static char tmp_open_path[MVHD_MAX_PATH_BYTES] = {0}; /** * \brief Populate data stuctures with content from a VHD footer * * \param [in] vhdm MiniVHD data structure */ static void read_footer(MVHDMeta* vhdm) { uint8_t buffer[MVHD_FOOTER_SIZE]; mvhd_fseeko64(vhdm->f, -MVHD_FOOTER_SIZE, SEEK_END); (void) !fread(buffer, sizeof buffer, 1, vhdm->f); mvhd_buffer_to_footer(&vhdm->footer, buffer); } /** * \brief Populate data stuctures with content from a VHD sparse header * * \param [in] vhdm MiniVHD data structure */ static void read_sparse_header(MVHDMeta* vhdm) { uint8_t buffer[MVHD_SPARSE_SIZE] = { 0 }; mvhd_fseeko64(vhdm->f, vhdm->footer.data_offset, SEEK_SET); (void) !fread(buffer, sizeof buffer, 1, vhdm->f); mvhd_buffer_to_header(&vhdm->sparse, buffer); } /** * \brief Validate VHD footer checksum * * This works by generating a checksum from the footer, and comparing it against the stored checksum. * * \param [in] vhdm MiniVHD data structure */ static bool footer_checksum_valid(MVHDMeta* vhdm) { return vhdm->footer.checksum == mvhd_gen_footer_checksum(&vhdm->footer); } /** * \brief Validate VHD sparse header checksum * * This works by generating a checksum from the sparse header, and comparing it against the stored checksum. * * \param [in] vhdm MiniVHD data structure */ static bool sparse_checksum_valid(MVHDMeta* vhdm) { return vhdm->sparse.checksum == mvhd_gen_sparse_checksum(&vhdm->sparse); } /** * \brief Read BAT into MiniVHD data structure * * The Block Allocation Table (BAT) is the structure in a sparse and differencing VHD which stores * the 4-byte sector offsets for each data block. This function allocates enough memory to contain * the entire BAT, and then reads the contents of the BAT into the buffer. * * \param [in] vhdm MiniVHD data structure * \param [out] err this is populated with MVHD_ERR_MEM if the calloc fails * * \retval -1 if an error occurrs. Check value of err in this case * \retval 0 if the function call succeeds */ static int read_bat(MVHDMeta *vhdm, MVHDError* err) { vhdm->block_offset = calloc(vhdm->sparse.max_bat_ent, sizeof *vhdm->block_offset); if (vhdm->block_offset == NULL) { *err = MVHD_ERR_MEM; return -1; } mvhd_fseeko64(vhdm->f, vhdm->sparse.bat_offset, SEEK_SET); for (uint32_t i = 0; i < vhdm->sparse.max_bat_ent; i++) { (void) !fread(&vhdm->block_offset[i], sizeof *vhdm->block_offset, 1, vhdm->f); vhdm->block_offset[i] = mvhd_from_be32(vhdm->block_offset[i]); } return 0; } /** * \brief Perform a one-time calculation of some sparse VHD values * * \param [in] vhdm MiniVHD data structure */ static void calc_sparse_values(MVHDMeta* vhdm) { vhdm->sect_per_block = vhdm->sparse.block_sz / MVHD_SECTOR_SIZE; int bm_bytes = vhdm->sect_per_block / 8; vhdm->bitmap.sector_count = bm_bytes / MVHD_SECTOR_SIZE; if (bm_bytes % MVHD_SECTOR_SIZE > 0) { vhdm->bitmap.sector_count++; } } /** * \brief Allocate memory for a sector bitmap. * * Each data block is preceded by a sector bitmap. Each bit indicates whether the corresponding sector * is considered 'clean' or 'dirty' (for sparse VHD images), or whether to read from the parent or current * image (for differencing images). * * \param [in] vhdm MiniVHD data structure * \param [out] err this is populated with MVHD_ERR_MEM if the calloc fails * * \retval -1 if an error occurrs. Check value of err in this case * \retval 0 if the function call succeeds */ static int init_sector_bitmap(MVHDMeta* vhdm, MVHDError* err) { vhdm->bitmap.curr_bitmap = calloc(vhdm->bitmap.sector_count, MVHD_SECTOR_SIZE); if (vhdm->bitmap.curr_bitmap == NULL) { *err = MVHD_ERR_MEM; return -1; } vhdm->bitmap.curr_block = -1; return 0; } /** * \brief Check if the path for a given platform code exists * * From the available paths, both relative and absolute, construct a full path * and attempt to open a file at that path. * * Note, this function makes no attempt to verify that the path is the correct * VHD image, or even a VHD image at all. * * \param [in] paths a struct containing all available paths to work with * \param [in] the platform code to try and obtain a path for. Setting this to zero * will try using the directory of the child image * * \retval true if a file is found * \retval false if a file is not found */ static bool mvhd_parent_path_exists(struct MVHDPaths* paths, uint32_t plat_code) { FILE* f; int ferr; size_t cwk_ret; enum cwk_path_style style; memset(paths->joined_path, 0, sizeof paths->joined_path); style = cwk_path_guess_style((const char*)paths->dir_path); cwk_path_set_style(style); cwk_ret = 1; if (plat_code == MVHD_DIF_LOC_W2RU && *paths->w2ru_path) { cwk_ret = cwk_path_join((const char*)paths->dir_path, (const char*)paths->w2ru_path, paths->joined_path, sizeof paths->joined_path); } else if (plat_code == MVHD_DIF_LOC_W2KU && *paths->w2ku_path) { memcpy(paths->joined_path, paths->w2ku_path, (sizeof paths->joined_path) - 1); cwk_ret = 0; } else if (plat_code == 0) { cwk_ret = cwk_path_join((const char*)paths->dir_path, (const char*)paths->file_name, paths->joined_path, sizeof paths->joined_path); } if (cwk_ret > MVHD_MAX_PATH_BYTES) { return false; } f = mvhd_fopen((const char*)paths->joined_path, "rb", &ferr); if (f != NULL) { /* We found a file at the requested path! */ memcpy(tmp_open_path, paths->joined_path, (sizeof paths->joined_path) - 1); tmp_open_path[sizeof tmp_open_path - 1] = '\0'; fclose(f); return true; } return false; } /** * \brief attempt to obtain a file path to a file that may be a valid VHD image * * Differential VHD images store both a UTF-16BE file name (or path), and up to * eight "parent locator" entries. Using this information, this function tries to * find a parent image. * * This function does not verify if the path returned is a valid parent image. * * \param [in] vhdm current MiniVHD data structure * \param [out] err any errors that may occurr. Check this if NULL is returned * * \return a pointer to the global string `tmp_open_path`, or NULL if a path could * not be found, or some error occurred */ static char* get_diff_parent_path(MVHDMeta* vhdm, int* err) { int utf_outlen, utf_inlen, utf_ret; char *par_fp = NULL; struct MVHDPaths *paths; size_t dirlen; /* We can't resolve relative paths if we don't have an absolute path to work with */ if (!cwk_path_is_absolute((const char*)vhdm->filename)) { *err = MVHD_ERR_PATH_REL; goto end; } paths = calloc(1, sizeof *paths); if (paths == NULL) { *err = MVHD_ERR_MEM; goto end; } cwk_path_get_dirname((const char*)vhdm->filename, &dirlen); if (dirlen >= sizeof paths->dir_path) { *err = MVHD_ERR_PATH_LEN; goto paths_cleanup; } memcpy(paths->dir_path, vhdm->filename, dirlen); /* Get the filename field from the sparse header. */ utf_outlen = (int)sizeof paths->file_name; utf_inlen = (int)sizeof vhdm->sparse.par_utf16_name; utf_ret = UTF16BEToUTF8((unsigned char*)paths->file_name, &utf_outlen, (const unsigned char*)vhdm->sparse.par_utf16_name, &utf_inlen); if (utf_ret < 0) { mvhd_set_encoding_err(utf_ret, err); goto paths_cleanup; } /* Now read the parent locator entries, both relative and absolute, if they exist */ unsigned char* loc_path; for (int i = 0; i < 8; i++) { utf_outlen = MVHD_MAX_PATH_BYTES - 1; if (vhdm->sparse.par_loc_entry[i].plat_code == MVHD_DIF_LOC_W2RU) { loc_path = (unsigned char*)paths->w2ru_path; } else if (vhdm->sparse.par_loc_entry[i].plat_code == MVHD_DIF_LOC_W2KU) { loc_path = (unsigned char*)paths->w2ku_path; } else { continue; } utf_inlen = vhdm->sparse.par_loc_entry[i].plat_data_len; if (utf_inlen > MVHD_MAX_PATH_BYTES) { *err = MVHD_ERR_PATH_LEN; goto paths_cleanup; } mvhd_fseeko64(vhdm->f, vhdm->sparse.par_loc_entry[i].plat_data_offset, SEEK_SET); (void) !fread(paths->tmp_src_path, sizeof (uint8_t), utf_inlen, vhdm->f); /* Note, the W2*u parent locators are UTF-16LE, unlike the filename field previously obtained, which is UTF-16BE */ utf_ret = UTF16LEToUTF8(loc_path, &utf_outlen, (const unsigned char*)paths->tmp_src_path, &utf_inlen); if (utf_ret < 0) { mvhd_set_encoding_err(utf_ret, err); goto paths_cleanup; } } /* We have paths in UTF-8. We should have enough info to try and find the parent VHD */ /* Does the relative path exist? */ if (mvhd_parent_path_exists(paths, MVHD_DIF_LOC_W2RU)) { par_fp = tmp_open_path; goto paths_cleanup; } /* What about trying the child directory? */ if (mvhd_parent_path_exists(paths, 0)) { par_fp = tmp_open_path; goto paths_cleanup; } /* Well, all else fails, try the stored absolute path, if it exists */ if (mvhd_parent_path_exists(paths, MVHD_DIF_LOC_W2KU)) { par_fp = tmp_open_path; goto paths_cleanup; } /* If we reach this point, we could not find a path with a valid file */ par_fp = NULL; *err = MVHD_ERR_PAR_NOT_FOUND; paths_cleanup: free(paths); paths = NULL; end: return par_fp; } /** * \brief Attach the read/write function pointers to read/write functions * * Depending on the VHD type, different sector reading and writing functions are used. * The functions are called via function pointers stored in the vhdm struct. * * \param [in] vhdm MiniVHD data structure */ static void assign_io_funcs(MVHDMeta* vhdm) { switch (vhdm->footer.disk_type) { case MVHD_TYPE_FIXED: vhdm->read_sectors = mvhd_fixed_read; vhdm->write_sectors = mvhd_fixed_write; break; case MVHD_TYPE_DYNAMIC: vhdm->read_sectors = mvhd_sparse_read; vhdm->write_sectors = mvhd_sparse_diff_write; break; case MVHD_TYPE_DIFF: vhdm->read_sectors = mvhd_diff_read; vhdm->write_sectors = mvhd_sparse_diff_write; break; } if (vhdm->readonly) vhdm->write_sectors = mvhd_noop_write; } /** * \brief Return the library version as a string */ MVHDAPI const char * mvhd_version(void) { return LIB_VERSION_4; } /** * \brief Return the library version as a number */ MVHDAPI uint32_t mvhd_version_id(void) { return (LIB_VER_MAJOR << 24) | (LIB_VER_MINOR << 16) | (LIB_VER_REV << 16) | LIB_VER_PATCH; } /** * \brief A simple test to see if a given file is a VHD * * \param [in] f file to test * * \retval 1 if f is a VHD * \retval 0 if f is not a VHD */ MVHDAPI int mvhd_file_is_vhd(FILE* f) { uint8_t con_str[8] = { 0 }; if (f == NULL) return 0; mvhd_fseeko64(f, -MVHD_FOOTER_SIZE, SEEK_END); (void) !fread(con_str, sizeof con_str, 1, f); if (mvhd_is_conectix_str(con_str)) return 1; return 0; } MVHDAPI MVHDGeom mvhd_calculate_geometry(uint64_t size) { MVHDGeom chs = { 0 }; uint32_t ts = (uint32_t)(size / MVHD_SECTOR_SIZE); uint32_t spt, heads, cyl, cth; if (ts > 65535 * 16 * 255) ts = 65535 * 16 * 255; if (ts >= 65535 * 16 * 63) { spt = 255; heads = 16; cth = ts / spt; } else { spt = 17; cth = ts / spt; heads = (cth + 1023) / 1024; if (heads < 4) heads = 4; if (cth >= (heads * 1024) || heads > 16) { spt = 31; heads = 16; cth = ts / spt; } if (cth >= (heads * 1024)) { spt = 63; heads = 16; cth = ts / spt; } } cyl = cth / heads; chs.heads = heads; chs.spt = spt; chs.cyl = cyl; return chs; } MVHDAPI MVHDMeta* mvhd_open(const char* path, int readonly, int* err) { MVHDError open_err = { 0 }; MVHDMeta *vhdm = calloc(sizeof *vhdm, 1); if (vhdm == NULL) { *err = MVHD_ERR_MEM; goto end; } if (strlen(path) >= sizeof vhdm->filename) { *err = MVHD_ERR_PATH_LEN; goto cleanup_vhdm; } //This is safe, as we've just checked for potential overflow above strcpy(vhdm->filename, path); if (readonly) vhdm->f = mvhd_fopen((const char*)vhdm->filename, "rb", err); else vhdm->f = mvhd_fopen((const char*)vhdm->filename, "rb+", err); if (vhdm->f == NULL) { /* note, mvhd_fopen sets err for us */ goto cleanup_vhdm; } vhdm->readonly = readonly; if (!mvhd_file_is_vhd(vhdm->f)) { *err = MVHD_ERR_NOT_VHD; goto cleanup_file; } read_footer(vhdm); if (!footer_checksum_valid(vhdm)) { *err = MVHD_ERR_FOOTER_CHECKSUM; goto cleanup_file; } if (vhdm->footer.disk_type == MVHD_TYPE_DIFF || vhdm->footer.disk_type == MVHD_TYPE_DYNAMIC) { read_sparse_header(vhdm); if (!sparse_checksum_valid(vhdm)) { *err = MVHD_ERR_SPARSE_CHECKSUM; goto cleanup_file; } if (read_bat(vhdm, &open_err) == -1) { *err = open_err; goto cleanup_file; } calc_sparse_values(vhdm); if (init_sector_bitmap(vhdm, &open_err) == -1) { *err = open_err; goto cleanup_bat; } } else if (vhdm->footer.disk_type != MVHD_TYPE_FIXED) { *err = MVHD_ERR_TYPE; goto cleanup_bitmap; } assign_io_funcs(vhdm); vhdm->format_buffer.zero_data = calloc(64, MVHD_SECTOR_SIZE); if (vhdm->format_buffer.zero_data == NULL) { *err = MVHD_ERR_MEM; goto cleanup_bitmap; } vhdm->format_buffer.sector_count = 64; if (vhdm->footer.disk_type == MVHD_TYPE_DIFF) { char* par_path = get_diff_parent_path(vhdm, err); if (par_path == NULL) goto cleanup_format_buff; uint32_t par_mod_ts = mvhd_file_mod_timestamp(par_path, err); if (*err != 0) goto cleanup_format_buff; if (vhdm->sparse.par_timestamp != par_mod_ts) { /* The last-modified timestamp is to fragile to make this a fatal error. Instead, we inform the caller of the potential problem. */ *err = MVHD_ERR_TIMESTAMP; } vhdm->parent = mvhd_open(par_path, true, err); if (vhdm->parent == NULL) goto cleanup_format_buff; if (memcmp(vhdm->sparse.par_uuid, vhdm->parent->footer.uuid, sizeof vhdm->sparse.par_uuid) != 0) { *err = MVHD_ERR_INVALID_PAR_UUID; goto cleanup_format_buff; } } /* * If we've reached this point, we are good to go, * so skip the cleanup steps. */ goto end; cleanup_format_buff: free(vhdm->format_buffer.zero_data); vhdm->format_buffer.zero_data = NULL; cleanup_bitmap: free(vhdm->bitmap.curr_bitmap); vhdm->bitmap.curr_bitmap = NULL; cleanup_bat: free(vhdm->block_offset); vhdm->block_offset = NULL; cleanup_file: fclose(vhdm->f); vhdm->f = NULL; cleanup_vhdm: free(vhdm); vhdm = NULL; end: return vhdm; } MVHDAPI void mvhd_close(MVHDMeta* vhdm) { if (vhdm == NULL) return; if (vhdm->parent != NULL) mvhd_close(vhdm->parent); fclose(vhdm->f); if (vhdm->block_offset != NULL) { free(vhdm->block_offset); vhdm->block_offset = NULL; } if (vhdm->bitmap.curr_bitmap != NULL) { free(vhdm->bitmap.curr_bitmap); vhdm->bitmap.curr_bitmap = NULL; } if (vhdm->format_buffer.zero_data != NULL) { free(vhdm->format_buffer.zero_data); vhdm->format_buffer.zero_data = NULL; } free(vhdm); } MVHDAPI int mvhd_diff_update_par_timestamp(MVHDMeta* vhdm, int* err) { uint8_t sparse_buff[1024] = { 0 }; if (vhdm == NULL || err == NULL) { *err = MVHD_ERR_INVALID_PARAMS; return -1; } if (vhdm->footer.disk_type != MVHD_TYPE_DIFF) { *err = MVHD_ERR_TYPE; return -1; } char* par_path = get_diff_parent_path(vhdm, err); if (par_path == NULL) { return -1; } uint32_t par_mod_ts = mvhd_file_mod_timestamp(par_path, err); if (*err != 0) { return -1; } /* Update the timestamp and sparse header checksum */ vhdm->sparse.par_timestamp = par_mod_ts; vhdm->sparse.checksum = mvhd_gen_sparse_checksum(&vhdm->sparse); /* Generate and write the updated sparse header */ mvhd_header_to_buffer(&vhdm->sparse, sparse_buff); mvhd_fseeko64(vhdm->f, (int64_t)vhdm->footer.data_offset, SEEK_SET); fwrite(sparse_buff, sizeof sparse_buff, 1, vhdm->f); return 0; } MVHDAPI int mvhd_read_sectors(MVHDMeta* vhdm, uint32_t offset, int num_sectors, void* out_buff) { return vhdm->read_sectors(vhdm, offset, num_sectors, out_buff); } MVHDAPI int mvhd_write_sectors(MVHDMeta* vhdm, uint32_t offset, int num_sectors, void* in_buff) { return vhdm->write_sectors(vhdm, offset, num_sectors, in_buff); } MVHDAPI int mvhd_format_sectors(MVHDMeta* vhdm, uint32_t offset, int num_sectors) { int num_full = num_sectors / vhdm->format_buffer.sector_count; int remain = num_sectors % vhdm->format_buffer.sector_count; for (int i = 0; i < num_full; i++) { vhdm->write_sectors(vhdm, offset, vhdm->format_buffer.sector_count, vhdm->format_buffer.zero_data); offset += vhdm->format_buffer.sector_count; } vhdm->write_sectors(vhdm, offset, remain, vhdm->format_buffer.zero_data); return 0; } MVHDAPI MVHDType mvhd_get_type(MVHDMeta* vhdm) { return vhdm->footer.disk_type; }