From b2c32f16a32f8071b12f24c2e03d947f108939c2 Mon Sep 17 00:00:00 2001 From: Ky Bishop Date: Wed, 4 Feb 2026 15:37:15 -0500 Subject: [PATCH] Delta validation cleanup --- lib/confuse/fwup.ex | 66 ++++++++++++------ test/fwup_test.exs | 166 ++++++++++++++++++++++++++------------------ 2 files changed, 144 insertions(+), 88 deletions(-) diff --git a/lib/confuse/fwup.ex b/lib/confuse/fwup.ex index 2d57b45..552c640 100644 --- a/lib/confuse/fwup.ex +++ b/lib/confuse/fwup.ex @@ -192,15 +192,15 @@ defmodule Confuse.Fwup do def validate_delta(source_meta_conf, target_meta_conf, using_fwup_version \\ nil) do with {:ok, source} <- Confuse.parse(source_meta_conf), {:ok, target} <- Confuse.parse(target_meta_conf) do - s = get_feature_by_resource(source) - t = get_feature_by_resource(target) + source_features_by_resource = get_features_by_resource(source) + target_features_by_resource = get_features_by_resource(target) fwup_warnings = if using_fwup_version do target_features = - t + target_features_by_resource |> Map.values() - |> Features.squash(t["require-fwup-version"]) + |> Features.squash(target_features_by_resource["require-fwup-version"]) if Version.compare(using_fwup_version, target_features.delta_fwup_version) == :gt do [] @@ -213,12 +213,15 @@ defmodule Confuse.Fwup do [] end + # Validate any resources in the source that exist in the target result = - s - |> Enum.flat_map(fn {resource, v} -> - case Enum.find(t, fn {r, _} -> r == resource end) do - {_, target_features} -> - validate_delta_resource(resource, v, target_features) + source_features_by_resource + |> Enum.flat_map(fn {source_resource, source_features} -> + case Enum.find(target_features_by_resource, fn {target_resource, _} -> + target_resource == source_resource + end) do + {_target_resource, target_features} -> + validate_delta_resource(source_resource, source_features, target_features) # If the resource doesn't exist in the target, it might have just been removed nil -> @@ -262,20 +265,41 @@ defmodule Confuse.Fwup do end end - defp validate_delta_resource(resource, sv, tv) do - [ - (tv.raw_deltas_valid? and not sv.raw_write?) && - "#{resource}: Target uses raw deltas but source has no raw writes.", - (tv.fat_deltas_valid? and not sv.fat_write?) && - "#{resource}: Target uses FAT deltas but source has no FAT writes.", - (tv.raw_deltas_valid? and sv.raw_write_cipher? and sv.raw_write_secret? and - not (tv.delta_source_raw_options_cipher? and tv.delta_source_raw_options_secret?)) && - "#{resource}: Target uses raw deltas and source firmware uses encryption for the same resource but target firmware has no cipher or secret options for the resource. This should not be able to work." - ] - |> Enum.filter(&is_binary/1) + defp validate_delta_resource(resource, source_features, target_features) do + errors = [] + + errors = + if target_features.raw_deltas_valid? and not source_features.raw_write? do + ["#{resource}: Target uses raw deltas but source has no raw writes." | errors] + else + errors + end + + errors = + if target_features.fat_deltas_valid? and not source_features.fat_write? do + ["#{resource}: Target uses FAT deltas but source has no FAT writes." | errors] + else + errors + end + + errors = + if target_features.raw_deltas_valid? and + source_features.raw_write_cipher? and + source_features.raw_write_secret? and + not (target_features.delta_source_raw_options_cipher? and + target_features.delta_source_raw_options_secret?) do + [ + "#{resource}: Target uses raw deltas and source firmware uses encryption for the same resource but target firmware has no cipher or secret options for the resource. This should not be able to work." + | errors + ] + else + errors + end + + errors end - defp get_feature_by_resource(parsed) do + defp get_features_by_resource(parsed) do parsed |> get_tasks() |> reduce_on_resource(%{}, &check_feature/3) diff --git a/test/fwup_test.exs b/test/fwup_test.exs index b616f5c..a5253a8 100644 --- a/test/fwup_test.exs +++ b/test/fwup_test.exs @@ -117,76 +117,108 @@ defmodule Confuse.FwupTest do Confuse.Fwup.get_feature_usage("test/fixtures/with_encrypted_deltas.conf") end - test "validate delta, essentially all configs work with themselves" do - ~w( - blank.conf - fat_deltas.conf - high_requirement.conf - rpi4-fwup.conf - rpi4-meta.conf - with_deltas.conf - encrypted.conf - libconfuse-test.conf - raw_deltas.conf - with_encrypted_deltas.conf) - |> Enum.each(fn file -> - conf = File.read!(Path.join("test/fixtures", file)) - assert {:ok, ^file} = {Confuse.Fwup.validate_delta(conf, conf), file} - end) - end - - test "validate firmware packaged meta.conf, rpi4, no fat_write" do - source_conf = File.read!("test/fixtures/rpi4-meta-no-fat-write.conf") - target_conf = File.read!("test/fixtures/rpi4-meta-fat-delta.conf") - assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) - - assert err =~ - "Target uses FAT deltas but source has no FAT writes" - end - - test "validate firmware packaged meta.conf, rpi4, no raw_write" do - source_conf = File.read!("test/fixtures/rpi4-meta-no-raw-write.conf") - target_conf = File.read!("test/fixtures/rpi4-meta.conf") - assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) - - assert err =~ - "Target uses raw deltas but source has no raw writes" - end - - test "validate delta, target uses raw deltas, source firmware is encrypted target firmware missing encrypted deltas" do - source_conf = File.read!("test/fixtures/encrypted.conf") - target_conf = File.read!("test/fixtures/with_deltas.conf") - assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) - - assert err =~ - "Target uses raw deltas and source firmware uses encryption for the same resource" - end - - test "validate delta, target uses raw encrypted deltas, low fwup version" do - source_conf = File.read!("test/fixtures/with_encrypted_deltas.conf") - target_conf = File.read!("test/fixtures/with_encrypted_deltas.conf") - - assert {:error, [err]} = - Confuse.Fwup.validate_delta(source_conf, target_conf, Version.parse!("1.0.0")) - - assert err =~ - "Delta firmware update requires fwup version" - end - - test "validate delta, unlikely raw deltas" do - source_conf = File.read!("test/fixtures/no_raw_writes.conf") - target_conf = File.read!("test/fixtures/raw_deltas.conf") - assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + describe "validate_delta/2" do + test "returns :ok when all configs work with themselves" do + ~w( + blank.conf + fat_deltas.conf + high_requirement.conf + rpi4-fwup.conf + rpi4-meta.conf + with_deltas.conf + encrypted.conf + libconfuse-test.conf + raw_deltas.conf + with_encrypted_deltas.conf + ) + |> Enum.each(fn file -> + conf = File.read!(Path.join("test/fixtures", file)) + assert {:ok, ^file} = {Confuse.Fwup.validate_delta(conf, conf), file} + end) + end + + test "returns an error when target uses FAT deltas but source has no fat_write" do + source_conf = File.read!("test/fixtures/rpi4-meta-no-fat-write.conf") + target_conf = File.read!("test/fixtures/rpi4-meta-fat-delta.conf") + assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + + assert err =~ + "Target uses FAT deltas but source has no FAT writes" + end + + test "returns an error when target uses raw deltas but source has no raw_write" do + source_conf = File.read!("test/fixtures/rpi4-meta-no-raw-write.conf") + target_conf = File.read!("test/fixtures/rpi4-meta.conf") + assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + + assert err =~ + "Target uses raw deltas but source has no raw writes" + end + + test "returns an error when target uses raw deltas, source is encrypted but target missing encrypted deltas" do + source_conf = File.read!("test/fixtures/encrypted.conf") + target_conf = File.read!("test/fixtures/with_deltas.conf") + assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + + assert err =~ + "Target uses raw deltas and source firmware uses encryption for the same resource" + end + + test "returns an error when target uses raw encrypted deltas with low fwup version" do + source_conf = File.read!("test/fixtures/with_encrypted_deltas.conf") + target_conf = File.read!("test/fixtures/with_encrypted_deltas.conf") + + assert {:error, [err]} = + Confuse.Fwup.validate_delta(source_conf, target_conf, Version.parse!("1.0.0")) + + assert err =~ + "Delta firmware update requires fwup version" + end + + test "returns an error when target uses unlikely raw deltas" do + source_conf = File.read!("test/fixtures/no_raw_writes.conf") + target_conf = File.read!("test/fixtures/raw_deltas.conf") + assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + + assert err =~ "Target uses raw deltas but source has no raw writes" + end + + test "returns an error when target uses unlikely FAT deltas" do + source_conf = File.read!("test/fixtures/no_fat_writes.conf") + target_conf = File.read!("test/fixtures/fat_deltas.conf") + assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + + assert err =~ "Target uses FAT deltas but source has no FAT writes" + end + + test "doesn't error when source contains resource missing in target" do + source_conf = """ + require-fwup-version="0.5.0" + + task complete { + on-resource config.txt { + raw_write(0) + } + on-resource rootfs.img { + raw_write(0) + } + } + """ - assert err =~ "Target uses raw deltas but source has no raw writes" - end + target_conf = """ + require-fwup-version="1.6.0" - test "validate delta, unlikely FAT deltas" do - source_conf = File.read!("test/fixtures/no_fat_writes.conf") - target_conf = File.read!("test/fixtures/fat_deltas.conf") - assert {:error, [err]} = Confuse.Fwup.validate_delta(source_conf, target_conf) + task upgrade { + on-resource config.txt { + delta-source-raw-offset=0 + delta-source-raw-count=100 + raw_write(0) + } + } + """ - assert err =~ "Target uses FAT deltas but source has no FAT writes" + assert :ok == Confuse.Fwup.validate_delta(source_conf, target_conf) + end end test "detect fwup file features, high version requirement, no features" do