Search code examples
c#xmliso

How to add a file to an existing ISO image using C# and save the updated ISO?


I am working on a project where I need to open an existing ISO file, add a specific file (e.g., an XML file) to it, and then save the updated ISO image. I've been using the DiscUtils library to read and write ISO files, but I am encountering issues when trying to modify the contents of the ISO.

Here's what I want to do:

Select an existing ISO file. Download an XML file from a URL (which I already have a method for). Add the downloaded XML file into the ISO. Save the updated ISO to a new file. I want to keep the existing contents of the ISO intact while adding the new file.

What I tried:

I used the DiscUtils library to open an existing ISO file, added a new XML file to the ISO, and then saved the updated ISO to a new file. Below is the code I used:

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using DiscUtils.Iso9660;

public async Task UpdateIsoWithXmlAsync(string isoFilePath, string xmlFilePath, string outputIsoPath)
{
    using (FileStream isoStream = File.OpenRead(isoFilePath))
    using (FileStream outputIsoStream = File.Create(outputIsoPath))
    {
        CDBuilder builder = new CDBuilder();
        builder.UseJoliet = true;
        builder.UseRockRidge = true;

        // Read the existing ISO content
        using (CDReader cdReader = new CDReader(isoStream, true))
        {
            foreach (string filePath in cdReader.GetFiles("/"))
            {
                string relativePath = filePath.TrimStart('/');
                using (Stream fileStream = cdReader.OpenFile(filePath, FileMode.Open, FileAccess.Read))
                {
                    builder.AddFile(relativePath, fileStream);
                }
            }
        }

        // Add the new XML file
        builder.AddFile("file.xml", xmlFilePath);

        // Build the updated ISO
        builder.Build(outputIsoStream);
    }
}

What I expected:

I expected the updated ISO to contain all the original files from the selected ISO, with the added XML file included. The size of the new ISO should be close to the original size, with the new XML file added at the appropriate location.

What actually happened:

The resulting ISO is much smaller than expected (around 3.81 MB), and the original contents do not seem to be correctly copied over. Instead of modifying the original ISO, it seems like a new ISO was created containing only the XML file. Additionally, the size of the ISO is significantly reduced.


