Skip to content

Commit 4a5e214

Browse files
Merge pull request #527 from OpenTouryoProject/2nd_agent
2nd agent
2 parents c1c4538 + ea05d4c commit 4a5e214

16 files changed

Lines changed: 1603 additions & 17 deletions

File tree

root/programs/BUILDING.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,52 @@ git push origin origin/develop:deps # 早送り。できないときは弾か
316316
> ⓪の push 自体が③を起こす(約 12 分)。`develop` の現状を検証する意味はある。
317317
> 公開リポジトリなので、GitHub ホステッド runner の費用はかからない。
318318
319+
### 複数の PR を `deps` に溜める
320+
321+
**溜めてよい。** `deps` は使い捨てではなく常設のブランチで、②を何回か繰り返してから
322+
④でまとめて `develop` へ戻せる。**それが受けブランチを置いた本来の使い方。**
323+
324+
```
325+
PR ─┐
326+
PR ─┼→ deps(②を繰り返す。マージのたびに③)→ ④ 1 本の PR → develop
327+
PR ─┘
328+
```
329+
330+
**切り分けは失われない。** ③のトリガは `push: branches: [deps]` なので、
331+
5 本マージすれば③が 5 回走る。どのコミットで赤くなったかが残る。
332+
333+
| やり方 | 向き |
334+
|---|---|
335+
| 1 本ずつ③の緑を確認して次へ | 確実。1 本あたり約 12 分 |
336+
| まとめてマージし、最後の③だけ見る | 速い。赤いときに戻す手間がかかる |
337+
338+
溜めるときの注意が 4 つある。
339+
340+
**`develop` が進んだら取り込む。** 溜めている間に `develop` が動くと④で競合する。
341+
⓪の基準が「②の前」なのは、この繰り返しでも同じ(**②のたびに⓪を見る**)。
342+
343+
**② 出力が変わる更新は `Result*.txt` を同じコミットに入れる。**
344+
`deps` の上で `2_RunAllTests.ps1` を回して再生成する。
345+
**ソースだけ先にコミットすると③が必ず赤くなる**(③は期待値を `HEAD` から取るため)。
346+
実際に `System.Security.Cryptography.Xml``EncAndDecUtilCUI` の期待値に影響し得る。
347+
348+
**③ 同時実行を制御していない。** `build-windows.yml``concurrency` の指定が無いため、
349+
連続してマージすると**古いコミットの run も最後まで走る**(12 分 × 本数)。
350+
「まとめて最後だけ見る」を常用するなら次を足すと無駄が消える。
351+
352+
```yaml
353+
concurrency:
354+
group: build-${{ github.ref }}
355+
cancel-in-progress: true
356+
```
357+
358+
**ただし「1 本ずつ確認する」ときは邪魔になる**(確認したい run が消える)。
359+
運用を決めてから入れること。現状は入れていない。
360+
361+
**④ Dependabot が上書き PR を出すことがある。** 溜めている間に次の版が出ると、
362+
Dependabot は**古い PR を閉じて新しい PR を開く**。既にマージ済みの分は `deps` に残るため、
363+
**同じパッケージの版が 2 回進む**コミットが並ぶ。害は無いが履歴は読みにくくなる。
364+
319365
### 任意のブランチで手動実行する
320366

321367
`build-windows.yml` は `workflow_dispatch` を持つので、**`deps` 以外のブランチでも同じ検証を回せる。**

