/* * MiniVHD Minimalist VHD implementation in C. * * This file is part of the MiniVHD Project. * * Version: @(#)create.c 1.0.3 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 #include "minivhd.h" #include "internal.h" #include "cwalk.h" #include "xml2_encoding.h" static const char MVHD_CONECTIX_COOKIE[] = "conectix"; static const char MVHD_CREATOR[] = "mVHD"; static const char MVHD_CREATOR_HOST_OS[] = "Wi2k"; static const char MVHD_CXSPARSE_COOKIE[] = "cxsparse"; /** * \brief Populate a VHD footer * * \param [in] footer to populate * \param [in] size_in_bytes is the total size of the virtual hard disk in bytes * \param [in] geom to use * \param [in] type of HVD that is being created * \param [in] sparse_header_off, an absolute file offset to the sparse header. Not used for fixed VHD images */ static void gen_footer(MVHDFooter* footer, uint64_t size_in_bytes, MVHDGeom* geom, MVHDType type, uint64_t sparse_header_off) { memcpy(footer->cookie, MVHD_CONECTIX_COOKIE, sizeof footer->cookie); footer->features = 0x00000002; footer->fi_fmt_vers = 0x00010000; footer->data_offset = (type == MVHD_TYPE_DIFF || type == MVHD_TYPE_DYNAMIC) ? sparse_header_off : 0xffffffffffffffff; footer->timestamp = vhd_calc_timestamp(); memcpy(footer->cr_app, MVHD_CREATOR, sizeof footer->cr_app); footer->cr_vers = 0x000e0000; memcpy(footer->cr_host_os, MVHD_CREATOR_HOST_OS, sizeof footer->cr_host_os); footer->orig_sz = footer->curr_sz = size_in_bytes; footer->geom.cyl = geom->cyl; footer->geom.heads = geom->heads; footer->geom.spt = geom->spt; footer->disk_type = type; mvhd_generate_uuid(footer->uuid); footer->checksum = mvhd_gen_footer_checksum(footer); } /** * \brief Populate a VHD sparse header * * \param [in] header for sparse and differencing images * \param [in] num_blks is the number of data blocks that the image contains * \param [in] bat_offset is the absolute file offset for start of the Block Allocation Table * \param [in] block_size_in_sectors is the block size in sectors. */ static void gen_sparse_header(MVHDSparseHeader* header, uint32_t num_blks, uint64_t bat_offset, uint32_t block_size_in_sectors) { memcpy(header->cookie, MVHD_CXSPARSE_COOKIE, sizeof header->cookie); header->data_offset = 0xffffffffffffffff; header->bat_offset = bat_offset; header->head_vers = 0x00010000; header->max_bat_ent = num_blks; header->block_sz = block_size_in_sectors * (uint32_t)MVHD_SECTOR_SIZE; header->checksum = mvhd_gen_sparse_checksum(header); } /** * \brief Generate parent locators for differencing VHD images * * \param [in] header the sparse header to populate with parent locator entries * \param [in] child_path is the full path to the VHD being created * \param [in] par_path is the full path to the parent image * \param [in] start_offset is the absolute file offset from where to start storing the entries themselves. Must be sector aligned. * \param [out] w2ku_path_buff is a buffer containing the full path to the parent, encoded as UTF16-LE * \param [out] w2ru_path_buff is a buffer containing the relative path to the parent, encoded as UTF16-LE * \param [out] err indicates what error occurred, if any * * \retval 0 if success * \retval < 0 if an error occurrs. Check value of *err for actual error */ static int gen_par_loc(MVHDSparseHeader* header, const char* child_path, const char* par_path, uint64_t start_offset, mvhd_utf16* w2ku_path_buff, mvhd_utf16* w2ru_path_buff, MVHDError* err) { /* Get our paths to store in the differencing VHD. We want both the absolute path to the parent, as well as the relative path from the child VHD */ int rv = 0; char* par_filename; size_t par_fn_len; char rel_path[MVHD_MAX_PATH_BYTES] = {0}; char child_dir[MVHD_MAX_PATH_BYTES] = {0}; size_t child_dir_len; if (strlen(child_path) < sizeof child_dir) { strcpy(child_dir, child_path); } else { *err = MVHD_ERR_PATH_LEN; rv = -1; goto end; } cwk_path_get_basename(par_path, (const char**)&par_filename, &par_fn_len); cwk_path_get_dirname(child_dir, &child_dir_len); child_dir[child_dir_len] = '\0'; size_t rel_len = cwk_path_get_relative(child_dir, par_path, rel_path, sizeof rel_path); if (rel_len > sizeof rel_path) { *err = MVHD_ERR_PATH_LEN; rv = -1; goto end; } /* We have our paths, now store the parent filename directly in the sparse header. */ int outlen = sizeof header->par_utf16_name; int utf_ret; utf_ret = UTF8ToUTF16BE((unsigned char*)header->par_utf16_name, &outlen, (const unsigned char*)par_filename, (int*)&par_fn_len); if (utf_ret < 0) { mvhd_set_encoding_err(utf_ret, (int*)err); rv = -1; goto end; } /* And encode the paths to UTF16-LE */ size_t par_path_len = strlen(par_path); outlen = sizeof *w2ku_path_buff * MVHD_MAX_PATH_CHARS; utf_ret = UTF8ToUTF16LE((unsigned char*)w2ku_path_buff, &outlen, (const unsigned char*)par_path, (int*)&par_path_len); if (utf_ret < 0) { mvhd_set_encoding_err(utf_ret, (int*)err); rv = -1; goto end; } int w2ku_len = utf_ret; outlen = sizeof *w2ru_path_buff * MVHD_MAX_PATH_CHARS; utf_ret = UTF8ToUTF16LE((unsigned char*)w2ru_path_buff, &outlen, (const unsigned char*)rel_path, (int*)&rel_len); if (utf_ret < 0) { mvhd_set_encoding_err(utf_ret, (int*)err); rv = -1; goto end; } int w2ru_len = utf_ret; /** * Finally populate the parent locaters in the sparse header. * This is the information needed to find the paths saved elsewhere * in the VHD image */ /* Note about the plat_data_space field: The VHD spec says this field stores the number of sectors needed to store the locator path. * However, Hyper-V and VPC store the number of bytes, not the number of sectors, and will refuse to open VHDs which have the * number of sectors in this field. * See https://stackoverflow.com/questions/40760181/mistake-in-virtual-hard-disk-image-format-specification */ header->par_loc_entry[0].plat_code = MVHD_DIF_LOC_W2KU; header->par_loc_entry[0].plat_data_len = (uint32_t)w2ku_len; header->par_loc_entry[0].plat_data_offset = (uint64_t)start_offset; header->par_loc_entry[0].plat_data_space = ((header->par_loc_entry[0].plat_data_len / MVHD_SECTOR_SIZE) + 1) * MVHD_SECTOR_SIZE; header->par_loc_entry[1].plat_code = MVHD_DIF_LOC_W2RU; header->par_loc_entry[1].plat_data_len = (uint32_t)w2ru_len; header->par_loc_entry[1].plat_data_offset = (uint64_t)start_offset + ((uint64_t)header->par_loc_entry[0].plat_data_space); header->par_loc_entry[1].plat_data_space = ((header->par_loc_entry[1].plat_data_len / MVHD_SECTOR_SIZE) + 1) * MVHD_SECTOR_SIZE; goto end; end: return rv; } MVHDAPI MVHDMeta* mvhd_create_fixed(const char* path, MVHDGeom geom, int* err, mvhd_progress_callback progress_callback) { uint64_t size_in_bytes = mvhd_calc_size_bytes(&geom); return mvhd_create_fixed_raw(path, NULL, size_in_bytes, &geom, err, progress_callback); } /** * \brief internal function that implements public mvhd_create_fixed() functionality * * Contains one more parameter than the public function, to allow using an existing * raw disk image as the data source for the new fixed VHD. * * \param [in] raw_image file handle to a raw disk image to populate VHD */ MVHDMeta* mvhd_create_fixed_raw(const char* path, FILE* raw_img, uint64_t size_in_bytes, MVHDGeom* geom, int* err, mvhd_progress_callback progress_callback) { uint8_t img_data[MVHD_SECTOR_SIZE] = {0}; uint8_t footer_buff[MVHD_FOOTER_SIZE] = {0}; if (geom == NULL || (geom->cyl == 0 || geom->heads == 0 || geom->spt == 0)) { *err = MVHD_ERR_INVALID_GEOM; return NULL; } MVHDMeta* vhdm = calloc(1, sizeof *vhdm); if (vhdm == NULL) { *err = MVHD_ERR_MEM; goto end; } FILE* fp = mvhd_fopen(path, "wb+", err); if (fp == NULL) { goto cleanup_vhdm; } mvhd_fseeko64(fp, 0, SEEK_SET); uint32_t size_sectors = (uint32_t)(size_in_bytes / MVHD_SECTOR_SIZE); uint32_t s; if (progress_callback) progress_callback(0, size_sectors); if (raw_img != NULL) { mvhd_fseeko64(raw_img, 0, SEEK_END); uint64_t raw_size = (uint64_t)mvhd_ftello64(raw_img); MVHDGeom raw_geom = mvhd_calculate_geometry(raw_size); if (mvhd_calc_size_bytes(&raw_geom) != raw_size) { *err = MVHD_ERR_CONV_SIZE; goto cleanup_vhdm; } gen_footer(&vhdm->footer, raw_size, geom, MVHD_TYPE_FIXED, 0); mvhd_fseeko64(raw_img, 0, SEEK_SET); for (s = 0; s < size_sectors; s++) { (void) !fread(img_data, sizeof img_data, 1, raw_img); fwrite(img_data, sizeof img_data, 1, fp); if (progress_callback) progress_callback(s + 1, size_sectors); } } else { gen_footer(&vhdm->footer, size_in_bytes, geom, MVHD_TYPE_FIXED, 0); for (s = 0; s < size_sectors; s++) { fwrite(img_data, sizeof img_data, 1, fp); if (progress_callback) progress_callback(s + 1, size_sectors); } } mvhd_footer_to_buffer(&vhdm->footer, footer_buff); fwrite(footer_buff, sizeof footer_buff, 1, fp); fclose(fp); fp = NULL; free(vhdm); vhdm = mvhd_open(path, false, err); goto end; cleanup_vhdm: free(vhdm); vhdm = NULL; end: return vhdm; } /** * \brief Create sparse or differencing VHD image. * * \param [in] path is the absolute path to the VHD file to create * \param [in] par_path is the absolute path to a parent image. If NULL, a sparse image is created, otherwise create a differencing image * \param [in] size_in_bytes is the total size in bytes of the virtual hard disk image * \param [in] geom is the HDD geometry of the image to create. Determines final image size * \param [in] block_size_in_sectors is the block size in sectors * \param [out] err indicates what error occurred, if any * * \return NULL if an error occurrs. Check value of *err for actual error. Otherwise returns pointer to a MVHDMeta struct */ static MVHDMeta* create_sparse_diff(const char* path, const char* par_path, uint64_t size_in_bytes, MVHDGeom* geom, uint32_t block_size_in_sectors, int* err) { uint8_t footer_buff[MVHD_FOOTER_SIZE] = {0}; uint8_t sparse_buff[MVHD_SPARSE_SIZE] = {0}; uint8_t bat_sect[MVHD_SECTOR_SIZE] = {0}; MVHDGeom par_geom = {0}; memset(bat_sect, 0xffffffff, sizeof bat_sect); MVHDMeta* vhdm = NULL; MVHDMeta* par_vhdm = NULL; mvhd_utf16* w2ku_path_buff = NULL; mvhd_utf16* w2ru_path_buff = NULL; uint32_t par_mod_timestamp = 0; if (par_path != NULL) { par_mod_timestamp = mvhd_file_mod_timestamp(par_path, err); if (*err != 0) { goto end; } par_vhdm = mvhd_open(par_path, true, err); if (par_vhdm == NULL) { goto end; } } vhdm = calloc(1, sizeof *vhdm); if (vhdm == NULL) { *err = MVHD_ERR_MEM; goto cleanup_par_vhdm; } if (par_vhdm != NULL) { /* We use the geometry from the parent VHD, not what was passed in */ par_geom.cyl = par_vhdm->footer.geom.cyl; par_geom.heads = par_vhdm->footer.geom.heads; par_geom.spt = par_vhdm->footer.geom.spt; geom = &par_geom; size_in_bytes = par_vhdm->footer.curr_sz; } else if (geom == NULL || (geom->cyl == 0 || geom->heads == 0 || geom->spt == 0)) { *err = MVHD_ERR_INVALID_GEOM; goto cleanup_vhdm; } FILE* fp = mvhd_fopen(path, "wb+", err); if (fp == NULL) { goto cleanup_vhdm; } mvhd_fseeko64(fp, 0, SEEK_SET); /* Note, the sparse header follows the footer copy at the beginning of the file */ if (par_path == NULL) { gen_footer(&vhdm->footer, size_in_bytes, geom, MVHD_TYPE_DYNAMIC, MVHD_FOOTER_SIZE); } else { gen_footer(&vhdm->footer, size_in_bytes, geom, MVHD_TYPE_DIFF, MVHD_FOOTER_SIZE); } mvhd_footer_to_buffer(&vhdm->footer, footer_buff); /* As mentioned, start with a copy of the footer */ fwrite(footer_buff, sizeof footer_buff, 1, fp); /** * Calculate the number of (2MB or 512KB) data blocks required to store the entire * contents of the disk image, followed by the number of sectors the * BAT occupies in the image. Note, the BAT is sector aligned, and is padded * to the next sector boundary * */ uint32_t size_in_sectors = (uint32_t)(size_in_bytes / MVHD_SECTOR_SIZE); uint32_t num_blks = size_in_sectors / block_size_in_sectors; if (size_in_sectors % block_size_in_sectors != 0) { num_blks += 1; } uint32_t num_bat_sect = num_blks / MVHD_BAT_ENT_PER_SECT; if (num_blks % MVHD_BAT_ENT_PER_SECT != 0) { num_bat_sect += 1; } /* Storing the BAT directly following the footer and header */ uint64_t bat_offset = MVHD_FOOTER_SIZE + MVHD_SPARSE_SIZE; uint64_t par_loc_offset = 0; /** * If creating a differencing VHD, populate the sparse header with additional * data about the parent image, and where to find it, and it's last modified timestamp * */ if (par_vhdm != NULL) { /** * Create output buffers to encode paths into. * The paths are not stored directly in the sparse header, hence the need to * store them in buffers to be written to the VHD image later */ w2ku_path_buff = calloc(MVHD_MAX_PATH_CHARS, sizeof * w2ku_path_buff); if (w2ku_path_buff == NULL) { *err = MVHD_ERR_MEM; goto end; } w2ru_path_buff = calloc(MVHD_MAX_PATH_CHARS, sizeof * w2ru_path_buff); if (w2ru_path_buff == NULL) { *err = MVHD_ERR_MEM; goto end; } memcpy(vhdm->sparse.par_uuid, par_vhdm->footer.uuid, sizeof vhdm->sparse.par_uuid); par_loc_offset = bat_offset + ((uint64_t)num_bat_sect * MVHD_SECTOR_SIZE) + (5 * MVHD_SECTOR_SIZE); if (gen_par_loc(&vhdm->sparse, path, par_path, par_loc_offset, w2ku_path_buff, w2ru_path_buff, (MVHDError*)err) < 0) { goto cleanup_vhdm; } vhdm->sparse.par_timestamp = par_mod_timestamp; } gen_sparse_header(&vhdm->sparse, num_blks, bat_offset, block_size_in_sectors); mvhd_header_to_buffer(&vhdm->sparse, sparse_buff); fwrite(sparse_buff, sizeof sparse_buff, 1, fp); /* The BAT sectors need to be filled with 0xffffffff */ for (uint32_t k = 0; k < num_bat_sect; k++) { fwrite(bat_sect, sizeof bat_sect, 1, fp); } mvhd_write_empty_sectors(fp, 5); /** * If creating a differencing VHD, the paths to the parent image need to be written * tp the file. Both absolute and relative paths are written * */ if (par_vhdm != NULL) { uint64_t curr_pos = (uint64_t)mvhd_ftello64(fp); /* Double check my sums... */ assert(curr_pos == par_loc_offset); /* Fill the space required for location data with zero */ uint8_t empty_sect[MVHD_SECTOR_SIZE] = {0}; for (int i = 0; i < 2; i++) { for (uint32_t j = 0; j < (vhdm->sparse.par_loc_entry[i].plat_data_space / MVHD_SECTOR_SIZE); j++) { fwrite(empty_sect, sizeof empty_sect, 1, fp); } } /* Now write the location entries */ mvhd_fseeko64(fp, vhdm->sparse.par_loc_entry[0].plat_data_offset, SEEK_SET); fwrite(w2ku_path_buff, vhdm->sparse.par_loc_entry[0].plat_data_len, 1, fp); mvhd_fseeko64(fp, vhdm->sparse.par_loc_entry[1].plat_data_offset, SEEK_SET); fwrite(w2ru_path_buff, vhdm->sparse.par_loc_entry[1].plat_data_len, 1, fp); /* and reset the file position to continue */ mvhd_fseeko64(fp, vhdm->sparse.par_loc_entry[1].plat_data_offset + vhdm->sparse.par_loc_entry[1].plat_data_space, SEEK_SET); mvhd_write_empty_sectors(fp, 5); } /* And finish with the footer */ fwrite(footer_buff, sizeof footer_buff, 1, fp); fclose(fp); fp = NULL; free(vhdm); vhdm = mvhd_open(path, false, err); goto end; cleanup_vhdm: free(vhdm); vhdm = NULL; cleanup_par_vhdm: if (par_vhdm != NULL) { mvhd_close(par_vhdm); } end: free(w2ku_path_buff); free(w2ru_path_buff); return vhdm; } MVHDAPI MVHDMeta* mvhd_create_sparse(const char* path, MVHDGeom geom, int* err) { uint64_t size_in_bytes = mvhd_calc_size_bytes(&geom); return create_sparse_diff(path, NULL, size_in_bytes, &geom, MVHD_BLOCK_LARGE, err); } MVHDAPI MVHDMeta* mvhd_create_diff(const char* path, const char* par_path, int* err) { return create_sparse_diff(path, par_path, 0, NULL, MVHD_BLOCK_LARGE, err); } MVHDAPI MVHDMeta* mvhd_create_ex(MVHDCreationOptions options, int* err) { uint32_t geom_sector_size; switch (options.type) { case MVHD_TYPE_FIXED: case MVHD_TYPE_DYNAMIC: geom_sector_size = mvhd_calc_size_sectors(&(options.geometry)); if ((options.size_in_bytes > 0 && (options.size_in_bytes % MVHD_SECTOR_SIZE) > 0) || (options.size_in_bytes > MVHD_MAX_SIZE_IN_BYTES) || (options.size_in_bytes == 0 && geom_sector_size == 0)) { *err = MVHD_ERR_INVALID_SIZE; return NULL; } if (options.size_in_bytes > 0 && ((uint64_t)geom_sector_size * MVHD_SECTOR_SIZE) > options.size_in_bytes) { *err = MVHD_ERR_INVALID_GEOM; return NULL; } if (options.size_in_bytes == 0) options.size_in_bytes = (uint64_t)geom_sector_size * MVHD_SECTOR_SIZE; if (geom_sector_size == 0) options.geometry = mvhd_calculate_geometry(options.size_in_bytes); break; case MVHD_TYPE_DIFF: if (options.parent_path == NULL) { *err = MVHD_ERR_FILE; return NULL; } break; default: *err = MVHD_ERR_TYPE; return NULL; } if (options.path == NULL) { *err = MVHD_ERR_FILE; return NULL; } if (options.type != MVHD_TYPE_FIXED) { if (options.block_size_in_sectors == MVHD_BLOCK_DEFAULT) options.block_size_in_sectors = MVHD_BLOCK_LARGE; if (options.block_size_in_sectors != MVHD_BLOCK_LARGE && options.block_size_in_sectors != MVHD_BLOCK_SMALL) { *err = MVHD_ERR_INVALID_BLOCK_SIZE; return NULL; } } switch (options.type) { case MVHD_TYPE_FIXED: return mvhd_create_fixed_raw(options.path, NULL, options.size_in_bytes, &(options.geometry), err, options.progress_callback); case MVHD_TYPE_DYNAMIC: return create_sparse_diff(options.path, NULL, options.size_in_bytes, &(options.geometry), options.block_size_in_sectors, err); case MVHD_TYPE_DIFF: return create_sparse_diff(options.path, options.parent_path, 0, NULL, options.block_size_in_sectors, err); } return NULL; /* Make the compiler happy */ } bool mvhd_is_conectix_str(const void* buffer) { if (strncmp(buffer, MVHD_CONECTIX_COOKIE, strlen(MVHD_CONECTIX_COOKIE)) == 0) { return true; } return false; }