resourcelib icon indicating copy to clipboard operation
resourcelib copied to clipboard

Loading from file doesn't seem to keep language id

Open redwyre opened this issue 13 years ago • 12 comments

When I load the resource info and save it back to the same file, it's appearing as "English (United States)" instead of the original language id. Is this correct behaviour?

I'm using the 1.3 binary. Here is the code I'm using (in powershell, but similar to the example in the help file):

Add-Type -path "Vestris.ResourceLib.dll"

function SetFileVersions([string]$file, [string]$fileVersion, [string]$productVersion)
{
    $filePath = Resolve-Path $file

    $versionResource = new-object -typename Vestris.ResourceLib.VersionResource

    $versionResource.LoadFrom($filePath)

    $versionResource.FileVersion = $fileVersion

    $versionResource.ProductVersion = $productVersion

    $stringFileInfo = $versionResource["StringFileInfo"];

    $stringFileInfo["FileVersion"] = $versionResource.FileVersion

    $stringFileInfo["ProductVersion"] = $versionResource.ProductVersion

    $versionResource.SaveTo($filePath);
}

SetFileVersions "test.exe" "1.1.1.1" "2.2.2.2"

redwyre avatar Jul 06 '12 10:07 redwyre

It's certainly not by design.

When you create an instance of VersionResource it's created as US-English by default, so I am going to guess that LoadFrom is not assigning the language in the resource.

It would be helpful if you wrote a failing C# unit test in the ResourceLib project with a tiny binary (there're a number of such tests already) and made a pull request with it. I could take a look. Of course once you have a test you might even find a fix :)

dblock avatar Jul 06 '12 12:07 dblock

Did you ever get to the bottom of this?

dblock avatar Apr 14 '13 14:04 dblock

No, sorry. I tried to follow it though the code but ended up confusing myself. From what I could tell it is loading the resource language, so I don't know what's going on. Due to time constraints I had to just manually set the language id.

redwyre avatar Apr 15 '13 09:04 redwyre

I am using the 1.4 version and I get a "The specified resource language ID cannot be found in the image file" exception when using LoadFrom() for a file with an NT_VERSION resource that isn't English. (As in the code provided by the OP.) The root cause of the exception is that the VersionResource class defaults it's Language property to English (LCID = 0x409 / 1033) and when you load a version resource of a different language it throws because it is explicitly looking for a resource tagged for the English LCID. If you create the VersionResource object and the set the Language property to Neutral then it will load the first located VersionInfo Resource regardless of it's language - there should only be one NT_VERSION resource anyhow.

VersionResource versionResource = new VersionResource();
versionResource.Language = ResourceUtil.NEUTRALLANGID;
versionResource.LoadFrom(filename);

Note that loading the file directly into a VersionResource object as in the code above WILL NOT update its Language property. Thus, I suspect the root cause of what you are seeing is due to English being the default. When you save you are accidentally overwriting the language in the file.

To avoid this problem load all of the resources of the file into a ResoruceInfo object and pull out it's VersionInfo Object as when it loads the resources it properly sets the VersionResource Language property. Make any modifications and save

using (ResourceInfo ri = new ResourceInfo())
{
     ri.Load(incomingFileName);
     incomingVersionResource = (VersionResource)ri[Kernel32.ResourceTypes.RT_VERSION].First();

     versionResource.SaveTo(incomingFileName);
}

I would probably consider loading using English as default and then not updating the Language property after the load as a bug.

bakerhillpins avatar May 14 '14 14:05 bakerhillpins

This sounds reasonable. I would take pull requests that address this.

dblock avatar May 14 '14 15:05 dblock

At present I don't have a lot of time to put together a solution. I suspect that integrating the ResourceInfo LoadFrom() code for the NT_VERSION resource into the VersionResource object would solve the problem as it parses out the language ID during the EnumResourceLanguages call.

bakerhillpins avatar May 14 '14 17:05 bakerhillpins

Issue is still here. Using 1.5.328.0 from nuget. When I just load VersionResource from exe and save it back untouched - instead of replace the existing resource (even the same), the ResourceLib adds new resource with English(US) language. However, the workaround from @bakerhillpins works, but this behavior doesn't sound logical.

yurigundorin avatar Nov 23 '16 18:11 yurigundorin

I debugged the project. The problem is that a direct load of the resource will be made with a fixed set of language to US English. If the file resource is neutral you will get the resource independend from the xhossen language. But when you save the resource it will create a second resource with the other (US English) language. I've tried with the VersionResource.

To get rid of this an EnumResourceLanguagesW must be done before (EnumResLangProc) and if there is one entry only, so choose them and take the language ID of this. Otherwise a handling of multiple languages is to implement. I would design the langId to UInt16?, when not set (NULL) then enumerate otherwise choose that language.


