Merge pull request #326 from absences/decrypt

Assetbundle加载增加解密方法
pull/342/head
何冠峰 2024-08-03 17:03:45 +08:00 committed by GitHub
commit 6b56275f87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 266 additions and 42 deletions

View File

@ -0,0 +1,23 @@

namespace YooAsset.Editor
{
public class TaskEncryption_RFBP : TaskEncryption, IBuildTask
{
/// <summary>
/// 加密文件
/// </summary>
/// <param name="context"></param>
public void Run(BuildContext context)
{
var buildParameters = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
var buildMode = buildParameters.Parameters.BuildMode;
if (buildMode == EBuildMode.ForceRebuild || buildMode == EBuildMode.IncrementalBuild)
{
EncryptingBundleFiles(buildParameters, buildMapContext);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3e156139dcc25f4c9440ec3d6cb96d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -32,6 +32,7 @@ namespace YooAsset.Editor
new TaskPrepare_RFBP(), new TaskPrepare_RFBP(),
new TaskGetBuildMap_RFBP(), new TaskGetBuildMap_RFBP(),
new TaskBuilding_RFBP(), new TaskBuilding_RFBP(),
new TaskEncryption_RFBP(),
new TaskUpdateBundleInfo_RFBP(), new TaskUpdateBundleInfo_RFBP(),
new TaskCreateManifest_RFBP(), new TaskCreateManifest_RFBP(),
new TaskCreateReport_RFBP(), new TaskCreateReport_RFBP(),

View File

@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using System.Text;
namespace YooAsset namespace YooAsset
{ {
@ -98,6 +99,11 @@ namespace YooAsset
/// 自定义参数:原生文件构建管线 /// 自定义参数:原生文件构建管线
/// </summary> /// </summary>
public bool RawFileBuildPipeline { private set; get; } = false; public bool RawFileBuildPipeline { private set; get; } = false;
/// <summary>
/// 自定义参数:解密方法类
/// </summary>
public IDecryptionServices DecryptionServices { private set; get; }
#endregion #endregion
@ -181,18 +187,22 @@ namespace YooAsset
public virtual void SetParameter(string name, object value) public virtual void SetParameter(string name, object value)
{ {
if (name == "FILE_VERIFY_LEVEL") if (name == FileSystemParameters.FILE_VERIFY_LEVEL)
{ {
FileVerifyLevel = (EFileVerifyLevel)value; FileVerifyLevel = (EFileVerifyLevel)value;
} }
else if (name == "APPEND_FILE_EXTENSION") else if (name == FileSystemParameters.APPEND_FILE_EXTENSION)
{ {
AppendFileExtension = (bool)value; AppendFileExtension = (bool)value;
} }
else if (name == "RAW_FILE_BUILD_PIPELINE") else if (name == FileSystemParameters.RAW_FILE_BUILD_PIPELINE)
{ {
RawFileBuildPipeline = (bool)value; RawFileBuildPipeline = (bool)value;
} }
else if (name == FileSystemParameters.DECRYPTION_SERVICES)
{
DecryptionServices = (IDecryptionServices)value;
}
else else
{ {
YooLogger.Warning($"Invalid parameter : {name}"); YooLogger.Warning($"Invalid parameter : {name}");
@ -210,10 +220,11 @@ namespace YooAsset
// 创建解压文件系统 // 创建解压文件系统
var remoteServices = new UnpackRemoteServices(_packageRoot); var remoteServices = new UnpackRemoteServices(_packageRoot);
_unpackFileSystem = new DefaultUnpackFileSystem(); _unpackFileSystem = new DefaultUnpackFileSystem();
_unpackFileSystem.SetParameter("REMOTE_SERVICES", remoteServices); _unpackFileSystem.SetParameter(FileSystemParameters.REMOTE_SERVICES, remoteServices);
_unpackFileSystem.SetParameter("FILE_VERIFY_LEVEL", FileVerifyLevel); _unpackFileSystem.SetParameter(FileSystemParameters.FILE_VERIFY_LEVEL, FileVerifyLevel);
_unpackFileSystem.SetParameter("APPEND_FILE_EXTENSION", AppendFileExtension); _unpackFileSystem.SetParameter(FileSystemParameters.APPEND_FILE_EXTENSION, AppendFileExtension);
_unpackFileSystem.SetParameter("RAW_FILE_BUILD_PIPELINE", RawFileBuildPipeline); _unpackFileSystem.SetParameter(FileSystemParameters.RAW_FILE_BUILD_PIPELINE, RawFileBuildPipeline);
_unpackFileSystem.SetParameter(FileSystemParameters.DECRYPTION_SERVICES, DecryptionServices);
_unpackFileSystem.OnCreate(packageName, null); _unpackFileSystem.OnCreate(packageName, null);
} }
public virtual void OnUpdate() public virtual void OnUpdate()
@ -257,7 +268,17 @@ namespace YooAsset
return null; return null;
string filePath = GetBuildinFileLoadPath(bundle); string filePath = GetBuildinFileLoadPath(bundle);
return FileUtility.ReadAllBytes(filePath); var data = FileUtility.ReadAllBytes(filePath);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Null!");
return null;
}
return DecryptionServices.ReadFileData(data);
}
return data;
} }
public virtual string ReadFileText(PackageBundle bundle) public virtual string ReadFileText(PackageBundle bundle)
{ {
@ -268,7 +289,18 @@ namespace YooAsset
return null; return null;
string filePath = GetBuildinFileLoadPath(bundle); string filePath = GetBuildinFileLoadPath(bundle);
return FileUtility.ReadAllText(filePath); var data = FileUtility.ReadAllBytes(filePath);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Null!");
return null;
}
data = DecryptionServices.ReadFileData(data);
}
return Encoding.UTF8.GetString(data);
} }
#region 内部方法 #region 内部方法
@ -310,7 +342,60 @@ namespace YooAsset
string rootPath = PathUtility.Combine(Application.dataPath, "StreamingAssets", YooAssetSettingsData.Setting.DefaultYooFolderName); string rootPath = PathUtility.Combine(Application.dataPath, "StreamingAssets", YooAssetSettingsData.Setting.DefaultYooFolderName);
return PathUtility.Combine(rootPath, PackageName); return PathUtility.Combine(rootPath, PackageName);
} }
public AssetBundle LoadAssetBundle(PackageBundle bundle)
{
string filePath = GetBuildinFileLoadPath(bundle);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Null!");
return null;
}
else
{
return DecryptionServices.LoadAssetBundle(new DecryptFileInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
}, out _);
}
}
else
{
return AssetBundle.LoadFromFile(filePath);
}
}
public AssetBundleCreateRequest LoadAssetBundleAsync(PackageBundle bundle)
{
string filePath = GetBuildinFileLoadPath(bundle);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Empty!");
return null;
}
else
{
return DecryptionServices.LoadAssetBundleAsync(new DecryptFileInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
}, out _);
}
}
else
{
return AssetBundle.LoadFromFileAsync(filePath);
}
}
/// <summary> /// <summary>
/// 记录文件信息 /// 记录文件信息
/// </summary> /// </summary>

