Skip to content

Commit fd3d037

Browse files
committed
Refactor FolderIconService and IconExtractor to streamline icon extraction logic. Update documentation to clarify index handling for resource IDs and ordinal indices. Enhance tests to verify extraction functionality for negative indices and ensure consistency between GetIcon and SaveIcon methods.
1 parent 43da8de commit fd3d037

3 files changed

Lines changed: 153 additions & 57 deletions

File tree

src/FolderIconManager.Core/Services/FolderIconService.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -326,26 +326,23 @@ public void ExtractIcon(IconResource resource, string outputPath)
326326
}
327327

328328
/// <summary>
329-
/// Extracts an icon from a fully resolved file path
329+
/// Extracts an icon from a fully resolved file path.
330+
///
331+
/// Index interpretation (Windows desktop.ini convention):
332+
/// - Negative values (e.g., -183) = resource ID (extracts icon with resource ID 183)
333+
/// - Positive values (e.g., 5) = ordinal index (extracts the 6th icon)
334+
///
335+
/// PrivateExtractIcons handles both conventions natively.
330336
/// </summary>
331337
public void ExtractIconFromPath(string sourcePath, int index, string outputPath)
332338
{
333-
// Handle negative indices (resource IDs) - convert to zero-based ordinal
334-
// For now, we treat negative as absolute value for ordinal lookup
335-
if (index < 0)
336-
index = Math.Abs(index);
337-
338339
using var extractor = new IconExtractor(sourcePath);
339340

340341
if (extractor.IconCount == 0)
341342
throw new InvalidOperationException($"No icons found in {sourcePath}");
342343

343-
if (index >= extractor.IconCount)
344-
{
345-
_log.Warning($"Icon index {index} out of range (max {extractor.IconCount - 1}), using index 0");
346-
index = 0;
347-
}
348-
344+
// Pass index directly - PrivateExtractIcons handles both positive (ordinal)
345+
// and negative (resource ID) indices natively
349346
extractor.SaveIcon(index, outputPath);
350347
}
351348

src/FolderIconManager.Core/Services/IconExtractor.cs

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -74,52 +74,17 @@ public IconExtractor(string filePath)
7474
public bool HasResourceId(int resourceId) => _iconIds.Contains(resourceId);
7575

7676
/// <summary>
77-
/// Resolves an index to the actual resource ID to use.
78-
///
79-
/// Windows desktop.ini convention:
80-
/// - Positive numbers (e.g., 266) = ordinal index (0-based position in icon list)
81-
/// - Negative numbers (e.g., -266) = direct resource ID (abs value)
77+
/// Checks if an index refers to a resource ID (negative) or ordinal (positive).
78+
/// This is for informational purposes - PrivateExtractIcons handles both natively.
8279
/// </summary>
83-
private int ResolveToResourceId(int index)
84-
{
85-
// Negative numbers are direct resource IDs (Windows convention)
86-
if (index < 0)
87-
{
88-
var resourceId = Math.Abs(index);
89-
// If this resource ID exists, use it
90-
if (_iconIds.Contains(resourceId))
91-
{
92-
return resourceId;
93-
}
94-
// If not found, log warning but still try to use it
95-
// (in case enumeration missed it)
96-
return resourceId;
97-
}
98-
99-
// Positive numbers are ordinal indices (0-based)
100-
if (index >= 0 && index < _iconIds.Count)
101-
{
102-
return _iconIds[index];
103-
}
104-
105-
// Out of range - fall back to first icon
106-
if (_iconIds.Count > 0)
107-
{
108-
return _iconIds[0];
109-
}
110-
111-
throw new ArgumentOutOfRangeException(nameof(index),
112-
$"No icon found for index {index}. File has {_iconIds.Count} icons.");
113-
}
80+
public bool IsResourceIdIndex(int index) => index < 0;
11481

