diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index f0b55d4..854b022 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -119,6 +119,29 @@ stages:
# displayName: 'Publish Quality Gate Result'
# continueOnError: true
+ - task: VisualStudioTestPlatformInstaller@1
+ condition: succeeded()
+ displayName: 'Install VS Test Platform'
+ inputs:
+ versionSelector: latestStable
+
+ - task: VSTest@2
+ condition: succeeded()
+ displayName: 'Running Unit Tests'
+ continueOnError: false
+ inputs:
+ testSelector: 'testAssemblies'
+ testAssemblyVer2: |
+ **\*Tests*.dll
+ !**\obj\**
+ !**\TestAdapter\**
+ searchFolder: '$(System.DefaultWorkingDirectory)'
+ platform: '$(BuildPlatform)'
+ configuration: '$(BuildConfiguration)'
+ diagnosticsEnabled: true
+ vsTestVersion: toolsInstaller
+ codeCoverageEnabled: true
+
- task: DotNetCoreCLI@2
displayName: Install Sign Client CLI
condition: >-
@@ -220,6 +243,22 @@ stages:
command: 'build'
projects: 'test/SmokeTest/SmokeTest.csproj'
arguments: '--configuration $(buildConfiguration) --no-restore'
+
+ - task: PowerShell@2
+ displayName: Validate SmokeTest native checksum
+ inputs:
+ targetType: inline
+ script: |
+ $expectedChecksum = '0x1DA81E42'
+ $actualChecksum = $env:NF_NATIVE_ASSEMBLY_CHECKSUM
+
+ if ($actualChecksum -ine $expectedChecksum)
+ {
+ Write-Host "##vso[task.logissue type=error]Smoke test assembly checksum mismatch: expected $expectedChecksum but got '$actualChecksum'."
+ exit 1
+ }
+
+ Write-Host "Smoke test assembly checksum matches expected value: $expectedChecksum."
- task: NuGetCommand@2
condition: >-
diff --git a/nanoFramework.NET.Sdk.sln b/nanoFramework.NET.Sdk.sln
index 2c0f67e..c52ad53 100644
--- a/nanoFramework.NET.Sdk.sln
+++ b/nanoFramework.NET.Sdk.sln
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "nanoFramework.Tools.BuildTa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "nanoFramework.NET.Sdk", "nanoFramework.NET.Sdk\nanoFramework.NET.Sdk.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildTasks.Tests", "test\BuildTasks.Tests\BuildTasks.Tests.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -21,5 +23,9 @@ Global
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets b/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets
index 9992936..c0aaacc 100644
--- a/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets
+++ b/nanoFramework.NET.Sdk/Sdk/nanoFramework.Resources.targets
@@ -28,15 +28,61 @@
Condition="'$(NF_MSBUILDTASK_PATH)' != ''"
AssemblyFile="$(NF_MSBUILDTASK_PATH)\nanoFramework.Tools.BuildTasks.dll" />
+
+
+
+ <_NanoResxItems Include="@(EmbeddedResource)"
+ Condition="'%(EmbeddedResource.Type)' == 'Resx'" />
+
+
+
+
+
+
+
+ <_NanoResManagerRef Include="@(PackageReference)"
+ Condition="'%(Identity)' == 'nanoFramework.ResourceManager'" />
+
+
+
+
+ DependsOnTargets="_NanoInterceptResxItems;_NanoCheckResourceManagerRef">
+ OutputResources="@(_NanoResxItems->'$(IntermediateOutputPath)%(ManifestResourceName).nanoresources')">
diff --git a/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj b/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj
index 8b66c0d..f5a5591 100644
--- a/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj
+++ b/nanoFramework.NET.Sdk/nanoFramework.NET.Sdk.csproj
@@ -102,6 +102,10 @@
+
+
+
+
diff --git a/nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs b/nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs
new file mode 100644
index 0000000..1360cc6
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/GenerateBinaryOutputTask.cs
@@ -0,0 +1,98 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// Portions Copyright (c) Microsoft Corporation. All rights reserved.
+// See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using nanoFramework.Tools.Utilities;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Linq;
+
+namespace nanoFramework.Tools
+{
+ [Description("GenerateBinaryOutputTaskEntry")]
+
+ public class GenerateBinaryOutputTask : Task
+ {
+ #region public properties for the task
+
+ public string Assembly { get; set; }
+
+ public string AssemblyPE { get; set; }
+
+ public ITaskItem[] AssemblyReferences { get; set; }
+
+
+ ///
+ /// The name(s) of binary file created.
+ ///
+ [Output]
+ public ITaskItem FileWritten { get; private set; }
+
+ #endregion
+
+ public override bool Execute()
+ {
+ // report to VS output window what step the build is
+ Log.LogMessage(MessageImportance.Normal, "Generating binary output file...");
+
+ // wait for debugger on var
+ DebuggerHelper.WaitForDebuggerIfEnabled(TasksConstants.BuildTaskDebugVar);
+
+ // default with null, indicating that we've generated nothing
+ FileWritten = null;
+
+ // get paths for PE files
+ List peCollection = AssemblyReferences?
+ .Select(a => Path.ChangeExtension(a.GetMetadata("FullPath"), ".pe"))
+ .ToList() ?? new List();
+
+ // add executable PE
+ peCollection.Add(AssemblyPE);
+
+ // get executable path and file name
+ // rename executable extension .exe with .bin
+ var binOutputFile = Assembly.Replace(".exe", ".bin");
+
+ using (FileStream binFile = new FileStream(binOutputFile, FileMode.Create))
+ {
+ // now we will re-deploy all system assemblies
+ foreach (string peItem in peCollection)
+ {
+ // append to the deploy blob the assembly
+ using (FileStream fs = File.Open(peItem, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ long length = (fs.Length + 3) / 4 * 4;
+ byte[] buffer = new byte[length];
+ int fileLength = (int)fs.Length;
+ int totalRead = 0;
+
+ while (totalRead < fileLength)
+ {
+ int bytesRead = fs.Read(buffer, totalRead, fileLength - totalRead);
+
+ if (bytesRead == 0)
+ {
+ throw new EndOfStreamException($"Unexpected end of file while reading '{peItem}'.");
+ }
+
+ totalRead += bytesRead;
+ }
+
+ // copy this assembly to the bin file too
+ binFile.Write(buffer, 0, (int)length);
+ }
+ }
+ }
+
+ // bin file written
+ FileWritten = new TaskItem(binOutputFile);
+
+ return true;
+ }
+ }
+}
diff --git a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs
deleted file mode 100644
index 1187d0e..0000000
--- a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.Stub.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-//
-// Copyright (c) .NET Foundation and Contributors
-// See LICENSE file in the project root for full license information.
-//
-// Stub implementation of GenerateNanoResourceTask for .NET 8+.
-// The full implementation uses System.Windows.Forms ResXResourceReader and
-// AppDomain APIs not available on modern .NET. Resource compilation is
-// supported when building with full .NET Framework (msbuild.exe / VS).
-//
-
-#if !NETFRAMEWORK
-
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-using System.ComponentModel;
-
-namespace nanoFramework.Tools
-{
- [Description("GenerateNanoResourceTaskEntry")]
- public class GenerateNanoResourceTask : Task
- {
- public ITaskItem[] Sources { get; set; }
- public ITaskItem[] References { get; set; }
- public bool UseSourcePath { get; set; }
- public string StateFile { get; set; }
-
- [Output]
- public ITaskItem[] OutputResources { get; set; }
-
- [Output]
- public ITaskItem[] FilesWritten { get; set; }
-
- public override bool Execute()
- {
- if (Sources == null || Sources.Length == 0)
- {
- return true;
- }
-
- Log.LogWarning(
- "NFSDK0001",
- "NFSDK0001",
- null, null, 0, 0, 0, 0,
- "GenerateNanoResourceTask is not yet supported when building with 'dotnet build'. " +
- "Resource (.resx) compilation requires building with full .NET Framework (msbuild.exe or Visual Studio). " +
- "This limitation will be addressed in a future SDK version.");
-
- return true;
- }
- }
-}
-
-#endif
diff --git a/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs
new file mode 100644
index 0000000..e33715e
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/GenerateNanoResourceTask.cs
@@ -0,0 +1,1282 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// Portions Copyright (c) Microsoft Corporation. All rights reserved.
+// See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using nanoFramework.Tools.Utilities;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Resources;
+using System.Security;
+using System.Text;
+using System.Xml;
+
+namespace nanoFramework.Tools
+{
+ static internal class ExceptionHandling
+ {
+ ///
+ /// If the given exception is file IO related then return.
+ /// Otherwise, rethrow the exception.
+ ///
+ /// The exception to check.
+ internal static void RethrowUnlessFileIO(Exception e)
+ {
+ if
+ (
+ e is UnauthorizedAccessException
+ || e is ArgumentNullException
+ || e is PathTooLongException
+ || e is DirectoryNotFoundException
+ || e is NotSupportedException
+ || e is ArgumentException
+ || e is SecurityException
+ || e is IOException
+ )
+ {
+ return;
+ }
+
+ Debug.Assert(false, "Exception unexpected for this File IO case. Please open a bug that we need to add a 'catch' block for this exception. Look at the build log for more details including a stack trace.");
+ throw e;
+ }
+ }
+
+
+ ///
+ /// Represents a cache of inputs to a compilation-style task.
+ ///
+ [Serializable()]
+ internal class Dependencies
+ {
+ ///
+ /// Hashtable of other dependency files.
+ /// Key is filename and value is DependencyFile.
+ ///
+ private Hashtable dependencies = new Hashtable();
+
+ ///
+ /// Look up a dependency file. Return null if its not there.
+ ///
+ ///
+ ///
+ internal DependencyFile GetDependencyFile(string filename)
+ {
+ return (DependencyFile)dependencies[filename];
+ }
+
+
+ ///
+ /// Add a new dependency file.
+ ///
+ ///
+ ///
+ internal void AddDependencyFile(string filename, DependencyFile file)
+ {
+ dependencies[filename] = file;
+ }
+
+ ///
+ /// Remove new dependency file.
+ ///
+ ///
+ ///
+ internal void RemoveDependencyFile(string filename)
+ {
+ dependencies.Remove(filename);
+ }
+
+ ///
+ /// Remove all entries from the dependency table.
+ ///
+ internal void Clear()
+ {
+ dependencies.Clear();
+ }
+
+ ///
+ /// All dependency files currently cached.
+ ///
+ internal ICollection Values
+ {
+ get { return dependencies.Values; }
+ }
+ }
+
+
+ ///
+ /// Represents a single input to a compilation-style task.
+ /// Keeps track of timestamp for later comparison.
+ ///
+ [Serializable]
+ internal class DependencyFile
+ {
+
+ // Whether the file exists or not.
+ readonly bool exists = false;
+
+ ///
+ /// The name of the file.
+ ///
+ ///
+ internal string FileName { get; }
+
+ ///
+ /// The last-modified timestamp when the class was instantiated.
+ ///
+ ///
+ internal DateTime LastModified { get; }
+
+ ///
+ /// Returns true if the file existed when this class was instantiated.
+ ///
+ ///
+ internal bool Exists
+ {
+ get { return exists; }
+ }
+
+ ///
+ /// Construct.
+ ///
+ /// The file name.
+ internal DependencyFile(string filename)
+ {
+ FileName = filename;
+
+ if (File.Exists(FileName))
+ {
+ LastModified = File.GetLastWriteTime(FileName);
+ exists = true;
+ }
+ else
+ {
+ exists = false;
+ }
+ }
+
+ ///
+ /// Construct from previously cached values (used when deserializing the state file).
+ ///
+ internal DependencyFile(string filename, DateTime lastModified, bool exists)
+ {
+ FileName = filename;
+ LastModified = lastModified;
+ this.exists = exists;
+ }
+
+ ///
+ /// Checks whether the file has changed since the last time a timestamp was recorded.
+ ///
+ ///
+ internal bool HasFileChanged()
+ {
+ // Obviously if the file no longer exists then we are not up to date.
+ if (!File.Exists(FileName))
+ {
+ return true;
+ }
+
+ // Check the saved timestamp against the current timestamp.
+ // If they are different then obviously we are out of date.
+ DateTime curLastModified = File.GetLastWriteTime(FileName);
+ if (curLastModified != LastModified)
+ {
+ return true;
+ }
+
+ // All checks passed -- the info should still be up to date.
+ return false;
+ }
+ }
+
+ ///
+ /// Base class for task state files.
+ ///
+ ///
+ /// Base class for task state files.
+ /// The state file is an incremental-build optimization (it caches the linked
+ /// files referenced by each .resx and their timestamps). It is serialized with
+ /// a simple, self-describing binary format so it works identically on .NET
+ /// Framework and modern .NET (the previous BinaryFormatter-based format is not
+ /// available on net8.0+). A corrupt or out-of-date cache is never fatal: callers
+ /// fall back to a fresh cache, so the format/version can change freely.
+ ///
+ internal abstract class StateFileBase
+ {
+ ///
+ /// Magic + version guarding the cache format. Bump
+ /// to invalidate caches written by an older build task.
+ ///
+ private const uint CacheMagic = 0x4E464443; // "NFDC"
+ protected const int CacheVersion = 1;
+
+ ///
+ /// Default constructor
+ ///
+ internal StateFileBase()
+ {
+ // do nothing
+ }
+
+ ///
+ /// Writes the derived state content to the (already opened) cache stream.
+ ///
+ protected abstract void WriteContent(BinaryWriter writer);
+
+ ///
+ /// Writes the contents of this object out to the specified file.
+ ///
+ ///
+ virtual internal void SerializeCache(string stateFile, TaskLoggingHelper log)
+ {
+ try
+ {
+ if (stateFile != null && stateFile.Length > 0)
+ {
+ if (File.Exists(stateFile))
+ {
+ File.Delete(stateFile);
+ }
+
+ using (FileStream s = new FileStream(stateFile, FileMode.CreateNew))
+ using (BinaryWriter writer = new BinaryWriter(s, Encoding.UTF8))
+ {
+ writer.Write(CacheMagic);
+ writer.Write(CacheVersion);
+ WriteContent(writer);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ // If there was a problem writing the file (like it's read-only or locked on disk, for
+ // example), then eat the exception and log a warning. Otherwise, rethrow.
+ ExceptionHandling.RethrowUnlessFileIO(e);
+
+ // Not being able to serialize the cache is not an error, but we let the user know anyway.
+ // Don't want to hold up processing just because we couldn't read the file.
+ log.LogWarning("Could not write state file {0} ({1})", stateFile, e.Message);
+ }
+ }
+
+ ///
+ /// Opens the state file and validates the header. Returns a positioned
+ /// ready for the derived content, or null when the
+ /// file is missing, unreadable, or written by an incompatible version.
+ ///
+ protected static BinaryReader OpenCacheForRead(string stateFile, TaskLoggingHelper log)
+ {
+ try
+ {
+ if (stateFile != null && stateFile.Length > 0 && File.Exists(stateFile))
+ {
+ FileStream s = new FileStream(stateFile, FileMode.Open);
+ BinaryReader reader = new BinaryReader(s, Encoding.UTF8);
+
+ if (reader.ReadUInt32() == CacheMagic && reader.ReadInt32() == CacheVersion)
+ {
+ return reader;
+ }
+
+ reader.Dispose();
+ }
+ }
+ catch (Exception e)
+ {
+ // Not being able to deserialize the cache is not an error, but we let the user know anyway.
+ log.LogWarning("Could not read state file {0} ({1})", stateFile, e.Message);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Deletes the state file from disk
+ ///
+ ///
+ ///
+ static internal void DeleteFile(string stateFile, TaskLoggingHelper log)
+ {
+ try
+ {
+ if (stateFile != null &&
+ stateFile.Length > 0 &&
+ File.Exists(stateFile))
+ {
+ File.Delete(stateFile);
+ }
+ }
+ catch (Exception e)
+ {
+ // If there was a problem deleting the file (like it's read-only or locked on disk, for
+ // example), then eat the exception and log a warning. Otherwise, rethrow.
+ ExceptionHandling.RethrowUnlessFileIO(e);
+
+ log.LogWarning("Could not delete state file {0} ({1})", stateFile, e.Message);
+ }
+ }
+ }
+
+
+ ///
+ /// This class is a caching mechanism for the resgen task to keep track of linked
+ /// files within processed .resx files.
+ ///
+ [Serializable()]
+ internal sealed class ResGenDependencies : StateFileBase
+ {
+ ///
+ /// The list of resx files.
+ ///
+ private Dependencies resXFiles = new Dependencies();
+
+ ///
+ /// A newly-created ResGenDependencies is not dirty.
+ /// What would be the point in saving the default?
+ ///
+ [NonSerialized]
+ private bool isDirty = false;
+
+ ///
+ /// This is the directory that will be used for resolution of files linked within a .resx.
+ /// If this is NULL then we use the directory in which the .resx is in (that should always
+ /// be the default!)
+ ///
+ private string baseLinkedFileDirectory;
+
+ ///
+ /// Construct.
+ ///
+ internal ResGenDependencies()
+ {
+ }
+
+ internal string BaseLinkedFileDirectory
+ {
+ get
+ {
+ return baseLinkedFileDirectory;
+ }
+ set
+ {
+ if (value == null && baseLinkedFileDirectory == null)
+ {
+ // No change
+ }
+ else if ((value == null && baseLinkedFileDirectory != null) ||
+ (value != null && baseLinkedFileDirectory == null) ||
+ (String.Compare(baseLinkedFileDirectory, value, true, CultureInfo.InvariantCulture) != 0))
+ {
+ // Ok, this is slightly complicated. Changing the base directory in any manner may
+ // result in changes to how we find .resx files. Therefore, we must clear our out
+ // cache whenever the base directory changes.
+ resXFiles.Clear();
+ isDirty = true;
+ baseLinkedFileDirectory = value;
+ }
+ }
+ }
+
+ internal bool UseSourcePath
+ {
+ set
+ {
+ // Ensure that the cache is properly initialized with respect to how resgen will
+ // resolve linked files within .resx files. ResGen has two different
+ // ways for resolving relative file-paths in linked files. The way
+ // that ResGen resolved relative paths before Whidbey was always to
+ // resolve from the current working directory. In Whidbey a new command-line
+ // switch "/useSourcePath" instructs ResGen to use the folder that
+ // contains the .resx file as the path from which it should resolve
+ // relative paths. So we should base our timestamp/existence checking
+ // on the same switch & resolve in the same manner as ResGen.
+ BaseLinkedFileDirectory = value ? null : Environment.CurrentDirectory;
+ }
+ }
+
+ internal ResXFile GetResXFileInfo(string resxFile)
+ {
+ // First, try to retrieve the resx information from our hashtable.
+ ResXFile retVal = (ResXFile)resXFiles.GetDependencyFile(resxFile);
+
+ if (retVal == null)
+ {
+ // Ok, the file wasn't there. Add it to our cache and return it to the caller.
+ retVal = AddResxFile(resxFile);
+ }
+ else
+ {
+ // The file was there. Is it up to date? If not, then we'll have to refresh the file
+ // by removing it from the hashtable and readding it.
+ if (retVal.HasFileChanged())
+ {
+ resXFiles.RemoveDependencyFile(resxFile);
+ isDirty = true;
+ retVal = AddResxFile(resxFile);
+ }
+ }
+
+ return retVal;
+ }
+
+ private ResXFile AddResxFile(string file)
+ {
+ // This method adds a .resx file "file" to our .resx cache. The method causes the file
+ // to be cracked for contained files.
+
+ ResXFile resxFile = new ResXFile(file, BaseLinkedFileDirectory);
+ resXFiles.AddDependencyFile(file, resxFile);
+ isDirty = true;
+ return resxFile;
+ }
+ ///
+ /// Writes the contents of this object out to the specified file.
+ ///
+ ///
+ override internal void SerializeCache(string stateFile, TaskLoggingHelper log)
+ {
+ base.SerializeCache(stateFile, log);
+ isDirty = false;
+ }
+
+ ///
+ /// Writes the cached .resx dependency information (see ).
+ ///
+ protected override void WriteContent(BinaryWriter writer)
+ {
+ writer.Write(baseLinkedFileDirectory != null);
+ if (baseLinkedFileDirectory != null)
+ {
+ writer.Write(baseLinkedFileDirectory);
+ }
+
+ ICollection values = resXFiles.Values;
+ writer.Write(values.Count);
+
+ foreach (ResXFile resxFile in values)
+ {
+ writer.Write(resxFile.FileName);
+ writer.Write(resxFile.LastModified.ToBinary());
+ writer.Write(resxFile.Exists);
+
+ if (resxFile.LinkedFiles == null)
+ {
+ writer.Write(-1);
+ }
+ else
+ {
+ writer.Write(resxFile.LinkedFiles.Length);
+ foreach (string linked in resxFile.LinkedFiles)
+ {
+ writer.Write(linked ?? string.Empty);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Reads the cached .resx dependency information back from the state file.
+ ///
+ private void ReadContent(BinaryReader reader)
+ {
+ baseLinkedFileDirectory = reader.ReadBoolean() ? reader.ReadString() : null;
+
+ int count = reader.ReadInt32();
+ for (int i = 0; i < count; i++)
+ {
+ string fileName = reader.ReadString();
+ DateTime lastModified = DateTime.FromBinary(reader.ReadInt64());
+ bool exists = reader.ReadBoolean();
+
+ int linkedCount = reader.ReadInt32();
+ string[] linkedFiles = null;
+ if (linkedCount >= 0)
+ {
+ linkedFiles = new string[linkedCount];
+ for (int j = 0; j < linkedCount; j++)
+ {
+ linkedFiles[j] = reader.ReadString();
+ }
+ }
+
+ resXFiles.AddDependencyFile(fileName, new ResXFile(fileName, lastModified, exists, linkedFiles));
+ }
+ }
+
+ ///
+ /// Reads the .cache file from disk into a ResGenDependencies object.
+ ///
+ ///
+ ///
+ ///
+ internal static ResGenDependencies DeserializeCache(string stateFile, bool useSourcePath, TaskLoggingHelper log)
+ {
+ ResGenDependencies retVal = new ResGenDependencies();
+
+ try
+ {
+ using (BinaryReader reader = OpenCacheForRead(stateFile, log))
+ {
+ if (reader != null)
+ {
+ retVal.ReadContent(reader);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ // A corrupt or partially-written cache is not fatal: start fresh.
+ log.LogWarning("Could not read state file {0} ({1})", stateFile, e.Message);
+ retVal = new ResGenDependencies();
+ }
+
+ // Ensure that the cache is properly initialized with respect to how resgen will
+ // resolve linked files within .resx files. ResGen has two different
+ // ways for resolving relative file-paths in linked files. The way
+ // that ResGen resolved relative paths before Whidbey was always to
+ // resolve from the current working directory. In Whidbey a new command-line
+ // switch "/useSourcePath" instructs ResGen to use the folder that
+ // contains the .resx file as the path from which it should resolve
+ // relative paths. So we should base our timestamp/existence checking
+ // on the same switch & resolve in the same manner as ResGen.
+ retVal.UseSourcePath = useSourcePath;
+
+ return retVal;
+ }
+
+ ///
+ /// Represents a single .resx file in the dependency cache.
+ ///
+ [Serializable()]
+ internal sealed class ResXFile : DependencyFile
+ {
+ internal string[] LinkedFiles { get; }
+
+ internal ResXFile(string filename, string baseLinkedFileDirectory)
+ : base(filename)
+ {
+ // Creates a new ResXFile object and populates the class member variables
+ // by computing a list of linked files within the .resx that was passed in.
+ //
+ // filename is the filename of the .resx file that is to be examined.
+
+ if (File.Exists(FileName))
+ {
+ LinkedFiles = ResXFile.GetLinkedFiles(filename, baseLinkedFileDirectory);
+ }
+ }
+
+ ///
+ /// Construct from previously cached values (used when deserializing the state file).
+ ///
+ internal ResXFile(string filename, DateTime lastModified, bool exists, string[] linkedFiles)
+ : base(filename, lastModified, exists)
+ {
+ LinkedFiles = linkedFiles;
+ }
+
+ ///
+ /// Given a .RESX file, returns all the linked files that are referenced within that .RESX.
+ ///
+ ///
+ ///
+ ///
+ /// May be thrown if Resx is invalid. May contain XmlException.
+ /// May be thrown if Resx is invalid
+ internal static string[] GetLinkedFiles(string filename, string baseLinkedFileDirectory)
+ {
+ // This method finds all linked .resx files for the .resx file that is passed in.
+ // filename is the filename of the .resx file that is to be examined.
+
+ // Construct the return array
+ ArrayList retVal = new ArrayList();
+
+#if NETFRAMEWORK
+ using (ResXResourceReader resxReader = new ResXResourceReader(filename))
+ {
+ // Tell the reader to return ResXDataNode's instead of the object type
+ // the resource becomes at runtime so we can figure out which files
+ // the .resx references
+ resxReader.UseResXDataNodes = true;
+
+ // First we need to figure out where the linked file resides in order
+ // to see if it exists & compare its timestamp, and we need to do that
+ // comparison in the same way ResGen does it. ResGen has two different
+ // ways for resolving relative file-paths in linked files. The way
+ // that ResGen resolved relative paths before Whidbey was always to
+ // resolve from the current working directory. In Whidbey a new command-line
+ // switch "/useSourcePath" instructs ResGen to use the folder that
+ // contains the .resx file as the path from which it should resolve
+ // relative paths. So we should base our timestamp/existence checking
+ // on the same switch & resolve in the same manner as ResGen.
+ resxReader.BasePath = baseLinkedFileDirectory ?? Path.GetDirectoryName(filename);
+
+ foreach (DictionaryEntry dictEntry in resxReader)
+ {
+ if (dictEntry.Value is ResXDataNode)
+ {
+ ResXFileRef resxFileRef = ((ResXDataNode)dictEntry.Value).FileRef;
+ if (resxFileRef != null)
+ retVal.Add(resxFileRef.FileName);
+ }
+ }
+ }
+#else
+ // Cross-platform: parse the .resx XML directly and collect the file path
+ // (first token) of every ResXFileRef. Paths are returned verbatim and
+ // resolved later against the base directory by the caller, matching the
+ // behaviour of ResXFileRef.FileName on .NET Framework.
+ XmlDocument doc = new XmlDocument();
+ doc.Load(filename);
+
+ XmlNodeList dataNodes = doc.SelectNodes("/root/data");
+ if (dataNodes != null)
+ {
+ foreach (XmlNode dataNode in dataNodes)
+ {
+ string type = dataNode.Attributes?["type"]?.Value;
+ if (string.IsNullOrEmpty(type) || !type.Contains("ResXFileRef"))
+ {
+ continue;
+ }
+
+ XmlNode valueNode = dataNode.SelectSingleNode("value");
+ string raw = valueNode != null ? valueNode.InnerText : null;
+ if (string.IsNullOrEmpty(raw))
+ {
+ continue;
+ }
+
+ // value is "path;type[;encoding]" — a quoted path may itself contain ';'.
+ string path = raw.Trim();
+ if (path.StartsWith("\""))
+ {
+ int closingQuote = path.IndexOf('"', 1);
+ if (closingQuote > 0)
+ {
+ path = path.Substring(1, closingQuote - 1);
+ }
+ }
+ else
+ {
+ int separator = path.IndexOf(';');
+ if (separator >= 0)
+ {
+ path = path.Substring(0, separator);
+ }
+ }
+
+ retVal.Add(path);
+ }
+ }
+#endif
+ return (string[])retVal.ToArray(typeof(string));
+ }
+ }
+
+ ///
+ /// Whether this cache is dirty or not.
+ ///
+ internal bool IsDirty
+ {
+ get
+ {
+ return isDirty;
+ }
+ }
+ }
+
+ [Description("GenerateNanoResourceTaskEntry")]
+ public class GenerateNanoResourceTask : Task
+ {
+ ///
+ /// List of output files that we failed to create due to an error.
+ /// See note in RemoveUnsuccessfullyCreatedResourcesFromOutputResources()
+ ///
+ private readonly List _unsuccessfullyCreatedOutFiles = new List();
+
+ // This cache helps us track the linked resource files listed inside of a resx resource file
+ private ResGenDependencies cache;
+
+ #region public properties for the task
+
+ ///
+ /// The names of the items to be converted. The extension must be one of the
+ // following: .txt, .resx or .resources.
+ ///
+ [Required]
+ public ITaskItem[] Sources { get; set; }
+
+ ///
+ /// Indicates whether the resource reader should use the source file's directory to
+ /// resolve relative file paths.
+ ///
+ public bool UseSourcePath { get; set; }
+
+ ///
+ /// Resolves types in ResX files (XML resources) for Strongly Typed Resources
+ ///
+ public ITaskItem[] References { get; set; }
+
+ ///
+ /// This is the path/name of the file containing the dependency cache
+ ///
+ public ITaskItem StateFile { get; set; }
+
+ ///
+ /// The name(s) of the resource file to create. If the user does not specify this
+ /// attribute, the task will append a .resources extension to each input filename
+ /// argument and write the file to the directory that contains the input file.
+ /// Includes any output files that were already up to date, but not any output files
+ /// that failed to be written due to an error.
+ ///
+ [Output]
+ public ITaskItem[] OutputResources { get; set; }
+
+ ///
+ /// Storage for names of *all files* written to disk. This is part of the implementation
+ /// for Clean, and contains the OutputResources items and the StateFile item.
+ /// Includes any output files that were already up to date, but not any output files
+ /// that failed to be written due to an error.
+ ///
+ [Output]
+ public ITaskItem[] FilesWritten { get { return _filesWritten.ToArray(); } }
+ private readonly List _filesWritten = new List();
+
+ ///
+ /// (default = false)
+ /// When true, a new AppDomain is always created to evaluate the .resx files.
+ /// When false, a new AppDomain is created only when it looks like a user's
+ /// assembly is referenced by the .resx.
+ ///
+ public bool NeverLockTypeAssemblies { get; set; }
+
+
+ #endregion
+
+ public override bool Execute()
+ {
+ // report to VS output window what step the build is
+ Log.LogMessage(MessageImportance.Normal, "Generating nanoResources nanoFramework assembly...");
+
+ // wait for debugger on var
+ DebuggerHelper.WaitForDebuggerIfEnabled(TasksConstants.BuildTaskDebugVar);
+
+ try
+ {
+ // If there are no sources to process, just return (with success) and report the condition.
+ if ((Sources == null) || (Sources.Length == 0))
+ {
+ Log.LogMessage(MessageImportance.Low, "GenerateResource.NoSources");
+
+ // Indicate we generated nothing
+ OutputResources = null;
+
+ return true;
+ }
+
+ if (!ValidateParameters())
+ {
+ // Indicate we generated nothing
+ OutputResources = null;
+ return false;
+ }
+
+ // In the case that OutputResources wasn't set, build up the outputs by transforming the Sources
+ if (!CreateOutputResourcesNames())
+ {
+ // Indicate we generated nothing
+ OutputResources = null;
+ return false;
+ }
+
+ // First we look to see if we have a resgen linked files cache. If so, then we can use that
+ // cache to speed up processing.
+ ReadStateFile();
+
+ bool nothingOutOfDate = true;
+
+ List inputsToProcess = new List();
+ List outputsToProcess = new List();
+
+ // decide what sources we need to build
+ for (int i = 0; i < Sources.Length; ++i)
+ {
+ // Attributes from input items are forwarded to output items.
+
+ if (!File.Exists(Sources[i].ItemSpec))
+ {
+ // Error but continue with the files that do exist
+ Log.LogError("GenerateResource.ResourceNotFound", Sources[i].ItemSpec);
+ _unsuccessfullyCreatedOutFiles.Add(OutputResources[i].ItemSpec);
+ }
+ else
+ {
+ // check to see if the output resources file (and, if it is a .resx, any linked files)
+ // is up to date compared to the input file
+ if (ShouldRebuildResgenOutputFile(Sources[i].ItemSpec, OutputResources[i].ItemSpec))
+ {
+ nothingOutOfDate = false;
+
+ inputsToProcess.Add(Sources[i]);
+ outputsToProcess.Add(OutputResources[i]);
+ }
+ }
+ }
+
+ if (nothingOutOfDate)
+ {
+ Log.LogMessage("GenerateResource.NothingOutOfDate");
+ }
+ else
+ {
+ // Prepare list of referenced assemblies
+ AssemblyName[] assemblyList;
+ try
+ { //only load system.drawing, mscorlib. no parameters needed here?!!
+#if NETFRAMEWORK
+ assemblyList = LoadReferences();
+#else
+ assemblyList = Array.Empty();
+#endif
+ }
+ catch (ArgumentException e)
+ {
+ Log.LogError("GenerateResource.ReferencedAssemblyNotFound - {0}: {1}", e.ParamName, e.Message);
+ OutputResources = null;
+ return false;
+ }
+
+ ProcessResourceFiles process = null;
+#if NETFRAMEWORK
+ // always create a separate AppDomain because an assembly would be locked.
+ AppDomain appDomain = null;
+#endif
+
+ try
+ {
+#if NETFRAMEWORK
+ appDomain = AppDomain.CreateDomain
+ (
+ "generateResourceAppDomain",
+ null,
+ AppDomain.CurrentDomain.SetupInformation
+ );
+
+ object obj = appDomain.CreateInstanceFromAndUnwrap
+ (
+ typeof(ProcessResourceFiles).Module.FullyQualifiedName,
+ typeof(ProcessResourceFiles).FullName
+ );
+
+ process = (ProcessResourceFiles)obj;
+#else
+ // On modern .NET there is a single AppDomain, so run in-process.
+ // The assembly-locking that motivates the separate AppDomain on
+ // .NET Framework does not apply to the cross-platform .resx reader.
+ process = new ProcessResourceFiles();
+#endif
+
+ //setup strongly typed class name??
+
+ process.Run(Log, assemblyList, inputsToProcess.ToArray(), outputsToProcess.ToArray(),
+ UseSourcePath);
+
+ if (null != process.UnsuccessfullyCreatedOutFiles)
+ {
+ foreach (string item in process.UnsuccessfullyCreatedOutFiles)
+ {
+ _unsuccessfullyCreatedOutFiles.Add(item);
+ }
+ }
+ }
+ finally
+ {
+#if NETFRAMEWORK
+ if (appDomain != null)
+ {
+ AppDomain.Unload(appDomain);
+ }
+#endif
+ }
+ }
+
+ // And now we serialize the cache to save our resgen linked file resolution for later use.
+ WriteStateFile();
+
+ RemoveUnsuccessfullyCreatedResourcesFromOutputResources();
+
+ RecordFilesWritten();
+ }
+ catch (Exception ex)
+ {
+ Log.LogError(".NET nanoFramework GenerateNanoResourceTask error: " + ex.Message);
+ }
+
+ // if we've logged any errors that's because there were errors (WOW!)
+ return !Log.HasLoggedErrors;
+ }
+
+ ///
+ /// Check for parameter errors.
+ ///
+ /// true if parameters are valid
+ private bool ValidateParameters()
+ {
+ // make sure that if the output resources were set, they exactly match the number of input sources
+ if ((OutputResources != null) && (OutputResources.Length != Sources.Length))
+ {
+ Log.LogError("General.TwoVectorsMustHaveSameLength", Sources.Length, OutputResources.Length, "Sources", "OutputResources");
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Make sure that OutputResources has 1 file name for each name in Sources.
+ ///
+ private bool CreateOutputResourcesNames()
+ {
+ if (OutputResources == null)
+ {
+ OutputResources = new ITaskItem[Sources.Length];
+
+ int i = 0;
+ try
+ {
+ for (i = 0; i < Sources.Length; ++i)
+ {
+ OutputResources[i] = new TaskItem(Path.ChangeExtension(Sources[i].ItemSpec, ".nanoresources"));
+ }
+ }
+ catch (ArgumentException e)
+ {
+ Log.LogError("GenerateResource.InvalidFilename", Sources[i].ItemSpec, e.Message);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// Remove any output resources that we didn't successfully create (due to error) from the
+ /// OutputResources list. Keeps the ordering of OutputResources the same.
+ ///
+ ///
+ /// Q: Why didn't we keep a "successfully created" list instead, like in the Copy task does, which
+ /// would save us doing the removal algorithm below?
+ /// A: Because we want the ordering of OutputResources to be the same as the ordering passed in.
+ /// Some items (the up to date ones) would be added to the successful output list first, and the other items
+ /// are added during processing, so the ordering would change. We could fix that up, but it's better to do
+ /// the fix up only in the rarer error case. If there were no errors, the algorithm below skips.
+ private void RemoveUnsuccessfullyCreatedResourcesFromOutputResources()
+ {
+ // Normally, there aren't any unsuccessful conversions.
+ if (_unsuccessfullyCreatedOutFiles == null ||
+ _unsuccessfullyCreatedOutFiles.Count == 0)
+ {
+ return;
+ }
+
+ Debug.Assert(OutputResources != null && OutputResources.Length != 0);
+
+ // We only get here if there was at least one resource generation error.
+ ITaskItem[] temp = new ITaskItem[OutputResources.Length - _unsuccessfullyCreatedOutFiles.Count];
+ int copied = 0;
+ int removed = 0;
+ foreach (ITaskItem item in OutputResources)
+ {
+ // Check whether this one is in the bad list.
+ if (removed < _unsuccessfullyCreatedOutFiles.Count &&
+ _unsuccessfullyCreatedOutFiles.Contains(item.ItemSpec))
+ {
+ removed++;
+ }
+ else
+ {
+ // Copy it to the okay list.
+ temp[copied] = item;
+ copied++;
+ }
+ }
+ OutputResources = temp;
+ }
+
+ ///
+ /// Read the state file if able.
+ ///
+ private void ReadStateFile()
+ {
+ // First we look to see if we have a resgen linked files cache. If so, then we can use that
+ // cache to speed up processing. If there's a problem reading the cache file (or it
+ // just doesn't exist, then this method will return a brand new cache object.
+
+ // This method eats IO Exceptions
+
+ cache = ResGenDependencies.DeserializeCache(StateFile?.ItemSpec, UseSourcePath, Log);
+
+ //RWOLFF -- throw here?
+ //ErrorUtilities.VerifyThrow(cache != null, "We did not create a cache!");
+ }
+
+ ///
+ /// Record the list of file that will be written to disk.
+ ///
+ private void RecordFilesWritten()
+ {
+ // Add any output resources that were successfully created,
+ // or would have been if they weren't already up to date (important for Clean)
+ foreach (ITaskItem item in OutputResources)
+ {
+ Debug.Assert(File.Exists(item.ItemSpec), item.ItemSpec + " doesn't exist but we're adding to FilesWritten");
+ _filesWritten.Add(new TaskItem(item));
+ }
+
+ // Add any state file
+ if (StateFile != null && StateFile.ItemSpec.Length > 0)
+ {
+ // It's possible the file wasn't actually written (eg the path was invalid)
+ // We can't easily tell whether that happened here, and I think it's fine to add it anyway.
+ _filesWritten.Add(new TaskItem(StateFile));
+ }
+ }
+
+
+ ///
+ /// Determines if the given output file is up to date with respect to the
+ /// the given input file by comparing timestamps of the two files as well as
+ /// (if the source is a .resx) the linked files inside the .resx file itself
+ ///
+ ///
+ ///
+ private bool ShouldRebuildResgenOutputFile(string sourceFilePath, string outputFilePath)
+ {
+ bool sourceFileExists = File.Exists(sourceFilePath);
+ bool destinationFileExists = File.Exists(outputFilePath);
+
+ // PERF: Regardless of whether the outputFile exists, if the source file is a .resx
+ // go ahead and retrieve it from the cache. This is because we want the cache
+ // to be populated so that incremental builds can be fast.
+ // Note that this is a trade-off: clean builds will be slightly slower. However,
+ // for clean builds we're about to read in this very same .resx file so reading
+ // it now will page it in. The second read should be cheap.
+ ResGenDependencies.ResXFile resxFileInfo = null;
+ if (String.Compare(Path.GetExtension(sourceFilePath), ".resx", true, CultureInfo.InvariantCulture) == 0)
+ {
+ try
+ {
+ resxFileInfo = cache.GetResXFileInfo(sourceFilePath);
+ }
+ catch (ArgumentException)
+ {
+ // Return true, so that resource processing will display the error
+ // No point logging a duplicate error here as well
+ return true;
+ }
+ catch (XmlException)
+ {
+ // Return true, so that resource processing will display the error
+ // No point logging a duplicate error here as well
+ return true;
+ }
+ catch (Exception e) // Catching Exception, but rethrowing unless it's a well-known exception.
+ {
+ ExceptionHandling.RethrowUnlessFileIO(e);
+ // Return true, so that resource processing will display the error
+ // No point logging a duplicate error here as well
+ return true;
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////
+ // If the output file does not exist, then we should rebuild it.
+ // Also, if the input file does not exist, we will also return saying that the
+ // the output file needs to be rebuilt, so that this pair of files will
+ // get added to the command-line which will let resgen output whatever error
+ // it normally outputs in the case when users call the tool with bad params
+ bool shouldRebuildOutputFile = (!destinationFileExists || !sourceFileExists);
+
+ // if both files do exist, then we need to do some timestamp comparisons
+ if (!shouldRebuildOutputFile)
+ {
+ Debug.Assert(destinationFileExists && sourceFileExists, "GenerateResource task should not check timestamps if neither the .resx nor the .resources files exist");
+
+ // cache the output file timestamps
+ DateTime outputFileTimeStamp = File.GetLastWriteTime(outputFilePath);
+
+ // If source file is NOT a .resx, timestamp checking is simple
+ if (resxFileInfo == null)
+ {
+ // We have a non .resx file. Don't attempt to parse it.
+
+ // cache the source file timestamp
+ DateTime sourceFileTimeStamp = File.GetLastWriteTime(sourceFilePath);
+
+ // we need to rebuild this output file if the source file has a
+ // more recent timestamp than the output file
+ shouldRebuildOutputFile = (sourceFileTimeStamp > outputFileTimeStamp);
+
+ return shouldRebuildOutputFile;
+ }
+
+ // Source file IS a .resx file so we need to do deep dependency analysis
+ Debug.Assert(resxFileInfo != null, "Why didn't we get resx file information?");
+
+ // cache the .resx file timestamps
+ DateTime resxTimeStamp = resxFileInfo.LastModified;
+
+ // we need to rebuild this .resources file if the .resx file has a
+ // more recent timestamp than the .resources file
+ shouldRebuildOutputFile = (resxTimeStamp > outputFileTimeStamp);
+
+ // Check the timestamp of each of the passed-in references against the .RESOURCES file.
+ if (!shouldRebuildOutputFile && (References != null))
+ {
+ foreach (ITaskItem reference in References)
+ {
+ // If the reference doesn't exist, then we want to rebuild this
+ // .resources file so the user sees an error from ResGen.exe
+ shouldRebuildOutputFile = !File.Exists(reference.ItemSpec);
+
+ // If the reference exists, then we need to compare the timestamp
+ // for the linked resource to see if it is more recent than the
+ // .resources file
+ if (!shouldRebuildOutputFile)
+ {
+ DateTime referenceTimeStamp = File.GetLastWriteTime(reference.ItemSpec);
+ shouldRebuildOutputFile = referenceTimeStamp > outputFileTimeStamp;
+ }
+
+ // If we found an instance where a reference is in a state
+ // that we should rebuild the .resources file, then we should
+ // bail from this loop & just return since the first file that
+ // forces a rebuild is enough
+ if (shouldRebuildOutputFile)
+ {
+ break;
+ }
+ }
+ }
+
+ // if the .resources is up to date with respect to the .resx file
+ // then we need to compare timestamps for each linked file inside
+ // the .resx file itself
+ if (!shouldRebuildOutputFile && resxFileInfo.LinkedFiles != null)
+ {
+ // Linked file paths in the .resx may be relative. Resolve them the same
+ // way ResXResourceReader does: against BaseLinkedFileDirectory if set,
+ // otherwise against the directory containing the .resx file.
+ string linkedFileBaseDir = cache.BaseLinkedFileDirectory ?? Path.GetDirectoryName(sourceFilePath);
+
+ foreach (string rawLinkedFilePath in resxFileInfo.LinkedFiles)
+ {
+ string linkedFilePath = Path.IsPathRooted(rawLinkedFilePath)
+ ? rawLinkedFilePath
+ : Path.GetFullPath(Path.Combine(linkedFileBaseDir, rawLinkedFilePath));
+
+ // If the linked file doesn't exist, then we want to rebuild this
+ // .resources file so the user sees an error from ResGen.exe
+ shouldRebuildOutputFile = !File.Exists(linkedFilePath);
+
+ // If the linked file exists, then we need to compare the timestamp
+ // for the linked resource to see if it is more recent than the
+ // .resources file
+ if (!shouldRebuildOutputFile)
+ {
+ DateTime linkedFileTimeStamp = File.GetLastWriteTime(linkedFilePath);
+ shouldRebuildOutputFile = linkedFileTimeStamp > outputFileTimeStamp;
+ }
+
+ // If we found an instance where a linked file is in a state
+ // that we should rebuild the .resources file, then we should
+ // bail from this loop & just return since the first file that
+ // forces a rebuild is enough
+ if (shouldRebuildOutputFile)
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ return shouldRebuildOutputFile;
+ //#endif
+ }
+
+ ///
+ /// Create the AssemblyName array that ProcessResources will need.
+ ///
+ /// AssemblyName array
+ /// danmose
+ /// ArgumentException
+ private AssemblyName[] LoadReferences()
+ {
+ if (References == null)
+ {
+ return new AssemblyName[0];
+ }
+
+ AssemblyName[] assemblyList = new AssemblyName[References.Length];
+
+ for (int i = 0; i < References.Length; i++)
+ {
+ try
+ {
+ assemblyList[i] = AssemblyName.GetAssemblyName(References[i].ItemSpec);
+ }
+ // We should never get passed in references we can't load. In the VS build process, for example,
+ // we're passed in @(ReferencePath), which only contains resolved references.
+ catch (ArgumentNullException e)
+ {
+ throw new ArgumentException(e.Message, References[i].ItemSpec);
+ }
+ catch (ArgumentException e)
+ {
+ throw new ArgumentException(e.Message, References[i].ItemSpec);
+ }
+ catch (FileNotFoundException e)
+ {
+ throw new ArgumentException(e.Message, References[i].ItemSpec);
+ }
+ /*catch (SecurityException e)
+ {
+ throw new ArgumentException(e.Message, References[i].ItemSpec);
+ }
+ */
+ catch (BadImageFormatException e)
+ {
+ throw new ArgumentException(e.Message, References[i].ItemSpec);
+ }
+ catch (FileLoadException e)
+ {
+ throw new ArgumentException(e.Message, References[i].ItemSpec);
+ }
+ }
+
+ return assemblyList;
+ }
+
+ ///
+ /// Write the state file if there is one to be written.
+ ///
+ private void WriteStateFile()
+ {
+ if (cache.IsDirty)
+ {
+ // And now we serialize the cache to save our resgen linked file resolution for later use.
+ cache.SerializeCache(StateFile?.ItemSpec, Log);
+ }
+ }
+ }
+}
diff --git a/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs
new file mode 100644
index 0000000..b4ea1fd
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.CrossPlatform.cs
@@ -0,0 +1,373 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// See LICENSE file in the project root for full license information.
+//
+// Cross-platform (net8.0+) companion for ProcessResourceFiles.
+//
+// On .NET Framework the resource pipeline relies on System.Windows.Forms
+// (ResXResourceReader) and System.Drawing (GDI+) which are Windows-only and
+// unavailable under `dotnet build`. This file provides the cross-platform
+// equivalents used when the build task runs on modern .NET:
+//
+// * NanoResxResourceReader - a focused .resx reader covering the cases the
+// nanoFramework pipeline uses (inline strings, ResXFileRef byte[]/string,
+// and base64 byte-array blobs).
+// * CrossPlatformBitmap - bitmap decoding + RGB565 conversion via ImageSharp,
+// producing the exact CLR_GFX_BitmapDescription + payload the nanoCLR expects.
+//
+
+#if !NETFRAMEWORK
+
+using System;
+using System.Collections;
+using System.Globalization;
+using System.IO;
+using System.Resources;
+using System.Text;
+using System.Xml;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.PixelFormats;
+
+using BitmapDescription = nanoFramework.Tools.ProcessResourceFiles.NanoResourceFile.CLR_GFX_BitmapDescription;
+
+namespace nanoFramework.Tools
+{
+ ///
+ /// Minimal cross-platform .resx reader.
+ /// Implements just enough of the .resx schema for the nanoFramework resource
+ /// pipeline: inline string values,
+ /// style file references (resolved to byte[] or string), and
+ /// inline base64 byte arrays. Anything requiring runtime type activation
+ /// (BinaryFormatter-serialized objects) is reported as unsupported.
+ ///
+ internal sealed class NanoResxResourceReader : IResourceReader
+ {
+ private const string ResXFileRefTypeMarker = "ResXFileRef";
+ private const string ByteArrayMimeType = "application/x-microsoft.net.object.bytearray.base64";
+
+ private readonly Hashtable _resources = new Hashtable();
+
+ internal NanoResxResourceReader(TextReader reader, string basePath)
+ {
+ Parse(reader, basePath);
+ }
+
+ private void Parse(TextReader reader, string basePath)
+ {
+ XmlDocument doc = new XmlDocument();
+ doc.Load(reader);
+
+ XmlNodeList dataNodes = doc.SelectNodes("/root/data");
+ if (dataNodes == null)
+ {
+ return;
+ }
+
+ foreach (XmlNode dataNode in dataNodes)
+ {
+ string name = dataNode.Attributes?["name"]?.Value;
+ if (string.IsNullOrEmpty(name))
+ {
+ continue;
+ }
+
+ string type = dataNode.Attributes?["type"]?.Value;
+ string mimetype = dataNode.Attributes?["mimetype"]?.Value;
+ XmlNode valueNode = dataNode.SelectSingleNode("value");
+ string rawValue = valueNode != null ? valueNode.InnerText : string.Empty;
+
+ object value = ResolveValue(type, mimetype, rawValue, basePath);
+ if (value != null)
+ {
+ _resources[name] = value;
+ }
+ }
+ }
+
+ private object ResolveValue(string type, string mimetype, string rawValue, string basePath)
+ {
+ // Inline base64 byte array (e.g. small binary blobs embedded in the .resx).
+ if (!string.IsNullOrEmpty(mimetype))
+ {
+ if (string.Equals(mimetype, ByteArrayMimeType, StringComparison.OrdinalIgnoreCase))
+ {
+ return Convert.FromBase64String(rawValue.Trim());
+ }
+
+ throw new NotSupportedException(
+ $"Resource mimetype '{mimetype}' is not supported when building with 'dotnet build'. " +
+ "Use full .NET Framework MSBuild / Visual Studio for this resource.");
+ }
+
+ // File reference (ResXFileRef): ";[;]".
+ if (!string.IsNullOrEmpty(type) && type.Contains(ResXFileRefTypeMarker))
+ {
+ return ResolveFileRef(rawValue, basePath);
+ }
+
+ // Anything else with an explicit non-primitive type is not supported here.
+ if (!string.IsNullOrEmpty(type) && !IsStringType(type))
+ {
+ throw new NotSupportedException(
+ $"Resource type '{type}' is not supported when building with 'dotnet build'. " +
+ "Use full .NET Framework MSBuild / Visual Studio for this resource.");
+ }
+
+ // Plain inline string.
+ return rawValue;
+ }
+
+ private object ResolveFileRef(string fileRef, string basePath)
+ {
+ // Format: filePath;typeName[;encoding]
+ string[] parts = SplitFileRef(fileRef);
+ string path = parts[0].Trim();
+ string typeName = parts.Length > 1 ? parts[1].Trim() : "System.Byte[]";
+
+ string resolvedPath = Path.IsPathRooted(path)
+ ? path
+ : Path.GetFullPath(Path.Combine(basePath ?? Directory.GetCurrentDirectory(), path));
+
+ if (IsStringType(typeName))
+ {
+ Encoding encoding = Encoding.UTF8;
+ if (parts.Length > 2 && !string.IsNullOrWhiteSpace(parts[2]))
+ {
+ try
+ {
+ encoding = Encoding.GetEncoding(parts[2].Trim());
+ }
+ catch (ArgumentException)
+ {
+ // fall back to UTF-8
+ }
+ }
+
+ return File.ReadAllText(resolvedPath, encoding);
+ }
+
+ if (IsMemoryStreamType(typeName))
+ {
+ return new MemoryStream(File.ReadAllBytes(resolvedPath));
+ }
+
+ // Default (and the common nanoFramework case): raw bytes (System.Byte[]).
+ return File.ReadAllBytes(resolvedPath);
+ }
+
+ ///
+ /// Splits a ResXFileRef value into [path, type, encoding]. The path may itself
+ /// contain ';' so we only split on the separators that precede the type/encoding.
+ ///
+ private static string[] SplitFileRef(string fileRef)
+ {
+ // ResXFileRef.Parse splits on ';' into at most 3 segments, but a quoted path
+ // ("path";type;encoding) protects a path containing ';'. Handle the quoted form.
+ fileRef = fileRef.Trim();
+ if (fileRef.StartsWith("\""))
+ {
+ int closingQuote = fileRef.IndexOf('"', 1);
+ if (closingQuote > 0)
+ {
+ string path = fileRef.Substring(1, closingQuote - 1);
+ string remainder = fileRef.Substring(closingQuote + 1).TrimStart(';');
+ string[] rest = remainder.Split(';');
+ string[] result = new string[1 + rest.Length];
+ result[0] = path;
+ Array.Copy(rest, 0, result, 1, rest.Length);
+ return result;
+ }
+ }
+
+ return fileRef.Split(';');
+ }
+
+ private static bool IsStringType(string typeName)
+ {
+ return typeName != null && typeName.StartsWith("System.String", StringComparison.Ordinal);
+ }
+
+ private static bool IsMemoryStreamType(string typeName)
+ {
+ return typeName != null && typeName.StartsWith("System.IO.MemoryStream", StringComparison.Ordinal);
+ }
+
+ #region IResourceReader / IEnumerable / IDisposable
+
+ public IDictionaryEnumerator GetEnumerator()
+ {
+ return _resources.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _resources.GetEnumerator();
+ }
+
+ public void Close()
+ {
+ _resources.Clear();
+ }
+
+ public void Dispose()
+ {
+ Close();
+ }
+
+ #endregion
+ }
+
+ ///
+ /// Cross-platform bitmap handling for the nanoFramework resource pipeline,
+ /// backed by ImageSharp. Mirrors the .NET Framework / GDI+ behaviour:
+ /// BMP images are converted to uncompressed 16bpp RGB565; JPEG and GIF
+ /// images are stored as-is with the matching bitmap type.
+ ///
+ internal static class CrossPlatformBitmap
+ {
+ ///
+ /// Returns true if the raw bytes decode as one of the bitmap formats the
+ /// nanoFramework runtime supports (BMP, GIF, JPEG). PNG and other formats
+ /// are intentionally rejected so they are treated as binary resources,
+ /// matching the .NET Framework implementation.
+ ///
+ internal static bool IsSupportedBitmap(byte[] rawValue)
+ {
+ return TryGetBitmapType(rawValue, out _);
+ }
+
+ internal static byte[] GenerateBitmapResourceData(byte[] rawValue)
+ {
+ if (!TryGetBitmapType(rawValue, out byte bitmapType))
+ {
+ throw new NotSupportedException("Bitmap format not supported.");
+ }
+
+ using (Image image = Image.Load(rawValue))
+ {
+ if (image.Width > 0xFFFF || image.Height > 0xFFFF)
+ {
+ throw new ArgumentException("bitmap dimensions out of range");
+ }
+
+ BitmapDescription description;
+ byte[] data;
+
+ if (bitmapType == BitmapDescription.c_TypeBitmap)
+ {
+ // BMP -> uncompressed 16bpp RGB565.
+ data = ConvertToRgb565(image);
+ description = new BitmapDescription(
+ (ushort)image.Width,
+ (ushort)image.Height,
+ 0,
+ 16,
+ BitmapDescription.c_TypeBitmap);
+ }
+ else
+ {
+ // JPEG / GIF -> stored encoded, decoded by the device at runtime.
+ data = rawValue;
+ description = new BitmapDescription(
+ (ushort)image.Width,
+ (ushort)image.Height,
+ 0,
+ 1,
+ bitmapType);
+ }
+
+ using (MemoryStream stream = new MemoryStream())
+ using (BinaryWriter writer = new BinaryWriter(stream))
+ {
+ description.Serialize(writer);
+ writer.Write(data);
+ writer.Flush();
+ return stream.ToArray();
+ }
+ }
+ }
+
+ private static bool TryGetBitmapType(byte[] rawValue, out byte bitmapType)
+ {
+ bitmapType = 0;
+
+ if (rawValue == null || rawValue.Length == 0)
+ {
+ return false;
+ }
+
+ try
+ {
+ IImageFormat format = Image.DetectFormat(rawValue);
+ if (format == null)
+ {
+ return false;
+ }
+
+ switch (format.Name.ToUpperInvariant())
+ {
+ case "BMP":
+ bitmapType = BitmapDescription.c_TypeBitmap;
+ return true;
+ case "GIF":
+ bitmapType = BitmapDescription.c_TypeGif;
+ return true;
+ case "JPEG":
+ bitmapType = BitmapDescription.c_TypeJpeg;
+ return true;
+ default:
+ return false;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Converts an image to contiguous 16bpp RGB565, row-major, top-down,
+ /// little-endian per pixel — the layout the nanoCLR expects (and that
+ /// GDI+ Format16bppRgb565 produces for even widths).
+ ///
+ private static byte[] ConvertToRgb565(Image image)
+ {
+ int width = image.Width;
+ int height = image.Height;
+ byte[] data = new byte[width * height * 2];
+
+ image.ProcessPixelRows(accessor =>
+ {
+ int offset = 0;
+ for (int y = 0; y < height; y++)
+ {
+ Span row = accessor.GetRowSpan(y);
+ for (int x = 0; x < width; x++)
+ {
+ Rgba32 p = row[x];
+
+ // Quantize 8-bit channels to RGB565. Green/blue match GDI+
+ // (the net472 System.Drawing path) exactly. Red uses round-to-
+ // nearest, which matches GDI+ except for colors that land exactly
+ // on a half-level boundary: there GDI+ applies ordered (Bayer)
+ // dithering by pixel position, which is NOT reproduced here. Such
+ // pixels can differ by 1 LSB of red from the legacy/net472 output.
+ // See SmokeTest Resources.resx parity notes.
+ int r5 = Math.Min(31, (p.R + 4) >> 3);
+ int g6 = p.G >> 2;
+ int b5 = Math.Min(31, (p.B + 4) >> 3);
+
+ ushort value = (ushort)((r5 << 11) | (g6 << 5) | b5);
+ data[offset++] = (byte)(value & 0xFF);
+ data[offset++] = (byte)(value >> 8);
+ }
+ }
+ });
+
+ return data;
+ }
+ }
+}
+
+#endif
diff --git a/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs
new file mode 100644
index 0000000..2da0f5c
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/ProcessResourceFiles.cs
@@ -0,0 +1,2182 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// Portions Copyright (c) Microsoft Corporation. All rights reserved.
+// See LICENSE file in the project root for full license information.
+//
+// ******************************************************************************
+// ** IMPORTANT: THIS FILE IS DUPLICATED. **
+// ** **
+// ** The canonical .NET Framework copy lives in the VS extension repo: **
+// ** nf-Visual-Studio-extension / Tools.BuildTasks-2019 / **
+// ** ProcessResourceFiles.cs **
+// ** **
+// ** This copy adds #if NETFRAMEWORK guards for cross-platform (net8.0+) **
+// ** support. Any logic change made here MUST be ported there, and vice-versa.**
+// ******************************************************************************
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using System;
+using System.CodeDom;
+using System.CodeDom.Compiler;
+using System.Collections;
+using System.Diagnostics;
+#if NETFRAMEWORK
+using System.Drawing;
+using System.Drawing.Imaging;
+#endif
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Resources;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Xml;
+
+namespace nanoFramework.Tools
+{
+
+ ///
+ /// This class handles the processing of source resource files into compiled resource files.
+ /// Its designed to be called from a separate AppDomain so that any files locked by ResXResourceReader
+ /// can be released.
+ ///
+ internal sealed class ProcessResourceFiles : MarshalByRefObject
+ {
+
+ #region fields
+ ///
+ /// Resource list (used to preserve resource ordering, primarily for easier testing)
+ ///
+ private ArrayList resources = new ArrayList();
+
+ ///
+ /// Mirror resource list, used to check for duplicates
+ ///
+ private Hashtable resourcesHashTable = new Hashtable(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// Logger for any messages or errors
+ ///
+ private TaskLoggingHelper logger = null;
+
+ public string StronglyTypedNamespace { get; set; }
+
+ ///
+ /// Class name for the strongly typed resources.
+ /// Getter provided since the processor may choose a default.
+ ///
+ public string StronglyTypedClassName { get; set; }
+
+ ///
+ /// List of assemblies to use for type resolution within resx files
+ ///
+ private AssemblyName[] assemblyList;
+
+ ///
+ /// List of input files to process.
+ ///
+ private ITaskItem[] inFiles;
+
+ ///
+ /// List of output files to process.
+ ///
+ private ITaskItem[] outFiles;
+
+ public bool GenerateNestedEnums { get; set; } = true;
+
+ public bool GenerateInternalClass { get; set; } = true;
+
+ public bool IsMscorlib { get; set; } = false;
+
+
+ ///
+ /// List of output files that we failed to create due to an error.
+ /// See note in RemoveUnsuccessfullyCreatedResourcesFromOutputResources()
+ ///
+ internal ArrayList UnsuccessfullyCreatedOutFiles
+ {
+ get
+ {
+ if (null == unsuccessfullyCreatedOutFiles)
+ {
+ unsuccessfullyCreatedOutFiles = new ArrayList();
+ }
+ return unsuccessfullyCreatedOutFiles;
+ }
+ }
+ private ArrayList unsuccessfullyCreatedOutFiles;
+
+ ///
+ /// Whether we successfully created the STR class
+ ///
+ internal bool StronglyTypedResourceSuccessfullyCreated { get; private set; } = false;
+
+ ///
+ /// Indicates whether the resource reader should use the source file's
+ /// directory to resolve relative file paths.
+ ///
+ private bool useSourcePath = false;
+
+ private ITaskItem _inTaskItem;
+ private ITaskItem _outTaskItem;
+
+ #endregion
+
+ ///
+ /// Process all files.
+ ///
+ internal void Run(TaskLoggingHelper log, AssemblyName[] assemblies, ITaskItem[] inputs, ITaskItem[] outputs, bool sourcePath)
+ {
+ logger = log;
+ assemblyList = assemblies;
+ inFiles = inputs;
+ outFiles = outputs;
+ useSourcePath = sourcePath;
+
+ for (int i = 0; i < inFiles.Length; ++i)
+ {
+ if (!ProcessFile(inFiles[i], outFiles[i]))
+ {
+ UnsuccessfullyCreatedOutFiles.Add(outFiles[i].ItemSpec);
+ }
+ }
+ }
+
+ private void Init()
+ {
+ resources.Clear();
+ resourcesHashTable.Clear();
+ }
+
+ private void InitFileProcessing(ITaskItem inTaskItem, ITaskItem outTaskItem)
+ {
+ Init();
+
+ _inTaskItem = inTaskItem;
+ _outTaskItem = outTaskItem;
+ }
+
+ #region Code from ResGen.EXE
+ ///
+ /// Read all resources from a file and write to a new file in the chosen format
+ ///
+ /// Uses the input and output file extensions to determine their format
+ /// Input resources file
+ /// Output resources file
+ /// True if conversion was successful, otherwise false
+ private bool ProcessFile(ITaskItem inTaskItem, ITaskItem outTaskItem)
+ {
+ InitFileProcessing(inTaskItem, outTaskItem);
+
+ string inFile = inTaskItem.ItemSpec;
+ string outFile = outTaskItem.ItemSpec;
+
+ if (GetFormat(inFile) == Format.Error)
+ {
+ logger.LogError("GenerateResource.UnknownFileExtension", Path.GetExtension(inFile), inFile);
+ return false;
+ }
+ if (GetFormat(outFile) == Format.Error)
+ {
+ logger.LogError("GenerateResource.UnknownFileExtension", Path.GetExtension(outFile), outFile);
+ return false;
+ }
+
+ // logger.LogMessage("GenerateResource.ProcessingFile", inFile, outFile);
+
+ try
+ {
+ ReadResources(inFile, useSourcePath);
+ }
+ catch (ArgumentException ae)
+ {
+ if (ae.InnerException is XmlException)
+ {
+ XmlException xe = (XmlException)ae.InnerException;
+ logger.LogError(null, inFile, xe.LineNumber, xe.LinePosition, 0, 0, "General.InvalidResxFile", xe.Message);
+ }
+ else
+ {
+ logger.LogError(null, inFile, 0, 0, 0, 0, "General.InvalidResxFile", ae.Message);
+ }
+ return false;
+ }
+ catch (XmlException xe)
+ {
+ logger.LogError(null, inFile, xe.LineNumber, xe.LinePosition, 0, 0, "General.InvalidResxFile", xe.Message);
+ return false;
+ }
+#if MSBUILD_SOURCES
+ catch (Exception e)
+ {
+ ExceptionHandling.RethrowUnlessFileIO(e);
+ logger.LogError(null, inFile, 0, 0, 0, 0, "General.InvalidResxFile", e.Message);
+
+ // We need to give meaningful error messages to the user.
+ // Note that ResXResourceReader wraps any exception it gets
+ // in an ArgumentException with the message "Invalid ResX input."
+ // If you don't look at the InnerException, you have to attach
+ // a debugger to find the problem.
+ if (e.InnerException != null)
+ {
+ Exception inner = e.InnerException;
+ StringBuilder sb = new StringBuilder(200);
+ sb.Append(e.Message);
+ while (inner != null)
+ {
+ sb.Append(" ---> ");
+ sb.Append(inner.GetType().Name);
+ sb.Append(": ");
+ sb.Append(inner.Message);
+ inner = inner.InnerException;
+ }
+ logger.LogError(null, inFile, 0, 0, 0, 0, "General.InvalidResxFile", sb.ToString());
+ }
+ return false;
+ }
+#endif
+ catch
+ {
+ throw;
+ }
+
+ try
+ {
+ WriteResources(outFile);
+ }
+ catch (IOException io)
+ {
+ logger.LogError("GenerateResource.CannotWriteOutput", outFile, io.Message);
+
+ if (File.Exists(outFile))
+ {
+ logger.LogError("GenerateResource.CorruptOutput", outFile);
+ try
+ {
+ File.Delete(outFile);
+ }
+ catch (Exception e)
+ {
+ logger.LogWarning("GenerateResource.DeleteCorruptOutputFailed", outFile, e.Message);
+ }
+ }
+ return false;
+ }
+#if MSBUILD_SOURCES
+ catch (Exception e)
+ {
+ ExceptionHandling.RethrowUnlessFileIO(e);
+ logger.LogError("GenerateResource.CannotWriteOutput", outFile, e.Message);
+ return false;
+ }
+#endif
+ catch
+ {
+ throw;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Figure out the format of an input resources file from the extension
+ ///
+ /// Input resources file
+ /// Resources format
+ private Format GetFormat(string filename)
+ {
+ string extension = Path.GetExtension(filename);
+ if (String.Compare(extension, ".txt", true, CultureInfo.InvariantCulture) == 0 ||
+ String.Compare(extension, ".restext", true, CultureInfo.InvariantCulture) == 0)
+ {
+ return Format.Text;
+ }
+ else if (String.Compare(extension, ".resx", true, CultureInfo.InvariantCulture) == 0)
+ {
+ return Format.XML;
+ }
+ else if (String.Compare(extension, ".resources", true, CultureInfo.InvariantCulture) == 0)
+ {
+ return Format.Binary;
+ }
+ else if (String.Compare(extension, ".nanoresources", true, CultureInfo.InvariantCulture) == 0)
+ {
+ return Format.NanoResources;
+ }
+ else
+ {
+ return Format.Error;
+ }
+ }
+
+ ///
+ /// Text files are just name/value pairs. ResText is the same format
+ /// with a unique extension to work around some ambiguities with MSBuild
+ /// ResX is our existing XML format from V1.
+ ///
+ private enum Format
+ {
+ Text, // .txt or .restext
+ XML, // .resx
+ Binary, // .resources
+ NanoResources, //.nanoresources
+ Error, // anything else
+ }
+
+ ///
+ /// Reads the resources out of the specified file and populates the
+ /// resources hashtable.
+ ///
+ /// Filename to load
+ /// Whether to resolve paths in the
+ /// resources file relative to the resources file location
+ public void ReadResources(String filename, bool shouldUseSourcePath)
+ {
+ // Reset state
+ // resources.Clear();
+ // resourcesHashTable.Clear();
+
+ Format format = GetFormat(filename);
+ switch (format)
+ {
+ case Format.Text:
+ ReadTextResources(filename);
+ break;
+
+ case Format.XML:
+#if NETFRAMEWORK
+ ResXResourceReader resXReader = null;
+ if (assemblyList != null)
+ {
+ resXReader = new ResXResourceReader(filename, assemblyList);
+ }
+ else
+ {
+ resXReader = new ResXResourceReader(filename);
+ }
+ if (shouldUseSourcePath)
+ {
+ String fullPath = Path.GetFullPath(filename);
+ resXReader.BasePath = Path.GetDirectoryName(fullPath);
+ }
+ // ReadResources closes the reader for us
+ ReadResources(resXReader, filename);
+#else
+ {
+ string basePath = shouldUseSourcePath
+ ? Path.GetDirectoryName(Path.GetFullPath(filename))
+ : null;
+ using (StreamReader sr = new StreamReader(filename))
+ {
+ ReadResources(new NanoResxResourceReader(sr, basePath), filename);
+ }
+ }
+#endif
+ break;
+
+ case Format.Binary:
+ ReadResources(new ResourceReader(filename), filename); // closes reader for us
+ break;
+ case Format.NanoResources:
+ Debug.Fail("Unknown format " + format.ToString());
+ break;
+
+ default:
+ // We should never get here, we've already checked the format
+ Debug.Fail("Unknown format " + format.ToString());
+ return;
+ }
+ //logger.LogMessage(BuildEventImportance.Low/*MessageImportance.Low*/, "GenerateResource.ReadResourceMessage", resources.Count, filename);
+ }
+
+ ///
+ /// Write resources from the resources ArrayList to the specified output file
+ ///
+ /// Output resources file
+ public void WriteResources(String filename)
+ {
+ Format format = GetFormat(filename);
+ switch (format)
+ {
+ case Format.Text:
+ WriteTextResources(filename);
+ break;
+
+ case Format.XML:
+#if NETFRAMEWORK
+ WriteResources(new ResXResourceWriter(filename)); // closes writer for us
+ break;
+#else
+ throw new NotSupportedException("Writing .resx output is not supported on this build host.");
+#endif
+
+ case Format.Binary:
+ WriteResources(new ResourceWriter(filename)); // closes writer for us
+ break;
+
+ case Format.NanoResources:
+ WriteResources(new NanoResourceWriter(filename)); // closes writer for us
+ break;
+ default:
+ // We should never get here, we've already checked the format
+ Debug.Fail("Unknown format " + format.ToString());
+ break;
+ }
+ }
+
+ private void CreateStronglyTypedResources(CodeDomProvider provider, TextWriter writer, string resourceName, out string[] errors)
+ {
+ CodeCompileUnit ccu = CreateStronglyTypedResourceFile(resourceName, resources,
+ StronglyTypedClassName, StronglyTypedNamespace, provider, out errors);
+ CodeGeneratorOptions codeGenOptions = new CodeGeneratorOptions();
+ codeGenOptions.BlankLinesBetweenMembers = false;
+ codeGenOptions.BracingStyle = "C";
+
+ provider.GenerateCodeFromCompileUnit(ccu, writer, codeGenOptions);
+ writer.Flush();
+ }
+
+ public void CreateStronglyTypedResources(string inputFileName, CodeDomProvider provider, TextWriter writer, string resourceName)
+ {
+ Init();
+
+ ReadResources(inputFileName, true);
+
+ string[] errors = null;
+
+ CreateStronglyTypedResources(provider, writer, resourceName, out errors);
+
+ if (errors != null && errors.Length > 0)
+ {
+ throw new ApplicationException(errors[0]);
+ }
+
+ StronglyTypedResourceSuccessfullyCreated = true;
+ }
+
+ ///
+ /// Generates a strongly typed resource class from in-memory .resx content.
+ /// Used by the VS custom tool so that unsaved designer edits are reflected
+ /// immediately without waiting for the file to be written to disk.
+ ///
+ public void CreateStronglyTypedResources(string inputFileName, string inputFileContent, CodeDomProvider provider, TextWriter writer, string resourceName)
+ {
+ Init();
+
+ ReadResources(inputFileName, inputFileContent, true);
+
+ string[] errors = null;
+
+ CreateStronglyTypedResources(provider, writer, resourceName, out errors);
+
+ if (errors != null && errors.Length > 0)
+ {
+ throw new ApplicationException(errors[0]);
+ }
+
+ StronglyTypedResourceSuccessfullyCreated = true;
+ }
+
+ ///
+ /// Reads resources from in-memory .resx content. For ResXFileRef entries the
+ /// linked files are still resolved from disk using the directory of
+ /// as the base path.
+ ///
+ public void ReadResources(String filename, string fileContent, bool shouldUseSourcePath)
+ {
+ Format format = GetFormat(filename);
+ if (format == Format.XML)
+ {
+ string basePath = shouldUseSourcePath
+ ? Path.GetDirectoryName(Path.GetFullPath(filename))
+ : null;
+#if NETFRAMEWORK
+ ResXResourceReader resXReader = assemblyList != null
+ ? new ResXResourceReader(new StringReader(fileContent), assemblyList)
+ : new ResXResourceReader(new StringReader(fileContent));
+ if (basePath != null)
+ {
+ resXReader.BasePath = basePath;
+ }
+ ReadResources(resXReader, filename);
+#else
+ using (StringReader sr = new StringReader(fileContent))
+ {
+ ReadResources(new NanoResxResourceReader(sr, basePath), filename);
+ }
+#endif
+ }
+ else
+ {
+ ReadResources(filename, shouldUseSourcePath);
+ }
+ }
+
+ private CodeNamespace CreateNamespace(CodeCompileUnit ccu, string ns, Hashtable tableNamespaces)
+ {
+ CodeNamespace codeNamespace = (CodeNamespace)tableNamespaces[ns];
+
+ if (codeNamespace == null)
+ {
+ codeNamespace = new CodeNamespace(ns);
+ ccu.Namespaces.Add(codeNamespace);
+ tableNamespaces[ns] = codeNamespace;
+ }
+
+ return codeNamespace;
+ }
+
+ private CodeTypeDeclaration CreateTypeDeclaration(CodeNamespace codeNamespace, string type, Hashtable tableTypes)
+ {
+ CodeTypeDeclaration codeTypeDeclaration = (CodeTypeDeclaration)tableTypes[type];
+
+ if (codeTypeDeclaration == null)
+ {
+ int iPlus = type.LastIndexOf('+');
+
+ if (iPlus < 0)
+ {
+ codeTypeDeclaration = new CodeTypeDeclaration(type);
+ codeTypeDeclaration.IsPartial = true;
+
+ codeNamespace.Types.Add(codeTypeDeclaration);
+ }
+ else
+ {
+ string typeBase = type.Substring(0, iPlus);
+ string typeNested = type.Substring(iPlus + 1);
+
+ CodeTypeDeclaration codeTypeDeclarationBase = CreateTypeDeclaration(codeNamespace, typeBase, tableTypes);
+ codeTypeDeclaration = new CodeTypeDeclaration(typeNested);
+
+ codeTypeDeclarationBase.Members.Add(codeTypeDeclaration);
+ }
+
+ MakeInternalIfNecessary(codeTypeDeclaration);
+ tableTypes[type] = codeTypeDeclaration;
+ }
+
+ return codeTypeDeclaration;
+ }
+
+ private void CreateHelperMethod(CodeTypeDeclaration codeTypeDeclaration, Entry.ResourceTypeDescription typeDesciption, string parameterType)
+ {
+
+ /*
+ public static ( id)
+ {
+ return ().GetObject( .ResourceManager, id );
+ }
+
+ for example
+
+ public static Font GetFont( FontTag id )
+ {
+ return (Font)Microsoft.SPOT.ResourcesUtility.GetObject( MyResources.ResourceManager, FontTag id );
+ }
+
+ */
+
+ CodeMemberMethod method = new CodeMemberMethod();
+ CodeParameterDeclarationExpression parameterIdDeclaration = new CodeParameterDeclarationExpression(parameterType, "id");
+ CodeVariableReferenceExpression parameterIdReference = new CodeVariableReferenceExpression(parameterIdDeclaration.Name);
+ codeTypeDeclaration.Members.Add(method);
+
+ method.Name = typeDesciption.helperName;
+ method.Parameters.Add(parameterIdDeclaration);
+ method.ReturnType = new CodeTypeReference(typeDesciption.runtimeType);
+
+ method.Attributes = MemberAttributes.Public | MemberAttributes.Static;
+ MakeInternalIfNecessary(method);
+
+ string getObjectClass = IsMscorlib ? "System.Resources.ResourceManager" : "nanoFramework.Runtime.Native.ResourceUtility";
+
+ CodeVariableReferenceExpression expresionId = new CodeVariableReferenceExpression("id");
+ CodeTypeReferenceExpression spotResourcesReference = new CodeTypeReferenceExpression(getObjectClass);
+ CodePropertyReferenceExpression resourceManagerReference = new CodePropertyReferenceExpression(null, "ResourceManager");
+ CodeExpression expressionGetObject = new CodeMethodInvokeExpression(spotResourcesReference, "GetObject", resourceManagerReference, expresionId);
+ CodeExpression expressionValue = new CodeCastExpression(typeDesciption.runtimeType, expressionGetObject);
+ CodeMethodReturnStatement statementReturn = new CodeMethodReturnStatement(expressionValue);
+ method.Statements.Add(statementReturn);
+ }
+
+ private void MakeInternalIfNecessary(CodeTypeDeclaration codeTypeDeclaration)
+ {
+ if (GenerateInternalClass)
+ {
+ codeTypeDeclaration.TypeAttributes &= ~TypeAttributes.VisibilityMask;
+ codeTypeDeclaration.TypeAttributes |= TypeAttributes.NestedAssembly;
+ }
+ }
+
+ private void MakeInternalIfNecessary(CodeTypeMember codeTypeMember)
+ {
+ if (GenerateInternalClass)
+ {
+ codeTypeMember.Attributes &= ~MemberAttributes.AccessMask;
+ codeTypeMember.Attributes |= MemberAttributes.Assembly;
+ }
+ }
+
+
+ private CodeCompileUnit CreateStronglyTypedResourceFile(string resourceName, ArrayList resources, string className, string ns, CodeDomProvider provider, out string[] errors)
+ {
+ //create list of classes needed to be emitted.
+ CodeCompileUnit ccu = new CodeCompileUnit();
+
+ Hashtable tableNamespaces = new Hashtable();
+ Hashtable tableTypes = new Hashtable();
+ Hashtable tableHelperFunctionsNeeded = new Hashtable();
+ ArrayList[] resourceTypesUsed = new ArrayList[NanoResourceFile.ResourceHeader.RESOURCE_Max + 1];
+
+ CodeNamespace codeNamespace;
+ CodeTypeDeclaration codeTypeDeclaration;
+
+ //break down resources by enum
+ for (int iEntry = 0; iEntry < resources.Count; iEntry++)
+ {
+ Entry entry = (Entry)resources[iEntry];
+
+ if (resourceTypesUsed[entry.ResourceType] == null)
+ {
+ resourceTypesUsed[entry.ResourceType] = new ArrayList();
+ }
+
+ if (!resourceTypesUsed[entry.ResourceType].Contains(entry.ClassName))
+ {
+ resourceTypesUsed[entry.ResourceType].Add(entry.ClassName);
+ }
+
+ codeNamespace = CreateNamespace(ccu, entry.Namespace, tableNamespaces);
+ codeTypeDeclaration = CreateTypeDeclaration(codeNamespace, entry.ClassName, tableTypes);
+
+ if (!codeTypeDeclaration.IsEnum)
+ {
+ //only initialize once
+ codeTypeDeclaration.IsEnum = true;
+ codeTypeDeclaration.CustomAttributes.Add(new CodeAttributeDeclaration("System.SerializableAttribute"));
+ codeTypeDeclaration.BaseTypes.Add(new CodeTypeReference(typeof(short)));
+ }
+
+ CodeMemberField codeMemberField = new CodeMemberField(entry.ClassName, entry.Field);
+ codeMemberField.Attributes = MemberAttributes.Const | MemberAttributes.Static;
+
+ CodePrimitiveExpression codeExpression = new CodePrimitiveExpression(entry.Id);
+ codeMemberField.InitExpression = codeExpression;
+ //set constant value??!!
+ int iField = codeTypeDeclaration.Members.Add(codeMemberField);
+
+ }
+
+ //emit helper functions
+
+ codeNamespace = CreateNamespace(ccu, ns, tableNamespaces);
+ codeTypeDeclaration = CreateTypeDeclaration(codeNamespace, className, tableTypes);
+ MakeInternalIfNecessary(codeTypeDeclaration);
+
+ CodeTypeReferenceExpression codeTypeReferenceExpression = new CodeTypeReferenceExpression(codeTypeDeclaration.Name);
+
+ //private static System.Resources.ResourceManager manager;
+ CodeMemberField fieldManager = new CodeMemberField("System.Resources.ResourceManager", "manager");
+ CodeFieldReferenceExpression fieldManagerExpression = new CodeFieldReferenceExpression(codeTypeReferenceExpression, fieldManager.Name);
+
+ fieldManager.Attributes = MemberAttributes.Static | MemberAttributes.Private;
+ codeTypeDeclaration.Members.Add(fieldManager);
+
+ /*
+ [private|internal] static System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if(manager == null)
+ {
+ manager = new System.Resources.ResourceManager( "Resources", typeof( Resources ).Assembly );
+ }
+
+ return manager;
+ }
+ }
+ */
+
+ CodeBinaryOperatorExpression getResourceManagerExpressionIfNull = new CodeBinaryOperatorExpression(fieldManagerExpression, CodeBinaryOperatorType.IdentityEquality, new CodePrimitiveExpression(null));
+ CodeObjectCreateExpression getResourceManagerExpressionNewManager = new CodeObjectCreateExpression(
+ "System.Resources.ResourceManager",
+ new CodePrimitiveExpression(resourceName),
+ new CodePropertyReferenceExpression(new CodeTypeOfExpression(codeTypeReferenceExpression.Type), "Assembly")
+ );
+ CodeAssignStatement getResourceManagerExpressionInitializeManager = new CodeAssignStatement(fieldManagerExpression, getResourceManagerExpressionNewManager);
+
+ CodeStatementCollection getResourceManagerExpression = new CodeStatementCollection();
+
+ getResourceManagerExpression.AddRange(new CodeStatement[] {
+ new CodeConditionStatement( getResourceManagerExpressionIfNull, getResourceManagerExpressionInitializeManager ),
+ new CodeMethodReturnStatement( fieldManagerExpression )
+ }
+ );
+
+ CodeMemberProperty propertyResourceManager = new CodeMemberProperty();
+ propertyResourceManager.Name = "ResourceManager";
+ propertyResourceManager.Type = new CodeTypeReference("System.Resources.ResourceManager");
+ propertyResourceManager.Attributes = MemberAttributes.Public | MemberAttributes.Static;
+ MakeInternalIfNecessary(propertyResourceManager);
+ propertyResourceManager.GetStatements.AddRange(getResourceManagerExpression);
+ codeTypeDeclaration.Members.Add(propertyResourceManager);
+
+ for (int i = 0; i < resourceTypesUsed.Length; i++)
+ {
+ ArrayList list = resourceTypesUsed[i];
+
+ if (list != null)
+ {
+ Entry.ResourceTypeDescription typeDescription = Entry.ResourceTypeDescriptionFromResourceType((byte)i);
+
+ for (int iClassName = 0; iClassName < list.Count; iClassName++)
+ {
+ CreateHelperMethod(codeTypeDeclaration, typeDescription, (string)list[iClassName]);
+ }
+ }
+ }
+
+ errors = new string[0];
+ return ccu;
+ }
+ ///
+ /// If no strongly typed resource class filename was specified, we come up with a default based on the
+ /// input file name and the default language extension. Broken out here so it can be called from GenerateResource class.
+ ///
+ /// A CodeDomProvider for the language
+ /// Name of the output resources file
+ /// Filename for strongly typed resource class
+ public static string GenerateDefaultStronglyTypedFilename(CodeDomProvider provider, string outputResourcesFile)
+ {
+ return Path.ChangeExtension(outputResourcesFile, provider.FileExtension);
+ }
+
+ ///
+ /// Read resources from an XML or binary format file
+ ///
+ /// Appropriate IResourceReader
+ /// Filename, for error messages
+ private void ReadResources(IResourceReader reader, String fileName)
+ {
+ using (reader)
+ {
+ IDictionaryEnumerator resEnum = reader.GetEnumerator();
+ while (resEnum.MoveNext())
+ {
+ string name = (string)resEnum.Key;
+ // Replace dot in the name with underscore.
+ // 1. First reason - this is what desktop resource generator does.
+ // 2. Second reason - Extra dots causes resource generator to create name space and enumerations.
+ // This complicates the syntax and finally create invalid code if 2 or more dots are present.
+ // So we just make longer name.
+ name = name.Replace('.', '_');
+ object value;
+ try
+ {
+ value = resEnum.Value;
+ }
+ catch (Exception ex)
+ {
+ logger?.LogWarning(null, fileName, 0, 0, 0, 0, "GenerateResource.CannotLoadResource", (string)resEnum.Key, ex.Message);
+ continue;
+ }
+ AddResource(name, value, fileName);
+ }
+ }
+
+ EnsureResourcesIds(resources);
+ }
+
+ private static short GenerateIdFromResourceName(string s)
+ {
+ //adapted from BCL implementation
+
+ int hash1 = (5381 << 16) + 5381;
+ int hash2 = hash1;
+
+ char[] chars = s.ToCharArray();
+
+ int len = s.Length;
+
+ for (int i = 0; i < len; i++)
+ {
+ char c = s[i];
+ if (i % 2 == 0)
+ {
+ hash1 = ((hash1 << 5) + hash1) ^ c;
+ }
+ else
+ {
+ hash2 = ((hash2 << 5) + hash2) ^ c;
+ }
+ }
+
+ int hash = hash1 + (hash2 * 1566083941);
+
+ short ret = (short)((short)(hash >> 16) ^ (short)hash);
+
+ return ret;
+ }
+
+ internal static void EnsureResourcesIds(ArrayList resources)
+ {
+ int iResource;
+ Hashtable table = new Hashtable();
+
+ if (resources.Count > UInt16.MaxValue)
+ {
+ throw new ApplicationException("Too many resources. Maximum number of resources per ResourceSet is 65535");
+ }
+
+ for (iResource = 0; iResource < resources.Count; iResource++)
+ {
+ Entry entry = (Entry)resources[iResource];
+
+ short id = entry.Id;
+
+ if (table.ContainsKey(id))
+ {
+ //rwolff -- check regarding boxed objects....
+
+ //duplicate id detected.
+ Entry entryDup = (Entry)table[id];
+
+ throw new ApplicationException(string.Format("Duplicate id detected. Resources '{0}' and '{1}' are generating the same id=0x{2}", entry.Name, entryDup.Name, id));
+ }
+
+ table[id] = entry;
+ }
+
+ resources.Sort();
+ }
+
+ ///
+ /// Read resources from a text format file
+ ///
+ /// Input resources filename
+ private void ReadTextResources(String fileName)
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Write resources to an XML or binary format resources file.
+ ///
+ /// Closes writer automatically
+ /// Appropriate IResourceWriter
+ private void WriteResources(IResourceWriter writer)
+ {
+ try
+ {
+ foreach (Entry entry in resources)
+ {
+ string key = entry.RawName;
+ object value = entry.Value;
+
+ if (writer is NanoResourceWriter)
+ {
+ ((NanoResourceWriter)writer).AddResource(entry);
+ }
+ else
+ {
+ writer.AddResource(key, value);
+ }
+ }
+
+ writer.Generate();
+ }
+ finally
+ {
+ writer.Close();
+ }
+ }
+
+ ///
+ /// Write resources to a text format resources file
+ ///
+ /// Output resources file
+ private void WriteTextResources(String fileName)
+ {
+ using (StreamWriter writer = new StreamWriter(fileName, false, Encoding.UTF8))
+ {
+ foreach (Entry entry in resources)
+ {
+ String key = entry.Name;
+ Object v = entry.Value;
+ String value = v as String;
+ if (value == null)
+ {
+ logger.LogError(null, fileName, 0, 0, 0, 0, "GenerateResource.OnlyStringsSupported", key, v.GetType().FullName);
+ }
+ else
+ {
+ // Escape any special characters in the String.
+ value = value.Replace("\\", "\\\\");
+ value = value.Replace("\n", "\\n");
+ value = value.Replace("\r", "\\r");
+ value = value.Replace("\t", "\\t");
+
+ writer.WriteLine("{0}={1}", key, value);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Add a resource from a text file to the internal data structures
+ ///
+ /// Resource name
+ /// Resource value
+ /// Input file for messages
+ /// Line number for messages
+ /// Column number for messages
+ private void AddResource(string name, object value, String inputFileName, int lineNumber, int linePosition)
+ {
+ if (resourcesHashTable.ContainsKey(name))
+ {
+ logger?.LogWarning(null, inputFileName, lineNumber, linePosition, 0, 0, "GenerateResource.DuplicateResourceName", name);
+ return;
+ }
+
+ if (value == null)
+ {
+ // ResXResourceReader returns a null value when a resource cannot be materialized
+ // (e.g. a 'bytearray.base64' mimetype with no declared 'type' attribute). Skip it
+ // with a clear warning instead of failing the whole build with an opaque NRE.
+ logger?.LogWarning($"Resource '{name}' in '{inputFileName}' could not be loaded (value is null; missing or unsupported type/mimetype) and was skipped.");
+ return;
+ }
+
+ Entry entry = Entry.CreateEntry(name, value, StronglyTypedNamespace, GenerateNestedEnums ? StronglyTypedClassName : string.Empty);
+
+ Debug.Assert(entry.ClassName.Length > 0);
+
+ resources.Add(entry);
+ resourcesHashTable[name] = entry;
+ }
+
+ private void AddResource(string name, object value, String inputFileName)
+ {
+ AddResource(name, value, inputFileName, 0, 0);
+ }
+
+ ///
+ /// Name value resource pair to go in resources list
+ ///
+ internal abstract class Entry : IComparable
+ {
+ public class ResourceTypeDescription
+ {
+ public byte resourceType;
+ public string helperName;
+ public string runtimeType;
+ public string defaultEnum;
+
+ public ResourceTypeDescription(byte resourceType, string helperName, string runtimeType, string defaultEnum)
+ {
+ this.resourceType = resourceType;
+ this.helperName = helperName;
+ this.runtimeType = runtimeType;
+ this.defaultEnum = defaultEnum;
+ }
+ }
+
+ private static ResourceTypeDescription[] typeDescriptions = new ResourceTypeDescription[]
+ {
+ null, //RESOURCE_Invalid
+ new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_Bitmap, "GetBitmap", "nanoFramework.UI.Bitmap", "BitmapResources"),
+ new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_Font, "GetFont", "nanoFramework.UI.Font", "FontResources"),
+ new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_String, "GetString", "System.String", "StringResources"),
+ new ResourceTypeDescription(NanoResourceFile.ResourceHeader.RESOURCE_Binary, "GetBytes", "System.Byte[]", "BinaryResources"),
+ };
+
+ public static ResourceTypeDescription ResourceTypeDescriptionFromResourceType(byte type)
+ {
+ if (type < NanoResourceFile.ResourceHeader.RESOURCE_Bitmap || type > NanoResourceFile.ResourceHeader.RESOURCE_Binary)
+ {
+ throw new ArgumentException();
+ }
+
+ return typeDescriptions[type];
+ }
+
+ public static Entry CreateEntry(string name, object value, string defaultNamespace, string defaultDeclaringClass)
+ {
+
+ Entry entry = null;
+
+ switch (value.GetType().Name)
+ {
+ case "String":
+ entry = new StringEntry(name, value as string);
+ break;
+
+ case "Byte[]":
+ byte[] rawValue = value as byte[];
+ // read raw content so it can be saved as a binary entry resource (byte[])
+#if NETFRAMEWORK
+ Bitmap bitmapImage = null;
+ bool imageJPeg = false;
+ bool imageGif = false;
+ bool imageBmp = false;
+ try
+ {
+ bitmapImage = Image.FromStream(new MemoryStream(rawValue)) as Bitmap;
+ imageJPeg = bitmapImage.RawFormat.Equals(ImageFormat.Jpeg);
+ imageGif = bitmapImage.RawFormat.Equals(ImageFormat.Gif);
+ imageBmp = bitmapImage.RawFormat.Equals(ImageFormat.Bmp);
+ }
+ catch
+ {
+ // Ignore error if not a known supported bitmap
+ }
+ if (imageJPeg || imageGif || imageBmp)
+ {
+ entry = new BitmapEntry(name, bitmapImage);
+ }
+#else
+ if (rawValue != null && CrossPlatformBitmap.IsSupportedBitmap(rawValue))
+ {
+ entry = new BitmapEntry(name, rawValue);
+ }
+#endif
+ else if (rawValue != null)
+ {
+ // This code handles fonts which have a resource header starting with a Magic Number
+ // followed by a number of resources, e.g. each font letter etc...
+ entry = NanoResourcesEntry.TryCreateNanoResourcesEntry(name, rawValue);
+
+ // If just a byte array, create a Binary Resource
+ if (entry == null)
+ {
+ // Create BinaryEntry
+ using (var ms = new MemoryStream(rawValue))
+ {
+ entry = new BinaryEntry(name, rawValue);
+ }
+ }
+ }
+ break;
+ case "MemoryStream":
+ // Examples - .wav
+ // this is a binary resource
+ MemoryStream msOther = (MemoryStream)value;
+ byte[] memoryData = msOther.ToArray();
+ entry = new BinaryEntry(name, memoryData);
+ break;
+ default:
+ break;
+ }
+
+ if (entry == null)
+ {
+ throw new Exception($"Resource '{name}' has unsupported type '{value.GetType().FullName}'.");
+ }
+
+ if (entry.Namespace.Length == 0)
+ {
+ entry.Namespace = defaultNamespace;
+ }
+
+ if (entry.ClassName.Length == 0)
+ {
+ ResourceTypeDescription typeDescription = ResourceTypeDescriptionFromResourceType(entry.ResourceType);
+ entry.ClassName = typeDescription.defaultEnum;
+
+ if (!string.IsNullOrEmpty(defaultDeclaringClass))
+ {
+ entry.ClassName = $"{defaultDeclaringClass}+{entry.ClassName}";
+ }
+ }
+
+ return entry;
+ }
+
+ private bool ParseId(string val, out short id)
+ {
+ val = val.Trim();
+ bool fSuccess = false;
+
+ if (val.StartsWith("0x", true, CultureInfo.InvariantCulture))
+ {
+ ushort us;
+
+ fSuccess = ushort.TryParse(val.Substring(2), NumberStyles.AllowHexSpecifier, null, out us);
+
+ id = (short)us;
+ }
+ else
+ {
+ fSuccess = short.TryParse(val, out id);
+ }
+
+ return fSuccess;
+ }
+
+ public Entry(string name, object value)
+ {
+ Value = value;
+ Namespace = string.Empty;
+ ClassName = string.Empty;
+ field = string.Empty;
+ rawName = name;
+
+ //parse name
+
+ string[] tokens = name.Split(';');
+ string idValue;
+ short idT;
+
+ switch (tokens.Length)
+ {
+ case 1:
+ name = tokens[0];
+ idValue = string.Empty;
+ break;
+ case 2:
+ name = tokens[0];
+ idValue = tokens[1];
+ break;
+ default:
+ throw new ArgumentException();
+ }
+
+ idValue = idValue.Trim();
+
+ if (idValue.Length > 0)
+ {
+ if (!ParseId(idValue, out idT))
+ {
+ throw new ApplicationException(string.Format("Cannot parse id '{0}' from resource '{1}'", idValue, name));
+ }
+
+ Id = idT;
+ }
+ else
+ {
+ Id = GenerateIdFromResourceName(name);
+ }
+
+ int iDotLast = name.LastIndexOf('.');
+
+ field = name;
+
+ if (iDotLast >= 0)
+ {
+ field = name.Substring(iDotLast + 1);
+
+ name = name.Substring(0, iDotLast);
+
+ iDotLast = name.LastIndexOf('.');
+ //iDotLast = name.LastIndexOfAny( new char[] { '.', '+' } );
+
+ ClassName = name.Trim();
+
+ if (iDotLast >= 0)
+ {
+ ClassName = name.Substring(iDotLast + 1).Trim();
+ Namespace = name.Substring(0, iDotLast).Trim();
+ }
+ }
+ }
+
+ private string field;
+ private string rawName;
+
+ public string Name
+ {
+ get { return string.Format("{0}.{1}.{2};0x{3}", Namespace, ClassName, field, Id.ToString("X4")); }
+ }
+
+ public string RawName
+ {
+ get { return rawName; }
+ }
+
+ public object Value { get; }
+
+ public short Id { get; set; }
+
+ public string Namespace { get; set; }
+
+ public string ClassName { get; set; }
+
+ public string Field
+ {
+ get { return field; }
+ set { field = value; }
+ }
+
+ #region IComparable Members
+
+ int IComparable.CompareTo(object obj)
+ {
+ Entry entry = obj as Entry;
+
+ if (entry == null)
+ {
+ return -1;
+ }
+
+ return Id.CompareTo(entry.Id);
+ }
+
+ #endregion
+
+ public virtual byte ResourceType
+ {
+ get
+ {
+ if (Value.GetType() == typeof(string)) return NanoResourceFile.ResourceHeader.RESOURCE_String;
+
+ return NanoResourceFile.ResourceHeader.RESOURCE_Invalid;
+ }
+ }
+
+ public virtual byte[] GenerateResourceData()
+ {
+ return null;
+ }
+ }
+
+ private class StringEntry : Entry
+ {
+ public StringEntry(string name, string value) : base(name, value)
+ {
+ }
+
+ private string StringValue
+ {
+ get { return Value as string; }
+ }
+
+ public override byte ResourceType
+ {
+ get
+ {
+ return NanoResourceFile.ResourceHeader.RESOURCE_String;
+ }
+ }
+
+ public override byte[] GenerateResourceData()
+ {
+ string val = StringValue + '\0';
+
+ byte[] data = Encoding.UTF8.GetBytes(val);
+
+ return data;
+ }
+ }
+
+ private class BitmapEntry : Entry
+ {
+#if NETFRAMEWORK
+ public BitmapEntry(string name, System.Drawing.Bitmap value) : base(name, value)
+ {
+ }
+
+ private System.Drawing.Bitmap BitmapValue
+ {
+ get { return Value as System.Drawing.Bitmap; }
+ }
+
+ public override byte ResourceType
+ {
+ get
+ {
+ return NanoResourceFile.ResourceHeader.RESOURCE_Bitmap;
+ }
+ }
+
+
+ private void Adjust1bppOrientation(byte[] buf)
+ {
+ //CLR_GFX_Bitmap::AdjustBitOrientation
+ //The nanoCLR treats 1bpp bitmaps reversed from Windows
+ //And most likely every other 1bpp format as well
+ byte[] reverseTable = new byte[]
+ {
+ 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0,
+ 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0,
+ 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8,
+ 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8,
+ 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4,
+ 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4,
+ 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC,
+ 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC,
+ 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2,
+ 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2,
+ 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA,
+ 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA,
+ 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6,
+ 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6,
+ 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE,
+ 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE,
+ 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1,
+ 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1,
+ 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9,
+ 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9,
+ 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5,
+ 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5,
+ 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED,
+ 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD,
+ 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3,
+ 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3,
+ 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB,
+ 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB,
+ 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7,
+ 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7,
+ 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF,
+ 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F,0xFF,
+ };
+
+ for (int i = buf.Length - 1; i >= 0; i--)
+ {
+ buf[i] = reverseTable[buf[i]];
+ }
+ }
+
+ private void Compress1bpp(NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription, ref byte[] buf)
+ {
+ MemoryStream ms = new MemoryStream(buf.Length);
+
+ //adapted from CLR_GFX_Bitmap::Compress
+ //CLR_RT_Buffer buffer;
+ int count = 0;
+ bool fSetSav = false;
+ bool fSet = false;
+ bool fFirst = true;
+ bool fRun = true;
+ byte data = 0;
+ bool fEmit = false;
+ int widthInWords = (int)((bitmapDescription.m_width + 31) / 32);
+ int iByte;
+ byte iPixelMask;
+
+ iByte = 0;
+ for (int y = 0; y < bitmapDescription.m_height; y++)
+ {
+ iPixelMask = 0x1;
+ iByte = y * (widthInWords * 4);
+
+ for (int x = 0; x < bitmapDescription.m_width; x++)
+ {
+ fSetSav = fSet;
+
+ fSet = (buf[iByte] & iPixelMask) != 0;
+ if (fFirst)
+ {
+ fFirst = false;
+ }
+ else
+ {
+ if (fRun)
+ {
+ fRun = (fSetSav == fSet);
+
+ if ((count == 0x3f + NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset) ||
+ (!fRun && count >= NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset))
+ {
+ data = NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRun;
+ data |= (fSetSav ? NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunSet : (byte)0x0);
+ data |= (byte)(count - NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset);
+ fEmit = true;
+ }
+ }
+
+ if (!fRun && count == NanoResourceFile.CLR_GFX_BitmapDescription.c_UncompressedRunLength)
+ {
+ fEmit = true;
+ }
+
+ if (fEmit)
+ {
+ ms.WriteByte(data);
+
+ data = 0;
+ count = 0;
+ fEmit = false;
+ fRun = true;
+ }
+ }
+
+ data |= (byte)((0x1 << count) & (fSet ? 0xff : 0x0));
+
+ iPixelMask <<= 1;
+ if (iPixelMask == 0)
+ {
+ iPixelMask = 0x1;
+ iByte++;
+ }
+
+ count++;
+ }
+ }
+
+ if (fRun && count >= NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset)
+ {
+ data = NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRun;
+ data |= (fSetSav ? NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunSet : (byte)0x0);
+ data |= (byte)(count - NanoResourceFile.CLR_GFX_BitmapDescription.c_CompressedRunOffset);
+ }
+
+ ms.WriteByte(data);
+
+ if (ms.Length < buf.Length)
+ {
+ ms.Capacity = (int)ms.Length;
+ buf = ms.GetBuffer();
+
+ bitmapDescription.m_flags |= NanoResourceFile.CLR_GFX_BitmapDescription.c_Compressed;
+ }
+ }
+ private byte[] GetBitmapDataBmp(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription)
+ {
+
+
+ try
+ {
+ byte bitsPerPixel = 16;
+ Bitmap clone = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format16bppRgb565);
+ using (Graphics gr = Graphics.FromImage(clone))
+ {
+ gr.DrawImageUnscaled(bitmap, 0, 0);
+ }
+
+ Rectangle rect = new Rectangle(0, 0, clone.Width, clone.Height);
+ BitmapData bitmapData = clone.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format16bppRgb565);
+
+ byte[] data = new byte[clone.Width * clone.Height * 2];
+
+ System.Runtime.InteropServices.Marshal.Copy(bitmapData.Scan0, data, 0, data.Length);
+ clone.UnlockBits(bitmapData);
+
+ bitmapDescription = new NanoResourceFile.CLR_GFX_BitmapDescription(
+ (ushort)bitmap.Width, (ushort)bitmap.Height, 0, bitsPerPixel, NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeBitmap);
+
+ return data;
+ }
+ catch
+ {
+ throw new NotSupportedException($"PixelFormat ({bitmap.PixelFormat.ToString()}) could not be converted to Format16bppRgb565.");
+ };
+ }
+
+ private byte[] GetBitmapDataRaw(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription, byte type)
+ {
+ bitmapDescription = new NanoResourceFile.CLR_GFX_BitmapDescription((ushort)bitmap.Width, (ushort)bitmap.Height, 0, 1, type);
+
+ MemoryStream stream = new MemoryStream();
+
+ // need to copy the Bitmap in order to access it, otherwise it will throw an exception
+ using (Bitmap bmpCopy = new Bitmap(bitmap))
+ {
+ bmpCopy.Save(stream, bitmap.RawFormat);
+ }
+
+ stream.Capacity = (int)stream.Length;
+ return stream.GetBuffer();
+ }
+
+ private byte[] GetBitmapDataJpeg(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription)
+ {
+ return GetBitmapDataRaw(bitmap, out bitmapDescription, NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeJpeg);
+ }
+
+ private byte[] GetBitmapDataGif(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription)
+ {
+ return GetBitmapDataRaw(bitmap, out bitmapDescription, NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeGif);
+ }
+
+ private byte[] GetBitmapData(Bitmap bitmap, out NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription)
+ {
+ byte[] data = null;
+
+ if (bitmap.Width > 0xFFFF || bitmap.Height > 0xFFFF)
+ {
+ throw new ArgumentException("bitmap dimensions out of range");
+ }
+
+ if (bitmap.RawFormat.Equals(ImageFormat.Jpeg))
+ {
+ data = GetBitmapDataJpeg(bitmap, out bitmapDescription);
+ }
+ else if (bitmap.RawFormat.Equals(ImageFormat.Gif))
+ {
+ data = GetBitmapDataGif(bitmap, out bitmapDescription);
+ }
+ else if (bitmap.RawFormat.Equals(ImageFormat.Bmp))
+ {
+ data = GetBitmapDataBmp(bitmap, out bitmapDescription);
+ }
+ else
+ {
+ throw new NotSupportedException(string.Format("Bitmap imageFormat not supported '{0}'", bitmap.RawFormat.Guid.ToString()));
+ }
+
+ return data;
+ }
+
+ public override byte[] GenerateResourceData()
+ {
+ Bitmap bitmap = BitmapValue;
+
+ NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription;
+
+ byte[] data = GetBitmapData(bitmap, out bitmapDescription);
+
+ MemoryStream stream = new MemoryStream();
+ BinaryWriter writer = new BinaryWriter(stream);
+
+ bitmapDescription.Serialize(writer);
+ writer.Write(data);
+
+ stream.Capacity = (int)stream.Length;
+ return stream.GetBuffer();
+ }
+
+ /*
+ public override byte[] GenerateResourceData()
+ {
+ byte[] data = null;
+
+ ushort flags = 0;
+ byte type = 0;
+ byte bitsPerPixel = 24;
+ BitmapData bitmapData = null;
+ Bitmap bitmap = this.BitmapValue;
+ PixelFormat formatDst = bitmap.PixelFormat;
+ ushort width = (ushort)bitmap.Width;
+ ushort height = (ushort)bitmap.Height;
+ NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription;
+
+ Rectangle rect = new Rectangle( 0, 0, this.BitmapValue.Width, this.BitmapValue.Height );
+
+ if(bitmap.RawFormat.Equals(ImageFormat.Jpeg))
+ {
+ type = NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeJpeg;
+ }
+ else if(bitmap.RawFormat.Equals(ImageFormat.Gif))
+ {
+ type = NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeGif;
+ }
+ else if(bitmap.RawFormat.Equals(ImageFormat.Bmp))
+ {
+ type = NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeBitmap;
+
+ //issue warning for formats that we lose information?
+ //other formats that we need to support??
+
+ switch(bitmap.PixelFormat)
+ {
+ case PixelFormat.Format1bppIndexed:
+ bitsPerPixel = 1;
+ formatDst = PixelFormat.Format1bppIndexed;
+ break;
+ case PixelFormat.Format24bppRgb:
+ case PixelFormat.Format32bppRgb:
+ case PixelFormat.Format48bppRgb:
+ //currently don't support more than 16bp...fall through..
+ case PixelFormat.Format16bppRgb555:
+ case PixelFormat.Format16bppRgb565:
+ bitsPerPixel = 16;
+ formatDst = PixelFormat.Format16bppRgb565;
+ break;
+ default:
+ throw new NotSupportedException( string.Format( "PixelFormat of '{0}' resource not supported", this.Name ) );
+ }
+
+ //turn bitmap data into a form we can use.
+
+ if(formatDst != bitmap.PixelFormat)
+ {
+ bitmap = bitmap.Clone( rect, formatDst );
+ }
+ }
+ else
+ {
+ throw new NotSupportedException( string.Format("Bitmap imageFormat not supported '{0}'", bitmap.RawFormat.Guid.ToString()) );
+ }
+
+ try
+ {
+ bitmapData = bitmap.LockBits( rect, ImageLockMode.ReadOnly, formatDst );
+
+ IntPtr p = bitmapData.Scan0;
+ data = new byte[bitmapData.Stride * height];
+
+ System.Runtime.InteropServices.Marshal.Copy( bitmapData.Scan0, data, 0, data.Length );
+
+ if(bitsPerPixel == 1)
+ {
+ //special case for 1pp with index 0 equals white???!!!???
+ if(bitmap.Palette.Entries[0].GetBrightness() < 0.5)
+ {
+ for(int i = 0; i < data.Length; i++)
+ {
+ data[i] = (byte)~data[i];
+ }
+ }
+
+ //special case for 1pp need to flip orientation??
+ //for some stupid reason, 1bpp is flipped compared to windows!!
+ Adjust1bppOrientation( data );
+ }
+ }
+ finally
+ {
+ if(bitmapData != null)
+ {
+ bitmap.UnlockBits( bitmapData );
+ }
+ }
+
+ NanoResourceFile.CLR_GFX_BitmapDescription bitmapDescription = new NanoResourceFile.CLR_GFX_BitmapDescription( width, height, flags, bitsPerPixel, type );
+
+ if(bitsPerPixel == 1 && type == NanoResourceFile.CLR_GFX_BitmapDescription.c_TypeBitmap)
+ {
+ //test compression;
+ Compress1bpp( bitmapDescription, ref data );
+ }
+
+ MemoryStream stream = new MemoryStream();
+ BinaryWriter writer = new BinaryWriter( stream );
+
+ bitmapDescription.Serialize( writer );
+ writer.Write( data );
+
+ byte[] buf = new byte[stream.Length];
+
+ Array.Copy( stream.GetBuffer(), buf, stream.Length );
+
+ return buf;
+ }
+ */
+#else
+ private byte[] RawValue
+ {
+ get { return Value as byte[]; }
+ }
+
+ public BitmapEntry(string name, byte[] value) : base(name, value)
+ {
+ }
+
+ public override byte ResourceType
+ {
+ get
+ {
+ return NanoResourceFile.ResourceHeader.RESOURCE_Bitmap;
+ }
+ }
+
+ public override byte[] GenerateResourceData()
+ {
+ return CrossPlatformBitmap.GenerateBitmapResourceData(RawValue);
+ }
+#endif
+ }
+
+ private class NanoResourcesEntry : Entry
+ {
+ NanoResourceFile.ResourceHeader resource;
+
+ public NanoResourcesEntry(string name, byte[] value) : base(name, value)
+ {
+ }
+
+ public static NanoResourcesEntry TryCreateNanoResourcesEntry(string name, byte[] value)
+ {
+ NanoResourcesEntry entry = null;
+
+ try
+ {
+ MemoryStream stream = new MemoryStream(value);
+
+ BinaryReader reader = new BinaryReader(stream);
+ uint magicNumber = reader.ReadUInt32();
+
+ stream.Position = 0;
+
+ if (magicNumber == NanoResourceFile.Header.MAGIC_NUMBER)
+ {
+ NanoResourceFile file = new NanoResourceFile();
+
+ file.Deserialize(reader);
+
+ if (file.resources.Length == 1)
+ {
+ NanoResourceFile.Resource resource = file.resources[0];
+
+ entry = new NanoResourcesEntry(name, resource.data);
+ entry.resource = resource.header;
+ }
+ }
+ }
+ catch
+ {
+ }
+
+ return entry;
+ }
+
+ private byte[] RawValue
+ {
+ get { return Value as byte[]; }
+ }
+
+ public override byte ResourceType
+ {
+ get
+ {
+ return resource.kind;
+ }
+ }
+
+ public override byte[] GenerateResourceData()
+ {
+ return RawValue;
+ }
+
+ }
+
+ private class BinaryEntry : Entry
+ {
+ public BinaryEntry(string name, byte[] value) : base(name, value)
+ {
+
+ }
+
+ private byte[] BinaryValue
+ {
+ get { return Value as byte[]; }
+ }
+
+ public override byte ResourceType
+ {
+ get
+ {
+ return NanoResourceFile.ResourceHeader.RESOURCE_Binary;
+ }
+ }
+
+ public override byte[] GenerateResourceData()
+ {
+ return BinaryValue;
+ }
+ }
+
+ #endregion // Code from ResGen.EXE
+
+ internal class NanoResourceFile
+ {
+ /*
+ .nanoresources file format. The Header is shared with the Metadataprocessor. Everything else
+ is shared with the nanoCLR>
+
+ -------
+ Header
+ -------
+
+ uint MagicNumber = 0xf995b0a8;
+ uint Version;
+ uint SizeOfHeader;
+ uint SizeOfResourceHeader; //size of all CLR_RECORD_RESOURCE structures to follow
+ uint NumberOfResources; //number of resources
+
+ ---------------------
+ Resource Headers
+ ---------------------
+
+ Starting at Header.SizeOfHeader in the stream,
+ Header.NumberOfResources resourceHeader structures
+
+
+
+ */
+
+ public Header header;
+ public Resource[] resources;
+
+ public class Resource
+ {
+ public ResourceHeader header;
+ public byte[] data;
+
+ public Resource(ResourceHeader header, byte[] data)
+ {
+ this.header = header;
+ this.data = data;
+ }
+ }
+
+ public NanoResourceFile()
+ {
+ resources = new Resource[0];
+ }
+
+ public NanoResourceFile(Header header) : this()
+ {
+ this.header = header;
+ }
+
+ public void AddResource(Resource resource)
+ {
+ int cResource = resources.Length;
+
+ Resource[] resourcesNew = new Resource[cResource + 1];
+ resources.CopyTo(resourcesNew, 0);
+ resourcesNew[cResource] = resource;
+ resources = resourcesNew;
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ header.Serialize(writer);
+
+ for (int iResource = 0; iResource < resources.Length; iResource++)
+ {
+ Resource resource = resources[iResource];
+
+ resource.header.Serialize(writer);
+ }
+
+ for (int iResource = 0; iResource < resources.Length; iResource++)
+ {
+ Resource resource = resources[iResource];
+
+ writer.Write(resource.data);
+ }
+ }
+
+ public void Deserialize(BinaryReader reader)
+ {
+ header = new Header();
+ header.Deserialize(reader);
+
+ if (header.NumberOfResources == 0)
+ {
+ throw new SerializationException("No resources found");
+ }
+
+ resources = new Resource[header.NumberOfResources];
+
+ for (int iResource = 0; iResource < resources.Length; iResource++)
+ {
+ Resource resource = new Resource(new ResourceHeader(), new byte[0]);
+
+ resources[iResource] = resource;
+ resource.header.Deserialize(reader);
+ }
+
+ reader.BaseStream.Position = header.OffsetOfResourceData;
+
+ for (int iResource = 0; iResource < resources.Length; iResource++)
+ {
+ Resource resource = resources[iResource];
+
+ resource.data = reader.ReadBytes((int)resource.header.size);
+ }
+ }
+
+ #region Records
+
+ public class Header
+ {
+ public const uint VERSION = 2;
+ public const uint MAGIC_NUMBER = 0xf995b0a8;
+ public const uint SIZE_FILE_HEADER = 5 * 4;
+ public const uint SIZE_RESOURCE_HEADER = 2 * 4;
+
+ public uint MagicNumber;
+ public uint Version;
+ public uint SizeOfHeader;
+ public uint SizeOfResourceHeader;
+ public uint NumberOfResources;
+
+ public Header(uint numResources)
+ {
+ MagicNumber = MAGIC_NUMBER;
+ Version = VERSION;
+ SizeOfHeader = SIZE_FILE_HEADER;
+ SizeOfResourceHeader = SIZE_RESOURCE_HEADER;
+ NumberOfResources = numResources;
+ }
+
+ public Header()
+ {
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(MagicNumber);
+ writer.Write(Version);
+ writer.Write(SizeOfHeader);
+ writer.Write(SizeOfResourceHeader);
+ writer.Write(NumberOfResources);
+ }
+
+ public void Deserialize(BinaryReader reader)
+ {
+ MagicNumber = reader.ReadUInt32();
+ Version = reader.ReadUInt32();
+ SizeOfHeader = reader.ReadUInt32();
+ SizeOfResourceHeader = reader.ReadUInt32();
+ NumberOfResources = reader.ReadUInt32();
+
+ reader.BaseStream.Position = SizeOfHeader;
+
+ if (MagicNumber != MAGIC_NUMBER ||
+ SizeOfHeader < SIZE_FILE_HEADER ||
+ SizeOfResourceHeader < SIZE_RESOURCE_HEADER
+ )
+ {
+ throw new SerializationException();
+ }
+ else if (Version != VERSION)
+ {
+ throw new SerializationException(string.Format("Incompatible version (version {0}) found, expecting version {1}.", Version, VERSION));
+ }
+ }
+
+ public uint OffsetOfResourceData
+ {
+ get
+ {
+ return SizeOfHeader + SizeOfResourceHeader * NumberOfResources;
+ }
+ }
+ }
+
+ public class ResourceHeader
+ {
+ public const byte RESOURCE_Invalid = 0x00;
+ public const byte RESOURCE_Bitmap = 0x01;
+ public const byte RESOURCE_Font = 0x02;
+ public const byte RESOURCE_String = 0x03;
+ public const byte RESOURCE_Binary = 0x04;
+ public const byte RESOURCE_Max = RESOURCE_Binary;
+
+ public short id;
+ public byte kind;
+ public byte pad;
+ public uint size;
+
+ public ResourceHeader(short id, byte kind, uint size)
+ {
+ this.id = id;
+ this.kind = kind;
+ pad = 0;
+ this.size = size;
+ }
+
+ public ResourceHeader()
+ {
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(id);
+ writer.Write(kind);
+ writer.Write(pad);
+ writer.Write(size);
+ }
+
+ public void Deserialize(BinaryReader reader)
+ {
+ id = reader.ReadInt16();
+ kind = reader.ReadByte();
+ pad = reader.ReadByte();
+ size = reader.ReadUInt32();
+ }
+ }
+
+ public class CLR_GFX_BitmapDescription
+ {
+ public const ushort c_ReadOnly = 0x0001;
+ public const ushort c_Compressed = 0x0002;
+
+ public const ushort c_Rotation0 = 0x0000;
+ public const ushort c_Rotation90 = 0x0004;
+ public const ushort c_Rotation180 = 0x0008;
+ public const ushort c_Rotation270 = 0x000b;
+ public const ushort c_RotationMask = 0x000b;
+
+ public const byte c_CompressedRun = 0x80;
+ public const byte c_CompressedRunSet = 0x40;
+ public const byte c_CompressedRunLengthMask = 0x3f;
+ public const byte c_UncompressedRunLength = 7;
+ public const byte c_CompressedRunOffset = c_UncompressedRunLength + 1;
+
+ // Note that these type definitions has to match the ones defined in
+ // Bitmap.BitmapImageType enum defined in Graphics.cs
+ public const byte c_TypeBitmap = 0;
+ public const byte c_TypeGif = 1;
+ public const byte c_TypeJpeg = 2;
+
+ // !!!!WARNING!!!!
+ // These fields should correspond to CLR_GFX_BitmapDescription in NanoCLR_Graphics.h
+ // and should be 4-byte aligned in size. When these fields are changed, the version number
+ // of the nanoresource file should be incremented, the nanofnts should be updated (buildhelper -convertfont ...)
+ // and the MMP should also be updated as well. (Consult rwolff before touching this.)
+ public uint m_width;
+ public uint m_height;
+ public ushort m_flags;
+ public byte m_bitsPerPixel;
+ public byte m_type;
+
+ public CLR_GFX_BitmapDescription(ushort width, ushort height, ushort flags, byte bitsPerPixel, byte type)
+ {
+ m_width = width;
+ m_height = height;
+ m_flags = flags;
+ m_bitsPerPixel = bitsPerPixel;
+ m_type = type;
+ }
+
+ public CLR_GFX_BitmapDescription()
+ {
+ }
+
+ public void Serialize(BinaryWriter writer)
+ {
+ writer.Write(m_width);
+ writer.Write(m_height);
+ writer.Write(m_flags);
+ writer.Write(m_bitsPerPixel);
+ writer.Write(m_type);
+ }
+
+ public void Deserialize(BinaryReader reader)
+ {
+ m_width = reader.ReadUInt16();
+ m_height = reader.ReadUInt16();
+ m_flags = reader.ReadUInt16();
+ m_bitsPerPixel = reader.ReadByte();
+ m_type = reader.ReadByte();
+ }
+ }
+ #endregion
+ }
+
+ internal class NanoResourceWriter : IResourceWriter
+ {
+ string fileName;
+ ArrayList resources;
+
+ public NanoResourceWriter(string fileName)
+ {
+ this.fileName = fileName;
+ resources = new ArrayList();
+ }
+
+ public void AddResource(Entry entry)
+ {
+ resources.Add(entry);
+ }
+
+ private void Add(string name, object value)
+ {
+ Entry entry = Entry.CreateEntry(name, value, string.Empty, string.Empty);
+ resources.Add(entry);
+ }
+
+ #region IResourceWriter Members
+
+ void IResourceWriter.AddResource(string name, byte[] value)
+ {
+ Add(name, value);
+ }
+
+ void IResourceWriter.AddResource(string name, object value)
+ {
+ Add(name, value);
+ }
+
+ void IResourceWriter.AddResource(string name, string value)
+ {
+ Add(name, value);
+ }
+
+ void IResourceWriter.Close()
+ {
+ ((IDisposable)this).Dispose();
+ }
+
+ void IResourceWriter.Generate()
+ {
+ //PrepareToGenerate();
+ ProcessResourceFiles.EnsureResourcesIds(resources);
+
+ NanoResourceFile.Header header = new NanoResourceFile.Header((uint)resources.Count);
+ NanoResourceFile file = new NanoResourceFile(header);
+
+ for (int iResource = 0; iResource < resources.Count; iResource++)
+ {
+ Entry entry = (Entry)resources[iResource];
+
+ byte[] data = entry.GenerateResourceData();
+ NanoResourceFile.ResourceHeader resource = new NanoResourceFile.ResourceHeader(entry.Id, entry.ResourceType, (uint)data.Length);
+
+ file.AddResource(new NanoResourceFile.Resource(resource, data));
+ }
+
+ using (FileStream fileStream = File.Open(fileName, FileMode.Create))
+ {
+ BinaryWriter writer = new BinaryWriter(fileStream);
+ file.Serialize(writer);
+ fileStream.Flush();
+ }
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ void IDisposable.Dispose()
+ {
+ }
+
+ #endregion
+ }
+
+ public class NanoResourceReader : IResourceReader
+ {
+
+ #region IResourceReader Members
+
+ void IResourceReader.Close()
+ {
+ throw new NotImplementedException();
+ }
+
+ IDictionaryEnumerator IResourceReader.GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+
+ #region IEnumerable Members
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+
+ #region IDisposable Members
+
+ void IDisposable.Dispose()
+ {
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+ }
+
+}
diff --git a/nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs b/nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs
new file mode 100644
index 0000000..624f78d
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/ResolveRuntimeDependenciesTask.cs
@@ -0,0 +1,66 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// Portions Copyright (c) Microsoft Corporation. All rights reserved.
+// See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using System;
+using System.ComponentModel;
+using System.Linq;
+
+namespace nanoFramework.Tools
+{
+ [Description("ResolveRuntimeDependenciesTaskEntry")]
+ public class ResolveRuntimeDependenciesTask : Task
+ {
+ #region public properties for the task
+
+ public string Assembly { get; set; }
+
+ public string StartProgram { get; set; }
+
+ public ITaskItem[] AssemblyReferences { get; set; }
+
+ public ITaskItem[] StartProgramReferences { get; set; }
+
+ #endregion
+
+
+ public override bool Execute()
+ {
+ // report to VS output window what step the build is
+ Log.LogMessage(MessageImportance.Normal, "Resolving Runtime Dependencies for nanoFramework assembly...");
+
+ try
+ {
+ object host = HostObject;
+
+ if (host != null)
+ {
+ Type typ = host.GetType();
+
+ typ.GetProperty("Assembly").SetValue(host, Assembly, null);
+ typ.GetProperty("StartProgram").SetValue(host, StartProgram, null);
+ typ.GetProperty("AssemblyReferences").SetValue(host, GetFullPathFromItems(AssemblyReferences), null);
+ typ.GetProperty("StartProgramReferences").SetValue(host, GetFullPathFromItems(StartProgramReferences), null);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.LogError(".NET nanoFramework ResolveRuntimeDependenciesTask error: " + ex.Message);
+ }
+
+ // if we've logged any errors that's because there were errors (WOW!)
+ return !Log.HasLoggedErrors;
+ }
+
+ protected string[] GetFullPathFromItems(ITaskItem[] items)
+ {
+ return items?.Select((item) => {
+ return item.GetMetadata("FullPath");
+ }).ToArray();
+ }
+ }
+}
diff --git a/nanoFramework.Tools.BuildTasks/TasksConstants.cs b/nanoFramework.Tools.BuildTasks/TasksConstants.cs
new file mode 100644
index 0000000..3dc3415
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/TasksConstants.cs
@@ -0,0 +1,13 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// Portions Copyright (c) Microsoft Corporation. All rights reserved.
+// See LICENSE file in the project root for full license information.
+//
+
+namespace nanoFramework.Tools
+{
+ internal class TasksConstants
+ {
+ public const string BuildTaskDebugVar = "NFBUILD_TASKS_DEBUG";
+ }
+}
diff --git a/nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs b/nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs
new file mode 100644
index 0000000..6246b3f
--- /dev/null
+++ b/nanoFramework.Tools.BuildTasks/Utilities/DebuggerHelper.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// Portions Copyright (c) Microsoft Corporation. All rights reserved.
+// See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+namespace nanoFramework.Tools.Utilities
+{
+ public static class DebuggerHelper
+ {
+ public static void WaitForDebuggerIfEnabled(string varName, int timeoutSeconds = 30)
+ {
+
+ // this wait should be only available on debug build
+ // to prevent unwanted wait on VS in machines where the variable is present
+#if DEBUG
+ TimeSpan waitForDebugToAttach = TimeSpan.FromSeconds(timeoutSeconds);
+
+ var debugEnabled = Environment.GetEnvironmentVariable(varName, EnvironmentVariableTarget.User);
+
+ if (!string.IsNullOrEmpty(debugEnabled) && debugEnabled.Equals("1", StringComparison.Ordinal))
+ {
+ Console.WriteLine($".NET nanoFramework Metadata Processor msbuild instrumentation task debugging is enabled. Waiting {timeoutSeconds} seconds for debugger attachment...");
+
+ var currentProcessId = Process.GetCurrentProcess().Id;
+ var currentProcessName = Process.GetProcessById(currentProcessId).ProcessName;
+ Console.WriteLine(
+ string.Format("Process Id: {0}, Name: {1}", currentProcessId, currentProcessName)
+ );
+
+ // wait N seconds for debugger to attach
+ while (!Debugger.IsAttached && waitForDebugToAttach.TotalSeconds > 0)
+ {
+ Thread.Sleep(1000);
+ waitForDebugToAttach -= TimeSpan.FromSeconds(1);
+ }
+
+ Debugger.Break();
+ }
+#endif
+ }
+ }
+}
diff --git a/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj b/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj
index 09305e9..4d1d658 100644
--- a/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj
+++ b/nanoFramework.Tools.BuildTasks/nanoFramework.Tools.BuildTasks.csproj
@@ -8,6 +8,12 @@
false
+
+
+ <_Parameter1>nanoFramework.Tools.BuildTasks.Tests
+
+
+
compile; build; native; contentfiles; analyzers; buildtransitive
@@ -17,37 +23,33 @@
-
+
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
+ true
+
diff --git a/test/BuildTasks.Tests/BuildTasks.Tests.csproj b/test/BuildTasks.Tests/BuildTasks.Tests.csproj
new file mode 100644
index 0000000..877f9d1
--- /dev/null
+++ b/test/BuildTasks.Tests/BuildTasks.Tests.csproj
@@ -0,0 +1,38 @@
+
+
+
+ net8.0;net472
+ nanoFramework.Tools.BuildTasks.Tests
+ nanoFramework.Tools.BuildTasks.Tests
+ false
+ true
+ latest
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs b/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs
new file mode 100644
index 0000000..c777b98
--- /dev/null
+++ b/test/BuildTasks.Tests/Infrastructure/TestBuildEngine.cs
@@ -0,0 +1,49 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Build.Framework;
+
+namespace nanoFramework.Tools.BuildTasks.Tests.Infrastructure
+{
+ ///
+ /// Minimal that captures logged events so tests can
+ /// assert on warnings and errors without a real MSBuild host.
+ ///
+ internal sealed class TestBuildEngine : IBuildEngine
+ {
+ public List Warnings { get; } = new List();
+ public List Errors { get; } = new List();
+ public List Messages { get; } = new List();
+
+ public void LogWarningEvent(BuildWarningEventArgs e) => Warnings.Add(e.Message ?? string.Empty);
+ public void LogErrorEvent(BuildErrorEventArgs e) => Errors.Add(e.Message ?? string.Empty);
+ public void LogMessageEvent(BuildMessageEventArgs e) => Messages.Add(e.Message ?? string.Empty);
+ public void LogCustomEvent(CustomBuildEventArgs e) { }
+
+ public bool BuildProjectFile(string projectFileName, string[] targetNames,
+ IDictionary globalProperties, IDictionary targetOutputs) =>
+ throw new NotImplementedException();
+
+ public bool ContinueOnError => false;
+ public int LineNumberOfTaskNode => 0;
+ public int ColumnNumberOfTaskNode => 0;
+ public string ProjectFileOfTaskNode => string.Empty;
+ }
+
+ ///
+ /// Minimal implementation that wires up a .
+ /// Used to construct a without a real MSBuild host.
+ ///
+ internal sealed class TestTask : ITask
+ {
+ public IBuildEngine BuildEngine { get; set; } = new TestBuildEngine();
+ public ITaskHost? HostObject { get; set; }
+ public bool Execute() => true;
+ }
+
+}
diff --git a/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs b/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs
new file mode 100644
index 0000000..61526c0
--- /dev/null
+++ b/test/BuildTasks.Tests/NanoResxResourceReaderTests.cs
@@ -0,0 +1,275 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// See LICENSE file in the project root for full license information.
+//
+
+// NanoResxResourceReader is only compiled on non-Framework targets (see
+// ProcessResourceFiles.CrossPlatform.cs). The tests here mirror that guard.
+#if !NETFRAMEWORK
+
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using nanoFramework.Tools;
+
+namespace nanoFramework.Tools.BuildTasks.Tests
+{
+ ///
+ /// Unit tests for , the cross-platform
+ /// (net8.0+) .resx parser used in place of
+ /// System.Resources.ResXResourceReader (WinForms / net472 only).
+ ///
+ [TestClass]
+ public class NanoResxResourceReaderTests
+ {
+ // ─── helpers ───────────────────────────────────────────────────────────────
+
+ private const string ResxHeader = @"
+
+ text/microsoft-resx
+ 2.0";
+
+ private const string ResxFooter = "\n";
+
+ private sealed class ParsedResx : IDisposable
+ {
+ private readonly StringReader _source;
+
+ internal ParsedResx(string xml, string? basePath)
+ {
+ _source = new StringReader(xml);
+ Reader = new NanoResxResourceReader(_source, basePath ?? Path.GetTempPath());
+ }
+
+ internal NanoResxResourceReader Reader { get; }
+
+ public static implicit operator NanoResxResourceReader(ParsedResx parsed) => parsed.Reader;
+
+ public void Dispose()
+ {
+ Reader.Dispose();
+ _source.Dispose();
+ }
+ }
+
+ private static ParsedResx ParseResx(string resxBody, string? basePath = null)
+ {
+ string xml = ResxHeader + resxBody + ResxFooter;
+ return new ParsedResx(xml, basePath);
+ }
+
+ private static System.Collections.Hashtable ToHashtable(NanoResxResourceReader reader)
+ {
+ var ht = new System.Collections.Hashtable();
+ var en = reader.GetEnumerator();
+ while (en.MoveNext())
+ ht[en.Key] = en.Value;
+ return ht;
+ }
+
+ // ─── inline string resources ───────────────────────────────────────────────
+
+ [TestMethod]
+ public void Parse_InlineString_ReturnsStringValue()
+ {
+ using var reader = ParseResx(@"
+ Hello nanoFramework");
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count);
+ Assert.IsInstanceOfType(resources["Greeting"], typeof(string));
+ Assert.AreEqual("Hello nanoFramework", (string)resources["Greeting"]!);
+ }
+
+ [TestMethod]
+ public void Parse_MultipleInlineStrings_AllPresent()
+ {
+ using var reader = ParseResx(@"
+ alpha
+ beta
+ gamma");
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(3, resources.Count);
+ Assert.AreEqual("alpha", resources["A"]);
+ Assert.AreEqual("beta", resources["B"]);
+ Assert.AreEqual("gamma", resources["C"]);
+ }
+
+ [TestMethod]
+ public void Parse_EmptyStringValue_ReturnsEmptyString()
+ {
+ using var reader = ParseResx(@"
+ ");
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count);
+ Assert.AreEqual(string.Empty, resources["Empty"]);
+ }
+
+ [TestMethod]
+ public void Parse_MissingNameAttribute_EntryIsSkipped()
+ {
+ // An entry without a 'name' attribute should be silently ignored.
+ using var reader = ParseResx(@"
+ no name
+ ok");
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count, "nameless entry should be skipped");
+ Assert.IsTrue(resources.ContainsKey("Valid"));
+ }
+
+ // ─── inline base64 byte arrays ─────────────────────────────────────────────
+
+ [TestMethod]
+ public void Parse_InlineBase64ByteArray_ReturnsByteArray()
+ {
+ byte[] expected = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+ string base64 = Convert.ToBase64String(expected);
+
+ using var reader = ParseResx($@"
+
+ {base64}
+ ");
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count);
+ Assert.IsInstanceOfType(resources["Blob"], typeof(byte[]));
+ CollectionAssert.AreEqual(expected, (byte[])resources["Blob"]!);
+ }
+
+ [TestMethod]
+ public void Parse_UnsupportedMimetype_ThrowsNotSupportedException()
+ {
+ Assert.ThrowsException(() =>
+ {
+ using var reader = ParseResx(@"
+
+ AQID
+ ");
+ // the exception is thrown during construction (inside Parse)
+ });
+ }
+
+ // ─── ResXFileRef (file references) ────────────────────────────────────────
+
+ [TestMethod]
+ public void Parse_FileRef_ByteArray_ReturnsByteArray()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ byte[] expected = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
+ File.WriteAllBytes(Path.Combine(dir, "blob.bin"), expected);
+
+ using var reader = ParseResx(@"
+
+ blob.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ", basePath: dir);
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count);
+ Assert.IsInstanceOfType(resources["Blob"], typeof(byte[]));
+ CollectionAssert.AreEqual(expected, (byte[])resources["Blob"]!);
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void Parse_FileRef_String_ReturnsStringContent()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ const string content = "Hello from file";
+ File.WriteAllText(Path.Combine(dir, "text.txt"), content, Encoding.UTF8);
+
+ using var reader = ParseResx(@"
+
+ text.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ", basePath: dir);
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count);
+ Assert.IsInstanceOfType(resources["FileText"], typeof(string));
+ Assert.AreEqual(content, (string)resources["FileText"]!);
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void Parse_FileRef_AbsolutePath_Resolved()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ byte[] expected = new byte[] { 0xCA, 0xFE };
+ string absPath = Path.Combine(dir, "abs.bin");
+ File.WriteAllBytes(absPath, expected);
+
+ // Use an absolute path in the file ref value
+ string absPathEscaped = absPath.Replace("\\", "/");
+ using var reader = ParseResx($@"
+
+ {absPath};System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ", basePath: null);
+
+ var resources = ToHashtable(reader);
+
+ Assert.AreEqual(1, resources.Count);
+ CollectionAssert.AreEqual(expected, (byte[])resources["Abs"]!);
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void Parse_UnsupportedType_ThrowsNotSupportedException()
+ {
+ // A resource with an explicit non-file-ref, non-string type is unsupported.
+ Assert.ThrowsException(() =>
+ {
+ using var reader = ParseResx(@"
+
+ AQID
+ ");
+ });
+ }
+
+ [TestMethod]
+ public void Parse_ResxHeaderEntries_Ignored()
+ {
+ // nodes must not appear in the resource enumeration.
+ using var reader = ParseResx(@"
+ hello");
+
+ var resources = ToHashtable(reader);
+
+ // Only the one entry — resheader nodes not counted
+ Assert.AreEqual(1, resources.Count);
+ Assert.IsTrue(resources.ContainsKey("MyString"));
+ }
+ }
+}
+
+#endif
diff --git a/test/BuildTasks.Tests/ProcessResourceFilesTests.cs b/test/BuildTasks.Tests/ProcessResourceFilesTests.cs
new file mode 100644
index 0000000..c340ab9
--- /dev/null
+++ b/test/BuildTasks.Tests/ProcessResourceFilesTests.cs
@@ -0,0 +1,320 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// See LICENSE file in the project root for full license information.
+//
+
+// ProcessResourceFiles has a field of type TaskLoggingHelper
+// (Microsoft.Build.Utilities.Core). On net8.0 that NuGet package ships only a
+// reference assembly (no runtime lib), so instantiating ProcessResourceFiles fails
+// at test-run time. The cross-platform components it exercises on net8.0
+// (NanoResxResourceReader, CrossPlatformBitmap) are covered directly by the other
+// test files. These integration tests therefore run on net472 only.
+#if NETFRAMEWORK
+
+using System;
+using System.IO;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using nanoFramework.Tools;
+
+namespace nanoFramework.Tools.BuildTasks.Tests
+{
+ ///
+ /// End-to-end tests for : creates real .resx
+ /// files on disk, runs the resource processor, and validates the produced
+ /// .nanoresources binary output.
+ ///
+ /// These tests use the public ReadResources / WriteResources API
+ /// rather than Run() so that the test project has no runtime dependency on
+ /// Microsoft.Build.Utilities.Core (whose assembly is not on the default
+ /// probe path when running dotnet test). This file is compiled and run only
+ /// for NETFRAMEWORK (net472).
+ ///
+ [TestClass]
+ public class ProcessResourceFilesTests
+ {
+ // ─── minimal .resx template ────────────────────────────────────────────────
+
+ private const string ResxHeader = @"
+
+ text/microsoft-resx
+ 2.0
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
+
+ private const string ResxFooter = "\n";
+
+ // ─── helpers ───────────────────────────────────────────────────────────────
+
+ ///
+ /// Runs +
+ /// on a temp .resx file and
+ /// returns the path to the produced .nanoresources file.
+ ///
+ private static string RunOnResx(string dir, string resxBody, bool useSourcePath = true)
+ {
+ string resxPath = Path.Combine(dir, "Resources.resx");
+ string outPath = Path.Combine(dir, "Resources.nanoresources");
+ File.WriteAllText(resxPath, ResxHeader + resxBody + ResxFooter, Encoding.UTF8);
+
+ var prf = new ProcessResourceFiles();
+ // StronglyTypedNamespace must be non-empty so Entry.CreateEntry works.
+ prf.StronglyTypedNamespace = "Test";
+ prf.StronglyTypedClassName = "Resources";
+
+ prf.ReadResources(resxPath, useSourcePath);
+ prf.WriteResources(outPath);
+
+ return outPath;
+ }
+
+ /// Deserializes a .nanoresources file.
+ private static ProcessResourceFiles.NanoResourceFile ReadNanoresources(string path)
+ {
+ var nrf = new ProcessResourceFiles.NanoResourceFile();
+ using var br = new BinaryReader(File.OpenRead(path));
+ nrf.Deserialize(br);
+ return nrf;
+ }
+
+ ///
+ /// Creates a minimal valid 24bpp 1×1 BMP with the given RGB pixel.
+ /// BMP pixels are stored as BGR, rows are bottom-up and
+ /// padded to a 4-byte boundary.
+ ///
+ private static byte[] MakeOnePxBmp(byte r, byte g, byte b)
+ {
+ // 14-byte file header + 40-byte BITMAPINFOHEADER + 4-byte pixel row
+ var bmp = new byte[58];
+
+ // File header
+ bmp[0] = 0x42; bmp[1] = 0x4D;
+ Buffer.BlockCopy(BitConverter.GetBytes(58u), 0, bmp, 2, 4); // file size
+ Buffer.BlockCopy(BitConverter.GetBytes(54u), 0, bmp, 10, 4); // pixel offset
+
+ // BITMAPINFOHEADER
+ Buffer.BlockCopy(BitConverter.GetBytes(40u), 0, bmp, 14, 4); // header size
+ Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 18, 4); // width = 1
+ Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 22, 4); // height = 1
+ bmp[26] = 1; bmp[27] = 0; // planes = 1
+ bmp[28] = 24; bmp[29] = 0; // bits per pixel = 24
+ Buffer.BlockCopy(BitConverter.GetBytes(4u), 0, bmp, 34, 4); // image size = 4
+
+ // Pixel: BGR + 1 padding byte
+ bmp[54] = b;
+ bmp[55] = g;
+ bmp[56] = r;
+
+ return bmp;
+ }
+
+ // ─── tests ─────────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public void ReadWrite_TwoStringResources_BothPresentWithCorrectKind()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ string resxBody = @"
+ Hello nanoFramework
+ Goodbye";
+
+ string outPath = RunOnResx(dir, resxBody);
+
+ Assert.IsTrue(File.Exists(outPath), "nanoresources file was not created");
+
+ var nrf = ReadNanoresources(outPath);
+ Assert.AreEqual(2, nrf.resources.Length, "expected 2 resources");
+
+ foreach (var res in nrf.resources)
+ {
+ Assert.AreEqual(
+ ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_String,
+ res.header.kind,
+ "resource kind should be RESOURCE_String");
+ }
+
+ // String values present (order may differ by ID hash)
+ var strings = new string[2];
+ for (int i = 0; i < 2; i++)
+ strings[i] = Encoding.UTF8.GetString(nrf.resources[i].data).TrimEnd('\0');
+
+ CollectionAssert.AreEquivalent(
+ new[] { "Hello nanoFramework", "Goodbye" },
+ strings,
+ "string resource values do not match");
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void ReadWrite_BinaryBlobFileRef_EmittedAsBinaryResource()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ byte[] blob = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 };
+ File.WriteAllBytes(Path.Combine(dir, "data.bin"), blob);
+
+ string resxBody = @"
+
+ data.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ";
+
+ string outPath = RunOnResx(dir, resxBody);
+
+ Assert.IsTrue(File.Exists(outPath), "nanoresources file was not created");
+
+ var nrf = ReadNanoresources(outPath);
+ Assert.AreEqual(1, nrf.resources.Length, "expected 1 resource");
+ Assert.AreEqual(
+ ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_Binary,
+ nrf.resources[0].header.kind,
+ "resource kind should be RESOURCE_Binary");
+
+ CollectionAssert.AreEqual(blob, nrf.resources[0].data, "binary blob content mismatch");
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void ReadWrite_BitmapFileRef_EmittedAsBitmapResource()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ // Write a minimal 1×1 pure-red BMP (dither-safe colour: no half-level boundaries)
+ File.WriteAllBytes(Path.Combine(dir, "logo.bmp"), MakeOnePxBmp(r: 255, g: 0, b: 0));
+
+ string resxBody = @"
+
+ logo.bmp;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ";
+
+ string outPath = RunOnResx(dir, resxBody);
+
+ Assert.IsTrue(File.Exists(outPath), "nanoresources file was not created");
+
+ var nrf = ReadNanoresources(outPath);
+ Assert.AreEqual(1, nrf.resources.Length, "expected 1 resource");
+ Assert.AreEqual(
+ ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_Bitmap,
+ nrf.resources[0].header.kind,
+ "resource kind should be RESOURCE_Bitmap");
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void ReadWrite_DuplicateResourceName_OnlyOneIsEmitted()
+ {
+ // ResXResourceReader (net472) uses a Hashtable internally: the last value
+ // for a duplicate key overwrites previous ones at the reader level, so only
+ // one entry reaches AddResource. We verify the count is 1, not which value
+ // wins (that is an implementation detail of the underlying reader).
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ string resxBody = @"
+ first value
+ second value";
+
+ string outPath = RunOnResx(dir, resxBody);
+
+ var nrf = ReadNanoresources(outPath);
+ Assert.AreEqual(1, nrf.resources.Length, "duplicate should be deduplicated — expected 1 resource");
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void ReadWrite_StringAndBinaryMixed_BothEmittedWithCorrectKinds()
+ {
+ string dir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir);
+ try
+ {
+ byte[] blob = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF };
+ File.WriteAllBytes(Path.Combine(dir, "blob.bin"), blob);
+
+ string resxBody = @"
+ nanoFramework
+
+ blob.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ";
+
+ string outPath = RunOnResx(dir, resxBody);
+
+ var nrf = ReadNanoresources(outPath);
+ Assert.AreEqual(2, nrf.resources.Length, "expected 2 resources");
+
+ bool hasString = false, hasBinary = false;
+ foreach (var res in nrf.resources)
+ {
+ if (res.header.kind == ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_String)
+ hasString = true;
+ else if (res.header.kind == ProcessResourceFiles.NanoResourceFile.ResourceHeader.RESOURCE_Binary)
+ hasBinary = true;
+ }
+
+ Assert.IsTrue(hasString, "no RESOURCE_String found");
+ Assert.IsTrue(hasBinary, "no RESOURCE_Binary found");
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [TestMethod]
+ public void ReadWrite_SameInputs_ProduceDeterministicOutput()
+ {
+ // Regression guard: identical inputs must always produce byte-identical output.
+ string dir1 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ string dir2 = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(dir1);
+ Directory.CreateDirectory(dir2);
+ try
+ {
+ byte[] blob = new byte[] { 0x01, 0x02, 0x03, 0x04 };
+ File.WriteAllBytes(Path.Combine(dir1, "data.bin"), blob);
+ File.WriteAllBytes(Path.Combine(dir2, "data.bin"), blob);
+
+ string resxBody = @"
+ SmokeApp
+
+ data.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ ";
+
+ byte[] out1 = File.ReadAllBytes(RunOnResx(dir1, resxBody));
+ byte[] out2 = File.ReadAllBytes(RunOnResx(dir2, resxBody));
+
+ CollectionAssert.AreEqual(out1, out2, "identical inputs must produce identical output");
+ }
+ finally
+ {
+ Directory.Delete(dir1, true);
+ Directory.Delete(dir2, true);
+ }
+ }
+ }
+}
+
+#endif
diff --git a/test/BuildTasks.Tests/Rgb565ConversionTests.cs b/test/BuildTasks.Tests/Rgb565ConversionTests.cs
new file mode 100644
index 0000000..5ddbe39
--- /dev/null
+++ b/test/BuildTasks.Tests/Rgb565ConversionTests.cs
@@ -0,0 +1,228 @@
+//
+// Copyright (c) .NET Foundation and Contributors
+// See LICENSE file in the project root for full license information.
+//
+
+// CrossPlatformBitmap is only compiled on non-Framework targets.
+#if !NETFRAMEWORK
+
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using nanoFramework.Tools;
+
+namespace nanoFramework.Tools.BuildTasks.Tests
+{
+ ///
+ /// Unit tests for : validates that the
+ /// ImageSharp-backed RGB565 converter produces the correct 16-bit pixel values
+ /// for known input colours, and that bitmap format detection works correctly.
+ ///
+ [TestClass]
+ public class Rgb565ConversionTests
+ {
+ // ─── CLR_GFX_BitmapDescription layout ────────────────────────────────────
+ //
+ // GenerateBitmapResourceData returns:
+ // [12 bytes: CLR_GFX_BitmapDescription serialised]
+ // [width * height * 2 bytes: RGB565 pixel data, little-endian]
+ //
+ // BitmapDescription.Serialize writes:
+ // m_width (uint32) = 4 bytes
+ // m_height (uint32) = 4 bytes
+ // m_flags (uint16) = 2 bytes
+ // m_bitsPerPixel (byte) = 1 byte
+ // m_type (byte) = 1 byte
+ // ────────────────────────────────
+ // Total = 12 bytes
+
+ private const int BitmapDescriptionSize = 12;
+
+ // ─── helpers ───────────────────────────────────────────────────────────────
+
+ ///
+ /// Creates a minimal valid 24bpp 1×1 BMP with the given RGB pixel.
+ /// BMP stores pixels as BGR, bottom-up, rows padded to 4-byte boundary.
+ ///
+ private static byte[] MakeOnePxBmp(byte r, byte g, byte b)
+ {
+ // 14-byte file header + 40-byte BITMAPINFOHEADER + 4-byte pixel row
+ var bmp = new byte[58];
+
+ // File header
+ bmp[0] = 0x42; bmp[1] = 0x4D;
+ Buffer.BlockCopy(BitConverter.GetBytes(58u), 0, bmp, 2, 4); // file size
+ Buffer.BlockCopy(BitConverter.GetBytes(54u), 0, bmp, 10, 4); // pixel data offset
+
+ // BITMAPINFOHEADER
+ Buffer.BlockCopy(BitConverter.GetBytes(40u), 0, bmp, 14, 4); // header size
+ Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 18, 4); // width = 1
+ Buffer.BlockCopy(BitConverter.GetBytes(1u), 0, bmp, 22, 4); // height = 1
+ bmp[26] = 1; bmp[27] = 0; // planes = 1
+ bmp[28] = 24; bmp[29] = 0; // bits per pixel = 24
+ Buffer.BlockCopy(BitConverter.GetBytes(4u), 0, bmp, 34, 4); // image size = 4
+
+ // Pixel data: BGR order + 1 padding byte (row must be 4-byte aligned)
+ bmp[54] = b;
+ bmp[55] = g;
+ bmp[56] = r;
+
+ return bmp;
+ }
+
+ ///
+ /// Calls on a
+ /// 1×1 BMP and returns the resulting RGB565 value.
+ ///
+ private static ushort ConvertPixelToRgb565(byte r, byte g, byte b)
+ {
+ byte[] bmpBytes = MakeOnePxBmp(r, g, b);
+ byte[] resourceData = CrossPlatformBitmap.GenerateBitmapResourceData(bmpBytes);
+
+ // Pixel immediately follows the 12-byte description
+ Assert.IsTrue(resourceData.Length >= BitmapDescriptionSize + 2,
+ "resource data too short");
+
+ return (ushort)(resourceData[BitmapDescriptionSize] |
+ (resourceData[BitmapDescriptionSize + 1] << 8));
+ }
+
+ // ─── RGB565 pixel conversion tests ────────────────────────────────────────
+ //
+ // Quantisation rules (from ProcessResourceFiles.CrossPlatform.cs):
+ // r5 = min(31, (R + 4) >> 3) ← round-to-nearest (5 bits)
+ // g6 = G >> 2 ← truncate (6 bits)
+ // b5 = min(31, (B + 4) >> 3) ← round-to-nearest (5 bits)
+ // RGB565 = (r5 << 11) | (g6 << 5) | b5
+
+ [TestMethod]
+ public void Convert_PureBlack_YieldsZero()
+ {
+ ushort rgb565 = ConvertPixelToRgb565(0, 0, 0);
+ Assert.AreEqual(0x0000, rgb565, "black should be 0x0000");
+ }
+
+ [TestMethod]
+ public void Convert_PureWhite_YieldsAllOnes()
+ {
+ // R=255 → r5=min(31,32)=31, G=255 → g6=63, B=255 → b5=31 → 0xFFFF
+ ushort rgb565 = ConvertPixelToRgb565(255, 255, 255);
+ Assert.AreEqual(0xFFFF, rgb565, "white should be 0xFFFF");
+ }
+
+ [TestMethod]
+ public void Convert_PureRed_YieldsExpectedValue()
+ {
+ // R=255 → r5=31, G=0 → g6=0, B=0 → b5=0 → 0xF800
+ ushort rgb565 = ConvertPixelToRgb565(255, 0, 0);
+ Assert.AreEqual(0xF800, rgb565, "pure red should be 0xF800");
+ }
+
+ [TestMethod]
+ public void Convert_PureGreen_YieldsExpectedValue()
+ {
+ // R=0 → r5=0, G=255 → g6=63, B=0 → b5=0 → 0x07E0
+ ushort rgb565 = ConvertPixelToRgb565(0, 255, 0);
+ Assert.AreEqual(0x07E0, rgb565, "pure green should be 0x07E0");
+ }
+
+ [TestMethod]
+ public void Convert_PureBlue_YieldsExpectedValue()
+ {
+ // R=0 → r5=0, G=0 → g6=0, B=255 → b5=31 → 0x001F
+ ushort rgb565 = ConvertPixelToRgb565(0, 0, 255);
+ Assert.AreEqual(0x001F, rgb565, "pure blue should be 0x001F");
+ }
+
+ [TestMethod]
+ public void Convert_Yellow_YieldsExpectedValue()
+ {
+ // R=255 → r5=31, G=255 → g6=63, B=0 → b5=0 → 0xFFE0
+ ushort rgb565 = ConvertPixelToRgb565(255, 255, 0);
+ Assert.AreEqual(0xFFE0, rgb565, "yellow should be 0xFFE0");
+ }
+
+ [TestMethod]
+ public void Convert_Cyan_YieldsExpectedValue()
+ {
+ // R=0 → r5=0, G=255 → g6=63, B=255 → b5=31 → 0x07FF
+ ushort rgb565 = ConvertPixelToRgb565(0, 255, 255);
+ Assert.AreEqual(0x07FF, rgb565, "cyan should be 0x07FF");
+ }
+
+ [TestMethod]
+ public void Convert_Magenta_YieldsExpectedValue()
+ {
+ // R=255 → r5=31, G=0 → g6=0, B=255 → b5=31 → 0xF81F
+ ushort rgb565 = ConvertPixelToRgb565(255, 0, 255);
+ Assert.AreEqual(0xF81F, rgb565, "magenta should be 0xF81F");
+ }
+
+ [TestMethod]
+ public void Convert_MidGray_ChannelsQuantisedIndependently()
+ {
+ // R=128 → (128+4)>>3 = 16, G=128 → 128>>2 = 32, B=128 → 16
+ // RGB565 = (16<<11)|(32<<5)|16 = 0x8410
+ ushort rgb565 = ConvertPixelToRgb565(128, 128, 128);
+ Assert.AreEqual(0x8410, rgb565, "mid-gray quantisation mismatch");
+ }
+
+ // ─── bitmap format detection tests ────────────────────────────────────────
+
+ [TestMethod]
+ public void IsSupportedBitmap_ValidBmp_ReturnsTrue()
+ {
+ byte[] bmp = MakeOnePxBmp(255, 0, 0);
+ Assert.IsTrue(CrossPlatformBitmap.IsSupportedBitmap(bmp), "1×1 BMP should be detected");
+ }
+
+ [TestMethod]
+ public void IsSupportedBitmap_PngBytes_ReturnsFalse()
+ {
+ // Minimal PNG magic bytes — not in the supported set (nanoFramework rejects PNG)
+ byte[] pngMagic = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
+ Assert.IsFalse(CrossPlatformBitmap.IsSupportedBitmap(pngMagic), "PNG should not be supported");
+ }
+
+ [TestMethod]
+ public void IsSupportedBitmap_RandomBytes_ReturnsFalse()
+ {
+ byte[] garbage = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05 };
+ Assert.IsFalse(CrossPlatformBitmap.IsSupportedBitmap(garbage), "garbage bytes should not be supported");
+ }
+
+ [TestMethod]
+ public void IsSupportedBitmap_EmptyArray_ReturnsFalse()
+ {
+ Assert.IsFalse(CrossPlatformBitmap.IsSupportedBitmap(Array.Empty()), "empty array should not be supported");
+ }
+
+ // ─── resource data structure tests ────────────────────────────────────────
+
+ [TestMethod]
+ public void GenerateBitmapResourceData_OnePxBmp_HasCorrectDimensions()
+ {
+ byte[] bmpBytes = MakeOnePxBmp(0, 0, 0);
+ byte[] data = CrossPlatformBitmap.GenerateBitmapResourceData(bmpBytes);
+
+ Assert.AreEqual(BitmapDescriptionSize + 2, data.Length,
+ "1×1 16bpp BMP should produce 12 (description) + 2 (pixel) bytes");
+
+ // m_width at offset 0: uint32 little-endian
+ uint width = BitConverter.ToUInt32(data, 0);
+ Assert.AreEqual(1u, width, "width should be 1");
+
+ // m_height at offset 4: uint32 little-endian
+ uint height = BitConverter.ToUInt32(data, 4);
+ Assert.AreEqual(1u, height, "height should be 1");
+
+ // m_bitsPerPixel at offset 10
+ Assert.AreEqual(16, data[10], "bits per pixel should be 16 for BMP");
+
+ // m_type at offset 11 (0 = c_TypeBitmap)
+ Assert.AreEqual(0, data[11], "type should be c_TypeBitmap (0)");
+ }
+ }
+}
+
+#endif
diff --git a/test/SmokeTest/Resources.resx b/test/SmokeTest/Resources.resx
new file mode 100644
index 0000000..dd9391b
--- /dev/null
+++ b/test/SmokeTest/Resources.resx
@@ -0,0 +1,48 @@
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ Hello .NET nanoFramework!
+
+
+ v1.0.0
+
+
+
+
+ logo.bmp;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+
+ data.bin;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
diff --git a/test/SmokeTest/SmokeTest.csproj b/test/SmokeTest/SmokeTest.csproj
index c324e91..0b1990c 100644
--- a/test/SmokeTest/SmokeTest.csproj
+++ b/test/SmokeTest/SmokeTest.csproj
@@ -9,10 +9,12 @@
true
+ true
+
diff --git a/test/SmokeTest/data.bin b/test/SmokeTest/data.bin
new file mode 100644
index 0000000..bf5af90
--- /dev/null
+++ b/test/SmokeTest/data.bin
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test/SmokeTest/logo.bmp b/test/SmokeTest/logo.bmp
new file mode 100644
index 0000000..3db2c73
Binary files /dev/null and b/test/SmokeTest/logo.bmp differ