Skip to content

Commit

Permalink
Store custom scripts in XML elements
Browse files Browse the repository at this point in the history
  • Loading branch information
cschneegans committed Apr 10, 2024
1 parent c7fa32f commit dd1ac96
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 99 deletions.
58 changes: 10 additions & 48 deletions Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,7 @@ abstract class CommandConfig
public readonly static SpecializeCommandConfig Specialize = new();
public readonly static OobeCommandConfig Oobe = new();

protected abstract XmlElement GetContainer(XmlDocument doc, XmlNamespaceManager ns);

public abstract XmlElement CreateElement(XmlDocument doc, XmlNamespaceManager ns);

public XmlComment CreateComment(XmlDocument doc, XmlNamespaceManager ns)
{
XmlComment comment = doc.CreateComment(null);
GetContainer(doc, ns).AppendChild(comment);
return comment;
}
}

/// <summary>
Expand All @@ -72,14 +63,10 @@ public XmlComment CreateComment(XmlDocument doc, XmlNamespaceManager ns)
/// </summary>
class WindowsPECommandConfig : CommandConfig
{
protected override XmlElement GetContainer(XmlDocument doc, XmlNamespaceManager ns)
{
return Util.GetOrCreateElement(Pass.windowsPE, "Microsoft-Windows-Setup", "RunSynchronous", doc, ns);
}

public override XmlElement CreateElement(XmlDocument doc, XmlNamespaceManager ns)
{
var outer = Util.NewElement("RunSynchronousCommand", GetContainer(doc, ns), doc, ns);
var container = Util.GetOrCreateElement(Pass.windowsPE, "Microsoft-Windows-Setup", "RunSynchronous", doc, ns);
var outer = Util.NewElement("RunSynchronousCommand", container, doc, ns);
return Util.NewElement("Path", outer, doc, ns);
}
}
Expand All @@ -96,14 +83,10 @@ public override XmlElement CreateElement(XmlDocument doc, XmlNamespaceManager ns
/// </summary>
class SpecializeCommandConfig : CommandConfig
{
protected override XmlElement GetContainer(XmlDocument doc, XmlNamespaceManager ns)
{
return Util.GetOrCreateElement(Pass.specialize, "Microsoft-Windows-Deployment", "RunSynchronous", doc, ns);
}

public override XmlElement CreateElement(XmlDocument doc, XmlNamespaceManager ns)
{
var outer = Util.NewElement("RunSynchronousCommand", GetContainer(doc, ns), doc, ns);
var container = Util.GetOrCreateElement(Pass.specialize, "Microsoft-Windows-Deployment", "RunSynchronous", doc, ns);
var outer = Util.NewElement("RunSynchronousCommand", container, doc, ns);
return Util.NewElement("Path", outer, doc, ns);
}
}
Expand All @@ -120,14 +103,10 @@ public override XmlElement CreateElement(XmlDocument doc, XmlNamespaceManager ns
/// </summary>
class OobeCommandConfig : CommandConfig
{
protected override XmlElement GetContainer(XmlDocument doc, XmlNamespaceManager ns)
{
return Util.GetOrCreateElement(Pass.oobeSystem, "Microsoft-Windows-Shell-Setup", "FirstLogonCommands", doc, ns);
}

public override XmlElement CreateElement(XmlDocument doc, XmlNamespaceManager ns)
{
var outer = Util.NewElement("SynchronousCommand", GetContainer(doc, ns), doc, ns);
var container = Util.GetOrCreateElement(Pass.oobeSystem, "Microsoft-Windows-Shell-Setup", "FirstLogonCommands", doc, ns);
var outer = Util.NewElement("SynchronousCommand", container, doc, ns);
return Util.NewElement("CommandLine", outer, doc, ns);
}
}
Expand All @@ -146,11 +125,6 @@ public void Append(IEnumerable<string> values)
Append(value);
}
}

public void AppendXmlComment(string comment)
{
config.CreateComment(doc, ns).Value = comment;
}
}