root/programs/CS/Frameworks/ANALYSIS.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ powershell.exe -NoProfile -Command "Set-Location 'root\programs'; .\3_SmokeTest.
577577
| `Public.Log` | `LogIF`(静的 façade)。ロガー名は慣習的に `"ACCESS"``"SQLTRACE"`。バックエンドは `LogLib` 設定で log4net / NLog |
578578
| `Public.Reflection` | `Latebind`(フレームワークの動的呼び出しの心臓部)、`MyAssemblies` |
579579
| `Public.FastReflection` | `AccessorCacher` `CompiledExpressionCreater` `InstanceCreator<T>` `EnumToString*Extensions` |
580-
| `Public.IO` | `ResourceLoader` `EmbeddedResourceLoader` `DeflateCompression` `ExponentialBackoff` `Zipper/UnZipper`(net48) |
580+
| `Public.IO` | `ResourceLoader` `EmbeddedResourceLoader` `DeflateCompression` `ExponentialBackoff` `ZipperV2/UnZipperV2` |
581581
| `Public.Util` | `GetConfigParameter` `PerformanceRecorder` `RandomValueGenerator` `EnvInfo` `PubCmnFunction` |
582582
| `Public.Diagnostics` | `MyDebug``OutputDebugAndConsole``ObjectInspector` `StackFrameOperator` |
583583
| `Public.Win32` / `WinProc` | P/Invoke 群(net48 のみ) |
@@ -613,7 +613,8 @@ powershell.exe -NoProfile -Command "Set-Location 'root\programs'; .\3_SmokeTest.
613613
net10.0 のフレームワーク参照(`Microsoft.AspNetCore.App`)ではない。安易に上げると壊れる可能性が高い。
614614

615615
**注意 2:** `System.Security.Cryptography.Xml`**`Public.Security` が 9.0.15、`Business` が 9.0.4**
616-
版がずれている(Dependabot が `Public.Security` のみ更新したため)。どちらも NU1903 が出る(12 節)。
616+
版がずれている(Dependabot が 1 プロジェクトずつしか上げないため)。どちらも NU1903 が出る。
617+
**修正版は 9.0.18**(12 節に advisory と該当 4 箇所を挙げてある)。
617618

618619
依存は Dependabot で随時更新される。**上表は目安であり、正確な値は csproj を直接見ること。**
619620