Solution

  • It looks like NuGet package LTRData.DiscUtils is a fork of DiscUtils that's been updated. The code below shows how one can copy files from an existing ISO to a new ISO and then add one (or more) additional files to it. It keeps the original CreationTime of the files/folders and reads both UDF and CDFS (ISO9660). See the comments within the code for additional information.

    Prerequisites:

    Add the following using directives:

    • using System.Diagnostics;
    • using DiscUtils;
    • using DiscUtils.Iso9660;
    • using DiscUtils.Udf;
    • using DiscUtils.Vfs;

    code:

    private Task AddDirectoriesAndFilesAsync(VfsFileSystemFacade reader, CDBuilder builder, CancellationToken token)
    {
        //to maintain the original CreationTime and LastWriteTime (ie: DateModified)
        //add the directories to the target ISO before adding the files
        //
        //get directories from source ISO
    
        //get files in all directories
        IEnumerable<DiscDirectoryInfo> ieDiscDirectoryInfo = reader.Root.GetDirectories("*", SearchOption.AllDirectories);
    
        int totalDirs = ieDiscDirectoryInfo.Count();
        Debug.WriteLine($"totalDirs: {totalDirs}");
    
        //foreach (var dInfo in reader.Root.GetDirectories("*", SearchOption.AllDirectories))
        foreach (DiscDirectoryInfo dInfo in ieDiscDirectoryInfo)
        {
            if (token.IsCancellationRequested)
                throw new TaskCanceledException();
    
            Debug.WriteLine($"Processing directory '{dInfo.FullName}'...");
    
            //create directory in target ISO 
            BuildDirectoryInfo bdInfo = builder.AddDirectory(dInfo.FullName);
    
            //set CreationTime so the original CreationTime is used
            bdInfo.CreationTime = dInfo.CreationTimeUtc;
        }
    
        //get files in all directories
        IEnumerable<DiscFileInfo> ieDiscFileInfo = reader.Root.GetFiles("*", SearchOption.AllDirectories);
        
        int totalFiles = ieDiscFileInfo.Count();
        Debug.WriteLine($"totalFiles: {totalFiles}");
    
        //foreach (DiscFileInfo dfInfo in reader.Root.GetFiles("*", SearchOption.AllDirectories))
        foreach (DiscFileInfo dfInfo in ieDiscFileInfo)
        {
            if (token.IsCancellationRequested)
                throw new TaskCanceledException();
    
            Debug.WriteLine($"Processing file '{dfInfo.FullName}'...");
    
            using (Stream readerFs = reader.OpenFile(dfInfo.FullName, FileMode.Open))
            {
                if (readerFs.Length > 0)
                {
                    //add file
                    BuildFileInfo bfInfo = builder.AddFile(dfInfo.FullName, readerFs);
    
                    //set CreationTime so the original CreationTime is used
                    bfInfo.CreationTime = dfInfo.CreationTimeUtc;
    
                    //flush
                    readerFs.Flush();
                }
            }
        }
    
        return Task.CompletedTask;
    }
    
    public async Task<string> CreateIsoFromExistingIsoAsync(string isoSourceFilename, string isoTargetFilename, string isoVolumeIdentifier, byte[] xmlBytes, CancellationToken token)
    {
        bool useCdReader = false;
        Debug.WriteLine($"isoSourceFilename: '{isoSourceFilename}' isoTargetFilename: '{isoTargetFilename}'");
    
        using (FileStream isoTargetStream = File.Open(isoTargetFilename, FileMode.Create, FileAccess.ReadWrite))
        {
            //create new instance and set properties
            //DiscUtils.Iso9660.CDBuilder builder = new CDBuilder() { UseJoliet = true, VolumeIdentifier = isoVolumeIdentifier };
            CDBuilder builder = new CDBuilder() { UseJoliet = true, VolumeIdentifier = isoVolumeIdentifier };
    
            /*
            builder.ProgressChanged += (sender, e) =>
            {
                Debug.WriteLine($"TotalItems: {e.TotalItems}  TotalFiles (processed): {e.TotalFiles}");
            };
            */
    
            using (FileStream isoSourceStream = File.Open(isoSourceFilename, FileMode.Open, FileAccess.Read))
            {
                
                try
                {
                    //create new instance
                    //
                    //if Iso9660 (CDFS) was used to generate the ISO the following line will throw
                    //System.IO.InvalidDataException; if this occurs, use ISO9660 (CDFS/CdReader) to read
                    //the file.
    
                    UdfReader udfReader = new UdfReader(isoSourceStream);
    
                    Debug.WriteLine("Using UdfReader...");
                    await AddDirectoriesAndFilesAsync(udfReader, builder, token);
                }
                catch (System.IO.InvalidDataException ex)
                {
                    //ISO isn't UDF
                    Debug.WriteLine($"InvalidDataException - {ex.Message}");
    
                    //set value to indicate that ISO9660 should be used (ie: CdReader)
                    useCdReader = true; //set value
                }
                catch(TaskCanceledException ex)
                {
                    //task cancelled by user
                    Debug.WriteLine($"TaskCanceledException - {ex.Message}");
    
                    return "Task canceled by user.";
                }
    
                if (useCdReader)
                {
                    try
                    {
                        CDReader cdReader = new CDReader(isoSourceStream, true, true);
                        await AddDirectoriesAndFilesAsync(cdReader, builder, token);
                    }
                    catch (TaskCanceledException ex)
                    {
                        //task cancelled by user
                        Debug.WriteLine($"TaskCanceledException - {ex.Message}");
    
                        return "Task canceled by user.";
                    }
                }
    
                //ToDo: add any additional desired file(s) to ISO
                builder.AddFile("myConfig.xml", xmlBytes);
    
                //build/create ISO
                await builder.BuildAsync(isoTargetStream, token);
    
                return $"Successfully completed. Filename: '{isoTargetFilename}'.";
            }
        }
    }
    

    Usage:

    Prerequisites:

    • Source.iso exists in your documents folder
    • myConfig.xml exists in your documents folder

    Note Add button (name: buttonCreateIsoFromIsoAsync) to form.

    
    private CancellationToken _token;
    private CancellationTokenSource _tokenSource = new CancellationTokenSource();
    
    private async void buttonCreateIsoFromIsoAsync_Click(object sender, EventArgs e)
    {
        //create new instance
        _tokenSource = new CancellationTokenSource();
    
        //create reference
        _token = _tokenSource.Token;
    
        string result = await CreateIsoFromExistingIsoAsync(@"Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Source.iso", Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Target.iso"), "Modified ISO", File.ReadAllBytes(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "myConfig.xml", _token);
        System.Diagnostics.Debug.WriteLine($"Info: {result}");
    }