static class CommandBuilder
Expand Down Expand Up @@ -283,21 +257,6 @@ public static IEnumerable<string> WriteToFile(string path, IEnumerable<string> l
{
return lines.SelectMany(line => WriteToFile(path, line));
}

public static IEnumerable<string> SafeWriteToFile(string path, string content, Encoding encoding)
{
byte[] bytes = Enumerable.Concat(
encoding.GetPreamble(),
encoding.GetBytes(content)
).ToArray();

int chunkSize = 256 - 70;
foreach (string base64 in Convert.ToBase64String(bytes).Chunk(chunkSize).Select(chars => new string(chars)))
{
yield return $@"cmd.exe /c "">>""{path}"" echo {base64}""";
}
yield return PowerShellCommand(@$"$p='{path}'; $f=[System.IO.File]; $f::WriteAllBytes($p, [convert]::FromBase64String($f::ReadAllText($p)));");
}
}

public class ConfigurationException(string? message) : Exception(message);
Expand Down Expand Up @@ -618,6 +577,8 @@ public static class Constants
public const int EspDefaultSize = 300;

public static readonly string DiskpartScript = DiskModifier.GetCustomDiskpartScript();

public const string MyNamespaceUri = "https://schneegans.de/windows/unattend-generator/";
}

public class UnattendGenerator
Expand Down Expand Up @@ -734,7 +695,7 @@ public ExplicitTimeZoneSettings CreateExplicitTimeZoneSettings(string id)
}

public IImmutableDictionary<string, TimeOffset> TimeZones { get; }

public IImmutableDictionary<string, GeoLocation> GeoLocations { get; }

public IImmutableDictionary<string, Component> Components { get; }
Expand Down Expand Up @@ -802,6 +763,7 @@ public XmlDocument GenerateXml(Configuration config)
var ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("u", "urn:schemas-microsoft-com:unattend");
ns.AddNamespace("wcm", "http://schemas.microsoft.com/WMIConfig/2002/State");
ns.AddNamespace("s", Constants.MyNamespaceUri);

ModifierContext context = new(
Configuration: config,
Expand Down
2 changes: 2 additions & 0 deletions UnattendGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
</PropertyGroup>

<ItemGroup>
<None Remove="resource\ExtractScripts.ps1" />
<None Remove="resource\GeoId.json" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="resource\autounattend.xml" />
<EmbeddedResource Include="resource\autounattend.xsd" />
<EmbeddedResource Include="resource\Component.json" />
<EmbeddedResource Include="resource\ExtractScripts.ps1" />
<EmbeddedResource Include="resource\GeoId.json" />
<EmbeddedResource Include="resource\ImageLanguage.json" />
<EmbeddedResource Include="resource\KeyboardIdentifier.json" />
Expand Down
108 changes: 57 additions & 51 deletions modifier/Script.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Xml;

namespace Schneegans.Unattend;

Expand All @@ -12,7 +13,25 @@ public enum ScriptType

public enum ScriptPhase
{
System, FirstLogon, UserOnce, DefaultUser
/// <summary>
/// Script is to run in the system context, before user accounts are created.
/// </summary>
System,

/// <summary>
/// Script is to run when the first user logs on.
/// </summary>
FirstLogon,

/// <summary>
/// Script is to run whenever a user logs on for the first time.
/// </summary>
UserOnce,

/// <summary>
/// Script is to modify the default user's registry hive.
/// </summary>
DefaultUser
}

public static class ScriptExtensions
Expand All @@ -30,24 +49,6 @@ public static string FileExtension(this ScriptType type)
{
return '.' + type.ToString().ToLowerInvariant();
}

public static Encoding PreferredEncoding(this ScriptType type)
{
return type switch
{
ScriptType.Ps1 => Encoding.UTF8,
ScriptType.Cmd => Encoding.Latin1,
ScriptType.Reg => Utf16WithBom(),
ScriptType.Vbs => Utf16WithBom(),
ScriptType.Js => Utf16WithBom(),
_ => throw new NotImplementedException(),
};

static UnicodeEncoding Utf16WithBom()
{
return new(bigEndian: false, byteOrderMark: true);
}
}
}

