defmodule Mix.Tasks.Pe.Update do @moduledoc """ SYNTAX: mix pe.update (options) pe.update updates the PE-checksum of the given pe file and additionally can add resources to it if needed. Options are: -h | -help This help --set-subsystem Update the PE files subsytem type --set-icon Embeds a given application icon --get-icon Extracts an embedded icon and stores it to the filename --set-manifest Embeds a given side-by-side manifest --set-info Embeds the given version information --set-resource Embeds any resources type --del-resource Remove any resources type Known info types are: "Comments", "CompanyName", "FileDescription", "FileVersion", "InternalName", "LegalCopyright", "LegalTrademarks", "OriginalFilename", "PrivateBuild", "ProductName", "ProductVersion", "SpecialBuild" Known resources types are: "RT_ACCELERATOR", "RT_ANICURSOR", "RT_ANIICON", "RT_BITMAP", "RT_CURSOR", "RT_DIALOG", "RT_DLGINCLUDE", "RT_FONT", "RT_FONTDIR", "RT_GROUP_CURSOR", "RT_GROUP_ICON", "RT_HTML", "RT_ICON", "RT_MANIFEST", "RT_MENU", "RT_MESSAGETABLE", "RT_PLUGPLAY", "RT_RCDATA", "RT_STRING", "RT_VERSION", "RT_VXD" """ use Mix.Task @doc false def run([]) do show_help() end def run(args) do %{files: files, resources: resources, resource_updates: resource_updates, updates: updates} = process_args(%{resources: [], resource_updates: %{}, files: [], updates: []}, args) if files == [] do error("No files given") end Enum.each(files, fn filename -> {:ok, pe} = LibPE.parse_file(filename) IO.puts("Updating file #{filename}") raw = update_resources(pe, resources, resource_updates) |> update_other(updates) |> LibPE.update_checksum() |> LibPE.encode() File.write!(filename, raw) end) end def error(msg) do Mix.Shell.IO.error(msg) # Mix.Shell.IO.info(@moduledoc) System.halt(1) end defp show_help() do Mix.Shell.IO.info(@moduledoc) System.halt() end defp update_other(pe, updates) do Enum.reduce(updates, pe, fn update, pe -> update.(pe) end) end defp update_resources(pe, replacements, resource_updates) do if replacements == [] and map_size(resource_updates) == 0 do pe else do_update_resources(pe, replacements, resource_updates) end end defp do_update_resources(pe, replacements, resource_updates) do resource_table = LibPE.get_resources(pe) resource_table = Enum.reduce(replacements, resource_table, fn {type, data}, resource_table -> LibPE.ResourceTable.set_resource(resource_table, type, data) end) resource_table = Enum.reduce(resource_updates, resource_table, fn {type, funs}, resource_table -> resource = LibPE.ResourceTable.get_resource(resource_table, type) new_resource = Enum.reduce(funs, resource, fn fun, resource -> fun.(resource) end) if new_resource != resource do LibPE.ResourceTable.set_resource(resource_table, type, new_resource) else resource_table end end) LibPE.set_resources(pe, resource_table) |> LibPE.update_layout() end defp process_args(opts, []) do opts end defp process_args(opts, ["--set-manifest", filename | rest]) do case File.read(filename) do {:ok, data} -> add_resource(opts, "RT_MANIFEST", data) |> process_args(rest) error -> error("Failed to read manifest file #{filename}: #{inspect(error)}") end |> process_args(rest) end defp process_args(opts, ["--set-subsystem", value | rest]) do LibPE.WindowsSubsystem.flags() |> Enum.find(fn {name, num, _desc} -> value == name or value == "#{num}" end) |> case do nil -> error( "Failed find subsystem value '#{value}'. Valid values are: #{inspect(LibPE.WindowsSubsystem.flags())}" ) sub -> add_update(opts, fn pe -> %{pe | coff_header: %{pe.coff_header | subsystem: sub}} end) |> process_args(rest) end end defp process_args(opts, ["--set-icon", filename | rest]) do case File.read(filename) do {:ok, data} -> opts |> add_resource("RT_ICON", data) |> drop_resource("RT_GROUP_ICON") |> process_args(rest) error -> error("Failed to read icon file #{filename}: #{inspect(error)}") end end defp process_args(opts, ["--get-icon", filename | rest]) do if File.exists?(filename) do error("The output file already exists: #{filename}") end update_resource(opts, "RT_ICON", fn icon -> if icon == nil do Mix.Shell.IO.error("No icon found in the file") else File.write!(filename, icon.entry.data) end icon end) |> process_args(rest) end defp process_args(opts, ["--set-resource", name, filename | rest]) do data = case File.read(filename) do {:ok, data} -> data error -> error("Failed to read resource file #{filename}: #{inspect(error)}") end add_resource(opts, name, data) |> process_args(rest) end defp process_args(opts, ["--del-resource", name | rest]) do add_resource(opts, name, nil) |> process_args(rest) end defp process_args(opts, ["--set-info", name, value | rest]) do update = fn version -> page = LibPE.Codepage.encode(0) lang = LibPE.Language.encode(1033) version = version || %LibPE.ResourceTable.DirEntry{ name: lang, entry: %LibPE.ResourceTable.DataBlob{ codepage: page, data: LibPE.VersionInfo.encode(LibPE.VersionInfo.new()) } } data = LibPE.VersionInfo.decode(version.entry.data) |> Map.update!(:strings, fn strings -> List.keystore(strings, name, 0, {name, value}) end) |> LibPE.VersionInfo.encode() %{version | entry: %{version.entry | data: data}} end update_resource(opts, "RT_VERSION", update) |> process_args(rest) end defp process_args(_opts, ["--help" | _rest]), do: show_help() defp process_args(_opts, ["-h" | _rest]), do: show_help() defp process_args(opts, [arg | rest]) do if String.starts_with?(arg, "-") do error("Unknown option string '#{arg}'") end %{opts | files: [arg | opts.files]} |> process_args(rest) end defp drop_resource(opts, name) do add_resource(opts, name, nil) end defp add_update(opts, update_fun) do %{opts | updates: [update_fun | opts.updates]} end defp add_resource(opts, name, data) do if not is_integer(name) and not LibPE.Flags.is_name(LibPE.ResourceTypes, name) do error(""" The specified resource name #{name} is not a known name. Known resource names are only: #{inspect(LibPE.Flags.names(LibPE.ResourceTypes))} """) end name = LibPE.ResourceTypes.encode(name) %{opts | resources: [{name, data} | opts.resources]} end defp update_resource(opts, name, fun) do if not is_integer(name) and not LibPE.Flags.is_name(LibPE.ResourceTypes, name) do error(""" The specified resource name #{name} is not a known name. Known resource names are only: #{inspect(LibPE.Flags.names(LibPE.ResourceTypes))} """) end name = LibPE.ResourceTypes.encode(name) %{ opts | resource_updates: Map.update(opts.resource_updates, name, [fun], fn rest -> rest ++ [fun] end) } end end