@@ -638,18 +639,46 @@ net10.0 のフレームワーク参照(`Microsoft.AspNetCore.App`)ではな
638639
アセンブリ分割の都合)。
639640
10. `TMProtocolDefinition2.xml` など「2」付きの定義ファイルが並存する。用途は用例違い。
640641
11. **`System.Security.Cryptography.Xml` に既知脆弱性(NU1903 / 高)**
641-
- `Public.Security` = 9.0.15 → `Nuget_netcore100.sln` のビルドで **20 警告**
642-
- `Business` = 9.0.4 → `Business_netcore100.sln` のビルドで **18 警告**
643-
- 修正版の有無を確認したうえで更新するか、`NoWarn` での抑止を検討する。
644-
版を上げる場合は net48 / netcore100 の両 csproj と `NuGet/*.nuspec``<dependencies>` を同時に直す。
642+
`1_BuildAll.ps1` の全体で **84 警告**`Public.Security` の 9.0.15 が 60、`Business` の 9.0.4 が 24)。
643+
644+
**修正版は 9.0.18。** 2 件の advisory が出ており、9.0.15 では**片方しか直っていない**
645+
646+
| Advisory | 影響(9 系) | 修正版 |
647+
|---|---|---|
648+
| `GHSA-23rf-6693-g89p`CVE-2026-50648) | `>= 9.0.0, <= 9.0.17` | **9.0.18** |
649+
| `GHSA-37gx-xxp4-5rgx`CVE-2026-33116) | `>= 9.0.0, <= 9.0.14` | 9.0.15 |
650+
651+
版を上げる場合は **net48 / netcore100 の両 csproj と `NuGet/*.nuspec`
652+
`<dependencies>` を同時に直す。** 現状ずれているのは次の 4 箇所。
653+
654+
```
655+
Public/Security/Public.Security_netcore100.csproj 9.0.15
656+
Business/Business_netcore100.csproj 9.0.4
657+
Tests/EncAndDecUtilCUI/core100/*.csproj 9.0.6 ← Dependabot PR #510 が 9.0.18 へ
658+
NuGet/Symbol_Public.Security.nuspec 9.0.15
659+
```
660+
661+
**Dependabot は 1 プロジェクトずつしか上げない。** #510 はテスト プロジェクトのみで、
662+
取り込んでも 84 警告は減らない。**残りは手で揃える**こと
663+
(手順は [`BUILDING.md`](../../BUILDING.md) 9 節「複数の PR を `deps` に溜める」)。
645664
12. **結果ファイル比較のテストは、検算しないと不具合を「正」として固定する。**
646665
`ArrayOperator.GetLongFromByte` は桁の重みを `256 * j`(掛け算)で求めており、
647666
**3 byte 目から誤った値**を返していた(2026/08/06 に修正、#522)。
648667
1〜2 byte は `256 * 1 == 256 ^ 1` でたまたま一致するため、呼び出し元
649668
(`CheckCharCode` → Shift_JIS の 1〜2 byte)では顕在化していなかった。
650669
**期待値を作るときは、出力をそのまま貼らず実装から検算し、境界を跨いだケースを置く**
651670
([`Tests/TestCode/README.md`](Tests/TestCode/README.md))。
652-
13. **`Symbol_Framework.RichClient.nuspec` の lib TFM が不正確**
671+
13. **`Public/IO` に、ビルド対象外の旧 ZIP 部品が残っている**。
672+
`ZipBase.cs` / `Zipper.cs` / `UnZipper.cs` は DotNetZip(非推奨・既知脆弱性
673+
`GHSA-xhg6-9j5j-w4vf`)に依存しており、**net48 / netcore100 の両方で除外**してある。
674+
- net48 … 列挙形式のため `<Compile>` に載っていない
675+
- netcore100 … `<Compile Remove="IO\Zipper.cs" />` 等で明示的に除外
676+
677+
**現役は `ZipperV2` / `UnZipperV2`**(SharpZipLib、#524)。
678+
旧ファイルを直しても何も起きない。**触るなら V2 の方。**
679+
I/F 互換は無いが、**引数の並びと名前、列挙体のメンバ名は旧に合わせて**ある。
680+
自己解凍書庫・選択条件の文字列・`StatusMSG` は落とした(代替は `ExtractedFiles`)。
681+
14. **`Symbol_Framework.RichClient.nuspec` の lib TFM が不正確**。
653682
`net10.0-windows7.0` ビルドを `lib\net10.0` に配置している。本来は `lib\net10.0-windows` が正しく、
654683
現状は Linux 上の net10.0 消費者も解決してしまう。直すとパッケージ解決セマンティクスが変わるため据え置き。
655684
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
#region Apache License
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
#endregion
16+
17+
//**********************************************************************************
18+
//* クラス名 :UnZipperV2
19+
//* クラス日本語名 :SharpZipLibを使用した解凍クラス
20+
//*
21+
//* 作成者 :玄人 幸道
22+
//* 更新履歴 :
23+
//*
24+
//* 日時 更新者 内容
25+
//* ---------- ---------------- -------------------------------------------------
26+
//* 2026/08/08 玄人 幸道 新規作成(#524)
27+
//**********************************************************************************
28+
29+
using System;
30+
using System.Collections.Generic;
31+
using System.IO;
32+
using System.Text;
33+
34+
using ICSharpCode.SharpZipLib.Zip;
35+
36+
namespace Touryo.Infrastructure.Public.IO
37+
{
38+
/// <summary>SharpZipLibを使用した解凍クラス</summary>
39+
/// <remarks>
40+
/// 旧 UnZipper(DotNetZip)の代替(#524)。
41+
/// **引数の並びと名前は旧に合わせてある。** 型だけが自前の列挙型に変わる。
42+
///
43+
/// **StatusMSG は無い。** 旧の呼び出し元(DeployZipPackWithHTTP)は
44+
/// StatusMSG のログ文言("extract file ...")を解析して解凍先の一覧を得ていたが、
45+
/// DotNetZip 固有の文字列であり再現できない。
46+
/// **ExtractedFiles で受け取ること。**
47+
/// </remarks>
48+
public class UnZipperV2 : ZipBaseV2
49+
{
50+
#region ExtractedFiles
51+
52+
/// <summary>解凍したファイルのパス</summary>
53+
private List<string> _extractedFiles = new List<string>();
54+
55+
/// <summary>解凍したファイルのパス</summary>
56+
/// <remarks>
57+
/// 旧 StatusMSG の解析で得ていたものの代替。
58+
/// **ExtractFileFromZip のたびに作り直す**(前回の内容は残らない)。
59+
/// 上書きせず飛ばしたファイルは含まない。
60+
/// </remarks>
61+
public string[] ExtractedFiles
62+
{
63+
get { return this._extractedFiles.ToArray(); }
64+
}
65+
66+
#endregion
67+
68+
#region ExtractFileFromZip
69+
70+
/// <summary>ZIPファイルを解凍</summary>
71+
/// <param name="zipFileName">ZIPファイル名</param>
72+
/// <param name="directoryToUnZip">解凍先</param>
73+
/// <param name="selectionDlgt">ファイル選択デリゲード</param>
74+
/// <param name="selectionCriteriaInfo">ファイル選択基準情報</param>
75+
/// <param name="extractExistingFile">上書き時の動作</param>
76+
/// <param name="enc">エンコーディング</param>
77+
/// <param name="zipPassword">パスワード</param>
78+
public void ExtractFileFromZip(
79+
string zipFileName,
80+
string directoryToUnZip,
81+
SelectionDelegate selectionDlgt,
82+
object selectionCriteriaInfo,
83+
ExtractExistingFileActionV2 extractExistingFile,
84+
Encoding enc,
85+
string zipPassword)
86+
{
87+
// ファイル選択基準
88+
base.SetSelectionCriteria(selectionDlgt, selectionCriteriaInfo);
89+
90+
this._extractedFiles = new List<string>();
91+
92+
using (FileStream fs = File.OpenRead(zipFileName))
93+
using (ZipFile zip = new ZipFile(fs, false, base.GetStringCodec(enc)))
94+
{
95+
if (!string.IsNullOrEmpty(zipPassword)) { zip.Password = zipPassword; }
96+
97+
ZipProgressEventArgsV2 e = new ZipProgressEventArgsV2(
98+
ZipProgressEventTypeV2.Extracting_Started, zipFileName);
99+
e.EntriesTotal = (int)zip.Count;
100+
base.OnExtractProgress(e);
101+
102+
int processed = 0;
103+
104+
foreach (ZipEntry entry in zip)
105+
{
106+
// ディレクトリのエントリは、ファイルの展開時に必要に応じて作る。
107+
if (!entry.IsFile) { continue; }
108+
109+
// 解凍対象ファイルを選択(ファイル選択デリゲードを使用)
110+
if (base.SelectionDlgt(entry.Name, base.SelectionCriteriaInfo))
111+
{
112+
this.ExtractEntry(zip, entry, zipFileName, directoryToUnZip,
113+
extractExistingFile, (int)zip.Count, processed);
114+
}
115+
116+
processed++;
117+
}
118+
119+
e = new ZipProgressEventArgsV2(
120+
ZipProgressEventTypeV2.Extracting_Completed, zipFileName);
121+
e.EntriesTotal = (int)zip.Count;
122+
e.EntriesProcessed = processed;
123+
base.OnExtractProgress(e);
124+
}
125+
}
126+
127+
#endregion
128+
129+
#region private
130+
131+
/// <summary>1エントリを解凍する</summary>
132+
/// <param name="zip">ZipFile</param>
133+
/// <param name="entry">ZipEntry</param>
134+
/// <param name="zipFileName">書庫のファイル名</param>
135+
/// <param name="directoryToUnZip">解凍先</param>
136+
/// <param name="extractExistingFile">上書き時の動作</param>
137+
/// <param name="entriesTotal">エントリの総数</param>
138+
/// <param name="entriesProcessed">処理済みのエントリ数</param>
139+
private void ExtractEntry(
140+
ZipFile zip,
141+
ZipEntry entry,
142+
string zipFileName,
143+
string directoryToUnZip,
144+
ExtractExistingFileActionV2 extractExistingFile,
145+
int entriesTotal,
146+
int entriesProcessed)
147+
{
148+
string path = UnZipperV2.GetSafePath(directoryToUnZip, entry.Name);
149+
150+
#region 既存ファイルの扱い
151+
152+
if (File.Exists(path))
153+
{
154+
if (extractExistingFile == ExtractExistingFileActionV2.Throw)
155+
{
156+
throw new IOException(path);
157+
}
158+
else if (extractExistingFile == ExtractExistingFileActionV2.DoNotOverwrite)
159+
{
160+
ZipProgressEventArgsV2 skip = new ZipProgressEventArgsV2(
161+
ZipProgressEventTypeV2.Extracting_ExtractEntryWouldOverwrite, zipFileName);
162+
skip.EntriesTotal = entriesTotal;
163+
skip.EntriesProcessed = entriesProcessed;
164+
skip.CurrentEntryName = entry.Name;
165+
base.OnExtractProgress(skip);
166+
167+
return;
168+
}
169+
}
170+
171+
#endregion
172+
173+
string dir = Path.GetDirectoryName(path);
174+
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
175+
{
176+
Directory.CreateDirectory(dir);
177+
}
178+
179+
ZipProgressEventArgsV2 e = new ZipProgressEventArgsV2(
180+
ZipProgressEventTypeV2.Extracting_BeforeExtractEntry, zipFileName);
181+
e.EntriesTotal = entriesTotal;
182+
e.EntriesProcessed = entriesProcessed;
183+
e.CurrentEntryName = entry.Name;
184+
e.TotalBytesToTransfer = entry.Size;
185+
base.OnExtractProgress(e);
186+
187+
byte[] buffer = new byte[81920];
188+
long transferred = 0;
189+
190+
using (Stream src = zip.GetInputStream(entry))
191+
using (FileStream dst = File.Create(path))
192+
{
193+
int read = src.Read(buffer, 0, buffer.Length);
194+
195+
while (0 < read)
196+
{
197+
dst.Write(buffer, 0, read);
198+
transferred += read;
199+
200+
e = new ZipProgressEventArgsV2(
201+
ZipProgressEventTypeV2.Extracting_EntryBytesWritten, zipFileName);
202+
e.EntriesTotal = entriesTotal;
203+
e.EntriesProcessed = entriesProcessed;
204+
e.CurrentEntryName = entry.Name;
205+
e.TotalBytesToTransfer = entry.Size;
206+
e.BytesTransferred = transferred;
207+
base.OnExtractProgress(e);
208+
209+
read = src.Read(buffer, 0, buffer.Length);
210+
}
211+
}
212+
213+
// 更新日時を書庫の値に合わせる。
214+
File.SetLastWriteTime(path, entry.DateTime);
215+
216+
this._extractedFiles.Add(path);
217+
218+
e = new ZipProgressEventArgsV2(
219+
ZipProgressEventTypeV2.Extracting_AfterExtractEntry, zipFileName);
220+
e.EntriesTotal = entriesTotal;
221+
e.EntriesProcessed = entriesProcessed + 1;
222+
e.CurrentEntryName = entry.Name;
223+
e.TotalBytesToTransfer = entry.Size;
224+
e.BytesTransferred = transferred;
225+
base.OnExtractProgress(e);
226+
}
227+
228+
/// <summary>解凍先のパスを求める</summary>
229+
/// <param name="directoryToUnZip">解凍先</param>
230+
/// <param name="entryName">エントリ名</param>
231+
/// <returns>解凍先のパス</returns>
232+
/// <remarks>
233+
/// **解凍先の外に出るエントリ名を弾く(Zip Slip 対策)。**
234+
/// エントリ名は書庫を作った側が自由に決められるため、
235+
/// "../" を含めて任意の場所へ書かせることができる。
236+
/// 旧実装が使っていた DotNetZip は、この脆弱性
237+
/// (GHSA-xhg6-9j5j-w4vf)で非推奨になった。**同じ轍を踏まない。**
238+
/// </remarks>
239+
private static string GetSafePath(string directoryToUnZip, string entryName)
240+
{
241+
string root = Path.GetFullPath(directoryToUnZip);
242+
string path = Path.GetFullPath(Path.Combine(root, entryName));
243+
244+
// 区切り文字を足してから比較する。
245+
// 足さないと "C:\dir" と "C:\dir2" が前方一致してしまう。
246+
string prefix = root.EndsWith(Path.DirectorySeparatorChar.ToString())
247+
? root : root + Path.DirectorySeparatorChar;
248+
249+
if (!path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
250+
{
251+
throw new IOException(entryName);
252+
}
253+
254+
return path;
255+
}
256+
257+
#endregion
258+
}
259+
}

0 commit comments

Comments
 (0)