2022年9月30日金曜日

ディープコピー(詳細コピー)で複製時に気を付けるべき点(C#)

どうも。ひっくです。

クラスのオブジェクトをディープコピー(詳細コピー)したい場合があると思います。

「ディープコピー C#」で検索すると上位に出てくる検索結果上位に、

「BinaryFormatter」クラスを使用した実装例が非常に多く見受けられます。

 

コード量もすっきりしていて一見良さそうに見えますが、

今後は可能な限り使用しない方が良いでしょう、というお話を今回はしようと思います。

BinaryFormatterを利用したディープコピー実装例


まず、コードを見てみましょう。

public static class Utils
{
    /// <summary>
    /// ディープコピー作成
    /// </summary>
    /// <typeparam name="T">型パラメータ</typeparam>
    /// <param name="src">コピー元情報</param>
    /// <returns>コピーオブジェクト</returns>
    public static T DeepClone<T>(this T src)
    {
        using (var memoryStream = new System.IO.MemoryStream())
        {
            var binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            binaryFormatter.Serialize(memoryStream, src);
            memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
            return (T)binaryFormatter.Deserialize(memoryStream);
        }
    }
}

[Serializable]
public class Customer
{
    public string CustomerID { get; set; }
    public string CustomerName { get; set; }
    public DateTime LastModified { get; set; }
    public List<PurchaseGoods> PurchaseGoodsList { get; set; }
}

public class PurchaseGoods
{
    public string Name { get; set; }

    public int Price { get; set; }
}

private void CopyTest()
{
    var goods1 = new List<PurchaseGoods>()
    {
        new PurchaseGoods() { Name = "milk", Price = 160 },
        new PurchaseGoods() { Name = "egg", Price = 250 },
    };

    var goods2 = new List<PurchaseGoods>()
    {
        new PurchaseGoods() { Name = "meat", Price = 350 },
        new PurchaseGoods() { Name = "fish", Price = 380 },
    };

    var customers = new List<Customer>()
    {
        new Customer() { CustomerID = "D0001", CustomerName = "Customer1", PurchaseGoodsList = goods1 },
        new Customer() { CustomerID = "D0002", CustomerName = "Customer2", PurchaseGoodsList = goods2 },
    };

    var cloneCustomers = customers.DeepClone();

    customers.First().CustomerID = "A0001";
    customers.First().LastModified = DateTime.Now;
    customers.First().PurchaseGoodsList.First().Name = "tomato";

    foreach (var customer in customers)
    {
        Debug.WriteLine($"CustomerID:{customer.CustomerID} CustomerName:{customer.CustomerName}");
        foreach (var goods in customer.PurchaseGoodsList)
        {
            Debug.WriteLine($"GoodsName:{goods.Name} GoodsPrice:{goods.Price}");
        }
    }
    foreach (var customer in cloneCustomers)
    {
        Debug.WriteLine($"CustomerID:{customer.CustomerID} CustomerName:{customer.CustomerName}");
        foreach (var goods in customer.PurchaseGoodsList)
        {
            Debug.WriteLine($"GoodsName:{goods.Name} GoodsPrice:{goods.Price}");
        }
    }
}

CopyTest() の実行結果は以下のようになります。

「customers」をディープコピーした「cloneCustomers」には、

customersに対する変更が反映されていないことが確認できます。(ディープコピーできていますね)

CustomerID:A0001 CustomerName:Customer1
GoodsName:tomato GoodsPrice:160
GoodsName:egg GoodsPrice:250
CustomerID:D0002 CustomerName:Customer2
GoodsName:meat GoodsPrice:350
GoodsName:fish GoodsPrice:380
CustomerID:D0001 CustomerName:Customer1
GoodsName:milk GoodsPrice:160
GoodsName:egg GoodsPrice:250
CustomerID:D0002 CustomerName:Customer2
GoodsName:meat GoodsPrice:350
GoodsName:fish GoodsPrice:380

 

BinaryFormatterが非推奨とされる理由


BinaryFormatterですが、現在MSDNでは使用が非推奨とされています。

.NET5では警告が表示されるものの使用は可能な状態だったようなのですが、

2022/11リリース予定の.NET7ではとうとうコンパイルエラーになる(破壊的変更が行われる)ようです。

 

そもそも.NET5で警告扱い(ASP.NETでは既定で実行時エラー)だった理由は、逆シリアル化の脆弱性があるからです。

MSDNでは以下の様に説明されています。

「逆シリアル化の脆弱性は、要求ペイロードが安全でない方法で処理される脅威のカテゴリです。 攻撃者がこのような脆弱性をアプリに対して利用することに成功すると、サービス拒否 (DoS)、情報漏えい、またはターゲット アプリ内でのリモート コード実行が発生する可能性があります。」

 

どんな場面で上記の脆弱性に対する危険が発生しうるのか、文章を読むだけではいまいちぴんと来なかったんですが、

OWASP Top 10 2017 A8(リンク先PDF 15ページ参照)を見ると、良くわかりました。

要は信頼できないデータに対してデシリアライズすると、逆シリアル化の脆弱性に対して対策されていない場合、

リモートコード実行や権限書き換えなどによる情報漏洩が起きる可能性があるよねってことなんですよね。

Webアプリのように外から受信したデータを処理することが多いなら、上記が起こりうる確率も高くなるというので

ASP.NETでは既定で実行時エラーになるようにされたんだろう、と個人的に納得しました。

 

.NET7で破壊的変更が行われる以上いずれ置き換えもしなくてはならなくなるし、今後は使用しない方が良いのだと思います。

 

代替手段は?


ICloneable.Cloneを実装して、シャローコピー(簡易コピー)とディープコピー(詳細コピー)の

両方に対応するコピー関数を用意するしか手はなさそうです。

コード量は増えるし、クラスのメンバが増えたり階層化したりするとその都度メンテしないといけないしで、

BinaryFormatterですっきり書きたくなっちゃいますが、可能な限りこっちで実装する方が良いと思われます。

public class Customer : ICloneable
{
    public string CustomerID { get; set; }
    public string CustomerName { get; set; }
    public DateTime LastModified { get; set; }
    public List<PurchaseGoods> PurchaseGoodsList { get; set; }

    public Customer Clone()
    {
        return (Customer)this.MemberwiseClone();
    }
    
    object ICloneable.Clone()
    {
        return this.Clone();
    }

    public Customer DeepCopy()
    {
        Customer customer = this.Clone();
        customer.PurchaseGoodsList = this.PurchaseGoodsList.Select(goods => goods.DeepCopy()).ToList();
        return customer;
    }
}

public class PurchaseGoods : ICloneable
{
    public string Name { get; set; }
    public int Price { get; set; }
    
    public PurchaseGoods Clone()
    {
        return (PurchaseGoods)this.MemberwiseClone();
    }
    
    object ICloneable.Clone()
    {
        return this.Clone();
    }

    public PurchaseGoods DeepCopy()
    {
        return this.Clone();
    }
}

private void CopyTest()
{
    var goods1 = new List<PurchaseGoods>()
    {
        new PurchaseGoods() { Name = "milk", Price = 160 },
        new PurchaseGoods() { Name = "egg", Price = 250 },
    };

    var goods2 = new List<PurchaseGoods>()
    {
        new PurchaseGoods() { Name = "meat", Price = 350 },
        new PurchaseGoods() { Name = "fish", Price = 380 },
    };

    var customers = new List<Customer>()
    {
        new Customer() { CustomerID = "D0001", CustomerName = "Customer1", PurchaseGoodsList = goods1 },
        new Customer() { CustomerID = "D0002", CustomerName = "Customer2", PurchaseGoodsList = goods2 },
    };

    var cloneCustomers = new List<Customer>();
    cloneCustomers.AddRange(customers.Select(customer => customer.DeepCopy()));

    customers.First().CustomerID = "A0001";
    customers.First().LastModified = DateTime.Now;
    customers.First().PurchaseGoodsList.First().Name = "tomato";

    foreach (var customer in customers)
    {
        Debug.WriteLine($"CustomerID:{customer.CustomerID} CustomerName:{customer.CustomerName}");
        foreach (var goods in customer.PurchaseGoodsList)
        {
            Debug.WriteLine($"GoodsName:{goods.Name} GoodsPrice:{goods.Price}");
        }
    }
    foreach (var customer in cloneCustomers)
    {
        Debug.WriteLine($"CustomerID:{customer.CustomerID} CustomerName:{customer.CustomerName}");
        foreach (var goods in customer.PurchaseGoodsList)
        {
            Debug.WriteLine($"GoodsName:{goods.Name} GoodsPrice:{goods.Price}");
        }
    }
}

 

CopyTest() の実行結果は以下で、BinaryFormatterの実装例と同じ結果になります。

CustomerID:A0001 CustomerName:Customer1
GoodsName:tomato GoodsPrice:160
GoodsName:egg GoodsPrice:250
CustomerID:D0002 CustomerName:Customer2
GoodsName:meat GoodsPrice:350
GoodsName:fish GoodsPrice:380
CustomerID:D0001 CustomerName:Customer1
GoodsName:milk GoodsPrice:160
GoodsName:egg GoodsPrice:250
CustomerID:D0002 CustomerName:Customer2
GoodsName:meat GoodsPrice:350
GoodsName:fish GoodsPrice:380

 

まとめ


以上、ディープコピー(詳細コピー)で複製時に気を付けるべき点について取り上げました。

既にBinaryFormatterでのディープコピーを使用してしまっている場合は、古いソースでは警告も表示されないので

脆弱性への対応が必要なケースかどうか、脆弱性への対応が行われているかを改めて確認した方が良いでしょう。

今後新たにコピー機能を追加する場合は、BinaryFormatterは使用しないことをお勧めします。

 

今回はこのへんで。ではまた!

2022年9月20日火曜日

パワーポイントのファイルを覗く

こんにちは、やっまむーです。

ワードやエクセルのファイル(docx、xlsxなど)が実はzipファイルということをご存じでしょうか?
以前、エクセルの削除できないスタイルを消す際にzip解凍してスタイルの情報を削除する方法をご紹介したことがあります。
では、その他のファイルはどうなっているのか、気になりませんか?
ということで、今回はパワーポイントのファイルの中身を調べてみることにしました。

解凍した中身

昔作成したパワーポイントのファイルを改名してさっそく解凍しました。

中身は主にXMLファイルで構成されています。

スライド本体はpptフォルダの下に収められています。

パワーポイント内の各スライドはslidesフォルダの下にあります。
また、使用されている画像ファイルはmediaフォルダの下に格納されます。
これは、エクセルやワードのファイルでも同様で、ドキュメント内で使用されている画像ファイルを自由に取り出せるようになっています。

スライドのファイルはそれぞれ個別のXMLファイルに分かれており、以下のように記述されています。

最初のタイトルページです。
実際にパワーポイントの資料ではこのようになっています。

タイトルのテキストが細切れになっているのは、フォントの設定が変わっているからだと思われます。
要素にはHTMLのタグに近い名前が使われているので、それぞれの意味を類推できます。
文法がわかれば、直接パワーポイントのファイルを編集することもできそうです。

ファイルの中身など普段意識することはありませんが、このように調べてみると意外と面白いです。
興味があれば、実際にワードやエクセルのファイルを解凍して、中身を確認してみてはいかがでしょうか。

ではではー。

2022年9月11日日曜日

「ポータブル電源」のコスパにやられた話

こんにちは。よっしーです。

昨年末ぐらいにコスパのいい「ポータブル電源」を購入し、

いろいろ使っていたのですが、

ある日、充電できなくなりました。。。


通電はするのですが、点滅した後に表示部の電気が消え、

バッテリーに充電されず、電源もつかない状態です。


まぁ、こんなこともあろうかと、しっかりと保証がついている商品を購入したので、

サポートへメールで連絡しました。



。。。


かれこれ1か月過ぎても返信が来ません。。。

なんで。。。


で、amazonレビュー久しぶりにを見ると、色々ひどいコメントばかりです。


4月頃までは、それなりの評価コメントで、

サポートもきちんとしてくれた。とのレビューもありました。

FLYLINKTECHさん。。。4月以降何かあったんですか。。。?


というわけで、保証も何も受けられない状況になり、

ただの文鎮化してしまったポータブル電源。。。

安い商品にはこういったこともある。といういい経験になりましたw


みなさんも安い商品を買う時は、

しっかりと見極めて買うようにしてくださいねw

ではまた~。

2022年9月5日月曜日

XAMLのお勉強メモ ⑦

 こんばんは。ざわです。

今回もXAMLについて、過去履歴はこちらです。


動作環境

 ・Windows10

 ・Microsoft Visual Studio Community 2022 Preview 7.0

 ・Xamarin.Forms

今回試してみたこと


 本日のテーマは「SwipeView」です。
 iPhoneメールなどで一覧のリスト上で横にスワイプした際に「ゴミ箱」や「フラグ」といったボタンが表示されますが、
 今回はこれを実装してみたいと思います。

 1. LabelでSwipeView

まずはラベルコントロールでSwipeViewの動きを試してみます。 
左方向にスワイプした際に「削除」と「お気に入り」ボタンを表示するようにします。 
(赤字の部分がSwipeView)
 
<StackLayout>
    <SwipeView>
        <SwipeView.RightItems>
            <SwipeItems>
                <SwipeItem Text="削除"
                            BackgroundColor="Red"
                            IconImageSource="delete.png"
                            Invoked="OnDeleteSwipeItemInvoked" />
                <SwipeItem Text="お気に入り"
                            BackgroundColor="RoyalBlue"
                            IconImageSource="favorite.png"
                            Invoked="OnFavoriteSwipeItemInvoked" />
            </SwipeItems>
        </SwipeView.RightItems>
        <StackLayout>
            <Label Text="Swipe View Test" FontSize="Large"  Padding="20,10"/>
        </StackLayout>
    </SwipeView>
</StackLayout>
 

コードビハインドにはボタンをタップしたときのイベントを追加。

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void OnDeleteSwipeItemInvoked(object sender, EventArgs e)
    {
        DisplayAlert("削除", "このアイテムを削除しますか?", "OK");
    }

    private void OnFavoriteSwipeItemInvoked(object sender, EventArgs e)
    {
        DisplayAlert("お気に入り", "このアイテムをお気に入りに追加しますか?", "OK");
    }
}
 