View File

@ -41,14 +41,13 @@ namespace YooAsset
if (_steps == ESteps.LoadBuidlinAssetBundle) if (_steps == ESteps.LoadBuidlinAssetBundle)
{ {
string filePath = _fileSystem.GetBuildinFileLoadPath(_bundle);
if (_isWaitForAsyncComplete) if (_isWaitForAsyncComplete)
{ {
Result = AssetBundle.LoadFromFile(filePath); Result = _fileSystem.LoadAssetBundle(_bundle);
} }
else else
{ {
_createRequest = AssetBundle.LoadFromFileAsync(filePath); _createRequest = _fileSystem.LoadAssetBundleAsync(_bundle);
} }
_steps = ESteps.CheckLoadBuildinResult; _steps = ESteps.CheckLoadBuildinResult;
} }

View File

@ -3,6 +3,7 @@ using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
using System.Text;
namespace YooAsset namespace YooAsset
{ {
@ -97,6 +98,11 @@ namespace YooAsset
/// 自定义参数:断点续传下载器关注的错误码 /// 自定义参数:断点续传下载器关注的错误码
/// </summary> /// </summary>
public List<long> ResumeDownloadResponseCodes { private set; get; } = null; public List<long> ResumeDownloadResponseCodes { private set; get; } = null;
/// <summary>
/// 自定义参数:解密方法类
/// </summary>
public IDecryptionServices DecryptionServices { private set; get; }
#endregion #endregion
@ -208,30 +214,34 @@ namespace YooAsset
public virtual void SetParameter(string name, object value) public virtual void SetParameter(string name, object value)
{ {
if (name == "REMOTE_SERVICES") if (name == FileSystemParameters.REMOTE_SERVICES)
{ {
RemoteServices = (IRemoteServices)value; RemoteServices = (IRemoteServices)value;
} }
else if (name == "FILE_VERIFY_LEVEL") else if (name == FileSystemParameters.FILE_VERIFY_LEVEL)
{ {
FileVerifyLevel = (EFileVerifyLevel)value; FileVerifyLevel = (EFileVerifyLevel)value;
} }
else if (name == "APPEND_FILE_EXTENSION") else if (name == FileSystemParameters.APPEND_FILE_EXTENSION)
{ {
AppendFileExtension = (bool)value; AppendFileExtension = (bool)value;
} }
else if (name == "RAW_FILE_BUILD_PIPELINE") else if (name == FileSystemParameters.RAW_FILE_BUILD_PIPELINE)
{ {
RawFileBuildPipeline = (bool)value; RawFileBuildPipeline = (bool)value;
} }
else if (name == "RESUME_DOWNLOAD_MINMUM_SIZE") else if (name == FileSystemParameters.RESUME_DOWNLOAD_MINMUM_SIZE)
{ {
ResumeDownloadMinimumSize = (long)value; ResumeDownloadMinimumSize = (long)value;
} }
else if (name == "RESUME_DOWNLOAD_RESPONSE_CODES") else if (name == FileSystemParameters.RESUME_DOWNLOAD_RESPONSE_CODES)
{ {
ResumeDownloadResponseCodes = (List<long>)value; ResumeDownloadResponseCodes = (List<long>)value;
} }
else if (name == FileSystemParameters.DECRYPTION_SERVICES)
{
DecryptionServices = (IDecryptionServices)value;
}
else else
{ {
YooLogger.Warning($"Invalid parameter : {name}"); YooLogger.Warning($"Invalid parameter : {name}");
@ -307,9 +317,19 @@ namespace YooAsset
{ {
if (Exists(bundle) == false) if (Exists(bundle) == false)
return null; return null;
string filePath = GetCacheFileLoadPath(bundle); string filePath = GetCacheFileLoadPath(bundle);
return FileUtility.ReadAllBytes(filePath); var data = FileUtility.ReadAllBytes(filePath);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Null!");
return null;
}
return DecryptionServices.ReadFileData(data);
}
return data;
} }
public virtual string ReadFileText(PackageBundle bundle) public virtual string ReadFileText(PackageBundle bundle)
{ {
@ -317,7 +337,18 @@ namespace YooAsset
return null; return null;
string filePath = GetCacheFileLoadPath(bundle); string filePath = GetCacheFileLoadPath(bundle);
return FileUtility.ReadAllText(filePath); var data = FileUtility.ReadAllBytes(filePath);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Null!");
return null;
}
data = DecryptionServices.ReadFileData(data);
}
return Encoding.UTF8.GetString(data);
} }
#region 内部方法 #region 内部方法
@ -528,6 +559,60 @@ namespace YooAsset
{ {
return _wrappers.Keys.ToList(); return _wrappers.Keys.ToList();
} }
internal AssetBundle LoadAssetBundle(PackageBundle bundle)
{
string filePath = GetCacheFileLoadPath(bundle);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Null!");
return null;
}
else
{
return DecryptionServices.LoadAssetBundle(new DecryptFileInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
}, out _);
}
}
else
{
return AssetBundle.LoadFromFile(filePath);
}
}
internal AssetBundleCreateRequest LoadAssetBundleAsync(PackageBundle bundle)
{
string filePath = GetCacheFileLoadPath(bundle);
if (bundle.Encrypted)
{
if (DecryptionServices == null)
{
YooLogger.Error($"DecryptionServices is Empty!");
return null;
}
else
{
return DecryptionServices.LoadAssetBundleAsync(new DecryptFileInfo()
{
BundleName = bundle.BundleName,
FileLoadCRC = bundle.UnityCRC,
FileLoadPath = filePath,
}, out _);
}
}
else
{
return AssetBundle.LoadFromFileAsync(filePath);
}
}
#endregion #endregion
} }
} }

