001package com.streamconverter.util; 002 003import java.io.InputStream; 004import java.net.URL; 005import java.util.Objects; 006 007/** 008 * クラスパスリソースの取得を行うユーティリティクラス 009 * 010 * <p>JAR内にバンドルされたリソースを安全に取得します。 011 * 012 * <p>セキュリティ: 013 * 014 * <ul> 015 * <li>ClassLoaderはクラスパス内でパス正規化を行う("hoge/../fuga" → "fuga") 016 * <li>クラスパス境界外へのアクセスは不可能("../etc/passwd" → リソース未発見) 017 * <li>JAR内リソースは読み取り専用(改ざん不可) 018 * <li>シンボリックリンク攻撃は不可能(ファイルシステムではない) 019 * </ul> 020 * 021 * <p>用途: 022 * 023 * <ul> 024 * <li>スキーマファイル(XSD, JSON Schema等) 025 * <li>テンプレートファイル 026 * <li>設定ファイル(application.properties等) 027 * </ul> 028 */ 029public final class ClasspathResourceValidator { 030 031 private ClasspathResourceValidator() { 032 throw new UnsupportedOperationException("Utility class"); 033 } 034 035 /** 036 * クラスパスリソースを取得します 037 * 038 * <p>ClassLoaderを使用してリソースを取得します。クラスパス境界外へのアクセスは不可能です。 039 * 040 * <p>注意事項: 041 * 042 * <ul> 043 * <li>リソース名はスラッシュ(/)区切りで指定(例: "schemas/test.xsd") 044 * <li>先頭にスラッシュを含めない(ClassLoaderの仕様) 045 * <li>パス正規化あり: "hoge/../fuga" → "fuga" として解決される 046 * <li>クラスパス外アクセス不可: "../etc/passwd" → リソース未発見 047 * </ul> 048 * 049 * @param resourcePath クラスパスからの相対パス(例: "schemas/test.xsd") 050 * @return リソースのInputStream 051 * @throws NullPointerException パスがnullの場合 052 * @throws IllegalArgumentException パスが空、またはリソースが存在しない場合 053 */ 054 public static InputStream getResourceAsStream(String resourcePath) { 055 Objects.requireNonNull(resourcePath, "Resource path cannot be null"); 056 057 // 先頭スラッシュを除去(ClassLoaderの仕様に合わせる) 058 String normalizedPath = resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath; 059 060 if (normalizedPath.isEmpty()) { 061 throw new IllegalArgumentException("Resource path cannot be empty"); 062 } 063 064 // Context ClassLoaderを優先し、なければクラスのClassLoaderを使用 065 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 066 if (classLoader == null) { 067 classLoader = ClasspathResourceValidator.class.getClassLoader(); 068 } 069 070 InputStream stream = classLoader.getResourceAsStream(normalizedPath); 071 072 if (stream == null) { 073 throw new IllegalArgumentException("Resource not found: " + normalizedPath); 074 } 075 076 return stream; 077 } 078 079 /** 080 * クラスパスリソースのURLを取得します 081 * 082 * <p>ClassLoaderを使用してリソースを取得します。クラスパス境界外へのアクセスは不可能です。 083 * 084 * <p>注意事項: 085 * 086 * <ul> 087 * <li>リソース名はスラッシュ(/)区切りで指定(例: "schemas/test.xsd") 088 * <li>先頭にスラッシュを含めない(ClassLoaderの仕様) 089 * <li>パス正規化あり: "hoge/../fuga" → "fuga" として解決される 090 * <li>クラスパス外アクセス不可: "../etc/passwd" → リソース未発見 091 * </ul> 092 * 093 * @param resourcePath クラスパスからの相対パス 094 * @return リソースのURL 095 * @throws NullPointerException パスがnullの場合 096 * @throws IllegalArgumentException パスが空、またはリソースが存在しない場合 097 */ 098 public static URL getResourceUrl(String resourcePath) { 099 Objects.requireNonNull(resourcePath, "Resource path cannot be null"); 100 101 // 先頭スラッシュを除去(ClassLoaderの仕様に合わせる) 102 String normalizedPath = resourcePath.startsWith("/") ? resourcePath.substring(1) : resourcePath; 103 104 if (normalizedPath.isEmpty()) { 105 throw new IllegalArgumentException("Resource path cannot be empty"); 106 } 107 108 // Context ClassLoaderを優先し、なければクラスのClassLoaderを使用 109 ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 110 if (classLoader == null) { 111 classLoader = ClasspathResourceValidator.class.getClassLoader(); 112 } 113 114 URL url = classLoader.getResource(normalizedPath); 115 116 if (url == null) { 117 throw new IllegalArgumentException("Resource not found: " + normalizedPath); 118 } 119 120 return url; 121 } 122}