An already existing way is to use ResourceInfo and Load, there the resource language is correct retrieved.

ResourceInfo resourceInfos = new ResourceInfo();
ResourceId resourceId = new ResourceId(Kernel32.ResourceTypes.RT_VERSION);
VersionResource versionResource = null;

resourceInfos.Load(typeof(Program).Assembly.Location);

if(resourceInfos.Resources.ContainsKey(resourceId))
{
  if(resourceInfos.Resources[resourceId].Count == 1)
  {
    versionResource = (VersionResource)resourceInfos.Resources[resourceId][0];
  }
}

Possible implementation in VersionResource or all resources could be

public void LoadFrom(String filename,
                     UInt16? lang = null)
{
    if(lang.HasValue)
    {
        LoadFrom(filename, _type, _name, lang.Value);
    }
    else
    {
        ResourceInfo resources = new ResourceInfo();

        resources.Load(filename);

        if(resources.ResourceTypes.Contains(_type) &&
           resources.Resources[_type].Count == 1)
        {
            LoadFrom(filename, _type, _name, resources.Resources[_type][0].Language);
        }
        else
        {
            throw new InvalidOperationException("More that one languages found for version resource.");
        }
    }
}

VBWebprofi avatar May 13 '23 15:05 VBWebprofi

@VBWebprofi That makes sense. Want to contribute an implementation and a test?

dblock avatar May 14 '23 12:05 dblock

It's open to contribute. A bit better solution could be to implement a static method Load at Resource class and add a handy overload:

/// <summary>
/// Load resource from file
/// </summary>
/// <param name="filename">Path to an executable file.</param>
/// <param name="type">Resource type.</param>
/// <param name="name">Resource name.</param>
/// <param name="lang">Optional resource language. Default is null.</param>
/// <returns>Resource if exactly one found otherwise null.</returns>
/// <remarks>When returning null and no language was chosen, more than one language could exist.</remarks>
public static Resource Load(string filename, ResourceId type, ResourceId name, UInt16? lang = null)
{
    ResourceInfo resources = new ResourceInfo();
    List<Resource> resourceItems;

    resources.Load(filename);
    
    if(resources.ResourceTypes.Contains(type))
    {
        resourceItems = resources.Resources[type].FindAll(r => r.Name.Equals(name) &&
                                                               ((lang.HasValue &&
                                                                 r.Language.Equals(lang.Value)) ||
                                                               (!lang.HasValue)));
      
        if(resourceItems.Count.Equals(1))
        {
            return resourceItems[0];
        }
    }

    return null;
}

/// <summary>
/// Load resource from file
/// </summary>
/// <typeparam name="T">Type of resource.</typeparam>
/// <param name="filename">Path to an executable file.</param>
/// <param name="type">Resource type.</param>
/// <param name="name">Resource name.</param>
/// <param name="lang">Optional resource language. Default is null.</param>
/// <returns>Resource if exactly one found otherwise null.</returns>
/// <remarks>Resource type <typeparamref name="T"/> must match <paramref name="type"/>. When returning null and no language was chosen, more than one language could exist.</remarks>
public static T Load<T>(string filename, ResourceId type, ResourceId name, UInt16? lang = null)
    where T : Resource
{
    return (T)Load(filename, type, name, lang = null);
}

BTW: Haven't build any test with Visual Studio yet.

VBWebprofi avatar May 16 '23 20:05 VBWebprofi

Two more improvements in the code:

VersionResource (summary and initial language):

/// <summary>
/// A new language-neutral version resource.
/// </summary>
public VersionResource()
    : base(IntPtr.Zero,
        IntPtr.Zero,
        new ResourceId(Kernel32.ResourceTypes.RT_VERSION),
        new ResourceId(1),
        ResourceUtil.NEUTRALLANGID,
        0)
{
    _header.Header = new Kernel32.RESOURCE_HEADER(_fixedfileinfo.Size);
}

ResourceUtil (restrict type to struct):

/// <summary>
/// Returns the memory representation of an object.
/// </summary>
/// <typeparam name="T">Object type.</typeparam>
/// <param name="anything">Data.</param>
/// <returns>Object's representation in memory.</returns>
internal static byte[] GetBytes<T>(T anything)
    where T: struct
{
    int rawsize = Marshal.SizeOf(anything);
    IntPtr buffer = Marshal.AllocHGlobal(rawsize);
    Marshal.StructureToPtr(anything, buffer, false);
    byte[] rawdatas = new byte[rawsize];
    Marshal.Copy(buffer, rawdatas, 0, rawsize);
    Marshal.FreeHGlobal(buffer);
    return rawdatas;
}

VBWebprofi avatar May 16 '23 20:05 VBWebprofi

Go for it!

dblock avatar May 16 '23 21:05 dblock