public record class ScriptSettings(
Expand Down Expand Up @@ -88,21 +89,30 @@ class ScriptModifier(ModifierContext context) : Modifier(context)
{
private int count = 0;

private const string ScriptsDirectory = @"C:\Windows\Setup\Scripts";

private bool directoryCreated = false;

public override void Process()
{
foreach (Script script in Configuration.ScriptSettings.Scripts)
var scriptsMap = Configuration.ScriptSettings.Scripts.ToImmutableDictionary(NewScriptId);
if (scriptsMap.IsEmpty)
{
if (!string.IsNullOrWhiteSpace(script.Content))
{
ScriptId scriptId = NewScriptId(script);
CreateScriptsDirectoryOnce();
WriteScriptContent(script, scriptId);
CallScript(script, scriptId);
}
return;
}
foreach (var pair in scriptsMap)
{
WriteScriptContent(pair.Value, pair.Key);
}
{
const string psPath = @"C:\Windows\Temp\ExtractScripts.ps1";
CommandAppender appender = new(Document, NamespaceManager, new SpecializeCommandConfig());
appender.Append(
CommandBuilder.WriteToFile(psPath, Util.SplitLines(Util.StringFromResource("ExtractScripts.ps1")))
);
appender.Append(
CommandBuilder.InvokePowerShellScript(psPath)
);
}
foreach (var pair in scriptsMap)
{
CallScript(pair.Value, pair.Key);
}
}

Expand All @@ -112,19 +122,7 @@ private ScriptId NewScriptId(Script script)
{
string name = $"unattend-{++count:x2}";
string extension = script.Type.ToString().ToLowerInvariant();
return new ScriptId(@$"{ScriptsDirectory}\{name}.{extension}", name);
}

private void CreateScriptsDirectoryOnce()
{
if (!directoryCreated)
{
var appender = new CommandAppender(Document, NamespaceManager, CommandConfig.Specialize);
appender.Append(
CommandBuilder.ShellCommand($"mkdir {ScriptsDirectory}")
);
directoryCreated = true;
}
return new ScriptId(@$"C:\Windows\Setup\Scripts\{name}.{extension}", name);
}

private void WriteScriptContent(Script script, ScriptId scriptId)
Expand All @@ -142,12 +140,20 @@ static string Clean(Script script)
return script.Content;
}

var appender = new CommandAppender(Document, NamespaceManager, CommandConfig.Specialize);
string content = Clean(script);
appender.AppendXmlComment($"\r\n{content}\r\n");
appender.Append(
CommandBuilder.SafeWriteToFile(scriptId.FullName, content, script.Type.PreferredEncoding())
);
{
XmlNode root = Document.SelectSingleNodeOrThrow("/u:unattend", NamespaceManager);
XmlNode? extensions = root.SelectSingleNode("s:Extensions", NamespaceManager);
if (extensions == null)
{
extensions = Document.CreateElement("Extensions", Constants.MyNamespaceUri);
root.AppendChild(extensions);
}

XmlElement file = Document.CreateElement("File", Constants.MyNamespaceUri);
file.SetAttribute("path", scriptId.FullName);
file.InnerText = Clean(script);
extensions.AppendChild(file);
}
}

private void CallScript(Script script, ScriptId scriptId)
Expand Down
14 changes: 14 additions & 0 deletions resource/ExtractScripts.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mkdir -Path "C:\Windows\Setup\Scripts" -ErrorAction 'SilentlyContinue';
$doc = [xml]::new();
$doc.Load( "C:\Windows\Panther\unattend.xml" );
$ns = [System.Xml.XmlNamespaceManager]::new($doc.NameTable);
$ns.AddNamespace( 's', 'https://schneegans.de/windows/unattend-generator/' );
foreach( $file in $doc.SelectNodes( '//s:File[@path]', $ns ) ) {
$path = $file.GetAttribute( 'path' );
$encoding = switch( [System.IO.Path]::GetExtension( $path ) ) {
'.ps1' { [System.Text.Encoding]::UTF8; }
'.cmd' { [System.Text.Encoding]::Default; }
{ $_ -in '.reg', '.vbs', '.js' } { [System.Text.UnicodeEncoding]::new( $false, $true ); }
};
[System.IO.File]::WriteAllBytes( $path, ( $encoding.GetPreamble() + $encoding.GetBytes( $file.InnerText ) ) );
}

0 comments on commit dd1ac96

Please sign in to comment.