diff --git a/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs b/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs
index a140229..0b0820d 100644
--- a/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs
+++ b/src/Everything2Everything.App/Shell/ContextMenuRegistrar.cs
@@ -4,7 +4,7 @@ using Microsoft.Win32;
namespace Everything2Everything.App.Shell;
-internal static class ContextMenuRegistrar
+public static class ContextMenuRegistrar
{
private const string MainVerb = "Everything2Everything";
private const string MainLabel = "Everything2Everything으로 변환";
@@ -25,8 +25,22 @@ internal static class ContextMenuRegistrar
(".gif", "GIF (.gif)", "10"),
(".tif", "TIFF (.tif)", "11"),
(".bmp", "BMP (.bmp)", "12"),
+ (".mp4", "MP4 (.mp4)", "13"),
+ (".mp3", "MP3 (.mp3)", "14"),
+ (".xlsx", "Excel (.xlsx)", "15"),
};
+ public static IReadOnlyList<(string Ext, string Label, string SortPrefix)> GetAvailableOutputs(ConversionEngine engine, string ext)
+ {
+ var outputs = engine.Providers.OutputsForInput(ext)
+ .Select(o => o.ToLowerInvariant())
+ .ToHashSet();
+
+ return PopularOutputs
+ .Where(p => outputs.Contains(p.Ext) && !string.Equals(p.Ext, ext, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ }
+
public static void Register(ConversionEngine engine)
{
var exe = GetAppExecutablePath();
@@ -34,14 +48,7 @@ internal static class ContextMenuRegistrar
foreach (var ext in CollectInputExtensions(engine))
{
- var outputs = engine.Providers.OutputsForInput(ext)
- .Select(o => o.ToLowerInvariant())
- .ToHashSet();
-
- var availableOutputs = PopularOutputs
- .Where(p => outputs.Contains(p.Ext) && !string.Equals(p.Ext, ext, StringComparison.OrdinalIgnoreCase))
- .ToList();
-
+ var availableOutputs = GetAvailableOutputs(engine, ext);
if (availableOutputs.Count == 0) continue;
WriteCascade(ext, exe, icon, availableOutputs);
diff --git a/src/Everything2Everything.App/Views/MainWindow.xaml.cs b/src/Everything2Everything.App/Views/MainWindow.xaml.cs
index cb6a76f..57b3635 100644
--- a/src/Everything2Everything.App/Views/MainWindow.xaml.cs
+++ b/src/Everything2Everything.App/Views/MainWindow.xaml.cs
@@ -325,6 +325,11 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
ProcessQueueButton.Content = "Idle — drop files to begin";
ProcessQueueButton.IsEnabled = false;
}
+ else if (string.IsNullOrEmpty(SelectedOutputExtension))
+ {
+ ProcessQueueButton.Content = "변환 불가 (공통 형식 없음)";
+ ProcessQueueButton.IsEnabled = false;
+ }
else
{
ProcessQueueButton.Content = $"Process Queue ({count})";
@@ -1128,33 +1133,49 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
_activeQueue.Select(q => q.SourcePath).ToList());
var visible = AllFormats.Where(f => available.Contains(f.Extension)).ToList();
- if (visible.Count == 0)
- visible = AllFormats.Where(f => f.Extension == ".jpg").ToList();
+ var hasCommonFormats = visible.Count > 0;
- var keepExt = SelectedOutputExtension;
- if (keepExt is null || !visible.Any(v => string.Equals(v.Extension, keepExt, StringComparison.OrdinalIgnoreCase)))
- keepExt = visible[0].Extension;
+ string? keepExt = null;
+ if (hasCommonFormats)
+ {
+ keepExt = SelectedOutputExtension;
+ if (keepExt is null || !visible.Any(v => string.Equals(v.Extension, keepExt, StringComparison.OrdinalIgnoreCase)))
+ keepExt = visible[0].Extension;
+ }
_suppressFormatChanged = true;
try
{
OutputFormatCombo.Items.Clear();
- foreach (var f in visible)
+ if (hasCommonFormats)
+ {
+ foreach (var f in visible)
+ {
+ OutputFormatCombo.Items.Add(new ComboBoxItem
+ {
+ Content = BuildFormatItemContent(f),
+ Tag = f.Extension,
+ });
+ }
+ for (var i = 0; i < OutputFormatCombo.Items.Count; i++)
+ {
+ if (((ComboBoxItem)OutputFormatCombo.Items[i]!).Tag is string tag
+ && string.Equals(tag, keepExt, StringComparison.OrdinalIgnoreCase))
+ {
+ OutputFormatCombo.SelectedIndex = i;
+ break;
+ }
+ }
+ }
+ else if (_activeQueue.Count > 0)
{
OutputFormatCombo.Items.Add(new ComboBoxItem
{
- Content = BuildFormatItemContent(f),
- Tag = f.Extension,
+ Content = "(공통 변환 형식 없음)",
+ Tag = string.Empty,
+ IsEnabled = false,
});
- }
- for (var i = 0; i < OutputFormatCombo.Items.Count; i++)
- {
- if (((ComboBoxItem)OutputFormatCombo.Items[i]!).Tag is string tag
- && string.Equals(tag, keepExt, StringComparison.OrdinalIgnoreCase))
- {
- OutputFormatCombo.SelectedIndex = i;
- break;
- }
+ OutputFormatCombo.SelectedIndex = 0;
}
}
finally
@@ -1167,12 +1188,15 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
UpdateQualityPanelForFormat(keepExt);
UpdateOutputDestHint(keepExt);
UpdateCombineState(keepExt);
+ UpdateProcessQueueButton();
if (OutputFormatHint is not null)
{
OutputFormatHint.Text = _activeQueue.Count == 0
? "큐에 파일을 추가하면 변환 가능한 형식으로 자동 필터링됩니다"
- : $"큐의 모든 파일이 변환 가능한 형식 ({visible.Count}개)";
+ : hasCommonFormats
+ ? $"큐의 모든 파일이 변환 가능한 형식 ({visible.Count}개)"
+ : "선택된 파일들의 공통 변환 형식이 없습니다 (서로 다른 미디어)";
}
}
@@ -1215,9 +1239,15 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void UpdateOutputFormatBadge(string? extension)
{
if (OutputFormatBadge is null || OutputFormatBadgeText is null) return;
+
var info = AllFormats.FirstOrDefault(f =>
string.Equals(f.Extension, extension, StringComparison.OrdinalIgnoreCase));
- if (info is null) return;
+ if (info is null)
+ {
+ OutputFormatBadgeText.Text = "—";
+ OutputFormatBadge.Background = System.Windows.Media.Brushes.Gray;
+ return;
+ }
OutputFormatBadgeText.Text = info.BadgeText;
var resource = TryFindResource(info.ColorResource);
diff --git a/src/Everything2Everything.Core/ConversionEngine.cs b/src/Everything2Everything.Core/ConversionEngine.cs
index 43508a4..28ec190 100644
--- a/src/Everything2Everything.Core/ConversionEngine.cs
+++ b/src/Everything2Everything.Core/ConversionEngine.cs
@@ -1,3 +1,4 @@
+using Everything2Everything.Core.Filters;
using Everything2Everything.Core.Providers;
namespace Everything2Everything.Core;
@@ -106,6 +107,11 @@ public sealed class ConversionEngine
var output = ConversionPair.Normalize(outputExtension);
var inputExt = ConversionPair.Normalize(Path.GetExtension(sourcePath));
+ if (!MediaConversionNegotiator.CanConvert(inputExt, output))
+ {
+ return ConvertResult.Fail(sourcePath, $"{inputExt} → {output} 변환은 지원하지 않는 미디어 전환입니다.");
+ }
+
// 그래프 경로 탐색: 직접 엣지가 있으면 1홉, 없으면 손실 가중치 기반 멀티홉을 자동 합성.
var maxHops = options.AllowMultiHop ? Math.Max(1, options.MaxHops) : 1;
var path = _registry.Graph.FindBestPath(inputExt, output, maxHops, !options.AvoidLossy);
diff --git a/src/Everything2Everything.Core/Filters/MediaConversionNegotiator.cs b/src/Everything2Everything.Core/Filters/MediaConversionNegotiator.cs
new file mode 100644
index 0000000..b1c1324
--- /dev/null
+++ b/src/Everything2Everything.Core/Filters/MediaConversionNegotiator.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Everything2Everything.Core.Providers;
+
+namespace Everything2Everything.Core.Filters;
+
+///
+/// 미디어 카테고리(Image, Document, Media, Data) 간의 변환 호환성 및 출력 필터링을 협상(Negotiation)하는 도메인 서비스.
+/// 이미지→Word(DOCX) 등 비정상적인 미디어 전이 노출 및 실행을 방지한다.
+///
+public static class MediaConversionNegotiator
+{
+ private static readonly HashSet AllowedImageToDocumentExtensions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ".pdf", // 이미지 문서 캡슐화 (PDF)
+ ".txt", // OCR 텍스트 추출 (TXT)
+ };
+
+ private static readonly HashSet VideoExtensions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ".mp4", ".mkv", ".webm", ".mov", ".avi", ".m4v"
+ };
+
+ private static readonly HashSet AudioExtensions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ".mp3", ".wav", ".flac", ".aac", ".m4a", ".ogg", ".opus"
+ };
+
+ private static readonly HashSet AllowedVideoToImageExtensions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ".gif", // 영상 → GIF 애니메이션
+ ".png", // 영상 → 대표 프레임 스틸컷
+ ".jpg",
+ ".jpeg",
+ ".webp",
+ ".bmp"
+ };
+
+ public static bool IsVideo(string ext) => VideoExtensions.Contains(ConversionPair.Normalize(ext));
+ public static bool IsAudio(string ext) => AudioExtensions.Contains(ConversionPair.Normalize(ext));
+
+ ///
+ /// 입력 확장자와 출력 확장자 간의 미디어 변환 가능 여부를 판정한다.
+ ///
+ public static bool CanConvert(string inputExt, string outputExt)
+ {
+ var input = ConversionPair.Normalize(inputExt);
+ var output = ConversionPair.Normalize(outputExt);
+
+ if (string.Equals(input, output, StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ var inCat = QueueFilterMatcher.GetCategory(input);
+ var outCat = QueueFilterMatcher.GetCategory(output);
+
+ // 1. 미디어(영상/음성) 입력 세분화
+ if (inCat == FilterCategory.Media)
+ {
+ var isVideoIn = IsVideo(input);
+ var isAudioIn = IsAudio(input);
+
+ if (outCat == FilterCategory.Media)
+ {
+ var isVideoOut = IsVideo(output);
+ var isAudioOut = IsAudio(output);
+
+ if (isVideoIn)
+ {
+ // 비디오는 비디오 간 변환 또는 오디오 추출 허용
+ return isVideoOut || isAudioOut;
+ }
+ if (isAudioIn)
+ {
+ // 오디오는 오디오 간 변환만 허용 (오디오→비디오 변환 불가)
+ return isAudioOut;
+ }
+ return true;
+ }
+
+ if (outCat == FilterCategory.Image)
+ {
+ // 비디오만 GIF 애니메이션 및 대표 프레임 추출 허용. 오디오는 이미지 불가!
+ return isVideoIn && AllowedVideoToImageExtensions.Contains(output);
+ }
+
+ // 미디어 → 문서/데이터 변환 불가
+ return false;
+ }
+
+ return (inCat, outCat) switch
+ {
+ // 2. 이미지 입력
+ (FilterCategory.Image, FilterCategory.Image) => true,
+ (FilterCategory.Image, FilterCategory.Document) => AllowedImageToDocumentExtensions.Contains(output),
+ (FilterCategory.Image, _) => false,
+
+ // 3. 데이터(CSV/JSON/XLSX) 입력: 표 데이터 상호 변환만 허용
+ (FilterCategory.Data, FilterCategory.Data) => true,
+ (FilterCategory.Data, _) => false,
+
+ // 4. 문서(PDF/DOCX/HWP/HTML/MD/TXT) 입력
+ (FilterCategory.Document, FilterCategory.Document) => true,
+ (FilterCategory.Document, FilterCategory.Image) => true, // 문서 페이지 렌더링
+ (FilterCategory.Document, _) => false,
+
+ // 기본: 동일 카테고리 허용
+ _ => inCat == outCat,
+ };
+ }
+
+ ///
+ /// 후보 출력 확장자 목록 중 입력 미디어와 호환되는 유효한 출력 확장자만 필터링하여 반환한다.
+ ///
+ public static IEnumerable FilterAvailableOutputs(string inputExt, IEnumerable candidateOutputs)
+ {
+ return candidateOutputs.Where(outExt => CanConvert(inputExt, outExt));
+ }
+}
diff --git a/src/Everything2Everything.Core/Providers/ConversionGraph.cs b/src/Everything2Everything.Core/Providers/ConversionGraph.cs
index b161e44..3102cc3 100644
--- a/src/Everything2Everything.Core/Providers/ConversionGraph.cs
+++ b/src/Everything2Everything.Core/Providers/ConversionGraph.cs
@@ -1,3 +1,5 @@
+using Everything2Everything.Core.Filters;
+
namespace Everything2Everything.Core.Providers;
///
@@ -55,7 +57,7 @@ public sealed class ConversionGraph
foreach (var e in EdgesFrom(node))
{
if (!allowLossy && e.Loss == LossClass.Rasterize) continue;
- if (!string.Equals(e.To, start, StringComparison.OrdinalIgnoreCase))
+ if (!string.Equals(e.To, start, StringComparison.OrdinalIgnoreCase) && MediaConversionNegotiator.CanConvert(start, e.To))
result.Add(e.To);
var next = (e.To, hop + 1);
if (visited.Add(next))
@@ -75,6 +77,9 @@ public sealed class ConversionGraph
var goal = ConversionPair.Normalize(outputExt);
if (maxHops < 1) maxHops = 1;
+ if (!MediaConversionNegotiator.CanConvert(start, goal))
+ return null;
+
if (string.Equals(start, goal, StringComparison.OrdinalIgnoreCase))
{
Edge? best = null;
diff --git a/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs b/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs
new file mode 100644
index 0000000..136a53a
--- /dev/null
+++ b/src/Everything2Everything.Tests/ContextMenuRegistrarTests.cs
@@ -0,0 +1,59 @@
+using System.Linq;
+using Everything2Everything.App.Shell;
+using Everything2Everything.Core;
+using Xunit;
+
+namespace Everything2Everything.Tests;
+
+///
+/// 윈도우 탐색기 우클릭 컨텍스트 메뉴 네고시에이션 테스트 (TDD).
+/// 동영상, 음성 추출, 엑셀 표 데이터 등 킬러 포맷의 정상 노출 및 비정상 변환(png→docx 등) 차단을 검증한다.
+///
+public class ContextMenuRegistrarTests
+{
+ private static ConversionEngine CreateEngine() => Everything2EverythingBootstrap.CreateDefault();
+
+ [Fact]
+ public void GetAvailableOutputs_Png_DoesNotContain_Docx()
+ {
+ var engine = CreateEngine();
+ var outputs = ContextMenuRegistrar.GetAvailableOutputs(engine, ".png");
+
+ Assert.DoesNotContain(outputs, o => o.Ext == ".docx");
+ Assert.Contains(outputs, o => o.Ext == ".jpg");
+ Assert.Contains(outputs, o => o.Ext == ".webp");
+ Assert.Contains(outputs, o => o.Ext == ".pdf");
+ }
+
+ [Fact]
+ public void GetAvailableOutputs_Video_Contains_Mp4_And_Mp3()
+ {
+ var engine = CreateEngine();
+ var outputs = ContextMenuRegistrar.GetAvailableOutputs(engine, ".mkv");
+
+ // 동영상 우클릭 메뉴에 MP4 변환 및 MP3 음원 추출이 노출되어야 한다.
+ Assert.Contains(outputs, o => o.Ext == ".mp4");
+ Assert.Contains(outputs, o => o.Ext == ".mp3");
+ }
+
+ [Fact]
+ public void GetAvailableOutputs_Csv_Contains_Xlsx()
+ {
+ var engine = CreateEngine();
+ var outputs = ContextMenuRegistrar.GetAvailableOutputs(engine, ".csv");
+
+ // CSV 표 데이터 우클릭 메뉴에 Excel(.xlsx) 변환이 노출되어야 한다.
+ Assert.Contains(outputs, o => o.Ext == ".xlsx");
+ }
+
+ [Fact]
+ public void GetAvailableOutputs_Audio_DoesNotContain_Gif_Or_Video()
+ {
+ var engine = CreateEngine();
+ var outputs = ContextMenuRegistrar.GetAvailableOutputs(engine, ".mp3");
+
+ // 오디오 파일 우클릭 메뉴에 GIF나 비디오가 노출되지 않아야 한다.
+ Assert.DoesNotContain(outputs, o => o.Ext == ".gif");
+ Assert.DoesNotContain(outputs, o => o.Ext == ".mp4");
+ }
+}
diff --git a/src/Everything2Everything.Tests/MediaNegotiationTests.cs b/src/Everything2Everything.Tests/MediaNegotiationTests.cs
new file mode 100644
index 0000000..9c26cb3
--- /dev/null
+++ b/src/Everything2Everything.Tests/MediaNegotiationTests.cs
@@ -0,0 +1,154 @@
+using System.IO;
+using System.Threading.Tasks;
+using Everything2Everything.Core;
+using Everything2Everything.Core.Filters;
+using Everything2Everything.Core.Providers;
+using Xunit;
+
+namespace Everything2Everything.Tests;
+
+///
+/// 미디어 전환 가능 Filter 네고시에이팅 기능 테스트 스위트 (TDD).
+/// PNG 등 이미지 입력에 대해 DOCX/Word 메뉴 및 변환이 노출/허용되지 않도록 보장한다.
+///
+public class MediaNegotiationTests
+{
+ private static ConversionEngine CreateEngine() => Everything2EverythingBootstrap.CreateDefault();
+ private static ProviderRegistry CreateRegistry() => CreateEngine().Providers;
+
+ [Theory]
+ [InlineData(".png")]
+ [InlineData(".jpg")]
+ [InlineData(".jpeg")]
+ [InlineData(".webp")]
+ [InlineData(".gif")]
+ [InlineData(".bmp")]
+ public void OutputsForInput_ImageFormats_DoNotContain_Docx_Or_OtherRichDocuments(string imageExt)
+ {
+ var reg = CreateRegistry();
+ var outputs = reg.OutputsForInput(imageExt);
+
+ // 이미지 입력은 Word(.docx, .doc), HWP, HTML, Markdown 등의 서식 문서로 변환될 수 없다.
+ Assert.DoesNotContain(".docx", outputs);
+ Assert.DoesNotContain(".doc", outputs);
+ Assert.DoesNotContain(".hwp", outputs);
+ Assert.DoesNotContain(".hwpx", outputs);
+ Assert.DoesNotContain(".html", outputs);
+ Assert.DoesNotContain(".md", outputs);
+ }
+
+ [Fact]
+ public void AvailableOutputsForFiles_PngQueue_DoesNotContain_Docx()
+ {
+ var reg = CreateRegistry();
+ var available = reg.AvailableOutputsForFiles(new[] { "C:\\test\\sample.png" });
+
+ // PNG 큐에 대해 UI 및 컨텍스트 메뉴용 공통 출력 목록에 .docx가 포함되지 않아야 한다.
+ Assert.DoesNotContain(".docx", available);
+ }
+
+ [Fact]
+ public void OutputsForInput_Png_Maintains_ValidOutputs()
+ {
+ var reg = CreateRegistry();
+ var outputs = reg.OutputsForInput(".png");
+
+ // 이미지 간 변환은 정상 유지
+ Assert.Contains(".jpg", outputs);
+ Assert.Contains(".webp", outputs);
+
+ // 이미지 캡슐화 PDF는 정상 유지
+ Assert.Contains(".pdf", outputs);
+
+ // OCR 텍스트 추출은 정상 유지
+ Assert.Contains(".txt", outputs);
+ }
+
+ [Fact]
+ public void OutputsForInput_Docx_Maintains_DocumentAndImageOutputs()
+ {
+ var reg = CreateRegistry();
+ var outputs = reg.OutputsForInput(".docx");
+
+ // DOCX는 PDF 변환 및 렌더링 이미지 출력이 가능해야 함
+ Assert.Contains(".pdf", outputs);
+ Assert.Contains(".txt", outputs);
+ }
+
+ [Fact]
+ public async Task ConvertOneAsync_PngToDocx_FailsWithUnsupportedMediaConversion()
+ {
+ var engine = CreateEngine();
+ var tempPng = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid():N}.png");
+ await File.WriteAllBytesAsync(tempPng, new byte[] { 0x89, 0x50, 0x4E, 0x47 }); // minimal png header
+
+ try
+ {
+ var result = await engine.ConvertOneAsync(tempPng, ".docx", new ConvertOptions());
+ Assert.Equal(ConvertStatus.Failed, result.Status);
+ Assert.Contains("미디어 전환", result.Message);
+ }
+ finally
+ {
+ if (File.Exists(tempPng)) File.Delete(tempPng);
+ }
+ }
+
+ [Fact]
+ public void ConversionGraph_FindBestPath_PngToDocx_ReturnsNull()
+ {
+ var graph = CreateEngine().Providers.Graph;
+ var path = graph.FindBestPath(".png", ".docx", maxHops: 3);
+ Assert.Null(path);
+ }
+
+ [Theory]
+ [InlineData(".png", ".docx", false)]
+ [InlineData(".jpg", ".docx", false)]
+ [InlineData(".png", ".hwp", false)]
+ [InlineData(".png", ".jpg", true)]
+ [InlineData(".png", ".pdf", true)]
+ [InlineData(".png", ".txt", true)]
+ [InlineData(".docx", ".pdf", true)]
+ [InlineData(".docx", ".png", true)]
+ [InlineData(".mp4", ".docx", false)]
+ [InlineData(".csv", ".docx", false)]
+ public void MediaConversionNegotiator_ValidatesCompatibility(string input, string output, bool expectedAllowed)
+ {
+ var allowed = MediaConversionNegotiator.CanConvert(input, output);
+ Assert.Equal(expectedAllowed, allowed);
+ }
+
+ [Theory]
+ [InlineData(".mp3", ".gif", false)]
+ [InlineData(".mp3", ".png", false)]
+ [InlineData(".mp3", ".jpg", false)]
+ [InlineData(".mp3", ".mp4", false)]
+ [InlineData(".wav", ".mkv", false)]
+ [InlineData(".mp3", ".wav", true)]
+ [InlineData(".flac", ".mp3", true)]
+ [InlineData(".wav", ".aac", true)]
+ [InlineData(".mp4", ".mkv", true)]
+ [InlineData(".mp4", ".mp3", true)]
+ [InlineData(".mp4", ".gif", true)]
+ [InlineData(".mp4", ".png", true)]
+ [InlineData(".mp4", ".jpg", true)]
+ [InlineData(".mkv", ".webp", true)]
+ [InlineData(".mp4", ".pdf", false)]
+ [InlineData(".mp4", ".docx", false)]
+ [InlineData(".mp4", ".csv", false)]
+ public void MediaConversionNegotiator_DistinguishesVideoAndAudio(string input, string output, bool expectedAllowed)
+ {
+ var allowed = MediaConversionNegotiator.CanConvert(input, output);
+ Assert.Equal(expectedAllowed, allowed);
+ }
+
+ [Fact]
+ public void AvailableOutputsForFiles_IncompatibleMixedQueue_ReturnsEmpty()
+ {
+ var reg = CreateRegistry();
+ // mp3와 csv는 공통 변환 출력이 전혀 없다
+ var available = reg.AvailableOutputsForFiles(new[] { "C:\\music.mp3", "C:\\data.csv" });
+ Assert.Empty(available);
+ }
+}