11582
/// <summary>
11683
/// Extracts an icon and saves it to a file with all resolutions.
11784
///
118-
/// Index interpretation (Windows convention):
119-
/// - Positive: ordinal index (e.g., 266 = the 267th icon)
120-
/// - Negative: resource ID (e.g., -266 = resource with ID 266)
121-
///
122-
/// Uses PrivateExtractIcons which matches Windows Shell behavior exactly.
85+
/// Index interpretation (Windows desktop.ini convention, handled natively by PrivateExtractIcons):
86+
/// - Positive: ordinal index (e.g., 5 = the 6th icon)
87+
/// - Negative: resource ID (e.g., -183 = icon with resource ID 183)
12388
/// </summary>
12489
public void SaveIcon(int index, string outputPath)
12590
{
@@ -130,8 +95,7 @@ public void SaveIcon(int index, string outputPath)
13095
return;
13196
}
13297

133-
// Use PrivateExtractIcons to match Windows Shell behavior exactly
134-
// This extracts at the same index that ExtractIconEx and Windows Shell use
98+
// PrivateExtractIcons handles both positive (ordinal) and negative (resource ID) indices natively
13599
SaveIconUsingPrivateExtractIcons(index, outputPath);
136100
}
137101

@@ -489,7 +453,10 @@ private struct BITMAPINFOHEADER
489453

490454
/// <summary>
491455
/// Gets an Icon object for preview purposes.
492-
/// Uses PrivateExtractIcons to match Windows Shell behavior exactly.
456+
///
457+
/// Index interpretation (Windows desktop.ini convention, handled natively by PrivateExtractIcons):
458+
/// - Positive: ordinal index (e.g., 5 = the 6th icon)
459+
/// - Negative: resource ID (e.g., -183 = icon with resource ID 183)
493460
/// </summary>
494461
public Icon? GetIcon(int index, int size = 32)
495462
{
@@ -506,7 +473,7 @@ private struct BITMAPINFOHEADER
506473
}
507474
}
508475

509-
// Use PrivateExtractIcons to get the same icon that Windows Shell uses
476+
// PrivateExtractIcons handles both positive (ordinal) and negative (resource ID) indices natively
510477
var icons = new IntPtr[1];
511478
var ids = new uint[1];
512479

tests/FolderIconManager.Tests/ExtractionTests.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,138 @@ public void IconExtractor_NegativeIndex_TreatedAsResourceId()
173173
Assert.True(fileInfo.Length > 1000, "Icon file should have content");
174174
}
175175