View File

@ -78,15 +78,15 @@ namespace YooAsset
if (_steps == ESteps.LoadAssetBundle) if (_steps == ESteps.LoadAssetBundle)
{ {
string filePath = _fileSystem.GetCacheFileLoadPath(_bundle);
if (_isWaitForAsyncComplete) if (_isWaitForAsyncComplete)
{ {
Result = AssetBundle.LoadFromFile(filePath); Result = _fileSystem.LoadAssetBundle(_bundle);
} }
else else
{ {
_createRequest = AssetBundle.LoadFromFileAsync(filePath); _createRequest = _fileSystem.LoadAssetBundleAsync(_bundle);
} }
_steps = ESteps.CheckResult; _steps = ESteps.CheckResult;
} }

View File

@ -114,7 +114,7 @@ namespace YooAsset
public virtual void SetParameter(string name, object value) public virtual void SetParameter(string name, object value)
{ {
if (name == "DISABLE_UNITY_WEB_CACHE") if (name == FileSystemParameters.DISABLE_UNITY_WEB_CACHE)
{ {
DisableUnityWebCache = (bool)value; DisableUnityWebCache = (bool)value;
} }

View File

@ -55,6 +55,15 @@ namespace YooAsset
/// </summary> /// </summary>
public class FileSystemParameters public class FileSystemParameters
{ {
public const string FILE_VERIFY_LEVEL = "FILE_VERIFY_LEVEL";
public const string DECRYPTION_SERVICES = "DECRYPTION_SERVICES";
public const string APPEND_FILE_EXTENSION = "APPEND_FILE_EXTENSION";
public const string RAW_FILE_BUILD_PIPELINE = "RAW_FILE_BUILD_PIPELINE";
public const string REMOTE_SERVICES = "REMOTE_SERVICES";
public const string DISABLE_UNITY_WEB_CACHE = "DISABLE_UNITY_WEB_CACHE";
public const string RESUME_DOWNLOAD_MINMUM_SIZE = "RESUME_DOWNLOAD_MINMUM_SIZE";
public const string RESUME_DOWNLOAD_RESPONSE_CODES = "RESUME_DOWNLOAD_RESPONSE_CODES";
internal Dictionary<string, object> CreateParameters = new Dictionary<string, object>(); internal Dictionary<string, object> CreateParameters = new Dictionary<string, object>();
/// <summary> /// <summary>
@ -101,11 +110,12 @@ namespace YooAsset
/// </summary> /// </summary>
/// <param name="verifyLevel">缓存文件的校验等级</param> /// <param name="verifyLevel">缓存文件的校验等级</param>
/// <param name="rootDirectory">内置文件的根路径</param> /// <param name="rootDirectory">内置文件的根路径</param>
public static FileSystemParameters CreateDefaultBuildinFileSystemParameters(EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null) public static FileSystemParameters CreateDefaultBuildinFileSystemParameters(IDecryptionServices decryptionServices = null, EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null)
{ {
string fileSystemClass = typeof(DefaultBuildinFileSystem).FullName; string fileSystemClass = typeof(DefaultBuildinFileSystem).FullName;
var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory); var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory);
fileSystemParams.AddParameter("FILE_VERIFY_LEVEL", verifyLevel); fileSystemParams.AddParameter(FILE_VERIFY_LEVEL, verifyLevel);
fileSystemParams.AddParameter(DECRYPTION_SERVICES, decryptionServices);
return fileSystemParams; return fileSystemParams;
} }
@ -114,13 +124,14 @@ namespace YooAsset
/// </summary> /// </summary>
/// <param name="verifyLevel">缓存文件的校验等级</param> /// <param name="verifyLevel">缓存文件的校验等级</param>
/// <param name="rootDirectory">内置文件的根路径</param> /// <param name="rootDirectory">内置文件的根路径</param>
public static FileSystemParameters CreateDefaultBuildinRawFileSystemParameters(EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null) public static FileSystemParameters CreateDefaultBuildinRawFileSystemParameters(IDecryptionServices decryptionServices = null, EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null)
{ {
string fileSystemClass = typeof(DefaultBuildinFileSystem).FullName; string fileSystemClass = typeof(DefaultBuildinFileSystem).FullName;
var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory); var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory);
fileSystemParams.AddParameter("FILE_VERIFY_LEVEL", verifyLevel); fileSystemParams.AddParameter(FILE_VERIFY_LEVEL, verifyLevel);
fileSystemParams.AddParameter("APPEND_FILE_EXTENSION", true); fileSystemParams.AddParameter(APPEND_FILE_EXTENSION, true);
fileSystemParams.AddParameter("RAW_FILE_BUILD_PIPELINE", true); fileSystemParams.AddParameter(RAW_FILE_BUILD_PIPELINE, true);
fileSystemParams.AddParameter(DECRYPTION_SERVICES, decryptionServices);
return fileSystemParams; return fileSystemParams;
} }
@ -130,12 +141,13 @@ namespace YooAsset
/// <param name="remoteServices">远端资源地址查询服务类</param> /// <param name="remoteServices">远端资源地址查询服务类</param>
/// <param name="verifyLevel">缓存文件的校验等级</param> /// <param name="verifyLevel">缓存文件的校验等级</param>
/// <param name="rootDirectory">文件系统的根目录</param> /// <param name="rootDirectory">文件系统的根目录</param>
public static FileSystemParameters CreateDefaultCacheFileSystemParameters(IRemoteServices remoteServices, EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null) public static FileSystemParameters CreateDefaultCacheFileSystemParameters(IRemoteServices remoteServices, IDecryptionServices decryptionServices = null, EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null)
{ {
string fileSystemClass = typeof(DefaultCacheFileSystem).FullName; string fileSystemClass = typeof(DefaultCacheFileSystem).FullName;
var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory); var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory);
fileSystemParams.AddParameter("REMOTE_SERVICES", remoteServices); fileSystemParams.AddParameter(REMOTE_SERVICES, remoteServices);
fileSystemParams.AddParameter("FILE_VERIFY_LEVEL", verifyLevel); fileSystemParams.AddParameter(FILE_VERIFY_LEVEL, verifyLevel);
fileSystemParams.AddParameter(DECRYPTION_SERVICES, decryptionServices);
return fileSystemParams; return fileSystemParams;
} }
@ -145,14 +157,15 @@ namespace YooAsset
/// <param name="remoteServices">远端资源地址查询服务类</param> /// <param name="remoteServices">远端资源地址查询服务类</param>
/// <param name="verifyLevel">缓存文件的校验等级</param> /// <param name="verifyLevel">缓存文件的校验等级</param>
/// <param name="rootDirectory">文件系统的根目录</param> /// <param name="rootDirectory">文件系统的根目录</param>
public static FileSystemParameters CreateDefaultCacheRawFileSystemParameters(IRemoteServices remoteServices, EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null) public static FileSystemParameters CreateDefaultCacheRawFileSystemParameters(IRemoteServices remoteServices, IDecryptionServices decryptionServices = null, EFileVerifyLevel verifyLevel = EFileVerifyLevel.Middle, string rootDirectory = null)
{ {
string fileSystemClass = typeof(DefaultCacheFileSystem).FullName; string fileSystemClass = typeof(DefaultCacheFileSystem).FullName;
var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory); var fileSystemParams = new FileSystemParameters(fileSystemClass, rootDirectory);
fileSystemParams.AddParameter("REMOTE_SERVICES", remoteServices); fileSystemParams.AddParameter(REMOTE_SERVICES, remoteServices);
fileSystemParams.AddParameter("FILE_VERIFY_LEVEL", verifyLevel); fileSystemParams.AddParameter(FILE_VERIFY_LEVEL, verifyLevel);
fileSystemParams.AddParameter("APPEND_FILE_EXTENSION", true); fileSystemParams.AddParameter(APPEND_FILE_EXTENSION, true);
fileSystemParams.AddParameter("RAW_FILE_BUILD_PIPELINE", true); fileSystemParams.AddParameter(RAW_FILE_BUILD_PIPELINE, true);
fileSystemParams.AddParameter(DECRYPTION_SERVICES, decryptionServices);
return fileSystemParams; return fileSystemParams;
} }
@ -164,7 +177,7 @@ namespace YooAsset
{ {
string fileSystemClass = typeof(DefaultWebFileSystem).FullName; string fileSystemClass = typeof(DefaultWebFileSystem).FullName;
var fileSystemParams = new FileSystemParameters(fileSystemClass, null); var fileSystemParams = new FileSystemParameters(fileSystemClass, null);
fileSystemParams.AddParameter("DISABLE_UNITY_WEB_CACHE", disableUnityWebCache); fileSystemParams.AddParameter(DISABLE_UNITY_WEB_CACHE, disableUnityWebCache);
return fileSystemParams; return fileSystemParams;
} }
} }

View File

@ -34,5 +34,12 @@ namespace YooAsset
/// 注意:加载流对象在资源包对象释放的时候会自动释放 /// 注意:加载流对象在资源包对象释放的时候会自动释放
/// </summary> /// </summary>
AssetBundleCreateRequest LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream); AssetBundleCreateRequest LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream);
/// <summary>
/// 解密字节数据
/// </summary>
/// <param name="encryptData"></param>
/// <returns></returns>
byte[] ReadFileData(byte[] encryptData);
} }
} }