ラベル部分を左方向にスワイプすると・・






 
 
「削除」と「お気に入り」ボタンが表示されました! 






 

 2. CollectionViewでSwipeView

SwipeViewは一般的にデータのコレクションで使用されると思いますので、
次は一覧(CollectionView)に対して、同じようにSwipeViewを使ってみます。 
(赤字の部分がSwipeView)

<CollectionView x:Name="myCollectionView">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <SwipeView>
                <SwipeView.RightItems>
                    <SwipeItems>
                        <SwipeItem Text="削除"
                        BackgroundColor="Red"
                        IconImageSource="delete.png"
                        Invoked="OnDeleteSwipeItemInvoked" />
                        <SwipeItem Text="お気に入り"
                        BackgroundColor="RoyalBlue"
                        IconImageSource="favorite.png"
                        Invoked="OnFavoriteSwipeItemInvoked" />
                    </SwipeItems>
                </SwipeView.RightItems>
                <StackLayout Orientation="Horizontal">
                    <Image Source="{Binding Image}"
                            HeightRequest="60"
                            WidthRequest="60"
                            Margin="5"/>
                    <Label Text="{Binding Name}"
                            FontSize="Medium" />
                </StackLayout>
            </SwipeView>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>
 
一覧のアイテム(あひる)部分を左方向にスワイプすると・・





 







ラベルのときと同じように「削除」と「お気に入り」ボタンが表示されましたですね。


 

 









スワイプでボタン表示機能を付けたいコントロールを<SwipeView>~</SwipeView>内に記述し、
スワイプジェスチャー(上下左右のどこに表示するのか、ボタンの属性等)を指定してあげる、
CollectionViewの場合は各行に設定されるので<DataTemplate>内に<SwipeView>を記述する、
というイメージですかね。
それではまた!