176+
/// <summary>
177+
/// Tests that negative indices (resource IDs) extract successfully.
178+
/// PrivateExtractIcons handles negative indices as resource IDs natively.
179+
/// </summary>
180+
[Fact]
181+
public void IconExtractor_NegativeIndex_ExtractsSuccessfully()
182+
{
183+
// Arrange
184+
var imageresPath = @"C:\Windows\System32\imageres.dll";
185+
if (!File.Exists(imageresPath))
186+
return;
187+
188+
using var extractor = new IconExtractor(imageresPath);
189+
190+
// Resource ID 183 exists in imageres.dll
191+
Assert.True(extractor.HasResourceId(183), "imageres.dll should have resource ID 183");
192+
193+
// Act - Extract with -183 (resource ID)
194+
var outputPath = Path.Combine(_fixture.TestDataPath, "test_imageres_neg183.ico");
195+
extractor.SaveIcon(-183, outputPath);
196+
197+
// Assert - File was created with content
198+
Assert.True(File.Exists(outputPath), "Icon file should be created");
199+
var fileInfo = new FileInfo(outputPath);
200+
Assert.True(fileInfo.Length > 1000, "Icon file should have content");
201+
}
202+
203+
/// <summary>
204+
/// Tests that GetIcon and SaveIcon produce consistent results for the same index.
205+
/// This is a regression test for the bug where preview showed a different icon than extraction.
206+
/// </summary>
207+
[Fact]
208+
public void IconExtractor_GetIconAndSaveIcon_ProduceConsistentResults()
209+
{
210+
// Arrange
211+
var imageresPath = @"C:\Windows\System32\imageres.dll";
212+
if (!File.Exists(imageresPath))
213+
return;
214+
215+
using var extractor = new IconExtractor(imageresPath);
216+
217+
// Act - Get icon via GetIcon (preview) and SaveIcon (extraction) with the same index
218+
var previewIcon = extractor.GetIcon(-183, 32);
219+
Assert.NotNull(previewIcon);
220+
221+
var outputPath = Path.Combine(_fixture.TestDataPath, "test_consistency.ico");
222+
extractor.SaveIcon(-183, outputPath);
223+
224+
// Assert - Both should succeed (we can't easily compare pixels, but both should work)
225+
Assert.True(File.Exists(outputPath), "Extracted icon file should exist");
226+
Assert.True(previewIcon.Width > 0, "Preview icon should have valid dimensions");
227+
228+
previewIcon.Dispose();
229+
}
230+
231+
/// <summary>
232+
/// End-to-end test for imageres.dll,-183 through the full extraction pipeline.
233+
/// This verifies that negative resource IDs are handled correctly by the entire system.
234+
/// </summary>
235+
[Fact]
236+
public void ExtractAndInstall_ImageresDll_Neg183_ExtractsCorrectIcon()
237+
{
238+
// Arrange - Create a test folder with imageres.dll,-183 (negative resource ID)
239+
var testFolderPath = Path.Combine(_fixture.TestDataPath, "NegativeResourceIdTest");
240+
Directory.CreateDirectory(testFolderPath);
241+
242+
// Write desktop.ini with negative resource ID (-183)
243+
var iniPath = Path.Combine(testFolderPath, "desktop.ini");
244+
File.WriteAllText(iniPath,
245+
"[.ShellClassInfo]\r\n" +
246+
"IconResource=%SystemRoot%\\System32\\imageres.dll,-183\r\n");
247+
248+
try
249+
{
250+
var service = new FolderIconService();
251+
252+
// Scan
253+
var scanResult = service.Scan(testFolderPath, recursive: false);
254+
Assert.Single(scanResult.Folders);
255+
256+
var folder = scanResult.Folders.First();
257+
Assert.Equal(-183, folder.CurrentIconResource?.Index);
258+
Assert.Equal(FolderIconStatus.ExternalAndValid, folder.Status);
259+
260+
// Act - Fix (extract icon)
261+
var extractResult = service.ExtractAndInstall(new[] { folder }, skipExisting: false);
262+
263+
// Assert
264+
Assert.Single(extractResult.Succeeded);
265+
266+
// Verify the icon was extracted
267+
var iconPath = Path.Combine(testFolderPath, "folder.ico");
268+
Assert.True(File.Exists(iconPath), "Icon should be extracted");
269+
270+
// Verify it's a valid ICO file with content
271+
var fileInfo = new FileInfo(iconPath);
272+
Assert.True(fileInfo.Length > 1000, "Icon file should have substantial content");
273+
274+
// Read the icon file header to verify it's valid
275+
using var fs = File.OpenRead(iconPath);
276+
var header = new byte[6];
277+
fs.Read(header, 0, 6);
278+
279+
Assert.Equal(0, header[0]); // Reserved
280+
Assert.Equal(0, header[1]); // Reserved
281+
Assert.Equal(1, header[2]); // Type = 1 (ICO)
282+
Assert.Equal(0, header[3]); // Type high byte
283+
284+
// Icon count should be > 0
285+
var iconCount = header[4] | (header[5] << 8);
286+
Assert.True(iconCount > 0, "Icon should have at least one image");
287+
}
288+
finally
289+
{
290+
// Cleanup
291+
if (Directory.Exists(testFolderPath))
292+
{
293+
foreach (var file in Directory.GetFiles(testFolderPath))
294+
{
295+
try
296+
{
297+
File.SetAttributes(file, FileAttributes.Normal);
298+
File.Delete(file);
299+
}
300+
catch { /* Ignore cleanup errors */ }
301+
}
302+
try { Directory.Delete(testFolderPath); }
303+
catch { /* Ignore cleanup errors */ }
304+
}
305+
}
306+
}
307+
176308
/// <summary>
177309
/// End-to-end test: Create a folder with a high ordinal index reference,
178310
/// fix it, and verify the correct icon is extracted.

0 commit comments

Comments
 (0)