diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..482baaca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,901 @@ +[*.cs] + +# CA1001: Types that own disposable fields should be disposable +dotnet_diagnostic.CA1001.severity = warning + +# CA1000: Do not declare static members on generic types +dotnet_diagnostic.CA1000.severity = warning + +# CA1002: Do not expose generic lists +dotnet_diagnostic.CA1002.severity = warning + +# CA1003: Use generic event handler instances +dotnet_diagnostic.CA1003.severity = warning + +# CA1005: Avoid excessive parameters on generic types +dotnet_diagnostic.CA1005.severity = warning + +# CA1008: Enums should have zero value +dotnet_diagnostic.CA1008.severity = warning + +# CA1010: Generic interface should also be implemented +dotnet_diagnostic.CA1010.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = warning + +# CA1014: Mark assemblies with CLSCompliant +dotnet_diagnostic.CA1014.severity = warning + +# CA1016: Mark assemblies with assembly version +dotnet_diagnostic.CA1016.severity = warning + +# CA1017: Mark assemblies with ComVisible +dotnet_diagnostic.CA1017.severity = warning + +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = warning + +# CA1019: Define accessors for attribute arguments +dotnet_diagnostic.CA1019.severity = warning + +# CA1021: Avoid out parameters +dotnet_diagnostic.CA1021.severity = warning + +# CA1024: Use properties where appropriate +dotnet_diagnostic.CA1024.severity = warning + +# CA1027: Mark enums with FlagsAttribute +dotnet_diagnostic.CA1027.severity = warning + +# CA1028: Enum Storage should be Int32 +dotnet_diagnostic.CA1028.severity = warning + +# CA1030: Use events where appropriate +dotnet_diagnostic.CA1030.severity = warning + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = warning + +# CA1033: Interface methods should be callable by child types +dotnet_diagnostic.CA1033.severity = warning + +# CA1034: Nested types should not be visible +dotnet_diagnostic.CA1034.severity = warning + +# CA1036: Override methods on comparable types +dotnet_diagnostic.CA1036.severity = warning + +# CA1040: Avoid empty interfaces +dotnet_diagnostic.CA1040.severity = warning + +# CA1041: Provide ObsoleteAttribute message +dotnet_diagnostic.CA1041.severity = warning + +# CA1043: Use Integral Or String Argument For Indexers +dotnet_diagnostic.CA1043.severity = warning + +# CA1044: Properties should not be write only +dotnet_diagnostic.CA1044.severity = warning + +# CA1045: Do not pass types by reference +dotnet_diagnostic.CA1045.severity = warning + +# CA1046: Do not overload equality operator on reference types +dotnet_diagnostic.CA1046.severity = warning + +# CA1050: Declare types in namespaces +dotnet_diagnostic.CA1050.severity = warning + +# CA1051: Do not declare visible instance fields +dotnet_diagnostic.CA1051.severity = warning + +# CA1052: Static holder types should be Static or NotInheritable +dotnet_diagnostic.CA1052.severity = warning + +# CA1054: URI-like parameters should not be strings +dotnet_diagnostic.CA1054.severity = warning + +# CA1055: URI-like return values should not be strings +dotnet_diagnostic.CA1055.severity = warning + +# CA1056: URI-like properties should not be strings +dotnet_diagnostic.CA1056.severity = warning + +# CA1058: Types should not extend certain base types +dotnet_diagnostic.CA1058.severity = warning + +# CA1060: Move pinvokes to native methods class +dotnet_diagnostic.CA1060.severity = warning + +# CA1061: Do not hide base class methods +dotnet_diagnostic.CA1061.severity = warning + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = warning + +# CA1063: Implement IDisposable Correctly +dotnet_diagnostic.CA1063.severity = warning + +# CA1064: Exceptions should be public +dotnet_diagnostic.CA1064.severity = warning + +# CA1065: Do not raise exceptions in unexpected locations +dotnet_diagnostic.CA1065.severity = warning + +# CA1066: Implement IEquatable when overriding Object.Equals +dotnet_diagnostic.CA1066.severity = warning + +# CA1067: Override Object.Equals(object) when implementing IEquatable +dotnet_diagnostic.CA1067.severity = warning + +# CA1068: CancellationToken parameters must come last +dotnet_diagnostic.CA1068.severity = warning + +# CA1069: Enums values should not be duplicated +dotnet_diagnostic.CA1069.severity = warning + +# CA1070: Do not declare event fields as virtual +dotnet_diagnostic.CA1070.severity = warning + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = warning + +# CA1304: Specify CultureInfo +dotnet_diagnostic.CA1304.severity = warning + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = warning + +# CA1307: Specify StringComparison for clarity +dotnet_diagnostic.CA1307.severity = warning + +# CA1308: Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = warning + +# CA1310: Specify StringComparison for correctness +dotnet_diagnostic.CA1310.severity = warning + +# CA1401: P/Invokes should not be visible +dotnet_diagnostic.CA1401.severity = warning + +# CA1416: Validate platform compatibility +dotnet_diagnostic.CA1416.severity = warning + +# CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes +dotnet_diagnostic.CA1417.severity = warning + +# CA1418: Use valid platform string +dotnet_diagnostic.CA1418.severity = warning + +# CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' +dotnet_diagnostic.CA1419.severity = warning + +# CA1420: Property, type, or attribute requires runtime marshalling +dotnet_diagnostic.CA1420.severity = warning + +# CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied +dotnet_diagnostic.CA1421.severity = warning + +# CA1422: Validate platform compatibility +dotnet_diagnostic.CA1422.severity = warning + +# CA1501: Avoid excessive inheritance +dotnet_diagnostic.CA1501.severity = warning + +# CA1502: Avoid excessive complexity +dotnet_diagnostic.CA1502.severity = warning + +# CA1505: Avoid unmaintainable code +dotnet_diagnostic.CA1505.severity = warning + +# CA1506: Avoid excessive class coupling +# dotnet_diagnostic.CA1506.severity = warning + +# CA1509: Invalid entry in code metrics rule specification file +dotnet_diagnostic.CA1509.severity = warning + +# CA1510: Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = warning + +# CA1511: Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = warning + +# CA1512: Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = warning + +# CA1513: Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = warning + +# CA1700: Do not name enum values 'Reserved' +dotnet_diagnostic.CA1700.severity = warning + +# CA1707: Identifiers should not contain underscores +dotnet_diagnostic.CA1707.severity = warning + +# CA1708: Identifiers should differ by more than case +dotnet_diagnostic.CA1708.severity = none + +# CA1710: Identifiers should have correct suffix +dotnet_diagnostic.CA1710.severity = warning + +# CA1711: Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1711.severity = warning + +# CA1712: Do not prefix enum values with type name +dotnet_diagnostic.CA1712.severity = warning + +# CA1713: Events should not have 'Before' or 'After' prefix +dotnet_diagnostic.CA1713.severity = warning + +# CA1715: Identifiers should have correct prefix +dotnet_diagnostic.CA1715.severity = warning + +# CA1716: Identifiers should not match keywords +dotnet_diagnostic.CA1716.severity = none + +# CA1720: Identifier contains type name +dotnet_diagnostic.CA1720.severity = warning + +# CA1721: Property names should not match get methods +dotnet_diagnostic.CA1721.severity = warning + +# CA1724: Type names should not match namespaces +dotnet_diagnostic.CA1724.severity = warning + +# CA1725: Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = warning + +# CA1727: Use PascalCase for named placeholders +dotnet_diagnostic.CA1727.severity = warning + +# CA1806: Do not ignore method results +dotnet_diagnostic.CA1806.severity = warning + +# CA1810: Initialize reference type static fields inline +dotnet_diagnostic.CA1810.severity = warning + +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = warning + +# CA1814: Prefer jagged arrays over multidimensional +dotnet_diagnostic.CA1814.severity = warning + +# CA1815: Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning + +# CA1816: Dispose methods should call SuppressFinalize +dotnet_diagnostic.CA1816.severity = warning + +# CA1819: Properties should not return arrays +# dotnet_diagnostic.CA1819.severity = warning + +# CA1820: Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = warning + +# CA1821: Remove empty Finalizers +dotnet_diagnostic.CA1821.severity = warning + +# CA1822: Mark members as static +dotnet_diagnostic.CA1822.severity = warning + +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning + +# CA1826: Do not use Enumerable methods on indexable collections +dotnet_diagnostic.CA1826.severity = warning + +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = warning + +# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +dotnet_diagnostic.CA1828.severity = warning + +# CA1829: Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = warning + +# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder +dotnet_diagnostic.CA1830.severity = warning + +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1831.severity = warning + +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1832.severity = warning + +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1833.severity = warning + +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_diagnostic.CA1834.severity = warning + +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_diagnostic.CA1835.severity = warning + +# CA1836: Prefer IsEmpty over Count +dotnet_diagnostic.CA1836.severity = warning + +# CA1837: Use 'Environment.ProcessId' +dotnet_diagnostic.CA1837.severity = warning + +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = warning + +# CA1839: Use 'Environment.ProcessPath' +dotnet_diagnostic.CA1839.severity = warning + +# CA1840: Use 'Environment.CurrentManagedThreadId' +dotnet_diagnostic.CA1840.severity = warning + +# CA1842: Do not use 'WhenAll' with a single task +dotnet_diagnostic.CA1842.severity = warning + +# CA1843: Do not use 'WaitAll' with a single task +dotnet_diagnostic.CA1843.severity = warning + +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1844.severity = warning + +# CA1846: Prefer 'AsSpan' over 'Substring' +dotnet_diagnostic.CA1846.severity = warning + +# CA1847: Use char literal for a single character lookup +dotnet_diagnostic.CA1847.severity = warning + +# CA1848: Use the LoggerMessage delegates +dotnet_diagnostic.CA1848.severity = warning + +# CA1849: Call async methods when in an async method +dotnet_diagnostic.CA1849.severity = warning + +# CA1850: Prefer static 'HashData' method over 'ComputeHash' +dotnet_diagnostic.CA1850.severity = warning + +# CA1852: Seal internal types +dotnet_diagnostic.CA1852.severity = warning + +# CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)' +dotnet_diagnostic.CA1853.severity = warning + +# CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method +dotnet_diagnostic.CA1854.severity = warning + +# CA1858: Use 'StartsWith' instead of 'IndexOf' +dotnet_diagnostic.CA1858.severity = warning + +# CA1859: Use concrete types when possible for improved performance +dotnet_diagnostic.CA1859.severity = warning + +# CA1860: Avoid using 'Enumerable.Any()' extension method +dotnet_diagnostic.CA1860.severity = warning + +# CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = warning + +# CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons +dotnet_diagnostic.CA1862.severity = warning + +# CA1863: Use 'CompositeFormat' +dotnet_diagnostic.CA1863.severity = warning + +# CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method +dotnet_diagnostic.CA1864.severity = warning + +# CA1868: Unnecessary call to 'Contains(item)' +dotnet_diagnostic.CA1868.severity = warning + +# CA1869: Cache and reuse 'JsonSerializerOptions' instances +dotnet_diagnostic.CA1869.severity = warning + +# CA2000: Dispose objects before losing scope +dotnet_diagnostic.CA2000.severity = warning + +# CA2002: Do not lock on objects with weak identity +dotnet_diagnostic.CA2002.severity = warning + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = warning + +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = warning + +# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value +dotnet_diagnostic.CA2009.severity = warning + +# CA2011: Avoid infinite recursion +dotnet_diagnostic.CA2011.severity = warning + +# CA2012: Use ValueTasks correctly +dotnet_diagnostic.CA2012.severity = warning + +# CA2013: Do not use ReferenceEquals with value types +dotnet_diagnostic.CA2013.severity = warning + +# CA2015: Do not define finalizers for types derived from MemoryManager +dotnet_diagnostic.CA2015.severity = warning + +# CA2017: Parameter count mismatch +dotnet_diagnostic.CA2017.severity = warning + +# CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument +dotnet_diagnostic.CA2018.severity = warning + +# CA2019: Improper 'ThreadStatic' field initialization +dotnet_diagnostic.CA2019.severity = warning + +# CA2021: Do not call Enumerable.Cast or Enumerable.OfType with incompatible types +dotnet_diagnostic.CA2021.severity = warning + +# CA2100: Review SQL queries for security vulnerabilities +dotnet_diagnostic.CA2100.severity = warning + +# CA2101: Specify marshaling for P/Invoke string arguments +dotnet_diagnostic.CA2101.severity = warning + +# CA2119: Seal methods that satisfy private interfaces +dotnet_diagnostic.CA2119.severity = warning + +# CA2153: Do Not Catch Corrupted State Exceptions +dotnet_diagnostic.CA2153.severity = warning + +# CA2200: Rethrow to preserve stack details +dotnet_diagnostic.CA2200.severity = warning + +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = warning + +# CA2207: Initialize value type static fields inline +dotnet_diagnostic.CA2207.severity = warning + +# CA2208: Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = warning + +# CA2211: Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = warning + +# CA2213: Disposable fields should be disposed +dotnet_diagnostic.CA2213.severity = warning + +# CA2214: Do not call overridable methods in constructors +dotnet_diagnostic.CA2214.severity = warning + +# CA2215: Dispose methods should call base class dispose +dotnet_diagnostic.CA2215.severity = warning + +# CA2216: Disposable types should declare finalizer +dotnet_diagnostic.CA2216.severity = warning + +# CA2217: Do not mark enums with FlagsAttribute +dotnet_diagnostic.CA2217.severity = warning + +# CA2219: Do not raise exceptions in finally clauses +dotnet_diagnostic.CA2219.severity = warning + +# CA2225: Operator overloads have named alternates +dotnet_diagnostic.CA2225.severity = warning + +# CA2226: Operators should have symmetrical overloads +dotnet_diagnostic.CA2226.severity = warning + +# CA2227: Collection properties should be read only +dotnet_diagnostic.CA2227.severity = warning + +# CA2231: Overload operator equals on overriding value type Equals +dotnet_diagnostic.CA2231.severity = warning + +# CA2235: Mark all non-serializable fields +dotnet_diagnostic.CA2235.severity = warning + +# CA2237: Mark ISerializable types with serializable +dotnet_diagnostic.CA2237.severity = warning + +# CA2241: Provide correct arguments to formatting methods +dotnet_diagnostic.CA2241.severity = warning + +# CA2242: Test for NaN correctly +dotnet_diagnostic.CA2242.severity = warning + +# CA2243: Attribute string literals should parse correctly +dotnet_diagnostic.CA2243.severity = warning + +# CA2244: Do not duplicate indexed element initializations +dotnet_diagnostic.CA2244.severity = warning + +# CA2245: Do not assign a property to itself +dotnet_diagnostic.CA2245.severity = warning + +# CA2246: Assigning symbol and its member in the same statement +dotnet_diagnostic.CA2246.severity = warning + +# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +dotnet_diagnostic.CA2247.severity = warning + +# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' +dotnet_diagnostic.CA2248.severity = warning + +# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' +dotnet_diagnostic.CA2249.severity = warning + +# CA2250: Use 'ThrowIfCancellationRequested' +dotnet_diagnostic.CA2250.severity = warning + +# CA2251: Use 'string.Equals' +dotnet_diagnostic.CA2251.severity = warning + +# CA2253: Named placeholders should not be numeric values +dotnet_diagnostic.CA2253.severity = warning + +# CA2254: Template should be a static expression +dotnet_diagnostic.CA2254.severity = warning + +# CA2255: The 'ModuleInitializer' attribute should not be used in libraries +dotnet_diagnostic.CA2255.severity = warning + +# CA2256: All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface +dotnet_diagnostic.CA2256.severity = warning + +# CA2257: Members defined on an interface with the 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +dotnet_diagnostic.CA2257.severity = warning + +# CA2258: Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +dotnet_diagnostic.CA2258.severity = warning + +# CA2259: 'ThreadStatic' only affects static fields +dotnet_diagnostic.CA2259.severity = warning + +# CA2261: Do not use ConfigureAwaitOptions.SuppressThrowing with Task +dotnet_diagnostic.CA2261.severity = warning + +# CA2300: Do not use insecure deserializer BinaryFormatter +dotnet_diagnostic.CA2300.severity = warning + +# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +dotnet_diagnostic.CA2301.severity = warning + +# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +dotnet_diagnostic.CA2302.severity = warning + +# CA2305: Do not use insecure deserializer LosFormatter +dotnet_diagnostic.CA2305.severity = warning + +# CA2310: Do not use insecure deserializer NetDataContractSerializer +dotnet_diagnostic.CA2310.severity = warning + +# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder +dotnet_diagnostic.CA2311.severity = warning + +# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing +dotnet_diagnostic.CA2312.severity = warning + +# CA2315: Do not use insecure deserializer ObjectStateFormatter +dotnet_diagnostic.CA2315.severity = warning + +# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +dotnet_diagnostic.CA2321.severity = warning + +# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +dotnet_diagnostic.CA2322.severity = warning + +# CA2326: Do not use TypeNameHandling values other than None +dotnet_diagnostic.CA2326.severity = warning + +# CA2327: Do not use insecure JsonSerializerSettings +dotnet_diagnostic.CA2327.severity = warning + +# CA2328: Ensure that JsonSerializerSettings are secure +dotnet_diagnostic.CA2328.severity = warning + +# CA2329: Do not deserialize with JsonSerializer using an insecure configuration +dotnet_diagnostic.CA2329.severity = warning + +# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing +dotnet_diagnostic.CA2330.severity = warning + +# CA2350: Do not use DataTable.ReadXml() with untrusted data +dotnet_diagnostic.CA2350.severity = warning + +# CA2351: Do not use DataSet.ReadXml() with untrusted data +dotnet_diagnostic.CA2351.severity = warning + +# CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data +dotnet_diagnostic.CA2361.severity = warning + +# CA3001: Review code for SQL injection vulnerabilities +dotnet_diagnostic.CA3001.severity = warning + +# CA3002: Review code for XSS vulnerabilities +dotnet_diagnostic.CA3002.severity = warning + +# CA3003: Review code for file path injection vulnerabilities +dotnet_diagnostic.CA3003.severity = warning + +# CA3004: Review code for information disclosure vulnerabilities +dotnet_diagnostic.CA3004.severity = warning + +# CA3005: Review code for LDAP injection vulnerabilities +dotnet_diagnostic.CA3005.severity = warning + +# CA3006: Review code for process command injection vulnerabilities +dotnet_diagnostic.CA3006.severity = warning + +# CA3007: Review code for open redirect vulnerabilities +dotnet_diagnostic.CA3007.severity = warning + +# CA3008: Review code for XPath injection vulnerabilities +dotnet_diagnostic.CA3008.severity = warning + +# CA3009: Review code for XML injection vulnerabilities +dotnet_diagnostic.CA3009.severity = warning + +# CA3010: Review code for XAML injection vulnerabilities +dotnet_diagnostic.CA3010.severity = warning + +# CA3011: Review code for DLL injection vulnerabilities +dotnet_diagnostic.CA3011.severity = warning + +# CA3012: Review code for regex injection vulnerabilities +dotnet_diagnostic.CA3012.severity = warning + +# CA3061: Do Not Add Schema By URL +dotnet_diagnostic.CA3061.severity = warning + +# CA3075: Insecure DTD processing in XML +dotnet_diagnostic.CA3075.severity = warning + +# CA3076: Insecure XSLT script processing +dotnet_diagnostic.CA3076.severity = warning + +# CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader +dotnet_diagnostic.CA3077.severity = warning + +# CA3147: Mark Verb Handlers With Validate Antiforgery Token +dotnet_diagnostic.CA3147.severity = warning + +# CA5350: Do Not Use Weak Cryptographic Algorithms +dotnet_diagnostic.CA5350.severity = warning + +# CA5351: Do Not Use Broken Cryptographic Algorithms +dotnet_diagnostic.CA5351.severity = warning + +# CA5358: Review cipher mode usage with cryptography experts +dotnet_diagnostic.CA5358.severity = warning + +# CA5359: Do Not Disable Certificate Validation +dotnet_diagnostic.CA5359.severity = warning + +# CA5360: Do Not Call Dangerous Methods In Deserialization +dotnet_diagnostic.CA5360.severity = warning + +# CA5361: Do Not Disable SChannel Use of Strong Crypto +dotnet_diagnostic.CA5361.severity = warning + +# CA5362: Potential reference cycle in deserialized object graph +dotnet_diagnostic.CA5362.severity = warning + +# CA5363: Do Not Disable Request Validation +dotnet_diagnostic.CA5363.severity = warning + +# CA5364: Do Not Use Deprecated Security Protocols +dotnet_diagnostic.CA5364.severity = warning + +# CA5365: Do Not Disable HTTP Header Checking +dotnet_diagnostic.CA5365.severity = warning + +# CA5366: Use XmlReader for 'DataSet.ReadXml()' +dotnet_diagnostic.CA5366.severity = warning + +# CA5367: Do Not Serialize Types With Pointer Fields +dotnet_diagnostic.CA5367.severity = warning + +# CA5368: Set ViewStateUserKey For Classes Derived From Page +dotnet_diagnostic.CA5368.severity = warning + +# CA5369: Use XmlReader for 'XmlSerializer.Deserialize()' +dotnet_diagnostic.CA5369.severity = warning + +# CA5370: Use XmlReader for XmlValidatingReader constructor +dotnet_diagnostic.CA5370.severity = warning + +# CA5371: Use XmlReader for 'XmlSchema.Read()' +dotnet_diagnostic.CA5371.severity = warning + +# CA5372: Use XmlReader for XPathDocument constructor +dotnet_diagnostic.CA5372.severity = warning + +# CA5373: Do not use obsolete key derivation function +dotnet_diagnostic.CA5373.severity = warning + +# CA5374: Do Not Use XslTransform +dotnet_diagnostic.CA5374.severity = warning + +# CA5375: Do Not Use Account Shared Access Signature +dotnet_diagnostic.CA5375.severity = warning + +# CA5376: Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5376.severity = warning + +# CA5377: Use Container Level Access Policy +dotnet_diagnostic.CA5377.severity = warning + +# CA5378: Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5378.severity = warning + +# CA5379: Ensure Key Derivation Function algorithm is sufficiently strong +dotnet_diagnostic.CA5379.severity = warning + +# CA5380: Do Not Add Certificates To Root Store +dotnet_diagnostic.CA5380.severity = warning + +# CA5381: Ensure Certificates Are Not Added To Root Store +dotnet_diagnostic.CA5381.severity = warning + +# CA5382: Use Secure Cookies In ASP.NET Core +dotnet_diagnostic.CA5382.severity = warning + +# CA5383: Ensure Use Secure Cookies In ASP.NET Core +dotnet_diagnostic.CA5383.severity = warning + +# CA5384: Do Not Use Digital Signature Algorithm (DSA) +dotnet_diagnostic.CA5384.severity = warning + +# CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size +dotnet_diagnostic.CA5385.severity = warning + +# CA5386: Avoid hardcoding SecurityProtocolType value +dotnet_diagnostic.CA5386.severity = warning + +# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +dotnet_diagnostic.CA5387.severity = warning + +# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +dotnet_diagnostic.CA5388.severity = warning + +# CA5389: Do Not Add Archive Item's Path To The Target File System Path +dotnet_diagnostic.CA5389.severity = warning + +# CA5390: Do not hard-code encryption key +dotnet_diagnostic.CA5390.severity = warning + +# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_diagnostic.CA5391.severity = warning + +# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_diagnostic.CA5392.severity = warning + +# CA5393: Do not use unsafe DllImportSearchPath value +dotnet_diagnostic.CA5393.severity = warning + +# CA5394: Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = warning + +# CA5395: Miss HttpVerb attribute for action methods +dotnet_diagnostic.CA5395.severity = warning + +# CA5396: Set HttpOnly to true for HttpCookie +dotnet_diagnostic.CA5396.severity = warning + +# CA5397: Do not use deprecated SslProtocols values +dotnet_diagnostic.CA5397.severity = warning + +# CA5398: Avoid hardcoded SslProtocols values +dotnet_diagnostic.CA5398.severity = warning + +# CA5399: HttpClients should enable certificate revocation list checks +dotnet_diagnostic.CA5399.severity = warning + +# CA5400: Ensure HttpClient certificate revocation list check is not disabled +dotnet_diagnostic.CA5400.severity = warning + +# CA5401: Do not use CreateEncryptor with non-default IV +dotnet_diagnostic.CA5401.severity = warning + +# CA5402: Use CreateEncryptor with the default IV +dotnet_diagnostic.CA5402.severity = warning + +# CA5403: Do not hard-code certificate +dotnet_diagnostic.CA5403.severity = warning + +# CA5404: Do not disable token validation checks +dotnet_diagnostic.CA5404.severity = warning + +# CA5405: Do not always skip token validation in delegates +dotnet_diagnostic.CA5405.severity = warning + +# CA1032: Implement standard exception constructors +dotnet_diagnostic.CA1032.severity = warning + +# CA1200: Avoid using cref tags with a prefix +dotnet_diagnostic.CA1200.severity = warning + +# CA1309: Use ordinal string comparison +dotnet_diagnostic.CA1309.severity = warning + +# CA1311: Specify a culture or use an invariant version +dotnet_diagnostic.CA1311.severity = warning + +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = warning + +# CA1508: Avoid dead conditional code +dotnet_diagnostic.CA1508.severity = warning + +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = warning + +# CA1805: Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = warning + +# CA1812: Avoid uninstantiated internal classes +dotnet_diagnostic.CA1812.severity = warning + +# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_diagnostic.CA1824.severity = warning + +# CA1825: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = warning + +# CA1841: Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1841.severity = warning + +# CA1845: Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = warning + +# CA1851: Possible multiple enumerations of 'IEnumerable' collection +dotnet_diagnostic.CA1851.severity = warning + +# CA1855: Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1855.severity = warning + +# CA1856: Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1856.severity = warning + +# CA1857: A constant is expected for the parameter +dotnet_diagnostic.CA1857.severity = warning + +# CA1865: Use char overload +dotnet_diagnostic.CA1865.severity = warning + +# CA1866: Use char overload +dotnet_diagnostic.CA1866.severity = warning + +# CA1867: Use char overload +dotnet_diagnostic.CA1867.severity = warning + +# CA1870: Use a cached 'SearchValues' instance +dotnet_diagnostic.CA1870.severity = warning + +# CA2014: Do not use stackalloc in loops +dotnet_diagnostic.CA2014.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods +dotnet_diagnostic.CA2016.severity = warning + +# CA2020: Prevent behavioral change +dotnet_diagnostic.CA2020.severity = warning + +# CA2234: Pass system uri objects instead of strings +dotnet_diagnostic.CA2234.severity = warning + +# CA2252: This API requires opting into preview features +dotnet_diagnostic.CA2252.severity = warning + +# CA2260: Use correct type parameter +dotnet_diagnostic.CA2260.severity = warning + +# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2352.severity = warning + +# CA2353: Unsafe DataSet or DataTable in serializable type +dotnet_diagnostic.CA2353.severity = warning + +# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2354.severity = warning + +# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph +dotnet_diagnostic.CA2355.severity = warning + +# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph +dotnet_diagnostic.CA2356.severity = warning + +# CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2362.severity = warning diff --git a/.forgejo/workflows/lint-build.yml b/.forgejo/workflows/lint-build.yml new file mode 100644 index 00000000..ce8b798e --- /dev/null +++ b/.forgejo/workflows/lint-build.yml @@ -0,0 +1,22 @@ +name: lint-build +on: + push: + pull_request: + +jobs: + lint-build: + name: dotnet${{ matrix.dotnet }} + runs-on: docker + container: git.frostfs.info/truecloudlab/env:dotnet-${{ matrix.dotnet }} + strategy: + matrix: + dotnet: + - '8.0' + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Dotnet runs code analyzers on build (if configured): + # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?tabs=net-8#enable-on-build + - run: dotnet build diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml new file mode 100644 index 00000000..fd79900c --- /dev/null +++ b/.forgejo/workflows/publish.yml @@ -0,0 +1,35 @@ +on: + workflow_dispatch: + +jobs: + image: + name: Publish NuGet packages + runs-on: docker + container: git.frostfs.info/truecloudlab/env:dotnet-8.0 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Build NuGet packages + # `dotnet build` implies and replaces `dotnet pack` thanks to `GeneratePackageOnBuild` + run: dotnet build + + - name: Publish unsigned NuGet packages + run: |- + dotnet nuget add source \ + --name "$NUGET_REGISTRY" \ + --username "$NUGET_REGISTRY_USER" \ + --password "$NUGET_REGISTRY_PASSWORD" \ + --store-password-in-clear-text \ + "$NUGET_REGISTRY_URL" + find -iname '*.nupkg' | grep . | xargs -d'\n' -t -n1 \ + dotnet nuget push --source "$NUGET_REGISTRY" + env: + NUGET_REGISTRY: TrueCloudLab + NUGET_REGISTRY_URL: https://git.frostfs.info/api/packages/TrueCloudLab/nuget/index.json + NUGET_REGISTRY_USER: ${{secrets.NUGET_REGISTRY_USER}} + NUGET_REGISTRY_PASSWORD: ${{secrets.NUGET_REGISTRY_PASSWORD}} + if: >- + startsWith(github.ref, 'refs/tags/v') && + (github.event_name == 'workflow_dispatch' || github.event_name == 'push') diff --git a/.gitignore b/.gitignore index cf6e05ab..449284e3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ vendor/ # IDE .idea .vscode +.vs # coverage coverage.txt @@ -31,5 +32,8 @@ antlr-*.jar # binary bin/ -release/ obj/ + +# Repository signing keys +release/maintainer.* +release/ca.* diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..03bb5e40 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +.* @PavelGrossSpb +.forgejo/.* @potyarkin +Makefile @potyarkin diff --git a/FrostFS.SDK.sln b/FrostFS.SDK.sln index d7d789e9..b96bda25 100644 --- a/FrostFS.SDK.sln +++ b/FrostFS.SDK.sln @@ -1,11 +1,20 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.ClientV2", "src\FrostFS.SDK.ClientV2\FrostFS.SDK.ClientV2.csproj", "{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}" + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Client", "src\FrostFS.SDK.Client\FrostFS.SDK.Client.csproj", "{50D8F61F-C302-4AC9-8D8A-AB0B8C0988C3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Cryptography", "src\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj", "{3D804F4A-B0B2-47A5-B006-BE447BE64B50}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.ModelsV2", "src\FrostFS.SDK.ModelsV2\FrostFS.SDK.ModelsV2.csproj", "{14DC8AC7-E310-40C2-ACDF-5BE78FC0E1B2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Protos", "src\FrostFS.SDK.Protos\FrostFS.SDK.Protos.csproj", "{5012EF96-9C9E-4E77-BC78-B4111EE54107}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.ProtosV2", "src\FrostFS.SDK.ProtosV2\FrostFS.SDK.ProtosV2.csproj", "{5012EF96-9C9E-4E77-BC78-B4111EE54107}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrostFS.SDK.Tests", "src\FrostFS.SDK.Tests\FrostFS.SDK.Tests.csproj", "{8FDA7E0D-9C75-4874-988E-6592CD28F76C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2F030ACD-F87C-4E83-9A68-4CC5DF03AD90}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,17 +30,16 @@ Global {3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Release|Any CPU.ActiveCfg = Release|Any CPU {3D804F4A-B0B2-47A5-B006-BE447BE64B50}.Release|Any CPU.Build.0 = Release|Any CPU - {14DC8AC7-E310-40C2-ACDF-5BE78FC0E1B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {14DC8AC7-E310-40C2-ACDF-5BE78FC0E1B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {14DC8AC7-E310-40C2-ACDF-5BE78FC0E1B2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {14DC8AC7-E310-40C2-ACDF-5BE78FC0E1B2}.Release|Any CPU.Build.0 = Release|Any CPU {5012EF96-9C9E-4E77-BC78-B4111EE54107}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5012EF96-9C9E-4E77-BC78-B4111EE54107}.Debug|Any CPU.Build.0 = Debug|Any CPU {5012EF96-9C9E-4E77-BC78-B4111EE54107}.Release|Any CPU.ActiveCfg = Release|Any CPU {5012EF96-9C9E-4E77-BC78-B4111EE54107}.Release|Any CPU.Build.0 = Release|Any CPU - {B738F3E1-654D-41A3-B068-58ED122BB688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B738F3E1-654D-41A3-B068-58ED122BB688}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B738F3E1-654D-41A3-B068-58ED122BB688}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B738F3E1-654D-41A3-B068-58ED122BB688}.Release|Any CPU.Build.0 = Release|Any CPU + {8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FDA7E0D-9C75-4874-988E-6592CD28F76C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..f02478ef --- /dev/null +++ b/Makefile @@ -0,0 +1,58 @@ +DOTNET?=dotnet +DOCKER?=docker + +NUGET_REGISTRY?=TrueCloudLab +NUGET_REGISTRY_URL?=https://git.frostfs.info/api/packages/TrueCloudLab/nuget/index.json +NUGET_REGISTRY_USER?= +NUGET_REGISTRY_PASSWORD?= + +NUPKG=find -iname '*.nupkg' | grep . | xargs -d'\n' -t -r -n1 +RFC3161_TSA?=http://timestamp.digicert.com + + +.PHONY: build +build: + $(DOTNET) build + + +.PHONY: sign +sign: export NUGET_CERT_REVOCATION_MODE=offline +sign: release/maintainer.pfx + $(NUPKG) $(DOTNET) nuget sign --overwrite --certificate-path $< --timestamper "$(RFC3161_TSA)" + @rm -v "$<" # maintainer.pfx is not password protected and must be ephemeral + $(NUPKG) $(DOTNET) nuget verify + + +.PHONY: publish +publish: + $(NUPKG) $(DOTNET) nuget verify + $(NUPKG) $(DOTNET) nuget push --source "$(NUGET_REGISTRY)" + + +.PHONY: nuget-registry +nuget-registry: +ifeq (,$(NUGET_REGISTRY_USER)) + $(error NUGET_REGISTRY_USER not set) +endif +ifeq (,$(NUGET_REGISTRY_PASSWORD)) + $(error NUGET_REGISTRY_PASSWORD not set) +endif + $(DOTNET) nuget add source \ + --name "$(NUGET_REGISTRY)" \ + --username "$(NUGET_REGISTRY_USER)" \ + --password "$(NUGET_REGISTRY_PASSWORD)" \ + --store-password-in-clear-text \ + "$(NUGET_REGISTRY_URL)" + + +.PHONY: clean +clean: + -$(NUPKG) rm -v + + +.PHONY: container +container: + $(DOCKER) run --pull=always --rm -it -v "$$PWD:/src" -w /src git.frostfs.info/truecloudlab/env:dotnet-8.0 + + +include release/codesign.mk diff --git a/README.md b/README.md index 832b42ff..b7738b76 100644 --- a/README.md +++ b/README.md @@ -21,60 +21,62 @@ neo-go wallet export -w -d ### Container ```csharp +using FrostFS.SDK; using FrostFS.SDK.ClientV2; -using FrostFS.SDK.ModelsV2; -using FrostFS.SDK.ModelsV2.Enums; -using FrostFS.SDK.ModelsV2.Netmap; -var fsClient = new Client(, ); +using Microsoft.Extensions.Options; -// List containers -var containersIds = await fsClient.ListContainersAsync(); +var Key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; +var Host = "http://172.22.33.44:8080"; -// Create container -var placementPolicy = new PlacementPolicy(true, new Replica(1)); -var containerId = await fsClient.CreateContainerAsync( - new Container( - BasicAcl.PublicRW, - placementPolicy - ) -); +var options = Options.Create(new SingleOwnerClientSettings +{ + Key = Key, + Host = Host +}); -// Get container -var container = await fsClient.GetContainerAsync(cId); +using var client = Client.GetSingleOwnerInstance(options); -// Delete container -await fsClient.DeleteContainerAsync(containerId); +await foreach (var cid in client.ListContainersAsync()) +{ + await client.DeleteContainerAsync(new PrmContainerDelete(cid)); +} + +var placementPolicy = new FrostFsPlacementPolicy(true, new FrostFsReplica(1)); + +var createContainerParam = new PrmContainerCreate( + new FrostFsContainerInfo(BasicAcl.PublicRW, new FrostFsPlacementPolicy(true, new FrostFsReplica(1)))); + +var containerId = await client.PutContainerAsync(createContainerParam); + +using var fileStream = File.OpenRead(@"cat.jpeg"); + +var param = new PrmObjectPut +{ + Header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttribute("fileName", "test")]), + Payload = fileStream +}; + +FrostFsObjectId objectId = await client.PutObjectAsync(param); + +var filter = new FilterByAttribute(FrostFsMatchType.Equals, "fileName", "test"); + +await foreach (var objId in client.SearchObjectsAsync(new PrmObjectSearch(containerId) { Filters = [filter] })) +{ + var objHeader = await client.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objId)); +} + +var @object = await client.GetObjectAsync(new PrmObjectGet(containerId, objectId)); + +var downloadedBytes = new byte[@object.Header.PayloadLength]; +MemoryStream ms = new(downloadedBytes); + +ReadOnlyMemory? chunk = null; +while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) +{ + ms.Write(chunk.Value.Span); +} ``` - -### Object - -```csharp -using FrostFS.SDK.ClientV2; -using FrostFS.SDK.ModelsV2; -using FrostFS.SDK.ModelsV2.Enums; -using FrostFS.SDK.ModelsV2.Netmap; - -var fsClient = new Client(, ); - -// Search regular objects -var objectsIds = await fsClient.SearchObjectAsync( - cId, - ObjectFilter.RootFilter() -); - -// Put object -var f = File.OpenRead("cat.jpg"); -var cat = new ObjectHeader( - containerId: cId, - type: ObjectType.Regular, - new ObjectAttribute("Filename", "cat.jpg") -); -var oId = await fsClient.PutObjectAsync(cat, f); - -// Get object header -var objHeader = await fsClient.GetObjectHeadAsync(cId, oId); - -// Get object -var obj = await fsClient.GetObjectAsync(cId, oId); -``` \ No newline at end of file diff --git a/keyfile.snk b/keyfile.snk new file mode 100644 index 00000000..a14537be Binary files /dev/null and b/keyfile.snk differ diff --git a/release/README.md b/release/README.md new file mode 100644 index 00000000..96f8c250 --- /dev/null +++ b/release/README.md @@ -0,0 +1,82 @@ +# Release process + +## Preparing release + +_TBD_ + +## Trusting TrueCloudLab code signing CA certificate + +Verifying signatures (and signing) TrueCloudLab packages requires adding +[TrueCloudLab Code Signing CA](ca.cert) to the list of trusted roots. + +On Linux this can be done by appending [release/ca.cert](ca.cert) to one of: + +- `/etc/pki/ca-trust/extracted/pem/objsign-ca-bundle.pem`: compatible with + [update-ca-trust] and originally proposed in [.NET design docs] +- `…/dotnet/sdk/X.Y.ZZZ/trustedroots/codesignctl.pem`: [fallback] codesigning certificate trust list for .NET + +[update-ca-trust]: https://www.linux.org/docs/man8/update-ca-trust.html +[.NET design docs]: https://github.com/dotnet/designs/blob/main/accepted/2021/signed-package-verification/re-enable-signed-package-verification-technical.md#linux +[fallback]: https://github.com/dotnet/sdk/blob/11150c0ec9020625308edeec555a8b78dbfb2aa5/src/Layout/redist/trustedroots/README.md + +## Signing Nuget packages + +Repository maintainer places `maintainer.cert` and `maintainer.key` (see below +regarding obtaining these files) into `release/` directory and then +executes: + +```console +$ make build sign +``` + +## Uploading packages to Nuget registry + +**IMPORTANT: the following steps upload all `*.nupkg` files located under +`src/`. Maintainer MUST make sure that no unnecessary package versions will be +uploaded to the registry.** + +Configure registry credentials (once per machine): + +```console +$ make nuget-registry NUGET_REGISTRY_USER=username NUGET_REGISTRY_PASSWORD=token +``` + +Publish all locally built packages (implicitly clear existing `*.nupkg` and +rebuild current version only): + +```console +$ make clean build sign publish +``` + + +## Obtaining release signing certificate + +Repository maintainer owns and keeps safe the release signing key +(`maintainer.key`). Private key should never leave maintainer's machine and +should be considered a highly sensitive secret. + +- Generating new maintainer key and the corresponding CSR: + + ```console + $ make maintainer.csr + ...lines skipped... + Enter PEM pass phrase: + Verifying - Enter PEM pass phrase: + ----- + IMPORTANT: Keep maintainer.key private! + + Certificate signing request is ready. + Send maintainer.csr to CA administrator to obtain the certificate. + ``` + + Resulting CSR (`maintainer.csr`) does not contain any sensitive + cryptographic material and may be passed to CA administrator through regular + communication channels. + +- CA administrator then issues the certificate (`make maintainer.cert`) and + sends it back to the maintainer to be used in combination with + `maintainer.key` + +This procedure should be repeated once per machine per `maintainer.cert` +lifetime (1 year) - typically just once per year since we expect the +maintainer to use only a single computer to sign releases. diff --git a/release/ca.cert b/release/ca.cert new file mode 100644 index 00000000..4f4c2f4a --- /dev/null +++ b/release/ca.cert @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF2zCCA8OgAwIBAgIUa9xC/RgvFtUG/xeR016nn0B4K0YwDQYJKoZIhvcNAQEL +BQAwdTELMAkGA1UEBhMCUlUxFTATBgNVBAoMDFRydWVDbG91ZExhYjEVMBMGA1UE +CwwMVHJ1ZUNsb3VkTGFiMTgwNgYDVQQDDC9UcnVlQ2xvdWRMYWIgQ29kZSBTaWdu +aW5nIENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0yNTA0MTAxNTI2MTFaFw0zNTA0 +MDgxNTI2MTFaMHUxCzAJBgNVBAYTAlJVMRUwEwYDVQQKDAxUcnVlQ2xvdWRMYWIx +FTATBgNVBAsMDFRydWVDbG91ZExhYjE4MDYGA1UEAwwvVHJ1ZUNsb3VkTGFiIENv +ZGUgU2lnbmluZyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCyANB4cjf+ZEAFx9RiUYXCAOPMV+jyqgcVbhzh2YKc +9SlvGKRlc00Ar1RlFcrycdkIrTKmhhobiWsFp7UgphwLqRTCb5NB6qfUoWhnfiD9 +m0OBgeVX5wivVaibRI9PSTbFDcIhYUiNvwFJ6GduH/9zOxf1BvuL7LMaoyhIDcg/ +XVLuekE2lnX83zsedv0v/2jyyMY9Ct6N2BXzyHSAzSdYYg0F9Qu9fIMAPjoKhWPc +PnotqaACjb1DScLUr3E/o2W1FfprTT2Pip/0AXxO4wixl4QWh9HeOKV22KRcCHo6 +3wNdg5q1ZVGTNBW0+yoB4jsSG8/JM+2Ujhc1ZnYH10armvGq/0Oc2YQE00960Wy8 +t0drCFWJUO1XHNeBxkkupmj7N1TAPbixtfiGZJhECOWOJwyMpcKixlt5P0cNH4N/ +p3vjyrGQxGLBIkgV/QgjfGkpTHKT1/H40YK6DliWJc01KfNTqn0K+/TIyF0n26kD +BWYVlvDh5P1+V9DGuD2zeXB3PstoifD/Pd7D8wuqpm17noFE19MLp94xv03q9nEa +jRMEd2J2buTLvMh5BBVH0Sm38QAHpSIZ9O3dSLvvjlALbVtwmcsNE9fgxiue3vTB +iXNW8wWs+/DMYwbWyBoCwORxVOdOyc1JLn7qAAEUBweilPVXpWuzMLdUsifPiqrV +dQIDAQABo2MwYTAdBgNVHQ4EFgQUEz4y/RvQMmbUFvf5JbGe/0PZR90wHwYDVR0j +BBgwFoAUEz4y/RvQMmbUFvf5JbGe/0PZR90wDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADggIBAF79W9hMGnrKUWLDOcoeXDEn ++gZxd5rjeF0tNEQGbWKGTJAGQdprOkzWQ47PewpFeS+ePpjOglBAt7cV2ZnOugAT +Brx31vYpNAvYnUCYn+/IUqD8S/U4uErVS9zG9QjirZWo56eJP62vnScKuApCQCbA +pp0zrIyJ+2lQKzlMOENRqzYYA/UTOqYTtnW6x2D8goVqCmDXpgpxEp5XliFjJSr6 +dOjiopNWMvaV3R/Bnd4i41taM7M6HpIV+gbXmEKEFS0ejVfzT8z1pTigN7GBqbxf +nXD03eLUIsbMDv4ZQPrheN7nKnjRUn8kxz0SSK1m2YDrXW51m8fOs6aTvwC/cNe+ +FJMqQMF32i4IXVfUbyUJi+JMvawqm2wEY46vrh7siprY6rXsAzCKJo07i6jvUwnT +TXMldSCPgTEqzT2JBzzr0tRfuPKsv0/NqflHvwfuMRCpcZ7jJZ700iN92xXkiQHP +DmCZOILXcNclAth3nAnyY4XE5a8myv8bwYaPdJdIFlV+BoU/8mClDeA8ck4rDy12 +T5YChKew2oiL4j4B6v9/yrDjD1IT0gv4BWyPhb/n390BCEXt8g9auNcT0s6O8kEc +VUDVc1519ocMCuWVuqUK9o2w0zu50/pBn4hVLfT3QyW8sqtlRKghOWtqZzigvCWF +VjATeO5F/Z7OSDebHUGv +-----END CERTIFICATE----- diff --git a/release/codesign.mk b/release/codesign.mk new file mode 100644 index 00000000..1c39361f --- /dev/null +++ b/release/codesign.mk @@ -0,0 +1,74 @@ +PKI_ROLE?=maintainer +PKI_DIR?=release + +# Note: Only RSA signatures are supported (NU3013) +# https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu3013) + + +ifeq ($(PKI_ROLE),maintainer) +.PHONY: maintainer.csr +maintainer.csr: $(PKI_DIR)/maintainer.csr +$(PKI_DIR)/maintainer.csr: KEY=$(patsubst %.csr,%.key,$@) +$(PKI_DIR)/maintainer.csr: + openssl req \ + -new \ + -newkey rsa:4096 \ + -keyout $(KEY) \ + -out $@ \ + -sha256 \ + -addext keyUsage=critical,digitalSignature \ + -addext extendedKeyUsage=critical,codeSigning,msCodeCom \ + -subj "/C=RU/O=TrueCloudLab/OU=TrueCloudLab/CN=frostfs-sdk-csharp Release Team" + @echo "IMPORTANT: Keep $(KEY) private!\n" + @echo "Certificate signing request is ready.\nSend $@ to CA administrator to obtain the certificate." + +$(PKI_DIR)/maintainer.pfx: $(PKI_DIR)/maintainer.cert $(PKI_DIR)/maintainer.key $(PKI_DIR)/ca.cert + openssl verify \ + -CAfile $(PKI_DIR)/ca.cert \ + $(PKI_DIR)/maintainer.cert + openssl pkcs12 \ + -export \ + -out $@ \ + -inkey $(PKI_DIR)/maintainer.key \ + -in $(PKI_DIR)/maintainer.cert \ + -CAfile $(PKI_DIR)/ca.cert \ + -chain \ + -passout pass: +endif + + +ifeq ($(PKI_ROLE),ca) +.PHONY: maintainer.cert +maintainer.cert: $(PKI_DIR)/maintainer.cert +$(PKI_DIR)/maintainer.cert: CSR=$(patsubst %.cert,%.csr,$@) +$(PKI_DIR)/maintainer.cert: $(PKI_DIR)/ca.key $(PKI_DIR)/ca.cert + openssl req -noout -text -in $(CSR) + @read -p "Review the CSR above. Press Enter to continue, Ctrl+C to cancel " -r null + openssl x509 \ + -req \ + -days 365 \ + -in $(CSR) \ + -copy_extensions copy \ + -ext keyUsage,extendedKeyUsage \ + -CA $(PKI_DIR)/ca.cert \ + -CAkey $(PKI_DIR)/ca.key \ + -CAcreateserial \ + -out $@ + echo >> $@ + cat $(PKI_DIR)/ca.cert >> $@ + openssl x509 -noout -text -in $@ -fingerprint -sha256 + @echo "Certificate is ready.\nSend $@ back to maintainer." + +$(PKI_DIR)/ca.key: CERT=$(patsubst %.key,%.cert,$@) +$(PKI_DIR)/ca.key: + openssl req \ + -x509 \ + -newkey rsa:4096 \ + -keyout $@ \ + -out $(CERT) \ + -sha256 \ + -days 3650 \ + -addext keyUsage=critical,keyCertSign \ + -subj "/C=RU/O=TrueCloudLab/OU=TrueCloudLab/CN=TrueCloudLab Code Signing Certificate Authority" + @echo "IMPORTANT: Keep $@ private!\n" +endif diff --git a/src/FrostFS.SDK.Client/ApeRules/Actions.cs b/src/FrostFS.SDK.Client/ApeRules/Actions.cs new file mode 100644 index 00000000..8bae3030 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Actions.cs @@ -0,0 +1,36 @@ +namespace FrostFS.SDK.Client; + +public struct Actions(bool inverted, string[] names) : System.IEquatable +{ + public bool Inverted { get; set; } = inverted; + + public string[] Names { get; set; } = names; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not Actions) + return false; + + return Equals((Actions)obj); + } + + public override readonly int GetHashCode() + { + return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode(); + } + + public static bool operator ==(Actions left, Actions right) + { + return left.Equals(right); + } + + public static bool operator !=(Actions left, Actions right) + { + return !(left == right); + } + + public readonly bool Equals(Actions other) + { + return this.GetHashCode().Equals(other.GetHashCode()); + } +} diff --git a/src/FrostFS.SDK.Client/ApeRules/ChainTarget.cs b/src/FrostFS.SDK.Client/ApeRules/ChainTarget.cs new file mode 100644 index 00000000..89b0d8a0 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/ChainTarget.cs @@ -0,0 +1,63 @@ +using System; + +using Frostfs.V2.Ape; + +namespace FrostFS.SDK.Client; + +public struct FrostFsChainTarget(FrostFsTargetType type, string name) : IEquatable +{ + private ChainTarget? chainTarget; + + public FrostFsTargetType Type { get; } = type; + + public string Name { get; } = name; + + internal ChainTarget GetChainTarget() + { + return chainTarget ??= new ChainTarget + { + Type = GetTargetType(Type), + Name = Name + }; + } + + private static TargetType GetTargetType(FrostFsTargetType type) + { + return type switch + { + FrostFsTargetType.Undefined => TargetType.Undefined, + FrostFsTargetType.Namespace => TargetType.Namespace, + FrostFsTargetType.Container => TargetType.Container, + FrostFsTargetType.User => TargetType.User, + FrostFsTargetType.Group => TargetType.Group, + _ => throw new ArgumentException("Unexpected value for TargetType", nameof(type)), + }; + } + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not FrostFsChainTarget) + return false; + + return Equals((FrostFsChainTarget)obj); + } + public readonly bool Equals(FrostFsChainTarget other) + { + return Type == other.Type && Name.Equals(other.Name, StringComparison.Ordinal); + } + + public override readonly int GetHashCode() + { + return Name.GetHashCode() ^ (int)Type; + } + + public static bool operator ==(FrostFsChainTarget left, FrostFsChainTarget right) + { + return left.Equals(right); + } + + public static bool operator !=(FrostFsChainTarget left, FrostFsChainTarget right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/ApeRules/Condition.cs b/src/FrostFS.SDK.Client/ApeRules/Condition.cs new file mode 100644 index 00000000..8b11b2b5 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Condition.cs @@ -0,0 +1,43 @@ +namespace FrostFS.SDK.Client; + +public struct Condition : System.IEquatable +{ + public ConditionType Op { get; set; } + + public ConditionKindType Kind { get; set; } + + public string? Key { get; set; } + + public string? Value { get; set; } + + public override bool Equals(object obj) + { + if (obj == null || obj is not Condition) + return false; + + return Equals((Condition)obj); + } + + public override readonly int GetHashCode() + { + return Op.GetHashCode() + ^ Kind.GetHashCode() + ^ (Key != null ? Key.GetHashCode() : 0) + ^ (Value != null ? Value.GetHashCode() : 0); + } + + public static bool operator ==(Condition left, Condition right) + { + return left.Equals(right); + } + + public static bool operator !=(Condition left, Condition right) + { + return !(left == right); + } + + public readonly bool Equals(Condition other) + { + return this.GetHashCode().Equals(other.GetHashCode()); + } +} diff --git a/src/FrostFS.SDK.Client/ApeRules/Enums/ConditionKindType.cs b/src/FrostFS.SDK.Client/ApeRules/Enums/ConditionKindType.cs new file mode 100644 index 00000000..fd138932 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Enums/ConditionKindType.cs @@ -0,0 +1,7 @@ +namespace FrostFS.SDK.Client; + +public enum ConditionKindType +{ + Resource, + Request +} diff --git a/src/FrostFS.SDK.Client/ApeRules/Enums/ConditionType.cs b/src/FrostFS.SDK.Client/ApeRules/Enums/ConditionType.cs new file mode 100644 index 00000000..a5f6ab4b --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Enums/ConditionType.cs @@ -0,0 +1,36 @@ +namespace FrostFS.SDK.Client; + +public enum ConditionType +{ + CondStringEquals, + + CondStringNotEquals, + CondStringEqualsIgnoreCase, + + CondStringNotEqualsIgnoreCase, + CondStringLike, + + CondStringNotLike, + CondStringLessThan, + + CondStringLessThanEquals, + CondStringGreaterThan, + + CondStringGreaterThanEquals, + + // Numeric condition operators. + CondNumericEquals, + + CondNumericNotEquals, + CondNumericLessThan, + + CondNumericLessThanEquals, + CondNumericGreaterThan, + + CondNumericGreaterThanEquals, + + CondSliceContains, + + CondIPAddress, + CondNotIPAddress, +} diff --git a/src/FrostFS.SDK.Client/ApeRules/Enums/FrostFsTargetType.cs b/src/FrostFS.SDK.Client/ApeRules/Enums/FrostFsTargetType.cs new file mode 100644 index 00000000..178ca63c --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Enums/FrostFsTargetType.cs @@ -0,0 +1,10 @@ +namespace FrostFS.SDK.Client; + +public enum FrostFsTargetType +{ + Undefined, + Namespace, + Container, + User, + Group +} diff --git a/src/FrostFS.SDK.Client/ApeRules/Enums/Status.cs b/src/FrostFS.SDK.Client/ApeRules/Enums/Status.cs new file mode 100644 index 00000000..547449b6 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Enums/Status.cs @@ -0,0 +1,9 @@ +namespace FrostFS.SDK.Client; + +public enum RuleStatus +{ + Allow, + NoRuleFound, + AccessDenied, + QuotaLimitReached +} diff --git a/src/FrostFS.SDK.Client/ApeRules/FrostFsChain.cs b/src/FrostFS.SDK.Client/ApeRules/FrostFsChain.cs new file mode 100644 index 00000000..c79bd7da --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/FrostFsChain.cs @@ -0,0 +1,10 @@ +namespace FrostFS.SDK.Client; + +public class FrostFsChain +{ + public byte[] ID { get; set; } = []; + + public FrostFsRule[] Rules { get; set; } = []; + + public RuleMatchType MatchType { get; set; } +} diff --git a/src/FrostFS.SDK.Client/ApeRules/FrostFsRule.cs b/src/FrostFS.SDK.Client/ApeRules/FrostFsRule.cs new file mode 100644 index 00000000..367a9897 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/FrostFsRule.cs @@ -0,0 +1,18 @@ +namespace FrostFS.SDK.Client; + +public class FrostFsRule +{ + public RuleStatus Status { get; set; } + + // Actions the operation is applied to. + public Actions Actions { get; set; } + + // List of the resources the operation is applied to. + public Resources Resources { get; set; } + + // True iff individual conditions must be combined with the logical OR. + // By default AND is used, so _each_ condition must pass. + public bool Any { get; set; } + + public Condition[]? Conditions { get; set; } +} diff --git a/src/FrostFS.SDK.Client/ApeRules/MatchType.cs b/src/FrostFS.SDK.Client/ApeRules/MatchType.cs new file mode 100644 index 00000000..fe3305eb --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/MatchType.cs @@ -0,0 +1,10 @@ +namespace FrostFS.SDK.Client; + +public enum RuleMatchType +{ + // DenyPriority rejects the request if any `Deny` is specified. + DenyPriority, + + // FirstMatch returns the first rule action matched to the request. + FirstMatch +} diff --git a/src/FrostFS.SDK.Client/ApeRules/Resources.cs b/src/FrostFS.SDK.Client/ApeRules/Resources.cs new file mode 100644 index 00000000..47ff3e1a --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/Resources.cs @@ -0,0 +1,36 @@ +namespace FrostFS.SDK.Client; + +public struct Resources(bool inverted, string[] names) : System.IEquatable +{ + public bool Inverted { get; set; } = inverted; + + public string[] Names { get; set; } = names; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not Resources) + return false; + + return Equals((Resources)obj); + } + + public override readonly int GetHashCode() + { + return Inverted.GetHashCode() ^ string.Join(string.Empty, Names).GetHashCode(); + } + + public static bool operator ==(Resources left, Resources right) + { + return left.Equals(right); + } + + public static bool operator !=(Resources left, Resources right) + { + return !(left == right); + } + + public readonly bool Equals(Resources other) + { + return this.GetHashCode().Equals(other.GetHashCode()); + } +} diff --git a/src/FrostFS.SDK.Client/ApeRules/RuleSerializer.cs b/src/FrostFS.SDK.Client/ApeRules/RuleSerializer.cs new file mode 100644 index 00000000..24f784e3 --- /dev/null +++ b/src/FrostFS.SDK.Client/ApeRules/RuleSerializer.cs @@ -0,0 +1,502 @@ +using System; + +namespace FrostFS.SDK.Client; + +internal static class RuleSerializer +{ + const byte Version = 0; // increase if breaking change + const int ByteSize = 1; + const int UInt8Size = ByteSize; + const int BoolSize = ByteSize; + const long NullSlice = -1; + const int NullSliceSize = 1; + const byte ByteTrue = 1; + const byte ByteFalse = 0; + + /// + /// maxSliceLen taken from https://github.com/neo-project/neo/blob/38218bbee5bbe8b33cd8f9453465a19381c9a547/src/Neo/IO/Helper.cs#L77 + /// + const int MaxSliceLen = 0x1000000; + + const int ChainMarshalVersion = 0; + + internal static byte[] Serialize(FrostFsChain chain) + { + int s = UInt8Size // Marshaller version + + UInt8Size // Chain version + + SliceSize(chain.ID, b => ByteSize) + + SliceSize(chain.Rules, RuleSize) + + UInt8Size; // MatchType + + byte[] buf = new byte[s]; + + int offset = UInt8Marshal(buf, 0, Version); + offset = UInt8Marshal(buf, offset, ChainMarshalVersion); + offset = SliceMarshal(buf, offset, chain.ID, ByteMarshal); + offset = SliceMarshal(buf, offset, chain.Rules, MarshalRule); + offset = UInt8Marshal(buf, offset, (byte)chain.MatchType); + + VerifyMarshal(buf, offset); + + return buf; + } + + internal static FrostFsChain Deserialize(byte[] data) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + FrostFsChain chain = new(); + + var (offset, version) = UInt8Unmarshal(data, 0); + + if (version != Version) + { + throw new FrostFsException($"unsupported marshaller version {version}"); + } + + (offset, byte chainVersion) = UInt8Unmarshal(data, offset); + + if (chainVersion != ChainMarshalVersion) + { + throw new FrostFsException($"unsupported chain version {chainVersion}"); + } + + (offset, chain.ID) = SliceUnmarshal(data, offset, UInt8Unmarshal); + + (offset, chain.Rules) = SliceUnmarshal(data, offset, UnmarshalRule); + + (offset, var matchTypeV) = UInt8Unmarshal(data, offset); + + chain.MatchType = (RuleMatchType)matchTypeV; + + VerifyUnmarshal(data, offset); + + return chain; + } + + private static int Int64Size(long value) + { + // https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=92;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c + // and + // https://cs.opensource.google/go/go/+/master:src/encoding/binary/varint.go;l=41;drc=dac9b9ddbd5160c5f4552410f5f8281bd5eed38c + ulong ux = (ulong)value << 1; + if (value < 0) + { + ux = ~ux; + } + + int size = 0; + while (ux >= 0x80) + { + size++; + ux >>= 7; + } + + return size + 1; + } + + private static int SliceSize(T[] slice, Func sizeOf) + { + if (slice == null) + { + return NullSliceSize; + } + + // Assuming Int64Size is the size of the slice + var size = Int64Size(slice.Length); + foreach (var v in slice) + { + size += sizeOf(v); + } + + return size; + } + + private static int StringSize(string? s) + { + var len = s != null ? s.Length : 0; + return Int64Size(len) + len; + } + + private static int ActionsSize(Actions action) + { + return BoolSize // Inverted + + SliceSize(action.Names, StringSize); + } + + private static int ResourcesSize(Resources resource) + { + return BoolSize // Inverted + + SliceSize(resource.Names, StringSize); + } + + private static int ConditionSize(Condition condition) + { + return ByteSize // Op + + ByteSize // Object + + StringSize(condition.Key) + + StringSize(condition.Value); + } + + private static int RuleSize(FrostFsRule rule) + { + if (rule is null) + { + throw new ArgumentNullException(nameof(rule)); + } + + return ByteSize // Status + + ActionsSize(rule.Actions) + + ResourcesSize(rule.Resources) + + BoolSize // Any + + SliceSize(rule.Conditions!, ConditionSize); + } + + private static int UInt8Marshal(byte[] buf, int offset, byte value) + { + if (buf.Length - offset < 1) + { + throw new FrostFsException("Not enough bytes left to serialize value of type byte"); + } + + buf[offset] = value; + + return offset + 1; + } + + private static int ByteMarshal(byte[] buf, int offset, byte value) + { + return UInt8Marshal(buf, offset, value); + } + + // PutVarint encodes an int64 into buf and returns the number of bytes written. + // If the buffer is too small, PutVarint will panic. + private static int PutVarint(byte[] buf, int offset, long x) + { + var ux = (ulong)x << 1; + + if (x < 0) + { + ux = ~ux; + } + + return PutUvarint(buf, offset, ux); + } + + private static int PutUvarint(byte[] buf, int offset, ulong x) + { + while (x >= 0x80) + { + buf[offset] = (byte)(x | 0x80); + x >>= 7; + offset++; + } + + buf[offset] = (byte)x; + + return offset + 1; + } + + private static int Int64Marshal(byte[] buf, int offset, long v) + { + if (buf.Length - offset < Int64Size(v)) + { + throw new FrostFsException("Not enough bytes left to serialize value of type long"); + } + + return PutVarint(buf, offset, v); + } + + private static int SliceMarshal(byte[] buf, int offset, T[] slice, Func marshalT) + { + if (slice == null) + { + return Int64Marshal(buf, offset, NullSlice); + } + + if (slice.Length > MaxSliceLen) + { + throw new FrostFsException($"slice size if too big: {slice.Length}"); + } + + offset = Int64Marshal(buf, offset, slice.Length); + + foreach (var v in slice) + { + offset = marshalT(buf, offset, v); + } + + return offset; + } + + private static int BoolMarshal(byte[] buf, int offset, bool value) + { + return UInt8Marshal(buf, offset, value ? ByteTrue : ByteFalse); + } + + private static int StringMarshal(byte[] buf, int offset, string value) + { + if (value == null) + { + throw new FrostFsException($"string value is null"); + } + + if (value.Length > MaxSliceLen) + { + throw new FrostFsException($"string is too long: {value.Length}"); + } + + if (buf.Length - offset < Int64Size(value.Length) + value.Length) + { + throw new FrostFsException($"Not enough bytes left to serialize value of type string with length {value.Length}"); + } + + offset = Int64Marshal(buf, offset, value.Length); + + if (string.IsNullOrEmpty(value)) + { + return offset; + } + + Buffer.BlockCopy(System.Text.Encoding.UTF8.GetBytes(value), 0, buf, offset, value.Length); + + return offset + value.Length; + } + + private static int MarshalActions(byte[] buf, int offset, Actions action) + { + offset = BoolMarshal(buf, offset, action.Inverted); + + return SliceMarshal(buf, offset, action.Names, StringMarshal); + } + + private static int MarshalCondition(byte[] buf, int offset, Condition condition) + { + offset = ByteMarshal(buf, offset, (byte)condition.Op); + + offset = ByteMarshal(buf, offset, (byte)condition.Kind); + + offset = StringMarshal(buf, offset, condition.Key!); + + return StringMarshal(buf, offset, condition.Value!); + } + + private static int MarshalRule(byte[] buf, int offset, FrostFsRule rule) + { + if (rule is null) + { + throw new ArgumentNullException(nameof(rule)); + } + + offset = ByteMarshal(buf, offset, (byte)rule.Status); + + offset = MarshalActions(buf, offset, rule.Actions); + + offset = MarshalResources(buf, offset, rule.Resources); + + offset = BoolMarshal(buf, offset, rule.Any); + + return SliceMarshal(buf, offset, rule.Conditions!, MarshalCondition); + } + + private static int MarshalResources(byte[] buf, int offset, Resources resources) + { + offset = BoolMarshal(buf, offset, resources.Inverted); + + return SliceMarshal(buf, offset, resources.Names, StringMarshal); + } + + private static void VerifyMarshal(byte[] buf, int lastOffset) + { + if (buf.Length != lastOffset) + { + throw new FrostFsException("actual data size differs from expected"); + } + } + + private static (int, bool) BoolUnmarshal(byte[] buf, int offset) + { + (offset, byte val) = UInt8Unmarshal(buf, offset); + return (offset, val == ByteTrue); + } + + private static (int, string) StringUnmarshal(byte[] buf, int offset) + { + (offset, long size) = Int64Unmarshal(buf, offset); + + if (size == 0) + { + return (offset, string.Empty); + } + + if (size > MaxSliceLen) + { + throw new FrostFsException($"string is too long: '{size}'"); + } + if (size < 0) + { + throw new FrostFsException($"invalid string size: '{size}'"); + } + + if (buf.Length - offset < size) + { + throw new FrostFsException($"not enough bytes left to string value"); + } + + return (offset + (int)size, System.Text.Encoding.UTF8.GetString(buf, offset, (int)size)); + } + + private static (int, Actions) UnmarshalActions(byte[] buf, int offset) + { + Actions action = new(); + (offset, action.Inverted) = BoolUnmarshal(buf, offset); + + (offset, action.Names) = SliceUnmarshal(buf, offset, StringUnmarshal); + + return (offset, action); + } + + private static (int, Resources) UnmarshalResources(byte[] buf, int offset) + { + Resources res = new(); + + (offset, res.Inverted) = BoolUnmarshal(buf, offset); + (offset, res.Names) = SliceUnmarshal(buf, offset, StringUnmarshal); + + return (offset, res); + } + + private static (int, Condition) UnmarshalCondition(byte[] buf, int offset) + { + Condition cond = new(); + (offset, var op) = UInt8Unmarshal(buf, offset); + + cond.Op = (ConditionType)op; + + (offset, var kind) = UInt8Unmarshal(buf, offset); + + cond.Kind = (ConditionKindType)kind; + + (offset, cond.Key) = StringUnmarshal(buf, offset); + + (offset, cond.Value) = StringUnmarshal(buf, offset); + + return (offset, cond); + } + + private static (int, FrostFsRule) UnmarshalRule(byte[] buf, int offset) + { + FrostFsRule rule = new(); + + (offset, byte statusV) = UInt8Unmarshal(buf, offset); + rule.Status = (RuleStatus)statusV; + + (offset, rule.Actions) = UnmarshalActions(buf, offset); + + (offset, rule.Resources) = UnmarshalResources(buf, offset); + + (offset, rule.Any) = BoolUnmarshal(buf, offset); + + (offset, rule.Conditions) = SliceUnmarshal(buf, offset, UnmarshalCondition); + + return (offset, rule); + } + + private static (int, byte) UInt8Unmarshal(byte[] buf, int offset) + { + if (buf.Length - offset < 1) + { + throw new FrostFsException($"not enough bytes left to read a value of type 'byte' from offset {offset}"); + } + + return (offset + 1, buf[offset]); + } + + private static (int, long) Int64Unmarshal(byte[] buf, int offset) + { + if (buf.Length - offset < sizeof(long)) + { + throw new FrostFsException($"not enough bytes left to read a value of type 'long' from offset {offset}"); + } + + return Varint(buf, offset); + } + + private static (int, T[]) SliceUnmarshal(byte[] buf, int offset, Func unmarshalT) + { + var (newOffset, size) = Varint(buf, offset); + + if (size == NullSlice) + { + return (newOffset, []); + } + + if (size > MaxSliceLen) + { + throw new FrostFsException($"slice size is too big: '{size}'"); + } + + if (size < 0) + { + throw new FrostFsException($"invalid slice size: '{size}'"); + } + + var result = new T[size]; + for (int i = 0; i < result.Length; i++) + { + (newOffset, result[i]) = unmarshalT(buf, newOffset); + } + + return (newOffset, result); + } + + private static void VerifyUnmarshal(byte[] buf, int offset) + { + if (buf.Length != offset) + { + throw new FrostFsException("unmarshalled bytes left"); + } + } + + private static int MaxVarIntLen64 = 10; + + public static (int, long) Varint(byte[] buf, int offset) + { + var (ux, n) = Uvarint(buf, offset); // ok to continue in presence of error + long x = (long)ux >> 1; + if ((ux & 1) != 0) + { + x = ~x; + } + return (n, x); + } + + public static (ulong, int) Uvarint(byte[] buf, int offset) + { + ulong x = 0; + int s = 0; + + for (int i = offset; i < buf.Length; i++) + { + byte b = buf[i]; + if (i == MaxVarIntLen64) + { + return (0, -(i + 1)); // overflow + } + if (b < 0x80) + { + if (i == MaxVarIntLen64 - 1 && b > 1) + { + return (0, -(i + 1)); // overflow + } + return (x | ((ulong)b << s), i + 1); + } + x |= (ulong)(b & 0x7f) << s; + s += 7; + } + return (0, 0); + } +} diff --git a/src/FrostFS.SDK.Client/AssemblyInfo.cs b/src/FrostFS.SDK.Client/AssemblyInfo.cs new file mode 100644 index 00000000..4fa6cd9f --- /dev/null +++ b/src/FrostFS.SDK.Client/AssemblyInfo.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyCompany("TrueCloudLab")] +[assembly: InternalsVisibleTo("FrostFS.SDK.Tests, PublicKey=" + + "002400000480000094000000060200000024000052534131000400000100010089b992fb14ebcf"+ + "4b85b4b1a3af1897290c52ff85a106035c47dc5604cbaa58ae3180f5c9b8523fee5dd1bb9ea9cf"+ + "e15ab287e6239c98d5dfa91615bd77485d523a3a3f65a4e5028454cedd5ac4d9eca6da18b81985"+ + "ac6905d33cc64b5a2587050c16f67b71ef8889dbd3c90ef7cc0b06bbbe09886601d195f5db179a"+ + "3c2a25b1")] +[assembly: AssemblyFileVersion("1.0.7.0")] +[assembly: AssemblyProduct("FrostFS.SDK.Client")] +[assembly: AssemblyTitle("FrostFS.SDK.Client")] +[assembly: AssemblyVersion("1.0.7")] diff --git a/src/FrostFS.SDK.Client/Caches.cs b/src/FrostFS.SDK.Client/Caches.cs new file mode 100644 index 00000000..e9d9b16f --- /dev/null +++ b/src/FrostFS.SDK.Client/Caches.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace FrostFS.SDK.Client; + +internal static class Caches +{ + private static readonly IMemoryCache _ownersCache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = 256 + }); + + internal static IMemoryCache Owners => _ownersCache; +} diff --git a/src/FrostFS.SDK.Client/CllientKey.cs b/src/FrostFS.SDK.Client/CllientKey.cs new file mode 100644 index 00000000..7a7943bd --- /dev/null +++ b/src/FrostFS.SDK.Client/CllientKey.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; + +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client; + +public class ClientKey(ECDsa key) +{ + internal ECDsa ECDsaKey { get; } = key; + + internal ByteString PublicKeyProto { get; } = ByteString.CopyFrom(key.PublicKey()); + + internal string PublicKey { get; } = Base58.Encode(key.PublicKey()); + + internal FrostFsOwner Owner { get; } = new FrostFsOwner(key.PublicKey().PublicKeyToAddress()); +} diff --git a/src/FrostFS.SDK.Client/Exceptions/FrostFsException.cs b/src/FrostFS.SDK.Client/Exceptions/FrostFsException.cs new file mode 100644 index 00000000..9f3f36ce --- /dev/null +++ b/src/FrostFS.SDK.Client/Exceptions/FrostFsException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class FrostFsException : Exception +{ + public FrostFsException() + { + } + + public FrostFsException(string message) : base(message) + { + } + + public FrostFsException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Exceptions/FrostFsInvalidObjectException.cs b/src/FrostFS.SDK.Client/Exceptions/FrostFsInvalidObjectException.cs new file mode 100644 index 00000000..de1a88da --- /dev/null +++ b/src/FrostFS.SDK.Client/Exceptions/FrostFsInvalidObjectException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class FrostFsInvalidObjectException : FrostFsException +{ + public FrostFsInvalidObjectException() + { + } + + public FrostFsInvalidObjectException(string message) : base(message) + { + } + + public FrostFsInvalidObjectException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Exceptions/FrostFsResponseException.cs b/src/FrostFS.SDK.Client/Exceptions/FrostFsResponseException.cs new file mode 100644 index 00000000..87799e21 --- /dev/null +++ b/src/FrostFS.SDK.Client/Exceptions/FrostFsResponseException.cs @@ -0,0 +1,26 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class FrostFsResponseException : FrostFsException +{ + public FrostFsResponseStatus? Status { get; private set; } + + public FrostFsResponseException() + { + } + + public FrostFsResponseException(FrostFsResponseStatus status) + : base(status != null ? status.Message != null ? "" : "" : "") + { + Status = status; + } + + public FrostFsResponseException(string message) : base(message) + { + } + + public FrostFsResponseException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Exceptions/FrostFsStreamException.cs b/src/FrostFS.SDK.Client/Exceptions/FrostFsStreamException.cs new file mode 100644 index 00000000..0bd40d5d --- /dev/null +++ b/src/FrostFS.SDK.Client/Exceptions/FrostFsStreamException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class FrostFsStreamException : FrostFsException +{ + public FrostFsStreamException() + { + } + + public FrostFsStreamException(string message) : base(message) + { + } + + public FrostFsStreamException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Exceptions/SessionExpiredException.cs b/src/FrostFS.SDK.Client/Exceptions/SessionExpiredException.cs new file mode 100644 index 00000000..0fa6a825 --- /dev/null +++ b/src/FrostFS.SDK.Client/Exceptions/SessionExpiredException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class SessionExpiredException : FrostFsException +{ + public SessionExpiredException() + { + } + + public SessionExpiredException(string message) : base(message) + { + } + + public SessionExpiredException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Exceptions/SessionNotFoundException.cs b/src/FrostFS.SDK.Client/Exceptions/SessionNotFoundException.cs new file mode 100644 index 00000000..c5be8485 --- /dev/null +++ b/src/FrostFS.SDK.Client/Exceptions/SessionNotFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class SessionNotFoundException : FrostFsException +{ + public SessionNotFoundException() + { + } + + public SessionNotFoundException(string message) : base(message) + { + } + + public SessionNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Extensions/FrostFsExtensions.cs b/src/FrostFS.SDK.Client/Extensions/FrostFsExtensions.cs new file mode 100644 index 00000000..87728d57 --- /dev/null +++ b/src/FrostFS.SDK.Client/Extensions/FrostFsExtensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Security.Cryptography; +using Google.Protobuf; + +namespace FrostFS.SDK.Cryptography; + +public static class FrostFsExtensions +{ + public static byte[] Sha256(this IMessage data) + { + using var sha256 = SHA256.Create(); + using HashStream stream = new(sha256); + data.WriteTo(stream); + return stream.Hash(); + } + + public static Guid ToUuid(this ByteString id) + { + if (id == null) + throw new ArgumentNullException(nameof(id)); + + return new Guid( + (id[0] << 24) + (id[1] << 16) + (id[2] << 8) + id[3], + (short)((id[4] << 8) + id[5]), + (short)((id[6] << 8) + id[7]), + id[8], + id[9], + id[10], + id[11], + id[12], + id[13], + id[14], + id[15]); + } + + /// + /// Serializes Guid to binary representation in direct order bytes format + /// + /// + /// + public unsafe static void ToBytes(this Guid id, Span span) + { + var pGuid = (byte*)&id; + + span[0] = pGuid[3]; + span[1] = pGuid[2]; + span[2] = pGuid[1]; + span[3] = pGuid[0]; + span[4] = pGuid[5]; + span[5] = pGuid[4]; + span[6] = pGuid[7]; + span[7] = pGuid[6]; + span[8] = pGuid[8]; + span[9] = pGuid[9]; + span[10] = pGuid[10]; + span[11] = pGuid[11]; + span[12] = pGuid[12]; + span[13] = pGuid[13]; + span[14] = pGuid[14]; + span[15] = pGuid[15]; + } +} diff --git a/src/FrostFS.SDK.Client/FrostFS.SDK.Client.csproj b/src/FrostFS.SDK.Client/FrostFS.SDK.Client.csproj new file mode 100644 index 00000000..88e20c6f --- /dev/null +++ b/src/FrostFS.SDK.Client/FrostFS.SDK.Client.csproj @@ -0,0 +1,58 @@ + + + + netstandard2.0 + 12.0 + enable + AllEnabledByDefault + FrostFS.SDK.Client + 1.0.7 + + C# SDK for FrostFS gRPC native protocol + + true + + + + true + + + + true + + + + <_SkipUpgradeNetAnalyzersNuGetWarning>true + + + + true + + + + false + True + True + .\\..\\..\\keyfile.snk + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/src/FrostFS.SDK.Client/FrostFSClient.cs b/src/FrostFS.SDK.Client/FrostFSClient.cs new file mode 100644 index 00000000..bce9104d --- /dev/null +++ b/src/FrostFS.SDK.Client/FrostFSClient.cs @@ -0,0 +1,434 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using FrostFS.SDK.Client.Interfaces; +using FrostFS.SDK.Client.Services; +using FrostFS.SDK.Cryptography; +using FrostFS.Session; + +using Grpc.Core; +using Grpc.Core.Interceptors; + +using Microsoft.Extensions.Options; + +using static Frostfs.V2.Apemanager.APEManagerService; +using static FrostFS.Accounting.AccountingService; +using static FrostFS.Container.ContainerService; +using static FrostFS.Netmap.NetmapService; +using static FrostFS.Object.ObjectService; +using static FrostFS.Session.SessionService; + +namespace FrostFS.SDK.Client; + +public class FrostFSClient : IFrostFSClient +{ + internal ContainerServiceClient? ContainerServiceClient { get; set; } + internal ContainerServiceProvider? ContainerServiceProvider { get; set; } + + internal NetmapServiceClient? NetmapServiceClient { get; set; } + internal NetmapServiceProvider? NetmapServiceProvider { get; set; } + + internal APEManagerServiceClient? ApeManagerServiceClient { get; set; } + internal ApeManagerServiceProvider? ApeManagerServiceProvider { get; set; } + + internal SessionServiceClient? SessionServiceClient { get; set; } + internal SessionServiceProvider? SessionServiceProvider { get; set; } + + internal ObjectServiceClient? ObjectServiceClient { get; set; } + internal ObjectServiceProvider? ObjectServiceProvider { get; set; } + + internal AccountingServiceClient? AccountingServiceClient { get; set; } + internal AccountingServiceProvider? AccountingServiceProvider { get; set; } + + internal ClientContext ClientCtx { get; set; } + + public static IFrostFSClient GetInstance(IOptions clientOptions, Func grpcChannelFactory) + { + if (clientOptions is null) + { + throw new ArgumentNullException(nameof(clientOptions)); + } + + if (grpcChannelFactory is null) + { + throw new ArgumentNullException(nameof(grpcChannelFactory)); + } + + return new FrostFSClient(clientOptions, grpcChannelFactory); + } + + /// + /// For test only. Provide custom implementation or mock object to inject required logic instead of internal gRPC client. + /// + /// Global setting for client + /// Setting for gRPC channel + /// ContainerService.ContainerServiceClient implementation + /// Netmap.NetmapService.NetmapServiceClient implementation + /// Session.SessionService.SessionServiceClient implementation + /// Object.ObjectService.ObjectServiceClient implementation + /// + public static IFrostFSClient GetTestInstance( + IOptions settings, + Func grpcChannelFactory, + NetmapServiceClient netmapService, + SessionServiceClient sessionService, + ContainerServiceClient containerService, + ObjectServiceClient objectService) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (grpcChannelFactory is null) + { + throw new ArgumentNullException(nameof(grpcChannelFactory)); + } + + if (netmapService is null) + { + throw new ArgumentNullException(nameof(netmapService)); + } + + if (sessionService is null) + { + throw new ArgumentNullException(nameof(sessionService)); + } + + if (containerService is null) + { + throw new ArgumentNullException(nameof(containerService)); + } + + if (objectService is null) + { + throw new ArgumentNullException(nameof(objectService)); + } + + return new FrostFSClient( + settings, channel: grpcChannelFactory(settings.Value.Host), containerService, netmapService, sessionService, objectService); + } + + private FrostFSClient( + IOptions settings, + ChannelBase channel, + ContainerServiceClient containerService, + NetmapServiceClient netmapService, + SessionServiceClient sessionService, + ObjectServiceClient objectService) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var ecdsaKey = settings.Value.Key.LoadWif(); + + ClientCtx = new ClientContext( + client: this, + key: new ClientKey(ecdsaKey), + owner: FrostFsOwner.FromKey(ecdsaKey), + channel: channel, + version: new FrostFsVersion(2, 13)) + { + SessionCache = new SessionCache(0), + Callback = settings.Value.Callback, + Interceptors = settings.Value.Interceptors + }; + + ContainerServiceClient = containerService ?? throw new ArgumentNullException(nameof(containerService)); + NetmapServiceClient = netmapService ?? throw new ArgumentNullException(nameof(netmapService)); + SessionServiceClient = sessionService ?? throw new ArgumentNullException(nameof(sessionService)); + ObjectServiceClient = objectService ?? throw new ArgumentNullException(nameof(objectService)); + } + + private FrostFSClient(IOptions settings, Func grpcChannelFactory) + { + var clientSettings = (settings?.Value) ?? throw new ArgumentNullException(nameof(settings), "Options value must be initialized"); + + clientSettings.Validate(); + + var ecdsaKey = clientSettings.Key.LoadWif(); + + ClientCtx = new ClientContext( + this, + key: new ClientKey(ecdsaKey), + owner: FrostFsOwner.FromKey(ecdsaKey), + channel: grpcChannelFactory(settings.Value.Host), + version: new FrostFsVersion(2, 13)) + { + SessionCache = new SessionCache(0), + Callback = settings.Value.Callback, + Interceptors = settings.Value.Interceptors + }; + } + + #region ApeManagerImplementation + public Task> AddChainAsync(PrmApeChainAdd args, CallContext ctx) + { + return GetApeManagerService().AddChainAsync(args, ctx); + } + + public Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx) + { + return GetApeManagerService().RemoveChainAsync(args, ctx); + } + + public Task ListChainAsync(PrmApeChainList args, CallContext ctx) + { + return GetApeManagerService().ListChainAsync(args, ctx); + } + #endregion + + #region ContainerImplementation + public Task GetContainerAsync(PrmContainerGet args, CallContext ctx) + { + return GetContainerService().GetContainerAsync(args, ctx); + } + + public IAsyncEnumerable ListContainersAsync(PrmContainerGetAll args, CallContext ctx) + { + return GetContainerService().ListContainersAsync(args, ctx); + } + + [Obsolete("Use PutContainerAsync method")] + public Task CreateContainerAsync(PrmContainerCreate args, CallContext ctx) + { + return GetContainerService().PutContainerAsync(args, ctx); + } + + public Task PutContainerAsync(PrmContainerCreate args, CallContext ctx) + { + return GetContainerService().PutContainerAsync(args, ctx); + } + + public Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx) + { + return GetContainerService().DeleteContainerAsync(args, ctx); + } + #endregion + + #region NetworkImplementation + public Task GetNetmapSnapshotAsync(CallContext ctx) + { + return GetNetmapService().GetNetmapSnapshotAsync(ctx); + } + + public Task GetNodeInfoAsync(CallContext ctx) + { + return GetNetmapService().GetLocalNodeInfoAsync(ctx); + } + + public Task GetNetworkSettingsAsync(CallContext ctx) + { + return GetNetmapService().GetNetworkSettingsAsync(ctx); + } + #endregion + + #region ObjectImplementation + public Task GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx) + { + return GetObjectService().GetObjectHeadAsync(args, ctx); + } + + public Task GetObjectAsync(PrmObjectGet args, CallContext ctx) + { + return GetObjectService().GetObjectAsync(args, ctx); + } + + public Task GetRangeAsync(PrmRangeGet args, CallContext ctx) + { + return GetObjectService().GetRangeAsync(args, ctx); + } + + public Task[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx) + { + return GetObjectService().GetRangeHashAsync(args, ctx); + } + + public Task PutObjectAsync(PrmObjectPut args, CallContext ctx) + { + return GetObjectService().PutStreamObjectAsync(args, ctx); + } + + public Task PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx) + { + return GetObjectService().PutClientCutSingleObjectAsync(args, ctx); + // return GetObjectService().PutClientCutObjectAsync(args, ctx); + } + + public Task PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx) + { + return GetObjectService().PutSingleObjectAsync(args, ctx); + } + + public Task PatchObjectAsync(PrmObjectPatch args, CallContext ctx) + { + return GetObjectService().PatchObjectAsync(args, ctx); + } + + public Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx) + { + return GetObjectService().DeleteObjectAsync(args, ctx); + } + + public IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args, CallContext ctx) + { + return GetObjectService().SearchObjectsAsync(args, ctx); + } + #endregion + + #region Session Implementation + public async Task CreateSessionAsync(PrmSessionCreate args, CallContext ctx) + { + var token = await CreateSessionInternalAsync(args, ctx).ConfigureAwait(false); + + return new FrostFsSessionToken(token); + } + + internal Task CreateSessionInternalAsync(PrmSessionCreate args, CallContext ctx) + { + var service = GetSessionService(); + return service.CreateSessionAsync(args, ctx); + } + #endregion + + #region Accounting Implementation + public async Task GetBalanceAsync(CallContext ctx) + { + return await GetAccouningService().GetBallance(ctx).ConfigureAwait(false); + } + #endregion + + private CallInvoker? CreateInvoker() + { + CallInvoker? callInvoker = null; + + if (ClientCtx.Interceptors != null) + { + foreach (var interceptor in ClientCtx.Interceptors) + callInvoker = AddInvoker(callInvoker, interceptor); + } + + if (ClientCtx.Callback != null) + callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ClientCtx.Callback)); + + if (ClientCtx.PoolErrorHandler != null) + callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ClientCtx.PoolErrorHandler)); + + return callInvoker; + + CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor) + { + if (callInvoker == null) + callInvoker = ClientCtx.Channel.Intercept(interceptor); + else + callInvoker = callInvoker.Intercept(interceptor); + + return callInvoker; + } + } + + private NetmapServiceProvider GetNetmapService() + { + if (NetmapServiceProvider == null) + { + var invoker = CreateInvoker(); + + NetmapServiceClient ??= ( + invoker != null + ? new NetmapServiceClient(invoker) + : new NetmapServiceClient(ClientCtx.Channel)); + + NetmapServiceProvider = new NetmapServiceProvider(NetmapServiceClient, ClientCtx); + } + + return NetmapServiceProvider; + } + + private SessionServiceProvider GetSessionService() + { + if (SessionServiceProvider == null) + { + var invoker = CreateInvoker(); + + SessionServiceClient ??= ( + invoker != null + ? new SessionServiceClient(invoker) + : new SessionServiceClient(ClientCtx.Channel)); + + SessionServiceProvider = new SessionServiceProvider(SessionServiceClient, ClientCtx); + } + + return SessionServiceProvider; + } + + private ApeManagerServiceProvider GetApeManagerService() + { + if (ApeManagerServiceProvider == null) + { + var invoker = CreateInvoker(); + + ApeManagerServiceClient ??= ( + invoker != null + ? new APEManagerServiceClient(invoker) + : new APEManagerServiceClient(ClientCtx.Channel)); + + ApeManagerServiceProvider = new ApeManagerServiceProvider(ApeManagerServiceClient, ClientCtx); + } + + return ApeManagerServiceProvider; + } + + private AccountingServiceProvider GetAccouningService() + { + if (this.AccountingServiceProvider == null) + { + var invoker = CreateInvoker(); + + AccountingServiceClient ??= ( + invoker != null + ? new AccountingServiceClient(invoker) + : new AccountingServiceClient(ClientCtx.Channel)); + + AccountingServiceProvider = new AccountingServiceProvider(AccountingServiceClient, ClientCtx); + } + + return AccountingServiceProvider; + } + + private ContainerServiceProvider GetContainerService() + { + if (this.ContainerServiceProvider == null) + { + var invoker = CreateInvoker(); + + ContainerServiceClient ??= ( + invoker != null + ? new ContainerServiceClient(invoker) + : new ContainerServiceClient(ClientCtx.Channel)); + + ContainerServiceProvider = new ContainerServiceProvider(ContainerServiceClient, ClientCtx); + } + + return ContainerServiceProvider; + } + + private ObjectServiceProvider GetObjectService() + { + if (this.ObjectServiceProvider == null) + { + var invoker = CreateInvoker(); + + ObjectServiceClient ??= ( + invoker != null + ? new ObjectServiceClient(invoker) + : new ObjectServiceClient(ClientCtx.Channel)); + + ObjectServiceProvider = new ObjectServiceProvider(ObjectServiceClient, ClientCtx); + } + + return ObjectServiceProvider; + } +} diff --git a/src/FrostFS.SDK.Client/GlobalSuppressions.cs b/src/FrostFS.SDK.Client/GlobalSuppressions.cs new file mode 100644 index 00000000..29a5b5c4 --- /dev/null +++ b/src/FrostFS.SDK.Client/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "", Scope = "member", Target = "~M:FrostFS.SDK.Client.Sampler.Next~System.Int32")] diff --git a/src/FrostFS.SDK.Client/Interceptors/ErrorInterceptor.cs b/src/FrostFS.SDK.Client/Interceptors/ErrorInterceptor.cs new file mode 100644 index 00000000..771a0f25 --- /dev/null +++ b/src/FrostFS.SDK.Client/Interceptors/ErrorInterceptor.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; + +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK.Client; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", + Justification = "parameters are provided by GRPC infrastructure")] +public class ErrorInterceptor(Action handler) : Interceptor +{ + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + var call = continuation(request, context); + + return new AsyncUnaryCall( + HandleUnaryResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) + { + var call = continuation(context); + + return new AsyncClientStreamingCall( + call.RequestStream, + HandleStreamResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + private async Task HandleUnaryResponse(AsyncUnaryCall call) + { + try + { + return await call; + } + catch (Exception ex) + { + handler(ex); + throw; + } + } + + private async Task HandleStreamResponse(AsyncClientStreamingCall call) + { + try + { + return await call; + } + catch (Exception ex) + { + handler(ex); + throw; + } + } +} diff --git a/src/FrostFS.SDK.Client/Interceptors/MetricsInterceptor.cs b/src/FrostFS.SDK.Client/Interceptors/MetricsInterceptor.cs new file mode 100644 index 00000000..414551e8 --- /dev/null +++ b/src/FrostFS.SDK.Client/Interceptors/MetricsInterceptor.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK.Client; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", + Justification = "parameters are provided by GRPC infrastructure")] +public class MetricsInterceptor(Action callback) : Interceptor +{ + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + var call = continuation(request, context); + + return new AsyncUnaryCall( + HandleUnaryResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) + { + var call = continuation(context); + + return new AsyncClientStreamingCall( + call.RequestStream, + HandleStreamResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + private async Task HandleUnaryResponse(AsyncUnaryCall call) + { + var watch = new Stopwatch(); + watch.Start(); + + var response = await call; + + watch.Stop(); + + var elapsed = watch.ElapsedTicks * 1_000_000 / Stopwatch.Frequency; + + callback(new CallStatistics { MethodName = call.ToString(), ElapsedMicroSeconds = elapsed }); + + return response; + } + + private async Task HandleStreamResponse(AsyncClientStreamingCall call) + { + var watch = new Stopwatch(); + watch.Start(); + + var response = await call; + + watch.Stop(); + + var elapsed = watch.ElapsedTicks * 1_000_000 / Stopwatch.Frequency; + + callback(new CallStatistics { MethodName = call.ToString(), ElapsedMicroSeconds = elapsed }); + + return response; + } +} diff --git a/src/FrostFS.SDK.Client/Interfaces/IFrostFSClient.cs b/src/FrostFS.SDK.Client/Interfaces/IFrostFSClient.cs new file mode 100644 index 00000000..43dc3ac4 --- /dev/null +++ b/src/FrostFS.SDK.Client/Interfaces/IFrostFSClient.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace FrostFS.SDK.Client.Interfaces; + +public interface IFrostFSClient +{ + #region Network + Task GetNetmapSnapshotAsync(CallContext ctx); + + Task GetNodeInfoAsync(CallContext ctx); + + Task GetNetworkSettingsAsync(CallContext ctx); + #endregion + + #region Session + Task CreateSessionAsync(PrmSessionCreate args, CallContext ctx); + #endregion + + #region ApeManager + Task> AddChainAsync(PrmApeChainAdd args, CallContext ctx); + + Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx); + + Task ListChainAsync(PrmApeChainList args, CallContext ctx); + #endregion + + #region Container + Task GetContainerAsync(PrmContainerGet args, CallContext ctx); + + IAsyncEnumerable ListContainersAsync(PrmContainerGetAll args, CallContext ctx); + + [Obsolete("Use PutContainerAsync method")] + Task CreateContainerAsync(PrmContainerCreate args, CallContext ctx); + + Task PutContainerAsync(PrmContainerCreate args, CallContext ctx); + + Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx); + #endregion + + #region Object + Task GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx); + + Task GetObjectAsync(PrmObjectGet args, CallContext ctx); + + Task GetRangeAsync(PrmRangeGet args, CallContext ctx); + + Task[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx); + + Task PutObjectAsync(PrmObjectPut args, CallContext ctx); + + Task PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx); + + Task PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx); + + Task PatchObjectAsync(PrmObjectPatch args, CallContext ctx); + + Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx); + + IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args, CallContext ctx); + #endregion + + #region Account + Task GetBalanceAsync(CallContext ctx); + #endregion +} diff --git a/src/FrostFS.SDK.Client/Interfaces/IObjectWriter.cs b/src/FrostFS.SDK.Client/Interfaces/IObjectWriter.cs new file mode 100644 index 00000000..2d1abda0 --- /dev/null +++ b/src/FrostFS.SDK.Client/Interfaces/IObjectWriter.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace FrostFS.SDK.Client.Interfaces +{ + public interface IObjectWriter : IDisposable + { + Task WriteAsync(ReadOnlyMemory memory); + + Task CompleteAsync(); + } +} diff --git a/src/FrostFS.SDK.Client/Logging/FrostFsMessages.cs b/src/FrostFS.SDK.Client/Logging/FrostFsMessages.cs new file mode 100644 index 00000000..b5e78abd --- /dev/null +++ b/src/FrostFS.SDK.Client/Logging/FrostFsMessages.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; + +namespace FrostFS.SDK.Client; + +internal static partial class FrostFsMessages +{ + [LoggerMessage(100, + LogLevel.Warning, + "Failed to create frostfs session token for client. Address {address}, {error}", + EventName = nameof(SessionCreationError))] + internal static partial void SessionCreationError(ILogger logger, string address, string error); + + [LoggerMessage(101, + LogLevel.Warning, + "Error threshold reached. Address {address}, threshold {threshold}", + EventName = nameof(ErrorЕhresholdReached))] + internal static partial void ErrorЕhresholdReached(ILogger logger, string address, uint threshold); + + [LoggerMessage(102, + LogLevel.Warning, + "Health has changed: {address} healthy {healthy}, reason {error}", + EventName = nameof(HealthChanged))] + internal static partial void HealthChanged(ILogger logger, string address, bool healthy, string error); +} diff --git a/src/FrostFS.SDK.Client/Mappers/Container.cs b/src/FrostFS.SDK.Client/Mappers/Container.cs new file mode 100644 index 00000000..7355c4aa --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Container.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; + +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class ContainerMapper +{ + public static FrostFsContainerInfo ToModel(this Container.Container container) + { + if (container == null) + throw new ArgumentNullException(nameof(container)); + + return new FrostFsContainerInfo( + container.PlacementPolicy.ToModel(), + container.Attributes?.Select(a => new FrostFsAttributePair(a.Key, a.Value)).ToArray(), + container.Version?.ToModel(), + container.OwnerId?.ToModel(), + container.Nonce?.ToUuid()); + } +} diff --git a/src/FrostFS.SDK.Client/Mappers/ContainerId.cs b/src/FrostFS.SDK.Client/Mappers/ContainerId.cs new file mode 100644 index 00000000..55b61799 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/ContainerId.cs @@ -0,0 +1,38 @@ +using System; + +using FrostFS.Refs; +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class ContainerIdMapper +{ + public static ContainerID ToMessage(this FrostFsContainerId model) + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + var containerId = model.GetValue() ?? throw new ArgumentNullException(nameof(model)); + + var message = new ContainerID + { + Value = UnsafeByteOperations.UnsafeWrap(Base58.Decode(containerId)) + }; + + return message!; + } + + public static FrostFsContainerId ToModel(this ContainerID message) + { + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } + + return new FrostFsContainerId(Base58.Encode(message.Value.Span)); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/MetaHeader.cs b/src/FrostFS.SDK.Client/Mappers/MetaHeader.cs new file mode 100644 index 00000000..b3fa5e34 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/MetaHeader.cs @@ -0,0 +1,23 @@ +using System; + +using FrostFS.Session; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class MetaHeaderMapper +{ + public static RequestMetaHeader ToMessage(this MetaHeader metaHeader) + { + if (metaHeader is null) + { + throw new ArgumentNullException(nameof(metaHeader)); + } + + return new RequestMetaHeader + { + Version = metaHeader.Version.ToMessage(), + Epoch = metaHeader.Epoch, + Ttl = metaHeader.Ttl + }; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/Netmap/Netmap.cs b/src/FrostFS.SDK.Client/Mappers/Netmap/Netmap.cs new file mode 100644 index 00000000..be378fc5 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Netmap/Netmap.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; + +using FrostFS.Netmap; + +namespace FrostFS.SDK.Client; + +public static class NetmapMapper +{ + public static FrostFsNetmapSnapshot ToModel(this NetmapSnapshotResponse netmap) + { + if (netmap is null) + { + throw new ArgumentNullException(nameof(netmap)); + } + + return new FrostFsNetmapSnapshot( + netmap.Body.Netmap.Epoch, + netmap.Body.Netmap.Nodes + .Select(n => n.ToModel(netmap.MetaHeader.Version)) + .ToArray()); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/Netmap/NodeInfo.cs b/src/FrostFS.SDK.Client/Mappers/Netmap/NodeInfo.cs new file mode 100644 index 00000000..d7340eb9 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Netmap/NodeInfo.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; + +using FrostFS.Netmap; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK.Client; + +public static class NodeInfoMapper +{ + public static FrostFsNodeInfo ToModel(this LocalNodeInfoResponse.Types.Body node) + { + if (node is null) + { + throw new ArgumentNullException(nameof(node)); + } + + return node.NodeInfo.ToModel(node.Version); + } + + public static FrostFsNodeInfo ToModel(this NodeInfo nodeInfo, Refs.Version version) + { + if (nodeInfo is null) + { + throw new ArgumentNullException(nameof(nodeInfo)); + } + + NodeState state = nodeInfo.State switch + { + NodeInfo.Types.State.Unspecified => NodeState.Unspecified, + NodeInfo.Types.State.Online => NodeState.Online, + NodeInfo.Types.State.Offline => NodeState.Offline, + NodeInfo.Types.State.Maintenance => NodeState.Maintenance, + _ => throw new ArgumentException($"Unknown NodeState. Value: '{nodeInfo.State}'.") + }; + + return new FrostFsNodeInfo( + version: version.ToModel(), + state: state, + addresses: [.. nodeInfo.Addresses], + attributes: nodeInfo.Attributes.ToDictionary(n => n.Key, n => n.Value), + publicKey: nodeInfo.PublicKey.Memory + ); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/Netmap/PlacementPolicy.cs b/src/FrostFS.SDK.Client/Mappers/Netmap/PlacementPolicy.cs new file mode 100644 index 00000000..ee1c8a44 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Netmap/PlacementPolicy.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; + +using FrostFS.Netmap; + +namespace FrostFS.SDK.Client; + +public static class PlacementPolicyMapper +{ + public static FrostFsPlacementPolicy ToModel(this PlacementPolicy placementPolicy) + { + if (placementPolicy is null) + { + throw new ArgumentNullException(nameof(placementPolicy)); + } + + return new FrostFsPlacementPolicy( + placementPolicy.Unique, + placementPolicy.ContainerBackupFactor, + [.. placementPolicy.Selectors.Select(s => s.ToModel())], + [.. placementPolicy.Filters.Select(f => f.ToModel())], + [.. placementPolicy.Replicas.Select(r => r.ToModel())] + ); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/Netmap/Replica.cs b/src/FrostFS.SDK.Client/Mappers/Netmap/Replica.cs new file mode 100644 index 00000000..4fa2edd8 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Netmap/Replica.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; + +using FrostFS.Netmap; + +namespace FrostFS.SDK.Client; + +public static class PolicyMapper +{ + public static Replica ToMessage(this FrostFsReplica replica) + { + return new Replica + { + Count = replica.Count, + Selector = replica.Selector, + EcDataCount = replica.EcDataCount, + EcParityCount = replica.EcParityCount + }; + } + + public static FrostFsReplica ToModel(this Replica replica) + { + if (replica is null) + { + throw new ArgumentNullException(nameof(replica)); + } + + return new FrostFsReplica(replica.Count, replica.Selector) + { + EcDataCount = replica.EcDataCount, + EcParityCount = replica.EcParityCount + }; + } + + public static Selector ToMessage(this FrostFsSelector selector) + { + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + return new Selector + { + Name = selector.Name, + Count = selector.Count, + Clause = (Clause)selector.Clause, + Attribute = selector.Attribute, + Filter = selector.Filter + }; + } + + public static FrostFsSelector ToModel(this Selector selector) + { + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + var model = new FrostFsSelector(selector.Name) + { + Count = selector.Count, + Clause = (int)selector.Clause, + Attribute = selector.Attribute, + Filter = selector.Filter + }; + + return model; + } + + public static Filter ToMessage(this FrostFsFilter filter) + { + if (filter is null) + { + throw new ArgumentNullException(nameof(filter)); + } + + var message = new Filter + { + Name = filter.Name, + Key = filter.Key, + Op = (Operation)filter.Operation, + Value = filter.Value, + }; + + message.Filters.AddRange(filter.Filters.Select(f => f.ToMessage())); + + return message; + } + + public static FrostFsFilter ToModel(this Filter filter) + { + if (filter is null) + { + throw new ArgumentNullException(nameof(filter)); + } + + var model = new FrostFsFilter( + filter.Name, + filter.Key, + (int)filter.Op, + filter.Value, + [.. filter.Filters.Select(f => f.ToModel())]); + + return model; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/Object/Object.cs b/src/FrostFS.SDK.Client/Mappers/Object/Object.cs new file mode 100644 index 00000000..5f85fc50 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Object/Object.cs @@ -0,0 +1,12 @@ +namespace FrostFS.SDK.Client.Mappers.GRPC; + +internal static class ObjectMapper +{ + internal static FrostFsObject ToModel(this Object.Object obj) + { + return new FrostFsObject(obj.Header.ToModel()) + { + ObjectId = FrostFsObjectId.FromHash(obj.ObjectId.Value.Span) + }; + } +} diff --git a/src/FrostFS.SDK.Client/Mappers/Object/ObjectAttributeMapper.cs b/src/FrostFS.SDK.Client/Mappers/Object/ObjectAttributeMapper.cs new file mode 100644 index 00000000..a28100e8 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Object/ObjectAttributeMapper.cs @@ -0,0 +1,27 @@ +using System; + +using FrostFS.Object; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class ObjectAttributeMapper +{ + public static Header.Types.Attribute ToMessage(this FrostFsAttributePair attribute) + { + return new Header.Types.Attribute + { + Key = attribute.Key, + Value = attribute.Value + }; + } + + public static FrostFsAttributePair ToModel(this Header.Types.Attribute attribute) + { + if (attribute is null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return new FrostFsAttributePair(attribute.Key, attribute.Value); + } +} diff --git a/src/FrostFS.SDK.Client/Mappers/Object/ObjectFilterMapper.cs b/src/FrostFS.SDK.Client/Mappers/Object/ObjectFilterMapper.cs new file mode 100644 index 00000000..249045dc --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Object/ObjectFilterMapper.cs @@ -0,0 +1,34 @@ +using System; + +using FrostFS.Object; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class ObjectFilterMapper +{ + public static SearchRequest.Types.Body.Types.Filter ToMessage(this IObjectFilter filter) + { + if (filter is null) + { + throw new ArgumentNullException(nameof(filter)); + } + + var objMatchTypeName = filter.MatchType switch + { + FrostFsMatchType.Unspecified => MatchType.Unspecified, + FrostFsMatchType.Equals => MatchType.StringEqual, + FrostFsMatchType.NotEquals => MatchType.StringNotEqual, + FrostFsMatchType.KeyAbsent => MatchType.NotPresent, + FrostFsMatchType.StartsWith => MatchType.CommonPrefix, + + _ => throw new ArgumentException($"Unknown MatchType. Value: '{filter.MatchType}'.") + }; + + return new SearchRequest.Types.Body.Types.Filter + { + MatchType = objMatchTypeName, + Key = filter.Key, + Value = filter.GetSerializedValue() + }; + } +} diff --git a/src/FrostFS.SDK.Client/Mappers/Object/ObjectHeaderMapper.cs b/src/FrostFS.SDK.Client/Mappers/Object/ObjectHeaderMapper.cs new file mode 100644 index 00000000..fde13ff4 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Object/ObjectHeaderMapper.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; + +using FrostFS.Object; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class ObjectHeaderMapper +{ + public static FrostFsObjectHeader ToModel(this Header header) + { + if (header is null) + { + throw new ArgumentNullException(nameof(header)); + } + + var objTypeName = header.ObjectType switch + { + ObjectType.Regular => FrostFsObjectType.Regular, + ObjectType.Lock => FrostFsObjectType.Lock, + ObjectType.Tombstone => FrostFsObjectType.Tombstone, + _ => throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.") + }; + + FrostFsSplit? split = header!.Split != null + ? header.Split.ToModel() + : null; + + var model = new FrostFsObjectHeader( + new FrostFsContainerId(Base58.Encode(header.ContainerId.Value.Span)), + objTypeName, + [.. header.Attributes.Select(attribute => attribute.ToModel())], + split, + header.OwnerId.ToModel(), + header.Version.ToModel()) + { + PayloadLength = header.PayloadLength, + }; + + return model; + } + + public static FrostFsSplit ToModel(this Header.Types.Split split) + { + if (split is null) + { + throw new ArgumentNullException(nameof(split)); + } + + var children = split!.Children.Count != 0 + ? new ReadOnlyCollection([.. split.Children.Select(x => x.ToModel())]) + : null; + + return new FrostFsSplit(new SplitId(split.SplitId.ToUuid()), + split.Previous?.ToModel(), + split.Parent?.ToModel(), + split.ParentHeader?.ToModel(), + null, + children); + } +} diff --git a/src/FrostFS.SDK.Client/Mappers/Object/ObjectId.cs b/src/FrostFS.SDK.Client/Mappers/Object/ObjectId.cs new file mode 100644 index 00000000..5562be5e --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Object/ObjectId.cs @@ -0,0 +1,33 @@ +using System; + +using FrostFS.Refs; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class ObjectIdMapper +{ + public static ObjectID ToMessage(this FrostFsObjectId objectId) + { + if (objectId is null) + { + throw new ArgumentNullException(nameof(objectId)); + } + + return new ObjectID + { + Value = UnsafeByteOperations.UnsafeWrap(objectId.ToHash()) + }; + } + + public static FrostFsObjectId ToModel(this ObjectID objectId) + { + if (objectId is null) + { + throw new ArgumentNullException(nameof(objectId)); + } + + return FrostFsObjectId.FromHash(objectId.Value.Span); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/OwnerId.cs b/src/FrostFS.SDK.Client/Mappers/OwnerId.cs new file mode 100644 index 00000000..54f7a21a --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/OwnerId.cs @@ -0,0 +1,54 @@ +using System; + +using FrostFS.Refs; +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +using Microsoft.Extensions.Caching.Memory; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class OwnerIdMapper +{ + private static readonly MemoryCacheEntryOptions _oneHourExpiration = new MemoryCacheEntryOptions() + .SetSlidingExpiration(TimeSpan.FromHours(1)) + .SetSize(1); + + public static OwnerID ToMessage(this FrostFsOwner model) + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (!Caches.Owners.TryGetValue(model, out OwnerID? message)) + { + message = new OwnerID + { + Value = UnsafeByteOperations.UnsafeWrap(model.ToHash()) + }; + + Caches.Owners.Set(model, message, _oneHourExpiration); + } + + return message!; + } + + public static FrostFsOwner ToModel(this OwnerID message) + { + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } + + if (!Caches.Owners.TryGetValue(message, out FrostFsOwner? model)) + { + model = new FrostFsOwner(Base58.Encode(message.Value.Span)); + + Caches.Owners.Set(message, model, _oneHourExpiration); + } + + return model!; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/SignatureMapper.cs b/src/FrostFS.SDK.Client/Mappers/SignatureMapper.cs new file mode 100644 index 00000000..ffae7463 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/SignatureMapper.cs @@ -0,0 +1,31 @@ +using System; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class SignatureMapper +{ + public static Refs.Signature ToMessage(this FrostFsSignature signature) + { + if (signature is null) + { + throw new ArgumentNullException(nameof(signature)); + } + + var scheme = signature.Scheme switch + { + SignatureScheme.EcdsaRfc6979Sha256 => Refs.SignatureScheme.EcdsaRfc6979Sha256, + SignatureScheme.EcdsaRfc6979Sha256WalletConnect => Refs.SignatureScheme.EcdsaRfc6979Sha256WalletConnect, + SignatureScheme.EcdsaSha512 => Refs.SignatureScheme.EcdsaSha512, + _ => throw new ArgumentException(nameof(signature.Scheme), $"Unexpected enum value: {signature.Scheme}") + }; + + return new Refs.Signature + { + Key = UnsafeByteOperations.UnsafeWrap(signature.Key), + Scheme = scheme, + Sign = UnsafeByteOperations.UnsafeWrap(signature.Sign) + }; + } +} diff --git a/src/FrostFS.SDK.Client/Mappers/Status.cs b/src/FrostFS.SDK.Client/Mappers/Status.cs new file mode 100644 index 00000000..f3da090a --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Status.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class StatusMapper +{ + public static FrostFsResponseStatus ToModel(this Status.Status status) + { + if (status is null) + return new FrostFsResponseStatus(FrostFsStatusCode.Success); + + var codeName = Enum.GetName(typeof(FrostFsStatusCode), status.Code); + + return codeName is null + ? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.") + : new FrostFsResponseStatus( + (FrostFsStatusCode)status.Code, + status.Message, + string.Join(", ", status.Details.Select(d => System.Text.Encoding.UTF8.GetString([.. d.Value])))); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Mappers/Version.cs b/src/FrostFS.SDK.Client/Mappers/Version.cs new file mode 100644 index 00000000..af8738d6 --- /dev/null +++ b/src/FrostFS.SDK.Client/Mappers/Version.cs @@ -0,0 +1,85 @@ +using System.Collections; +using System.Threading; + +using FrostFS.Refs; + +namespace FrostFS.SDK.Client.Mappers.GRPC; + +public static class VersionMapper +{ + private static readonly Hashtable _cacheMessages = []; + private static readonly Hashtable _cacheModels = []; + private static SpinLock _spinlock; + + public static Version ToMessage(this FrostFsVersion model) + { + if (model is null) + { + throw new System.ArgumentNullException(nameof(model)); + } + + var key = (int)model.Major << 16 + (int)model.Minor; + + if (!_cacheMessages.ContainsKey(key)) + { + bool lockTaken = false; + try + { + _spinlock.Enter(ref lockTaken); + var message = new Version + { + Major = model.Major, + Minor = model.Minor + }; + + _cacheMessages.Add(key, message); + return message; + } + catch (System.ArgumentException) + { + // ignore attempt to add duplicate + } + finally + { + if (lockTaken) + _spinlock.Exit(false); + } + } + + return (Version)_cacheMessages[key]; + } + + public static FrostFsVersion ToModel(this Version message) + { + if (message is null) + { + throw new System.ArgumentNullException(nameof(message)); + } + + var key = (int)message.Major << 16 + (int)message.Minor; + + if (!_cacheModels.ContainsKey(key)) + { + bool lockTaken = false; + try + { + _spinlock.Enter(ref lockTaken); + var model = new FrostFsVersion(message.Major, message.Minor); + + _cacheModels.Add(key, model); + return model; + } + catch (System.ArgumentException) + { + // ignore attempt to add duplicate + } + finally + { + if (lockTaken) + _spinlock.Exit(false); + } + } + + return (FrostFsVersion)_cacheModels[key]; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Client/ClientSettings.cs b/src/FrostFS.SDK.Client/Models/Client/ClientSettings.cs new file mode 100644 index 00000000..4aa00f9c --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Client/ClientSettings.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text; + +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK; + +public class ClientSettings +{ + protected static readonly string errorTemplate = "{0} is required parameter"; + + public string Host { get; set; } = string.Empty; + + public string Key { get; set; } = string.Empty; + + public Action? Callback { get; set; } + + public Collection Interceptors { get; } = []; + + public void Validate() + { + StringBuilder? errors = null; + + if (string.IsNullOrWhiteSpace(Host)) + (errors = new StringBuilder(128)).AppendLine(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Host))); + + if (string.IsNullOrWhiteSpace(Key)) + (errors ??= new StringBuilder(128)).AppendLine(string.Format(CultureInfo.InvariantCulture, errorTemplate, nameof(Key))); + + if (errors != null) + { + throw new ArgumentException(errors.ToString()); + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Containers/FrostFsContainerId.cs b/src/FrostFS.SDK.Client/Models/Containers/FrostFsContainerId.cs new file mode 100644 index 00000000..e91e1b83 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Containers/FrostFsContainerId.cs @@ -0,0 +1,55 @@ +using FrostFS.Refs; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK; + +public class FrostFsContainerId +{ + private string? modelId; + private ContainerID? containerID; + + public FrostFsContainerId(string id) + { + this.modelId = id; + } + + internal FrostFsContainerId(ContainerID id) + { + this.containerID = id; + } + + public string GetValue() + { + if (this.modelId != null) + return this.modelId; + + if (containerID != null) + { + this.modelId = Base58.Encode(containerID.Value.Span); + return this.modelId; + } + + throw new FrostFsInvalidObjectException(); + } + + public ContainerID GetContainerID() + { + if (this.containerID != null) + return this.containerID; + + if (modelId != null) + { + this.containerID = this.ToMessage(); + return this.containerID; + } + + throw new FrostFsInvalidObjectException(); + } + + public override string ToString() + { + return GetValue(); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Containers/FrostFsContainerInfo.cs b/src/FrostFS.SDK.Client/Models/Containers/FrostFsContainerInfo.cs new file mode 100644 index 00000000..c7dc782c --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Containers/FrostFsContainerInfo.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; + +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +namespace FrostFS.SDK; + +public class FrostFsContainerInfo +{ + private Container.Container.Types.Attribute[]? grpsAttributes; + private ReadOnlyCollection? attributes; + private FrostFsPlacementPolicy? placementPolicy; + private Guid? nonce; + + private Container.Container? container; + + public FrostFsContainerInfo( + FrostFsPlacementPolicy placementPolicy, + FrostFsAttributePair[]? attributes = null, + FrostFsVersion? version = null, + FrostFsOwner? owner = null, + Guid? nonce = null) + { + this.placementPolicy = placementPolicy; + Version = version; + Owner = owner; + this.nonce = nonce; + + if (attributes != null) + this.attributes = new ReadOnlyCollection(attributes); + } + + internal FrostFsContainerInfo(Container.Container container) + { + this.container = container; + } + + public Guid Nonce + { + get + { + nonce ??= container?.Nonce != null ? container.Nonce.ToUuid() : Guid.NewGuid(); + return nonce.Value; + } + } + + public FrostFsPlacementPolicy? PlacementPolicy + { + get + { + placementPolicy ??= container?.PlacementPolicy?.ToModel(); + return placementPolicy; + } + } + + public ReadOnlyCollection? Attributes + { + get + { + if (attributes == null && grpsAttributes != null) + attributes = new ReadOnlyCollection(grpsAttributes.Select(a => new FrostFsAttributePair(a.Key, a.Value)).ToList()); + + return attributes; + } + } + + public FrostFsVersion? Version { get; private set; } + + public FrostFsOwner? Owner { get; private set; } + + internal Container.Container.Types.Attribute[]? GetGrpsAttributes() + { + grpsAttributes ??= Attributes? + .Select(a => new Container.Container.Types.Attribute { Key = a.Key, Value = a.Value }) + .ToArray(); + + return grpsAttributes; + } + + internal Container.Container GetContainer() + { + if (this.container == null) + { + if (PlacementPolicy == null) + { + throw new ArgumentNullException("PlacementPolicy is null"); + } + + Span nonce = stackalloc byte[16]; + Nonce.ToBytes(nonce); + + this.container = new Container.Container() + { + PlacementPolicy = PlacementPolicy.Value.GetPolicy(), + Nonce = ByteString.CopyFrom(nonce), + OwnerId = Owner?.OwnerID, + Version = Version?.VersionID + }; + + var attribs = GetGrpsAttributes(); + if (attribs != null) + this.container.Attributes.AddRange(attribs); + } + + return this.container; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Enums/ObjectMatchType.cs b/src/FrostFS.SDK.Client/Models/Enums/FrostFsMatchType.cs similarity index 59% rename from src/FrostFS.SDK.ModelsV2/Enums/ObjectMatchType.cs rename to src/FrostFS.SDK.Client/Models/Enums/FrostFsMatchType.cs index 4d6026c9..b9b3659c 100644 --- a/src/FrostFS.SDK.ModelsV2/Enums/ObjectMatchType.cs +++ b/src/FrostFS.SDK.Client/Models/Enums/FrostFsMatchType.cs @@ -1,6 +1,6 @@ -namespace FrostFS.SDK.ModelsV2.Enums; +namespace FrostFS.SDK; -public enum ObjectMatchType +public enum FrostFsMatchType { Unspecified = 0, Equals = 1, diff --git a/src/FrostFS.SDK.Client/Models/Enums/FrostFsObjectType.cs b/src/FrostFS.SDK.Client/Models/Enums/FrostFsObjectType.cs new file mode 100644 index 00000000..793fe2ef --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Enums/FrostFsObjectType.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK; + +public enum FrostFsObjectType +{ + Regular = 0, + Tombstone = 1, + Lock = 3 +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Enums/StatusCode.cs b/src/FrostFS.SDK.Client/Models/Enums/FrostFsStatusCode.cs similarity index 88% rename from src/FrostFS.SDK.ModelsV2/Enums/StatusCode.cs rename to src/FrostFS.SDK.Client/Models/Enums/FrostFsStatusCode.cs index df0fa57f..7eb185c8 100644 --- a/src/FrostFS.SDK.ModelsV2/Enums/StatusCode.cs +++ b/src/FrostFS.SDK.Client/Models/Enums/FrostFsStatusCode.cs @@ -1,6 +1,6 @@ -namespace FrostFS.SDK.ModelsV2.Enums; +namespace FrostFS.SDK; -public enum StatusCode +public enum FrostFsStatusCode { Success = 0, Internal = 1024, diff --git a/src/FrostFS.SDK.ModelsV2/Enums/NodeState.cs b/src/FrostFS.SDK.Client/Models/Enums/NodeState.cs similarity index 72% rename from src/FrostFS.SDK.ModelsV2/Enums/NodeState.cs rename to src/FrostFS.SDK.Client/Models/Enums/NodeState.cs index 2293c0f6..2821e557 100644 --- a/src/FrostFS.SDK.ModelsV2/Enums/NodeState.cs +++ b/src/FrostFS.SDK.Client/Models/Enums/NodeState.cs @@ -1,4 +1,4 @@ -namespace FrostFS.SDK.ModelsV2.Enums; +namespace FrostFS.SDK; public enum NodeState { diff --git a/src/FrostFS.SDK.Client/Models/Enums/SignatureScheme.cs b/src/FrostFS.SDK.Client/Models/Enums/SignatureScheme.cs new file mode 100644 index 00000000..4b5634c2 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Enums/SignatureScheme.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK; + +public enum SignatureScheme +{ + EcdsaSha512, + EcdsaRfc6979Sha256, + EcdsaRfc6979Sha256WalletConnect +} diff --git a/src/FrostFS.SDK.Client/Models/Misc/CallStatistics.cs b/src/FrostFS.SDK.Client/Models/Misc/CallStatistics.cs new file mode 100644 index 00000000..706b3f24 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Misc/CallStatistics.cs @@ -0,0 +1,7 @@ +namespace FrostFS.SDK; + +public class CallStatistics +{ + public string? MethodName { get; set; } + public long ElapsedMicroSeconds { get; set; } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Misc/CheckSum.cs b/src/FrostFS.SDK.Client/Models/Misc/CheckSum.cs new file mode 100644 index 00000000..22412279 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Misc/CheckSum.cs @@ -0,0 +1,21 @@ +using System; + +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK; + +public class CheckSum +{ + private byte[]? hash; + private string? text; + + public static CheckSum CreateCheckSum(byte[] content) + { + return new CheckSum { hash = DataHasher.Sha256(content.AsSpan()) }; + } + + public override string ToString() + { + return text ??= BitConverter.ToString(hash).Replace("-", ""); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Misc/Constants.cs b/src/FrostFS.SDK.Client/Models/Misc/Constants.cs new file mode 100644 index 00000000..28c8cf02 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Misc/Constants.cs @@ -0,0 +1,52 @@ +namespace FrostFS.SDK; + +public static class Constants +{ + public const int ObjectChunkSize = 3 * (1 << 20); + public const int Sha256HashLength = 32; + + // HeaderPrefix is a prefix of key to object header value or property. + public const string HeaderPrefix = "$Object:"; + + // FilterHeaderVersion is a filter key to "version" field of the object header. + public const string FilterHeaderVersion = HeaderPrefix + "version"; + + // FilterHeaderObjectID is a filter key to "object_id" field of the object. + public const string FilterHeaderObjectID = HeaderPrefix + "objectID"; + + // FilterHeaderContainerID is a filter key to "container_id" field of the object header. + public const string FilterHeaderContainerID = HeaderPrefix + "containerID"; + + // FilterHeaderOwnerID is a filter key to "owner_id" field of the object header. + public const string FilterHeaderOwnerID = HeaderPrefix + "ownerID"; + + // FilterHeaderCreationEpoch is a filter key to "creation_epoch" field of the object header. + public const string FilterHeaderCreationEpoch = HeaderPrefix + "creationEpoch"; + + // FilterHeaderPayloadLength is a filter key to "payload_length" field of the object header. + public const string FilterHeaderPayloadLength = HeaderPrefix + "payloadLength"; + + // FilterHeaderPayloadHash is a filter key to "payload_hash" field of the object header. + public const string FilterHeaderPayloadHash = HeaderPrefix + "payloadHash"; + + // FilterHeaderObjectType is a filter key to "object_type" field of the object header. + public const string FilterHeaderObjectType = HeaderPrefix + "objectType"; + + // FilterHeaderHomomorphicHash is a filter key to "homomorphic_hash" field of the object header. + public const string FilterHeaderHomomorphicHash = HeaderPrefix + "homomorphicHash"; + + // FilterHeaderParent is a filter key to "split.parent" field of the object header. + public const string FilterHeaderParent = HeaderPrefix + "split.parent"; + + // FilterHeaderSplitID is a filter key to "split.splitID" field of the object header. + public const string FilterHeaderSplitID = HeaderPrefix + "split.splitID"; + + // FilterHeaderECParent is a filter key to "ec.parent" field of the object header. + public const string FilterHeaderECParent = HeaderPrefix + "ec.parent"; + + // FilterPropertyRoot is a filter key to check if regular object is on top of split hierarchy. + public const string FilterHeaderRoot = HeaderPrefix + "ROOT"; + + // FilterPropertyPhy is a filter key to check if an object physically stored on a node. + public const string FilterHeaderPhy = HeaderPrefix + "PHY"; +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs new file mode 100644 index 00000000..11ee079b --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsFilter.cs @@ -0,0 +1,28 @@ +using System.Linq; +using FrostFS.Netmap; + +namespace FrostFS.SDK; + +public class FrostFsFilter(string name, string key, int operation, string value, FrostFsFilter[] filters) : IFrostFsFilter +{ + public string Name { get; } = name; + public string Key { get; } = key; + public int Operation { get; } = operation; + public string Value { get; } = value; + public FrostFsFilter[] Filters { get; } = filters; + + internal Filter GetMessage() + { + var filter = new Filter() + { + Name = Name, + Key = Key, + Op = (Operation)Operation, + Value = Value, + }; + + filter.Filters.AddRange(Filters.Select(f => f.GetMessage())); + + return filter; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs new file mode 100644 index 00000000..e15c2a85 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNetmapSnapshot.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Models.Netmap.Placement; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK; + +public class FrostFsNetmapSnapshot(ulong epoch, IReadOnlyList nodeInfoCollection) +{ + public ulong Epoch { get; private set; } = epoch; + + public IReadOnlyList NodeInfoCollection { get; private set; } = nodeInfoCollection; + + internal static INormalizer NewReverseMinNorm(double minV) + { + return new ReverseMinNorm { min = minV }; + } + + // newSigmoidNorm returns a normalizer which + // normalize values in range of 0.0 to 1.0 to a scaled sigmoid. + internal static INormalizer NewSigmoidNorm(double scale) + { + return new SigmoidNorm(scale); + } + + // PlacementVectors sorts container nodes returned by ContainerNodes method + // and returns placement vectors for the entity identified by the given pivot. + // For example, in order to build node list to store the object, binary-encoded + // object identifier can be used as pivot. Result is deterministic for + // the fixed NetMap and parameters. + public FrostFsNodeInfo[][] PlacementVectors(FrostFsNodeInfo[][] vectors, byte[] pivot) + { + if (vectors is null) + { + throw new ArgumentNullException(nameof(vectors)); + } + + using var murmur3 = new Murmur3(0); + var hash = murmur3.GetCheckSum64(pivot); + + var wf = Tools.DefaultWeightFunc(NodeInfoCollection.ToArray()); + + var result = new FrostFsNodeInfo[vectors.Length][]; + var maxSize = vectors.Max(x => x.Length); + + var spanWeigths = new double[maxSize]; + + for (int i = 0; i < vectors.Length; i++) + { + result[i] = new FrostFsNodeInfo[vectors[i].Length]; + + for (int j = 0; j < vectors[i].Length; j++) + { + result[i][j] = vectors[i][j]; + } + + Tools.AppendWeightsTo(result[i], wf, ref spanWeigths); + + result[i] = Tools.SortHasherSliceByWeightValue(result[i].ToList(), spanWeigths, hash).ToArray(); + } + + return result; + } + + // SelectFilterNodes returns a two-dimensional list of nodes as a result of applying the + // given SelectFilterExpr to the NetMap. + // If the SelectFilterExpr contains only filters, the result contains a single row with the + // result of the last filter application. + // If the SelectFilterExpr contains only selectors, the result contains the selection rows + // of the last select application. + List> SelectFilterNodes(SelectFilterExpr expr) + { + var policy = new FrostFsPlacementPolicy(false, expr.Cbf, [expr.Selector], expr.Filters); + + var ctx = new Context(this) + { + Cbf = expr.Cbf + }; + + ctx.ProcessFilters(policy); + ctx.ProcessSelectors(policy); + + var ret = new List>(); + + if (expr.Selector == null) + { + var lastFilter = expr.Filters.Last(); + + var subCollestion = new List(); + ret.Add(subCollestion); + + foreach (var nodeInfo in NodeInfoCollection) + { + if (ctx.Match(ctx.ProcessedFilters[lastFilter.Name], nodeInfo)) + { + subCollestion.Add(nodeInfo); + } + } + } + else if (expr.Selector.Name != null) + { + var sel = ctx.GetSelection(ctx.ProcessedSelectors[expr.Selector.Name]); + + foreach (var ns in sel) + { + var subCollestion = new List(); + ret.Add(subCollestion); + foreach (var n in ns) + { + subCollestion.Add(n); + } + } + } + + return ret; + } + + internal static Func NewWeightFunc(INormalizer capNorm, INormalizer priceNorm) + { + return new Func((FrostFsNodeInfo nodeInfo) => + { + return capNorm.Normalize(nodeInfo.GetCapacity()) * priceNorm.Normalize(nodeInfo.Price); + }); + } + + private static FrostFsNodeInfo[] FlattenNodes(List> nodes) + { + int sz = 0; + foreach (var ns in nodes) + { + sz += ns.Count; + } + + var result = new FrostFsNodeInfo[sz]; + + int i = 0; + foreach (var ns in nodes) + { + foreach (var n in ns) + { + result[i++] = n; + } + } + + return result; + } + + // ContainerNodes returns two-dimensional list of nodes as a result of applying + // given PlacementPolicy to the NetMap. Each line of the list corresponds to a + // replica descriptor. Line order corresponds to order of ReplicaDescriptor list + // in the policy. Nodes are pre-filtered according to the Filter list from + // the policy, and then selected by Selector list. Result is deterministic for + // the fixed NetMap and parameters. + // + // Result can be used in PlacementVectors. + public FrostFsNodeInfo[][] ContainerNodes(FrostFsPlacementPolicy p, byte[]? pivot) + { + var c = new Context(this) + { + Cbf = p.BackupFactor == 0 ? 3 : p.BackupFactor + }; + + if (pivot != null && pivot.Length > 0) + { + c.HrwSeed = pivot; + + using var murmur = new Murmur3(0); + c.HrwSeedHash = murmur.GetCheckSum64(pivot); + } + + c.ProcessFilters(p); + c.ProcessSelectors(p); + + var unique = p.IsUnique(); + + var result = new List>(p.Replicas.Length); + for (int i = 0; i < p.Replicas.Length; i++) + { + result.Add([]); + } + + // Note that the cached selectors are not used when the policy contains the UNIQUE flag. + // This is necessary because each selection vector affects potentially the subsequent vectors + // and thus we call getSelection in such case, in order to take into account nodes previously + // marked as used by earlier replicas. + for (int i = 0; i < p.Replicas.Length; i++) + { + var sName = p.Replicas[i].Selector; + + if (string.IsNullOrEmpty(sName) && !(p.Replicas.Length == 1 && p.Selectors.Count == 1)) + { + var s = new FrostFsSelector(string.Empty) + { + Count = p.Replicas[i].CountNodes(), + Filter = Context.mainFilterName + }; + + var nodes = c.GetSelection(s); + result[i].AddRange(FlattenNodes(nodes)); + + if (unique) + { + foreach (var n in result[i]) + { + c.UsedNodes[n.Hash()] = true; + } + } + + continue; + } + + if (unique) + { + if (!c.ProcessedSelectors.TryGetValue(sName, out var s) || s == null) + { + throw new FrostFsException($"selector not found: {sName}"); + } + + var nodes = c.GetSelection(c.ProcessedSelectors[sName]); + + result[i].AddRange(FlattenNodes(nodes)); + + foreach (var n in result[i]) + { + c.UsedNodes[n.Hash()] = true; + } + } + else + { + var nodes = c.Selections[sName]; + result[i].AddRange(FlattenNodes(nodes)); + } + } + + var collection = new FrostFsNodeInfo[result.Count][]; + for (int i = 0; i < result.Count; i++) + { + collection[i] = [.. result[i]]; + } + + return collection; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs new file mode 100644 index 00000000..ea7d7c57 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsNodeInfo.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +using FrostFS.SDK.Client.Models.Netmap.Placement; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK; + +public class FrostFsNodeInfo( + FrostFsVersion version, + NodeState state, + IReadOnlyCollection addresses, + IReadOnlyDictionary attributes, + ReadOnlyMemory publicKey) : IHasher +{ + private ulong _hash; + + // attrPrice is a key to the node attribute that indicates the + // price in GAS tokens for storing one GB of data during one Epoch. + internal const string AttrPrice = "Price"; + + // attrCapacity is a key to the node attribute that indicates the + // total available disk space in Gigabytes. + internal const string AttrCapacity = "Capacity"; + + // attrExternalAddr is a key for the attribute storing node external addresses. + internal const string AttrExternalAddr = "ExternalAddr"; + + // sepExternalAddr is a separator for multi-value ExternalAddr attribute. + internal const string SepExternalAddr = ","; + + private ulong price = ulong.MaxValue; + + public NodeState State { get; } = state; + + public FrostFsVersion Version { get; } = version; + + public IReadOnlyCollection Addresses { get; } = addresses; + + public IReadOnlyDictionary Attributes { get; } = attributes; + + public ReadOnlyMemory PublicKey { get; } = publicKey; + + public ulong Hash() + { + if (_hash == 0) + { + using var murmur3 = new Murmur3(0); + murmur3.Initialize(); + _hash = murmur3.GetCheckSum64(PublicKey.ToArray()); + } + + return _hash; + } + + internal ulong GetCapacity() + { + if (!Attributes.TryGetValue(AttrCapacity, out var val)) + return 0; + + return ulong.Parse(val, CultureInfo.InvariantCulture); + } + + internal ulong Price + { + get + { + if (price == ulong.MaxValue) + { + if (!Attributes.TryGetValue(AttrPrice, out var val)) + price = 0; + else + price = uint.Parse(val, CultureInfo.InvariantCulture); + } + + return price; + } + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs new file mode 100644 index 00000000..61bdb82c --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsPlacementPolicy.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; + +using FrostFS.Netmap; +using FrostFS.SDK.Client; + +namespace FrostFS.SDK; + +public struct FrostFsPlacementPolicy(bool unique, + uint backupFactor, + Collection selectors, + Collection filters, + params FrostFsReplica[] replicas) + : IEquatable +{ + private PlacementPolicy? policy; + + public FrostFsReplica[] Replicas { get; } = replicas; + + public Collection Selectors { get; } = selectors; + + public Collection Filters { get; } = filters; + + public bool Unique { get; } = unique; + + public uint BackupFactor { get; } = backupFactor; + + public override readonly bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + var other = (FrostFsPlacementPolicy)obj; + + return Equals(other); + } + + public PlacementPolicy GetPolicy() + { + if (policy == null) + { + policy = new PlacementPolicy + { + Filters = { }, + Selectors = { }, + Replicas = { }, + Unique = Unique, + ContainerBackupFactor = BackupFactor + }; + + if (Selectors != null && Selectors.Count > 0) + { + policy.Selectors.AddRange(Selectors.Select(s => s.GetMessage())); + } + + if (Filters != null && Filters.Count > 0) + { + policy.Filters.AddRange(Filters.Select(s => s.ToMessage())); + } + + foreach (var replica in Replicas) + { + policy.Replicas.Add(replica.ToMessage()); + } + } + + return policy; + } + + internal readonly bool IsUnique() + { + return Unique || Replicas.Any(r => r.EcDataCount != 0 || r.EcParityCount != 0); + } + + public override readonly int GetHashCode() + { + return Unique ? 17 : 0 + Replicas.GetHashCode(); + } + + public static bool operator ==(FrostFsPlacementPolicy left, FrostFsPlacementPolicy right) + { + return left.Equals(right); + } + + public static bool operator !=(FrostFsPlacementPolicy left, FrostFsPlacementPolicy right) + { + return !(left == right); + } + + public readonly bool Equals(FrostFsPlacementPolicy other) + { + var notEqual = Unique != other.Unique + || Replicas.Length != other.Replicas.Length; + + if (notEqual) + return false; + + foreach (var replica in Replicas) + { + if (!other.Replicas.Any(r => r.Equals(replica))) + return false; + } + + return true; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs new file mode 100644 index 00000000..82e2240d --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsReplica.cs @@ -0,0 +1,59 @@ +using System; + +namespace FrostFS.SDK; + +public struct FrostFsReplica : IEquatable +{ + public uint Count { get; set; } + public string Selector { get; set; } + public uint EcDataCount { get; set; } + public uint EcParityCount { get; set; } + + public FrostFsReplica(uint count, string? selector = null) + { + selector ??= string.Empty; + + Count = count; + Selector = selector; + } + + public override readonly bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + var other = (FrostFsReplica)obj; + + return Count == other.Count && Selector == other.Selector; + } + + public readonly uint CountNodes() + { + return Count != 0 ? Count : EcDataCount + EcParityCount; + } + + public override readonly int GetHashCode() + { + return Count.GetHashCode() ^ Selector.GetHashCode() ^ (int)EcDataCount ^ (int)EcParityCount; + } + + public static bool operator ==(FrostFsReplica left, FrostFsReplica right) + { + return left.Equals(right); + } + + public static bool operator !=(FrostFsReplica left, FrostFsReplica right) + { + return !(left == right); + } + + public readonly bool Equals(FrostFsReplica other) + { + return Count == other.Count + && Selector == other.Selector + && EcDataCount == other.EcDataCount + && EcParityCount == other.EcParityCount; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs new file mode 100644 index 00000000..c0589673 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsSelector.cs @@ -0,0 +1,24 @@ +using FrostFS.Netmap; + +namespace FrostFS.SDK; + +public class FrostFsSelector(string name) +{ + public string Name { get; } = name; + public uint Count { get; set; } + public int Clause { get; set; } + public string? Attribute { get; set; } + public string? Filter { get; set; } + + internal Selector GetMessage() + { + return new Selector() + { + Name = Name, + Clause = (Clause)Clause, + Count = Count, + Filter = Filter ?? string.Empty, + Attribute = Attribute ?? string.Empty, + }; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/FrostFsVersion.cs b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsVersion.cs new file mode 100644 index 00000000..9ed95224 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/FrostFsVersion.cs @@ -0,0 +1,36 @@ +using FrostFS.Refs; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK; + +public class FrostFsVersion(uint major, uint minor) +{ + private Version? version; + + public uint Major { get; set; } = major; + public uint Minor { get; set; } = minor; + + internal Version VersionID + { + get + { + this.version ??= this.ToMessage(); + return this.version; + } + } + + public bool IsSupported(FrostFsVersion version) + { + if (version is null) + { + throw new System.ArgumentNullException(nameof(version)); + } + + return Major == version.Major; + } + + public override string ToString() + { + return $"v{Major}.{Minor}"; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs b/src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs new file mode 100644 index 00000000..e13875aa --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/IFrostFsFilter.cs @@ -0,0 +1,11 @@ +namespace FrostFS.SDK +{ + public interface IFrostFsFilter + { + FrostFsFilter[] Filters { get; } + string Key { get; } + string Name { get; } + int Operation { get; } + string Value { get; } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs b/src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs new file mode 100644 index 00000000..3c014fcc --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/NodeAttrPair.cs @@ -0,0 +1,7 @@ +namespace FrostFS.SDK; + +struct NodeAttrPair +{ + internal string attr; + internal FrostFsNodeInfo[] nodes; +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs new file mode 100644 index 00000000..b9820270 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Clause.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +public enum FrostFsClause +{ + Unspecified = 0, + Same, + Distinct +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs new file mode 100644 index 00000000..aedfd358 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Context.cs @@ -0,0 +1,456 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal struct Context +{ + private const string errInvalidFilterName = "filter name is invalid"; + private const string errInvalidFilterOp = "invalid filter operation"; + private const string errFilterNotFound = "filter not found"; + private const string errNonEmptyFilters = "simple filter contains sub-filters"; + private const string errNotEnoughNodes = "not enough nodes to SELECT from"; + private const string errUnnamedTopFilter = "unnamed top-level filter"; + + internal const string mainFilterName = "*"; + internal const string likeWildcard = "*"; + + // network map to operate on + internal FrostFsNetmapSnapshot NetMap { get; } + + // cache of processed filters + internal Dictionary ProcessedFilters { get; } = []; + + // cache of processed selectors + internal Dictionary ProcessedSelectors { get; } = []; + + // stores results of selector processing + internal Dictionary>> Selections { get; } = []; + + // cache of parsed numeric values + internal Dictionary NumCache { get; } = []; + + internal byte[]? HrwSeed { get; set; } + + // hrw.Hash of hrwSeed + internal ulong HrwSeedHash { get; set; } + + // container backup factor + internal uint Cbf { get; set; } + + // nodes already used in previous selections, which is needed when the placement + // policy uses the UNIQUE flag. Nodes marked as used are not used in subsequent + // base selections. + internal Dictionary UsedNodes { get; } = []; + + // If true, returns an error when netmap does not contain enough nodes for selection. + // By default best effort is taken. + internal bool Strict { get; set; } + + // weightFunc is a weighting function for determining node priority + // which combines low price and high performance + private readonly Func weightFunc; + + public Context(FrostFsNetmapSnapshot netMap) + { + NetMap = netMap; + weightFunc = Tools.DefaultWeightFunc(NetMap.NodeInfoCollection); + } + + internal void ProcessFilters(FrostFsPlacementPolicy policy) + { + foreach (var filter in policy.Filters) + { + ProcessFilter(filter, true); + } + } + + readonly void ProcessFilter(FrostFsFilter filter, bool top) + { + var filterName = filter.Name; + if (filterName == mainFilterName) + { + throw new FrostFsException($"{errInvalidFilterName}: '{errInvalidFilterName}' is reserved"); + } + + if (top && string.IsNullOrEmpty(filterName)) + { + throw new FrostFsException(errUnnamedTopFilter); + } + + if (!top && !string.IsNullOrEmpty(filterName) && !ProcessedFilters.ContainsKey(filterName)) + { + throw new FrostFsException(errFilterNotFound); + } + + if (filter.Operation == (int)Operation.AND || + filter.Operation == (int)Operation.OR || + filter.Operation == (int)Operation.NOT) + { + foreach (var f in filter.Filters) + ProcessFilter(f, false); + } + else + { + if (filter.Filters.Length != 0) + { + throw new FrostFsException(errNonEmptyFilters); + } + else if (!top && !string.IsNullOrEmpty(filterName)) + { + // named reference + return; + } + + switch (filter.Operation) + { + case (int)Operation.EQ: + case (int)Operation.NE: + case (int)Operation.LIKE: + break; + case (int)Operation.GT: + case (int)Operation.GE: + case (int)Operation.LT: + case (int)Operation.LE: + { + var n = uint.Parse(filter.Value, CultureInfo.InvariantCulture); + NumCache[filter.Value] = n; + break; + } + default: + throw new FrostFsException($"{errInvalidFilterOp}: {filter.Operation}"); + } + } + + if (top) + { + ProcessedFilters[filterName] = filter; + } + } + + // processSelectors processes selectors and returns error is any of them is invalid. + internal void ProcessSelectors(FrostFsPlacementPolicy policy) + { + foreach (var selector in policy.Selectors) + { + var filterName = selector.Filter; + if (filterName != mainFilterName) + { + if (selector.Filter == null || !ProcessedFilters.ContainsKey(selector.Filter)) + { + throw new FrostFsException($"{errFilterNotFound}: SELECT FROM '{filterName}'"); + } + } + + ProcessedSelectors[selector.Name] = selector; + + var selection = GetSelection(selector); + + Selections[selector.Name] = selection; + } + } + + // calcNodesCount returns number of buckets and minimum number of nodes in every bucket + // for the given selector. + static (int bucketCount, int nodesInBucket) CalcNodesCount(FrostFsSelector selector) + { + return selector.Clause == (int)FrostFsClause.Same + ? (1, (int)selector.Count) + : ((int)selector.Count, 1); + } + + // getSelectionBase returns nodes grouped by selector attribute. + // It it guaranteed that each pair will contain at least one node. + internal NodeAttrPair[] GetSelectionBase(FrostFsSelector selector) + { + var fName = selector.Filter ?? throw new FrostFsException("Filter name for selector is empty"); + + _ = ProcessedFilters.TryGetValue(fName, out var f); + + var isMain = fName == mainFilterName; + var result = new List(); + + var nodeMap = new Dictionary>(); + var attr = selector.Attribute; + + foreach (var node in NetMap.NodeInfoCollection) + { + if (UsedNodes.ContainsKey(node.Hash())) + { + continue; + } + + if (isMain || Match(f, node)) + { + if (attr == null) + { + // Default attribute is transparent identifier which is different for every node. + result.Add(new NodeAttrPair { attr = "", nodes = [node] }); + } + else + { + var v = node.Attributes[attr]; + if (!nodeMap.TryGetValue(v, out var nodes) || nodes == null) + { + nodeMap[v] = []; + } + + nodeMap[v].Add(node); + } + } + } + + if (!string.IsNullOrEmpty(attr)) + { + foreach (var v in nodeMap) + { + result.Add(new NodeAttrPair() { attr = v.Key, nodes = [.. v.Value] }); + } + } + + if (HrwSeed != null && HrwSeed.Length != 0) + { + double[] ws = []; + + var sortedNodes = new NodeAttrPair[result.Count]; + + for (int i = 0; i < result.Count; i++) + { + var res = result[i]; + Tools.AppendWeightsTo(res.nodes, weightFunc, ref ws); + sortedNodes[i].nodes = Tools.SortHasherSliceByWeightValue(res.nodes.ToList(), ws, HrwSeedHash).ToArray(); + sortedNodes[i].attr = result[i].attr; + } + + return sortedNodes; + } + return [.. result]; + } + + static double CalcBucketWeight(List ns, MeanIQRAgg a, Func wf) + { + foreach (var node in ns) + { + a.Add(wf(node)); + } + + return a.Compute(); + } + + // getSelection returns nodes grouped by s.attribute. + // Last argument specifies if more buckets can be used to fulfill CBF. + internal List> GetSelection(FrostFsSelector s) + { + var (bucketCount, nodesInBucket) = CalcNodesCount(s); + + var buckets = GetSelectionBase(s); + + if (Strict && buckets.Length < bucketCount) + throw new FrostFsException($"errNotEnoughNodes: '{s.Name}'"); + + // We need deterministic output in case there is no pivot. + // If pivot is set, buckets are sorted by HRW. + // However, because initial order influences HRW order for buckets with equal weights, + // we also need to have deterministic input to HRW sorting routine. + if (HrwSeed == null || HrwSeed.Length == 0) + { + buckets = string.IsNullOrEmpty(s.Attribute) + ? [.. buckets.OrderBy(b => b.nodes[0].Hash())] + : [.. buckets.OrderBy(b => b.attr)]; + } + + var maxNodesInBucket = nodesInBucket * (int)Cbf; + + var res = new List>(buckets.Length); + var fallback = new List>(buckets.Length); + + for (int i = 0; i < buckets.Length; i++) + { + var ns = buckets[i].nodes; + if (ns.Length >= maxNodesInBucket) + { + res.Add(ns.Take(maxNodesInBucket).ToList()); + } + else if (ns.Length >= nodesInBucket) + { + fallback.Add(new List(ns)); + } + } + + if (res.Count < bucketCount) + { + // Fallback to using minimum allowed backup factor (1). + res.AddRange(fallback); + + if (Strict && res.Count < bucketCount) + { + throw new FrostFsException($"{errNotEnoughNodes}: {s}"); + } + } + + if (HrwSeed != null && HrwSeed.Length != 0) + { + var weights = new double[res.Count]; + var a = new MeanIQRAgg(); + + for (int i = 0; i < res.Count; i++) + { + a.Clear(); + weights[i] = CalcBucketWeight(res[i], a, weightFunc); + } + + var hashers = res.Select(r => new HasherList(r)).ToList(); + hashers = Tools.SortHasherSliceByWeightValue(hashers, weights, HrwSeedHash); + + for (int i = 0; i < res.Count; i++) + { + res[i] = hashers[i].Nodes; + } + } + + if (res.Count < bucketCount) + { + if (Strict && res.Count == 0) + { + throw new FrostFsException(errNotEnoughNodes); + } + + bucketCount = res.Count; + } + + if (string.IsNullOrEmpty(s.Attribute)) + { + fallback = res.Skip(bucketCount).ToList(); + res = res.Take(bucketCount).ToList(); + + for (int i = 0; i < fallback.Count; i++) + { + var index = i % bucketCount; + if (res[index].Count >= maxNodesInBucket) + { + break; + } + + res[index].AddRange(fallback[i]); + } + } + + return res.Take(bucketCount).ToList(); + } + + internal bool MatchKeyValue(FrostFsFilter f, FrostFsNodeInfo nodeInfo) + { + switch (f.Operation) + { + case (int)Operation.EQ: + return nodeInfo.Attributes.TryGetValue(f.Key, out var val) && val == f.Value; + case (int)Operation.LIKE: + { + var hasPrefix = f.Value.StartsWith(likeWildcard, StringComparison.Ordinal); + var hasSuffix = f.Value.EndsWith(likeWildcard, StringComparison.Ordinal); + + var start = hasPrefix ? likeWildcard.Length : 0; + var end = hasSuffix ? f.Value.Length - likeWildcard.Length : f.Value.Length; + var str = f.Value.Substring(start, end - start); + + if (hasPrefix && hasSuffix) + return nodeInfo.Attributes[f.Key].Contains(str); + + if (hasPrefix && !hasSuffix) + return nodeInfo.Attributes[f.Key].EndsWith(str, StringComparison.Ordinal); + + if (!hasPrefix && hasSuffix) + return nodeInfo.Attributes[f.Key].StartsWith(str, StringComparison.Ordinal); + + + return nodeInfo.Attributes[f.Key] == f.Value; + } + case (int)Operation.NE: + return nodeInfo.Attributes[f.Key] != f.Value; + default: + { + ulong attr; + switch (f.Key) + { + case FrostFsNodeInfo.AttrPrice: + attr = nodeInfo.Price; + break; + + case FrostFsNodeInfo.AttrCapacity: + attr = nodeInfo.GetCapacity(); + break; + default: + if (!ulong.TryParse(nodeInfo.Attributes[f.Key], NumberStyles.Integer, CultureInfo.InvariantCulture, out attr)) + return false; + break; + } + + switch (f.Operation) + { + case (int)Operation.GT: + return attr > NumCache[f.Value]; + case (int)Operation.GE: + return attr >= NumCache[f.Value]; + case (int)Operation.LT: + return attr < NumCache[f.Value]; + case (int)Operation.LE: + return attr <= NumCache[f.Value]; + default: + // do nothing and return false + break; + } + } + break; + } + + // will not happen if context was created from f (maybe panic?) + return false; + } + + // match matches f against b. It returns no errors because + // filter should have been parsed during context creation + // and missing node properties are considered as a regular fail. + internal bool Match(FrostFsFilter f, FrostFsNodeInfo nodeInfo) + { + switch (f.Operation) + { + case (int)Operation.NOT: + { + var inner = f.Filters; + var fSub = inner[0]; + + if (!string.IsNullOrEmpty(inner[0].Name)) + { + fSub = ProcessedFilters[inner[0].Name]; + } + return !Match(fSub, nodeInfo); + } + case (int)Operation.AND: + case (int)Operation.OR: + { + for (int i = 0; i < f.Filters.Length; i++) + { + var fSub = f.Filters[i]; + + if (!string.IsNullOrEmpty(f.Filters[i].Name)) + { + fSub = ProcessedFilters[f.Filters[i].Name]; + } + + var ok = Match(fSub, nodeInfo); + + if (ok == (f.Operation == (int)Operation.OR)) + { + return ok; + } + } + + return f.Operation == (int)Operation.AND; + } + default: + return MatchKeyValue(f, nodeInfo); + } + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs new file mode 100644 index 00000000..17fabfef --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/HasherList.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal sealed class HasherList : IHasher +{ + private readonly List _nodes; + + internal HasherList(List nodes) + { + _nodes = nodes; + } + + internal List Nodes + { + get + { + return _nodes; + } + } + + public ulong Hash() + { + return _nodes.Count > 0 ? _nodes[0].Hash() : 0; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs new file mode 100644 index 00000000..774dbc28 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IAggregator.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal interface IAggregator +{ + void Add(double d); + + double Compute(); +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs new file mode 100644 index 00000000..6a0f5361 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/IHasher.cs @@ -0,0 +1,6 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal interface IHasher +{ + ulong Hash(); +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs new file mode 100644 index 00000000..ddf4169a --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/INormalizer.cs @@ -0,0 +1,6 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +interface INormalizer +{ + double Normalize(double w); +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs new file mode 100644 index 00000000..98187b34 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanAgg.cs @@ -0,0 +1,20 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal struct MeanAgg +{ + private double mean; + private int count; + + internal void Add(double n) + { + int c = count + 1; + mean = mean * count / c + n / c; + + count++; + } + + internal readonly double Compute() + { + return mean; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs new file mode 100644 index 00000000..e7f6fcaa --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MeanIQRAgg.cs @@ -0,0 +1,65 @@ +using System.Collections.ObjectModel; +using System.Linq; + +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal struct MeanIQRAgg : IAggregator +{ + private const int minLn = 4; + internal Collection arr = []; + + public MeanIQRAgg() + { + } + + public readonly void Add(double d) + { + arr.Add(d); + } + + public readonly double Compute() + { + var length = arr.Count; + if (length == 0) + { + return 0; + } + + var sorted = arr.OrderBy(p => p).ToArray(); + + double minV, maxV; + + if (arr.Count < minLn) + { + minV = sorted[0]; + maxV = sorted[length - 1]; + } + else + { + var start = length / minLn; + var end = length * 3 / minLn - 1; + + minV = sorted[start]; + maxV = sorted[end]; + } + + var count = 0; + double sum = 0; + + foreach (var e in sorted) + { + if (e >= minV && e <= maxV) + { + sum += e; + count++; + } + } + + return sum / count; + } + + internal readonly void Clear() + { + arr.Clear(); + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs new file mode 100644 index 00000000..7b68fc08 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/MinAgg.cs @@ -0,0 +1,27 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal struct MinAgg +{ + private double min; + private bool minFound; + + internal void Add(double n) + { + if (!minFound) + { + min = n; + minFound = true; + return; + } + + if (n < min) + { + min = n; + } + } + + internal readonly double Compute() + { + return min; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs new file mode 100644 index 00000000..705827ef --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Operation.cs @@ -0,0 +1,16 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +public enum Operation +{ + Unspecified = 0, + EQ, + NE, + GT, + GE, + LT, + LE, + OR, + AND, + NOT, + LIKE +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs new file mode 100644 index 00000000..d3ffe5c1 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/ReverseMinNorm.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal struct ReverseMinNorm : INormalizer +{ + internal double min; + + public readonly double Normalize(double w) => (min + 1) / (w + 1); +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs new file mode 100644 index 00000000..22cb9617 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SelectFilterExpr.cs @@ -0,0 +1,10 @@ +using System.Collections.ObjectModel; + +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal struct SelectFilterExpr(uint cbf, FrostFsSelector selector, Collection filters) +{ + internal uint Cbf { get; } = cbf; + internal FrostFsSelector Selector { get; } = selector; + internal Collection Filters { get; } = filters; +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs new file mode 100644 index 00000000..e4cd416b --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/SigmoidNorm.cs @@ -0,0 +1,23 @@ +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +internal readonly struct SigmoidNorm : INormalizer +{ + private readonly double _scale; + + internal SigmoidNorm(double scale) + { + _scale = scale; + } + + public readonly double Normalize(double w) + { + if (_scale == 0) + { + return 0; + } + + var x = w / _scale; + + return x / (1 + x); + } +} diff --git a/src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs new file mode 100644 index 00000000..e87994ad --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Netmap/Placement/Tools.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using static FrostFS.SDK.FrostFsNetmapSnapshot; + +namespace FrostFS.SDK.Client.Models.Netmap.Placement; + +public static class Tools +{ + internal static ulong Distance(ulong x, ulong y) + { + var acc = x ^ y; + acc ^= acc >> 33; + acc *= 0xff51afd7ed558ccd; + acc ^= acc >> 33; + acc *= 0xc4ceb9fe1a85ec53; + acc ^= acc >> 33; + + return acc; + } + + internal static double ReverceNormalize(double r, double w) + { + return (r + 1) / (w + 1); + } + + internal static double Normalize(double r, double w) + { + if (r == 0) + { + return 0; + } + + var x = w / r; + return x / (1 + x); + } + + internal static void AppendWeightsTo(FrostFsNodeInfo[] nodes, Func wf, ref double[] weights) + { + if (weights.Length < nodes.Length) + { + weights = new double[nodes.Length]; + } + + for (int i = 0; i < nodes.Length; i++) + { + weights[i] = wf(nodes[i]); + } + } + + internal static List SortHasherSliceByWeightValue(List nodes, Span weights, ulong hash) where T : IHasher + { + if (nodes.Count == 0) + { + return nodes; + } + + var allEquals = true; + + if (weights.Length > 1) + { + for (int i = 1; i < weights.Length; i++) + { + if (weights[i] != weights[0]) + { + allEquals = false; + break; + } + } + } + + var dist = new double[nodes.Count]; + + if (allEquals) + { + for (int i = 0; i < dist.Length; i++) + { + var x = nodes[i].Hash(); + dist[i] = Distance(x, hash); + } + + return SortHasherByDistance(nodes, dist, true); + } + + for (int i = 0; i < dist.Length; i++) + { + var d = Distance(nodes[i].Hash(), hash); + dist[i] = (ulong.MaxValue - d) * weights[i]; + } + + return SortHasherByDistance(nodes, dist, false); + } + + internal static List SortHasherByDistance(List nodes, N[] dist, bool asc) + { + IndexedValue[] indexes = new IndexedValue[nodes.Count]; + for (int i = 0; i < dist.Length; i++) + { + indexes[i] = new IndexedValue() { nodeInfo = nodes[i], dist = dist[i] }; + } + + if (asc) + { + return new List(indexes + .OrderBy(x => x.dist) + .Select(x => x.nodeInfo).ToArray()); + } + else + { + return new List(indexes + .OrderByDescending(x => x.dist) + .Select(x => x.nodeInfo)); + } + } + + internal static Func DefaultWeightFunc(IReadOnlyList nodes) + { + MeanAgg mean = new(); + MinAgg minV = new(); + + foreach (var node in nodes) + { + mean.Add(node.GetCapacity()); + minV.Add(node.Price); + } + + return NewWeightFunc( + NewSigmoidNorm(mean.Compute()), + NewReverseMinNorm(minV.Compute())); + } + + private struct IndexedValue + { + internal T nodeInfo; + internal N dist; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsAddress.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsAddress.cs new file mode 100644 index 00000000..bf17f18d --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsAddress.cs @@ -0,0 +1,48 @@ +using FrostFS.Refs; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK; + +public class FrostFsAddress +{ + private FrostFsObjectId? frostFsObjectId; + private FrostFsContainerId? frostFsContainerId; + private ObjectID? objectId; + private ContainerID? containerId; + + public FrostFsAddress(FrostFsContainerId frostFsContainerId, FrostFsObjectId frostFsObjectId) + { + FrostFsObjectId = frostFsObjectId ?? throw new System.ArgumentNullException(nameof(frostFsObjectId)); + FrostFsContainerId = frostFsContainerId ?? throw new System.ArgumentNullException(nameof(frostFsContainerId)); + } + + internal FrostFsAddress(ObjectID objectId, ContainerID containerId) + { + ObjectId = objectId ?? throw new System.ArgumentNullException(nameof(objectId)); + ContainerId = containerId ?? throw new System.ArgumentNullException(nameof(containerId)); + } + + public FrostFsObjectId FrostFsObjectId + { + get => frostFsObjectId ??= objectId!.ToModel(); + set => frostFsObjectId = value; + } + + public FrostFsContainerId FrostFsContainerId + { + get => frostFsContainerId ??= containerId!.ToModel(); + set => frostFsContainerId = value; + } + + public ObjectID ObjectId + { + get => objectId ??= frostFsObjectId!.ToMessage(); + set => objectId = value; + } + + public ContainerID ContainerId + { + get => containerId ??= frostFsContainerId!.ToMessage(); + set => containerId = value; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsAttributePair.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsAttributePair.cs new file mode 100644 index 00000000..9db78980 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsAttributePair.cs @@ -0,0 +1,36 @@ +namespace FrostFS.SDK; + +public struct FrostFsAttributePair(string key, string value) : System.IEquatable +{ + public string Key { get; set; } = key; + + public string Value { get; set; } = value; + + public override bool Equals(object obj) + { + if (obj == null || obj is not FrostFsAttributePair) + return false; + + return Equals((FrostFsAttributePair)obj); + } + + public override int GetHashCode() + { + return Key.GetHashCode() ^ Value.GetHashCode(); + } + + public static bool operator ==(FrostFsAttributePair left, FrostFsAttributePair right) + { + return left.Equals(right); + } + + public static bool operator !=(FrostFsAttributePair left, FrostFsAttributePair right) + { + return !(left == right); + } + + public bool Equals(FrostFsAttributePair other) + { + return GetHashCode().Equals(other.GetHashCode()); + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsHeaderResult.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsHeaderResult.cs new file mode 100644 index 00000000..43ea2996 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsHeaderResult.cs @@ -0,0 +1,8 @@ +namespace FrostFS.SDK; + +public class FrostFsHeaderResult +{ + public FrostFsObjectHeader? HeaderInfo { get; internal set; } + + public FrostFsSplitInfo? SplitInfo { get; internal set; } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsLargeObject.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsLargeObject.cs new file mode 100644 index 00000000..12a95c68 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsLargeObject.cs @@ -0,0 +1,9 @@ +namespace FrostFS.SDK; + +public class FrostFsLargeObject(FrostFsContainerId container) : FrostFsObject(container) +{ + public ulong PayloadLength + { + get { return Header!.PayloadLength; } + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsLinkObject.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsLinkObject.cs new file mode 100644 index 00000000..f43c6d05 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsLinkObject.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace FrostFS.SDK; + +public class FrostFsLinkObject : FrostFsObject +{ + public FrostFsLinkObject(FrostFsContainerId containerId, + SplitId splitId, + FrostFsObjectHeader largeObjectHeader, + IList children) + : base(containerId) + { + Header!.Split = new FrostFsSplit(splitId, + null, + null, + largeObjectHeader, + null, + new ReadOnlyCollection(children)); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsObject.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsObject.cs new file mode 100644 index 00000000..e22016a4 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsObject.cs @@ -0,0 +1,64 @@ +using System; + +namespace FrostFS.SDK; + +public class FrostFsObject +{ + /// + /// Creates new instance from ObjectHeader + /// + /// + public FrostFsObject(FrostFsObjectHeader header) + { + Header = header; + } + + /// + /// Creates new instance with specified parameters + /// + /// + /// + public FrostFsObject(FrostFsContainerId container, FrostFsObjectType objectType = FrostFsObjectType.Regular) + { + Header = new FrostFsObjectHeader(containerId: container, type: objectType); + } + + /// + /// Header contains metadata for the object + /// + /// + public FrostFsObjectHeader Header { get; set; } + + /// + /// The value is calculated internally as a hash of ObjectHeader. Do not use pre-calculated value is the object has been changed. + /// + public FrostFsObjectId? ObjectId + { + get; set; + } + + /// + /// A payload is obtained via stream reader + /// + /// Reader for received data + public IObjectReader? ObjectReader { get; set; } + + public ReadOnlyMemory SingleObjectPayload { get; set; } + + /// + /// Provide SHA256 hash of the payload. If null, the hash is calculated by internal logic + /// + public byte[]? PayloadHash { get; set; } + + /// + /// Applied only for the last Object in chain in case of manual multipart uploading + /// + /// Parent for multipart object + public void SetParent(FrostFsObjectHeader largeObjectHeader) + { + if (Header?.Split == null) + throw new ArgumentNullException(nameof(largeObjectHeader), "Split value must not be null"); + + Header.Split.ParentHeader = largeObjectHeader; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectFilter.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectFilter.cs new file mode 100644 index 00000000..f2f21d7d --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectFilter.cs @@ -0,0 +1,111 @@ +namespace FrostFS.SDK; + +public interface IObjectFilter +{ + public FrostFsMatchType MatchType { get; set; } + public string Key { get; set; } + + string? GetSerializedValue(); +} + +public abstract class FrostFsObjectFilter(FrostFsMatchType matchType, string key, T value) : IObjectFilter +{ + public FrostFsMatchType MatchType { get; set; } = matchType; + public string Key { get; set; } = key; + + public T Value { get; set; } = value; + + public string? GetSerializedValue() + { + return Value?.ToString(); + } +} + +/// +/// Creates filter to search by Attribute +/// +/// Match type +/// Attribute key +/// Attribute value +public class FilterByAttributePair(FrostFsMatchType matchType, string key, string value) : FrostFsObjectFilter(matchType, key, value) { } + +/// +/// Creates filter to search by ObjectId +/// +/// Match type +/// ObjectId +public class FilterByObjectId(FrostFsMatchType matchType, FrostFsObjectId objectId) : FrostFsObjectFilter(matchType, Constants.FilterHeaderObjectID, objectId) { } + +/// +/// Creates filter to search by OwnerId +/// +/// Match type +/// ObjectId +public class FilterByOwnerId(FrostFsMatchType matchType, FrostFsOwner ownerId) : FrostFsObjectFilter(matchType, Constants.FilterHeaderOwnerID, ownerId) { } + +/// +/// Creates filter to search by Version +/// +/// Match type +/// Version +public class FilterByVersion(FrostFsMatchType matchType, FrostFsVersion version) : FrostFsObjectFilter(matchType, Constants.FilterHeaderVersion, version) { } + +/// +/// Creates filter to search by ContainerId +/// +/// Match type +/// ContainerId +public class FilterByContainerId(FrostFsMatchType matchType, FrostFsContainerId containerId) : FrostFsObjectFilter(matchType, Constants.FilterHeaderContainerID, containerId) { } + +/// +/// Creates filter to search by creation Epoch +/// +/// Match type +/// Creation Epoch +public class FilterByEpoch(FrostFsMatchType matchType, ulong epoch) : FrostFsObjectFilter(matchType, Constants.FilterHeaderCreationEpoch, epoch) { } + +/// +/// Creates filter to search by Payload Length +/// +/// Match type +/// Payload Length +public class FilterByPayloadLength(FrostFsMatchType matchType, ulong payloadLength) : FrostFsObjectFilter(matchType, Constants.FilterHeaderPayloadLength, payloadLength) { } + +/// +/// Creates filter to search by Payload Hash +/// +/// Match type +/// Payload Hash +public class FilterByPayloadHash(FrostFsMatchType matchType, CheckSum payloadHash) : FrostFsObjectFilter(matchType, Constants.FilterHeaderPayloadHash, payloadHash) { } + +/// +/// Creates filter to search by Parent +/// +/// Match type +/// Parent +public class FilterByParent(FrostFsMatchType matchType, FrostFsObjectId parentId) : FrostFsObjectFilter(matchType, Constants.FilterHeaderParent, parentId) { } + +/// +/// Creates filter to search by SplitId +/// +/// Match type +/// SplitId +public class FilterBySplitId(FrostFsMatchType matchType, SplitId splitId) : FrostFsObjectFilter(matchType, Constants.FilterHeaderSplitID, splitId) { } + +/// +/// Creates filter to search by Payload Hash +/// +/// Match type +/// Payload Hash +public class FilterByECParent(FrostFsMatchType matchType, FrostFsObjectId ecParentId) : FrostFsObjectFilter(matchType, Constants.FilterHeaderECParent, ecParentId) { } + +/// +/// Creates filter to search Root objects +/// +public class FilterByRootObject() : FrostFsObjectFilter(FrostFsMatchType.Unspecified, Constants.FilterHeaderRoot, string.Empty) { } + +/// +/// Creates filter to search objects that are physically stored on the server +/// (FrostFsMatchType.Unspecified, Constants.FilterHeaderPhy, string.Empty) { } + diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectHeader.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectHeader.cs new file mode 100644 index 00000000..4f1c5d05 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectHeader.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; + +using FrostFS.Object; + +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK; + +public class FrostFsObjectHeader( + FrostFsContainerId containerId, + FrostFsObjectType type = FrostFsObjectType.Regular, + FrostFsAttributePair[]? attributes = null, + FrostFsSplit? split = null, + FrostFsOwner? owner = null, + FrostFsVersion? version = null) +{ + private Header? header; + private Container.Container.Types.Attribute[]? grpsAttributes; + + public ReadOnlyCollection? Attributes { get; internal set; } = + attributes == null ? null : + new ReadOnlyCollection(attributes); + + public FrostFsContainerId ContainerId { get; } = containerId; + + public ulong PayloadLength { get; set; } + + public byte[]? PayloadCheckSum { get; set; } + + public FrostFsObjectType ObjectType { get; } = type; + + public FrostFsOwner? OwnerId { get; internal set; } = owner; + + public FrostFsVersion? Version { get; internal set; } = version; + + public FrostFsSplit? Split { get; internal set; } = split; + + internal Container.Container.Types.Attribute[]? GetGrpsAttributes() + { + grpsAttributes ??= Attributes? + .Select(a => new Container.Container.Types.Attribute { Key = a.Key, Value = a.Value }) + .ToArray(); + + return grpsAttributes; + } + + public Header GetHeader() + { + if (header == null) + { + var objTypeName = ObjectType switch + { + FrostFsObjectType.Regular => Object.ObjectType.Regular, + FrostFsObjectType.Lock => Object.ObjectType.Lock, + FrostFsObjectType.Tombstone => Object.ObjectType.Tombstone, + _ => throw new ArgumentException($"Unknown ObjectType. Value: '{ObjectType}'.") + }; + + this.header = new Header + { + OwnerId = OwnerId?.ToMessage(), + Version = Version?.ToMessage(), + ContainerId = ContainerId.ToMessage(), + ObjectType = objTypeName, + PayloadLength = PayloadLength + }; + + if (Attributes != null) + { + foreach (var attribute in Attributes) + { + this.header.Attributes.Add(attribute.ToMessage()); + } + } + + var split = Split; + if (split != null) + { + this.header.Split = new Header.Types.Split + { + SplitId = split!.SplitId != null ? split.SplitId.GetSplitId() : null + }; + } + } + + return this.header; + } +} diff --git a/src/FrostFS.SDK.ModelsV2/ObjectId.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectId.cs similarity index 56% rename from src/FrostFS.SDK.ModelsV2/ObjectId.cs rename to src/FrostFS.SDK.Client/Models/Object/FrostFsObjectId.cs index 2bcf3ab1..57300a36 100644 --- a/src/FrostFS.SDK.ModelsV2/ObjectId.cs +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsObjectId.cs @@ -2,24 +2,18 @@ using FrostFS.SDK.Cryptography; -namespace FrostFS.SDK.ModelsV2; +namespace FrostFS.SDK; -public class ObjectId +public class FrostFsObjectId(string id) { - public string Value { get; } + public string Value { get; } = id; - public ObjectId(string id) - { - Value = id; - } - - public static ObjectId FromHash(byte[] hash) + public static FrostFsObjectId FromHash(ReadOnlySpan hash) { if (hash.Length != Constants.Sha256HashLength) - { throw new FormatException("ObjectID must be a sha256 hash."); - } - return new ObjectId(Base58.Encode(hash)); + + return new FrostFsObjectId(Base58.Encode(hash)); } public byte[] ToHash() diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsOwner.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsOwner.cs new file mode 100644 index 00000000..228edf27 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsOwner.cs @@ -0,0 +1,39 @@ +using System.Security.Cryptography; + +using FrostFS.Refs; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; + + +namespace FrostFS.SDK; + +public class FrostFsOwner(string id) +{ + private OwnerID? ownerID; + + public string Value { get; } = id; + + public static FrostFsOwner FromKey(ECDsa key) + { + return new FrostFsOwner(key.PublicKey().PublicKeyToAddress()); + } + + internal OwnerID OwnerID + { + get + { + ownerID ??= this.ToMessage(); + return ownerID; + } + } + + public byte[] ToHash() + { + return Base58.Decode(Value); + } + + public override string ToString() + { + return Value; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsRange.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsRange.cs new file mode 100644 index 00000000..b50568ff --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsRange.cs @@ -0,0 +1,27 @@ +namespace FrostFS.SDK; + +public readonly struct FrostFsRange(ulong offset, ulong length) : System.IEquatable +{ + public ulong Offset { get; } = offset; + + public ulong Length { get; } = length; + + public override readonly bool Equals(object obj) => this == (FrostFsRange)obj; + + public override readonly int GetHashCode() => $"{Offset}{Length}".GetHashCode(); + + public static bool operator ==(FrostFsRange left, FrostFsRange right) + { + return left.Equals(right); + } + + public static bool operator !=(FrostFsRange left, FrostFsRange right) + { + return !(left == right); + } + + public readonly bool Equals(FrostFsRange other) + { + return this == other; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsSplit.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsSplit.cs new file mode 100644 index 00000000..5afd46b6 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsSplit.cs @@ -0,0 +1,53 @@ +using System.Collections.ObjectModel; +using System.Linq; +using FrostFS.Object; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK; + +public class FrostFsSplit(SplitId splitId, + FrostFsObjectId? previous = null, + FrostFsObjectId? parent = null, + FrostFsObjectHeader? parentHeader = null, + FrostFsSignature? parentSignature = null, + ReadOnlyCollection? children = null) +{ + private Header.Types.Split? _split; + + public FrostFsSplit() : this(new SplitId()) + { + } + + public SplitId SplitId { get; private set; } = splitId; + + public FrostFsObjectId? Previous { get; } = previous; + + public FrostFsObjectId? Parent { get; } = parent; + + public FrostFsSignature? ParentSignature { get; } = parentSignature; + + public FrostFsObjectHeader? ParentHeader { get; set; } = parentHeader; + + public ReadOnlyCollection? Children { get; } = children; + + public Header.Types.Split GetSplit() + { + if (_split == null) + { + _split = new Header.Types.Split + { + SplitId = SplitId?.GetSplitId(), + Parent = Parent?.ToMessage(), + ParentHeader = ParentHeader?.GetHeader(), + ParentSignature = ParentSignature?.ToMessage() + }; + + if (Children != null) + { + _split.Children.AddRange(Children.Select(x => x.ToMessage())); + } + } + + return _split; + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/FrostFsSplitInfo.cs b/src/FrostFS.SDK.Client/Models/Object/FrostFsSplitInfo.cs new file mode 100644 index 00000000..d8f87839 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/FrostFsSplitInfo.cs @@ -0,0 +1,28 @@ +using FrostFS.Object; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK; + +public class FrostFsSplitInfo +{ + private readonly SplitInfo _splitInfo; + + private SplitId? _splitId; + + private FrostFsObjectId? _link; + + private FrostFsObjectId? _lastPart; + + internal FrostFsSplitInfo(SplitInfo splitInfo) + { + _splitInfo = splitInfo; + } + + public SplitId SplitId => _splitId ??= new SplitId(_splitInfo.SplitId.ToUuid()); + + public FrostFsObjectId? Link => _link ??= _splitInfo.Link == null + ? null : FrostFsObjectId.FromHash(_splitInfo.Link.Value.Span); + + public FrostFsObjectId? LastPart => _lastPart ??= _splitInfo.LastPart == null + ? null : FrostFsObjectId.FromHash(_splitInfo.LastPart.Value.Span); +} diff --git a/src/FrostFS.SDK.Client/Models/Object/IObjectReader.cs b/src/FrostFS.SDK.Client/Models/Object/IObjectReader.cs new file mode 100644 index 00000000..8b08e6b7 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/IObjectReader.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FrostFS.SDK; + +public interface IObjectReader : IDisposable +{ + ValueTask?> ReadChunk(CancellationToken cancellationToken = default); +} diff --git a/src/FrostFS.SDK.Client/Models/Object/PartUploadedEventArgs.cs b/src/FrostFS.SDK.Client/Models/Object/PartUploadedEventArgs.cs new file mode 100644 index 00000000..16ad3266 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/PartUploadedEventArgs.cs @@ -0,0 +1,8 @@ +using System; + +namespace FrostFS.SDK.Client; + +public class PartUploadedEventArgs(ObjectPartInfo part) : EventArgs +{ + public ObjectPartInfo Part { get; } = part; +} diff --git a/src/FrostFS.SDK.Client/Models/Object/SplitId.cs b/src/FrostFS.SDK.Client/Models/Object/SplitId.cs new file mode 100644 index 00000000..a1133611 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/SplitId.cs @@ -0,0 +1,57 @@ +using System; + +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +namespace FrostFS.SDK; + +public class SplitId +{ + private readonly Guid id; + + private ByteString? message; + + public SplitId() + { + this.id = Guid.NewGuid(); + } + + public SplitId(Guid id) + { + this.id = id; + } + + private SplitId(byte[] binary) + { + this.id = new Guid(binary); + } + + private SplitId(string str) + { + this.id = new Guid(str); + } + + public static SplitId CreateFromBinary(byte[] binaryData) + { + return new SplitId(binaryData); + } + + public static SplitId CreateFromString(string stringData) + { + return new SplitId(stringData); + } + + public override string ToString() + { + return this.id.ToString(); + } + + public ByteString? GetSplitId() + { + Span span = stackalloc byte[16]; + id.ToBytes(span); + + return this.message ??= ByteString.CopyFrom(span); + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/UploadInfo.cs b/src/FrostFS.SDK.Client/Models/Object/UploadInfo.cs new file mode 100644 index 00000000..ca96c7dc --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/UploadInfo.cs @@ -0,0 +1,36 @@ +namespace FrostFS.SDK.Client; + +public readonly struct ObjectPartInfo(long offset, int length, FrostFsObjectId objectId) : System.IEquatable +{ + public long Offset { get; } = offset; + public int Length { get; } = length; + public FrostFsObjectId ObjectId { get; } = objectId; + + public override bool Equals(object obj) + { + if (obj == null || obj is not ObjectPartInfo) + return false; + + return Equals((ObjectPartInfo)obj); + } + + public override int GetHashCode() + { + return ((int)(Offset >> 32)) ^ (int)Offset ^ Length ^ ObjectId.Value.GetHashCode(); + } + + public static bool operator ==(ObjectPartInfo left, ObjectPartInfo right) + { + return left.Equals(right); + } + + public static bool operator !=(ObjectPartInfo left, ObjectPartInfo right) + { + return !(left == right); + } + + public bool Equals(ObjectPartInfo other) + { + return GetHashCode() == other.GetHashCode(); + } +} diff --git a/src/FrostFS.SDK.Client/Models/Object/UploadProgressInfo.cs b/src/FrostFS.SDK.Client/Models/Object/UploadProgressInfo.cs new file mode 100644 index 00000000..a6fa9057 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Object/UploadProgressInfo.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace FrostFS.SDK.Client; + +public class UploadProgressInfo +{ + private List _parts; + + public UploadProgressInfo(Guid splitId, int capacity = 8) + { + _parts = new List(capacity); + SplitId = new SplitId(splitId); + } + + public UploadProgressInfo(Guid splitId, Collection parts) + { + _parts = [.. parts]; + SplitId = new SplitId(splitId); + } + + public event EventHandler? Notify; + + public SplitId SplitId { get; } + + internal void AddPart(ObjectPartInfo part) + { + _parts.Add(part); + Notify?.Invoke(this, new PartUploadedEventArgs(part)); + } + + public ObjectPartInfo GetPart(int index) + { + return _parts[index]; + } + + public ObjectPartInfo GetLast() + { + return _parts.LastOrDefault(); + } + + public ReadOnlyCollection GetParts() + { + return new ReadOnlyCollection(_parts); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Response/FrostFsResponseStatus.cs b/src/FrostFS.SDK.Client/Models/Response/FrostFsResponseStatus.cs new file mode 100644 index 00000000..7b003e7b --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Response/FrostFsResponseStatus.cs @@ -0,0 +1,17 @@ +namespace FrostFS.SDK; + +public class FrostFsResponseStatus(FrostFsStatusCode code, string? message = null, string? details = null) +{ + public FrostFsStatusCode Code { get; set; } = code; + + public string Message { get; set; } = message ?? string.Empty; + + public string Details { get; set; } = details ?? string.Empty; + + public bool IsSuccess => Code == FrostFsStatusCode.Success; + + public override string ToString() + { + return $"Response status: {Code}. Message: {Message}. Details: {Details}"; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Response/FrostFsSignature.cs b/src/FrostFS.SDK.Client/Models/Response/FrostFsSignature.cs new file mode 100644 index 00000000..995b161c --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Response/FrostFsSignature.cs @@ -0,0 +1,10 @@ +namespace FrostFS.SDK; + +public class FrostFsSignature() +{ + public byte[]? Key { get; set; } + + public byte[]? Sign { get; set; } + + public SignatureScheme Scheme { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Models/Response/MetaHeader.cs b/src/FrostFS.SDK.Client/Models/Response/MetaHeader.cs new file mode 100644 index 00000000..547b2b5f --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Response/MetaHeader.cs @@ -0,0 +1,20 @@ +namespace FrostFS.SDK; + +public class MetaHeader(FrostFsVersion version, ulong epoch, uint ttl) +{ + public FrostFsVersion Version { get; set; } = version; + public ulong Epoch { get; set; } = epoch; + public uint Ttl { get; set; } = ttl; + + public static MetaHeader Default() + { + return new MetaHeader( + new FrostFsVersion( + major: 2, + minor: 13 + ), + epoch: 0, + ttl: 2 + ); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Models/Session/FrostFsSessionToken.cs b/src/FrostFS.SDK.Client/Models/Session/FrostFsSessionToken.cs new file mode 100644 index 00000000..808e91b9 --- /dev/null +++ b/src/FrostFS.SDK.Client/Models/Session/FrostFsSessionToken.cs @@ -0,0 +1,123 @@ +using System; +using FrostFS.Refs; + +using FrostFS.SDK.Client; +using FrostFS.SDK.Cryptography; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.SDK; + +public class FrostFsSessionToken +{ + private Guid _id; + private ReadOnlyMemory _sessionKey; + private readonly SessionToken.Types.Body _body; + + private FrostFsSessionToken() + { + ProtoId = ByteString.Empty; + ProtoSessionKey = ByteString.Empty; + _body = new SessionToken.Types.Body(); + } + + internal FrostFsSessionToken(SessionToken token) + { + ProtoId = token.Body.Id; + ProtoSessionKey = token.Body.SessionKey; + + _body = token.Body; + } + + public Guid Id + { + get + { + if (_id == Guid.Empty) + { + _id = ProtoId.ToUuid(); + } + + return _id; + } + } + + public ReadOnlyMemory SessionKey + { + get + { + if (_sessionKey.IsEmpty) + { + _sessionKey = ProtoSessionKey.Memory; + } + + return _sessionKey; + } + } + + internal ByteString ProtoId { get; } + + internal ByteString ProtoSessionKey { get; } + + public SessionToken CreateContainerToken(ContainerID? containerId, ContainerSessionContext.Types.Verb verb, ClientKey key) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + SessionToken sessionToken = new() { Body = _body.Clone() }; + + sessionToken.Body.Container = new() { Verb = verb }; + + if (containerId != null) + { + sessionToken.Body.Container.ContainerId = containerId; + } + else + { + sessionToken.Body.Container.Wildcard = true; + } + + sessionToken.Body.SessionKey = key.PublicKeyProto; + sessionToken.Signature = key.SignMessagePart(sessionToken.Body); + + return sessionToken; + } + + public SessionToken CreateObjectTokenContext(Address address, ObjectSessionContext.Types.Verb verb, ClientKey key) + { + if (address is null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + SessionToken sessionToken = new() + { + Body = _body.Clone() + }; + + ObjectSessionContext.Types.Target target = new() { Container = address.ContainerId }; + + if (address.ObjectId != null) + { + target.Objects.Add(address.ObjectId); + } + + sessionToken.Body.Object = new() + { + Target = target, + Verb = verb + }; + + sessionToken.Signature = key.SignMessagePart(sessionToken.Body); + + return sessionToken; + } +} diff --git a/src/FrostFS.SDK.Client/ObjectWriter.cs b/src/FrostFS.SDK.Client/ObjectWriter.cs new file mode 100644 index 00000000..a1859cc0 --- /dev/null +++ b/src/FrostFS.SDK.Client/ObjectWriter.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; + +using FrostFS.Object; +using FrostFS.SDK.Client.Interfaces; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client +{ + internal sealed class ObjectWriter : IObjectWriter + { + private readonly ClientContext ctx; + private readonly PrmObjectPutBase args; + private readonly ObjectStreamer streamer; + private bool disposedValue; + + internal ObjectWriter(ClientContext ctx, PrmObjectPutBase args, ObjectStreamer streamer) + { + this.ctx = ctx; + this.args = args; + this.streamer = streamer; + } + + public async Task WriteAsync(ReadOnlyMemory memory) + { + var chunkRequest = new PutRequest + { + Body = new PutRequest.Types.Body + { + Chunk = UnsafeByteOperations.UnsafeWrap(memory) + } + }; + + chunkRequest.AddMetaHeader(args.XHeaders); + + chunkRequest.Sign(this.ctx.Key); + + await streamer.Write(chunkRequest).ConfigureAwait(false); + } + + public async Task CompleteAsync() + { + var response = await streamer.Close().ConfigureAwait(false); + + Verifier.CheckResponse(response); + + return FrostFsObjectId.FromHash(response.Body.ObjectId.Value.Span); + } + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + streamer.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/CallContext.cs b/src/FrostFS.SDK.Client/Parameters/CallContext.cs new file mode 100644 index 00000000..018b7fcf --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/CallContext.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; + +namespace FrostFS.SDK.Client; + +public readonly struct CallContext(TimeSpan timeout, CancellationToken cancellationToken = default) : IEquatable +{ + public CancellationToken CancellationToken { get; } = cancellationToken; + + public TimeSpan Timeout { get; } = timeout; + + internal readonly DateTime? GetDeadline() + { + return Timeout.Ticks > 0 ? DateTime.UtcNow.Add(Timeout) : null; + } + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not CallContext) + return false; + + return Equals((CallContext)obj); + } + + public bool Equals(CallContext other) + { + return Timeout == other.Timeout && CancellationToken.Equals(other.CancellationToken); + } + + public override int GetHashCode() + { + return CancellationToken.GetHashCode() ^ Timeout.GetHashCode(); + } + + public static bool operator ==(CallContext left, CallContext right) + { + return left.Equals(right); + } + + public static bool operator !=(CallContext left, CallContext right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/ISessionToken.cs b/src/FrostFS.SDK.Client/Parameters/ISessionToken.cs new file mode 100644 index 00000000..de1a3765 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/ISessionToken.cs @@ -0,0 +1,12 @@ +namespace FrostFS.SDK.Client; + +public interface ISessionToken +{ + /// + /// Object represents token of the FrostFS Object session. A session is opened between any two sides of the + /// system, and implements a mechanism for transferring the power of attorney of actions to another network + /// member. The session has a limited validity period, and applies to a strictly defined set of operations. + /// + /// Instance of the session obtained from the server + FrostFsSessionToken? SessionToken { get; } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Parameters/PrmApeChainAdd.cs b/src/FrostFS.SDK.Client/Parameters/PrmApeChainAdd.cs new file mode 100644 index 00000000..adcc1b90 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmApeChainAdd.cs @@ -0,0 +1,43 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmApeChainAdd(FrostFsChainTarget target, FrostFsChain chain, string[]? xheaders = null) : System.IEquatable +{ + public FrostFsChainTarget Target { get; } = target; + + public FrostFsChain Chain { get; } = chain; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmApeChainAdd) + return false; + + return Equals((PrmApeChainAdd)obj); + } + + public readonly bool Equals(PrmApeChainAdd other) + { + return Target == other.Target + && Chain == other.Chain + && XHeaders == other.XHeaders; + } + + public override readonly int GetHashCode() + { + return Chain.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmApeChainAdd left, PrmApeChainAdd right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmApeChainAdd left, PrmApeChainAdd right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmApeChainList.cs b/src/FrostFS.SDK.Client/Parameters/PrmApeChainList.cs new file mode 100644 index 00000000..a9468408 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmApeChainList.cs @@ -0,0 +1,40 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmApeChainList(FrostFsChainTarget target, string[]? xheaders = null) : System.IEquatable +{ + public FrostFsChainTarget Target { get; } = target; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmApeChainList) + return false; + + return Equals((PrmApeChainList)obj); + } + + public override readonly int GetHashCode() + { + return Target.GetHashCode() ^ XHeaders.GetHashCode(); + } + + public readonly bool Equals(PrmApeChainList other) + { + return Target == other.Target + && XHeaders == other.XHeaders; + } + + public static bool operator ==(PrmApeChainList left, PrmApeChainList right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmApeChainList left, PrmApeChainList right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmApeChainRemove.cs b/src/FrostFS.SDK.Client/Parameters/PrmApeChainRemove.cs new file mode 100644 index 00000000..23037766 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmApeChainRemove.cs @@ -0,0 +1,46 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmApeChainRemove( + FrostFsChainTarget target, + byte[] chainId, + string[]? xheaders = null) : System.IEquatable +{ + public FrostFsChainTarget Target { get; } = target; + + public byte[] ChainId { get; } = chainId; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmApeChainRemove) + return false; + + return Equals((PrmApeChainRemove)obj); + } + + public readonly bool Equals(PrmApeChainRemove other) + { + return Target == other.Target + && ChainId.Equals(other.ChainId) + && XHeaders == other.XHeaders; + } + + public override readonly int GetHashCode() + { + return ChainId.GetHashCode() ^ Target.GetHashCode() ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmApeChainRemove left, PrmApeChainRemove right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmApeChainRemove left, PrmApeChainRemove right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmContainerCreate.cs b/src/FrostFS.SDK.Client/Parameters/PrmContainerCreate.cs new file mode 100644 index 00000000..d5c6ca06 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmContainerCreate.cs @@ -0,0 +1,61 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmContainerCreate( + FrostFsContainerInfo container, + PrmWait waitParams, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsContainerInfo Container { get; } = container; + + /// + /// Since the container becomes available with some delay, it needs to poll the container status + /// + /// Rules for polling the result + public PrmWait WaitParams { get; } = waitParams; + + /// + /// Blank session token + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmContainerCreate) + return false; + + return Equals((PrmContainerCreate)obj); + } + + public readonly bool Equals(PrmContainerCreate other) + { + return Container == other.Container + && WaitParams == other.WaitParams + && SessionToken == other.SessionToken + && XHeaders == other.XHeaders; + } + + public override readonly int GetHashCode() + { + return Container.GetHashCode() + ^ WaitParams.GetHashCode() + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()) + ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmContainerCreate left, PrmContainerCreate right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmContainerCreate left, PrmContainerCreate right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmContainerDelete.cs b/src/FrostFS.SDK.Client/Parameters/PrmContainerDelete.cs new file mode 100644 index 00000000..1ad1b33b --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmContainerDelete.cs @@ -0,0 +1,52 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmContainerDelete( + FrostFsContainerId containerId, + PrmWait waitParams, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsContainerId ContainerId { get; } = containerId; + + /// + /// Since the container is removed with some delay, it needs to poll the container status + /// + /// Rules for polling the result + public PrmWait WaitParams { get; } = waitParams; + + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmContainerDelete) + return false; + + return Equals((PrmContainerDelete)obj); + } + + public readonly bool Equals(PrmContainerDelete other) + { + return ContainerId == other.ContainerId + && WaitParams.Equals(other.WaitParams); + } + + public override int GetHashCode() + { + return ContainerId.GetHashCode() ^ WaitParams.GetHashCode(); + } + + public static bool operator ==(PrmContainerDelete left, PrmContainerDelete right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmContainerDelete left, PrmContainerDelete right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmContainerGet.cs b/src/FrostFS.SDK.Client/Parameters/PrmContainerGet.cs new file mode 100644 index 00000000..a2e34ae8 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmContainerGet.cs @@ -0,0 +1,40 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmContainerGet(FrostFsContainerId container, string[]? xheaders = null) : System.IEquatable +{ + public FrostFsContainerId Container { get; } = container; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmContainerGet) + return false; + + return Equals((PrmContainerGet)obj); + } + + public readonly bool Equals(PrmContainerGet other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return Container.GetHashCode() + ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmContainerGet left, PrmContainerGet right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmContainerGet left, PrmContainerGet right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmContainerGetAll.cs b/src/FrostFS.SDK.Client/Parameters/PrmContainerGetAll.cs new file mode 100644 index 00000000..50b408e0 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmContainerGetAll.cs @@ -0,0 +1,39 @@ +using System; + +namespace FrostFS.SDK.Client; + +public readonly struct PrmContainerGetAll(string[]? xheaders = null) : IEquatable +{ + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmContainerGetAll) + return false; + + return Equals((PrmContainerGetAll)obj); + } + + public readonly bool Equals(PrmContainerGetAll other) + { + return XHeaders == other.XHeaders; + } + + public override readonly int GetHashCode() + { + return XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmContainerGetAll left, PrmContainerGetAll right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmContainerGetAll left, PrmContainerGetAll right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectClientCutPut.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectClientCutPut.cs new file mode 100644 index 00000000..46f25e09 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectClientCutPut.cs @@ -0,0 +1,81 @@ +using System.IO; + +namespace FrostFS.SDK.Client; + +public readonly struct PrmObjectClientCutPut( + FrostFsObjectHeader? header, + Stream? payload, + int bufferMaxSize = 0, + FrostFsSessionToken? sessionToken = null, + byte[]? customBuffer = null, + string[]? xheaders = null, + UploadProgressInfo? progress = null) : PrmObjectPutBase, System.IEquatable +{ + /// + /// Need to provide values like ContainerId and ObjectType to create and object. + /// Optional parameters ike Attributes can be provided as well. + /// + /// Header with required parameters to create an object + public FrostFsObjectHeader? Header { get; } = header; + + /// + /// A stream with source data + /// + public Stream? Payload { get; } = payload; + + /// + /// Overrides default size of the buffer for stream transferring. + /// + /// Size of the buffer + public int BufferMaxSize { get; } = bufferMaxSize; + + /// + /// Allows to define a buffer for chunks to manage by the memory allocation and releasing. + /// + public byte[]? CustomBuffer { get; } = customBuffer; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public UploadProgressInfo? Progress { get; } = progress; + + internal PutObjectContext PutObjectContext { get; } = new(); + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectClientCutPut) + return false; + + return Equals((PrmObjectClientCutPut)obj); + } + + public readonly bool Equals(PrmObjectClientCutPut other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return BufferMaxSize + ^ (Header == null ? 0 : Header.GetHashCode()) + ^ (Payload == null ? 0 : Payload.GetHashCode()) + ^ (CustomBuffer == null ? 0 : CustomBuffer.GetHashCode()) + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()) + ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmObjectClientCutPut left, PrmObjectClientCutPut right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectClientCutPut left, PrmObjectClientCutPut right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectDelete.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectDelete.cs new file mode 100644 index 00000000..fa127554 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectDelete.cs @@ -0,0 +1,51 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmObjectDelete( + FrostFsContainerId containerId, + FrostFsObjectId objectId, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsContainerId ContainerId { get; } = containerId; + + public FrostFsObjectId ObjectId { get; } = objectId; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectDelete) + return false; + + return Equals((PrmObjectDelete)obj); + } + + public readonly bool Equals(PrmObjectDelete other) + { + return ContainerId == other.ContainerId + && ObjectId == other.ObjectId + && SessionToken == other.SessionToken + && XHeaders == other.XHeaders; + } + + public override readonly int GetHashCode() + { + return ContainerId.GetHashCode() ^ ObjectId.GetHashCode() ^ (SessionToken != null ? SessionToken.GetHashCode() : 1); + } + + public static bool operator ==(PrmObjectDelete left, PrmObjectDelete right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectDelete left, PrmObjectDelete right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectGet.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectGet.cs new file mode 100644 index 00000000..b5ed7f8b --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectGet.cs @@ -0,0 +1,52 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmObjectGet( + FrostFsContainerId containerId, + FrostFsObjectId objectId, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsContainerId ContainerId { get; } = containerId; + + public FrostFsObjectId ObjectId { get; } = objectId; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectGet) + return false; + + return Equals((PrmObjectGet)obj); + } + + public override readonly int GetHashCode() + { + return ContainerId.GetHashCode() + ^ ObjectId.GetHashCode() + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()); + } + + public readonly bool Equals(PrmObjectGet other) + { + return ContainerId == other.ContainerId + && ObjectId == other.ObjectId + && SessionToken == other.SessionToken; + } + + public static bool operator ==(PrmObjectGet left, PrmObjectGet right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectGet left, PrmObjectGet right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectHeadGet.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectHeadGet.cs new file mode 100644 index 00000000..15e2a25d --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectHeadGet.cs @@ -0,0 +1,56 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmObjectHeadGet( + FrostFsContainerId containerId, + FrostFsObjectId objectId, + bool raw = false, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) + : ISessionToken, System.IEquatable +{ + public FrostFsContainerId ContainerId { get; } = containerId; + + public FrostFsObjectId ObjectId { get; } = objectId; + + public bool Raw { get; } = raw; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectHeadGet) + return false; + + return Equals((PrmObjectHeadGet)obj); + } + + public readonly bool Equals(PrmObjectHeadGet other) + { + return ContainerId == other.ContainerId + && ObjectId == other.ObjectId + && SessionToken == other.SessionToken; + } + + public override readonly int GetHashCode() + { + return ContainerId.GetHashCode() + ^ ObjectId.GetHashCode() + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()); + } + + public static bool operator ==(PrmObjectHeadGet left, PrmObjectHeadGet right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectHeadGet left, PrmObjectHeadGet right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectPatch.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectPatch.cs new file mode 100644 index 00000000..7b26f5ba --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectPatch.cs @@ -0,0 +1,71 @@ +using System.IO; + +namespace FrostFS.SDK.Client; + +public readonly struct PrmObjectPatch( + FrostFsAddress address, + FrostFsRange range, + Stream? payload, + int maxChunkLength, + FrostFsSessionToken? sessionToken = null, + bool replaceAttributes = false, + FrostFsAttributePair[]? newAttributes = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsAddress Address { get; } = address; + + public FrostFsRange Range { get; } = range; + + /// + /// A stream with source data + /// + public Stream? Payload { get; } = payload; + + public FrostFsAttributePair[]? NewAttributes { get; } = newAttributes; + + public bool ReplaceAttributes { get; } = replaceAttributes; + + public int MaxChunkLength { get; } = maxChunkLength; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectPatch) + return false; + + return Equals((PrmObjectPatch)obj); + } + + public readonly bool Equals(PrmObjectPatch other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return Address.GetHashCode() + ^ Range.GetHashCode() + ^ (Payload == null ? 0 : Payload.GetHashCode()) + ^ (NewAttributes == null ? 0 : NewAttributes.GetHashCode()) + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()) + ^ (ReplaceAttributes ? 1 : 0) + ^ MaxChunkLength; + } + + public static bool operator ==(PrmObjectPatch left, PrmObjectPatch right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectPatch left, PrmObjectPatch right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectPut.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectPut.cs new file mode 100644 index 00000000..78eaf1ee --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectPut.cs @@ -0,0 +1,62 @@ +namespace FrostFS.SDK.Client; + +internal interface PrmObjectPutBase : ISessionToken +{ + FrostFsObjectHeader? Header { get; } + + string[] XHeaders { get; } +} + + +public readonly struct PrmObjectPut( + FrostFsObjectHeader? header, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : PrmObjectPutBase, System.IEquatable +{ + /// + /// Need to provide values like ContainerId and ObjectType to create and object. + /// Optional parameters ike Attributes can be provided as well. + /// + /// Header with required parameters to create an object + public FrostFsObjectHeader? Header { get; } = header; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + internal PutObjectContext PutObjectContext { get; } = new(); + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectPut) + return false; + + return Equals((PrmObjectPut)obj); + } + + public readonly bool Equals(PrmObjectPut other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return (Header == null ? 0 : Header.GetHashCode()) + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()) + ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmObjectPut left, PrmObjectPut right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectPut left, PrmObjectPut right) + { + return !(left == right); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Parameters/PrmObjectSearch.cs b/src/FrostFS.SDK.Client/Parameters/PrmObjectSearch.cs new file mode 100644 index 00000000..508fbd0b --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmObjectSearch.cs @@ -0,0 +1,59 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmObjectSearch( + FrostFsContainerId containerId, + FrostFsSessionToken? token, + string[]? xheaders = null, + params IObjectFilter[] filters) : ISessionToken, System.IEquatable +{ + /// + /// Defines container for the search + /// + /// + public FrostFsContainerId ContainerId { get; } = containerId; + + /// + /// Defines the search criteria + /// + /// Collection of filters + public IObjectFilter[] Filters { get; } = filters; + + /// + public FrostFsSessionToken? SessionToken { get; } = token; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmObjectSearch) + return false; + + return Equals((PrmObjectSearch)obj); + } + + public readonly bool Equals(PrmObjectSearch other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return ContainerId.GetHashCode() + ^ Filters.GetHashCode() + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()) + ^ XHeaders.GetHashCode(); + } + + public static bool operator ==(PrmObjectSearch left, PrmObjectSearch right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmObjectSearch left, PrmObjectSearch right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmRangeGet.cs b/src/FrostFS.SDK.Client/Parameters/PrmRangeGet.cs new file mode 100644 index 00000000..ecb53edd --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmRangeGet.cs @@ -0,0 +1,57 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmRangeGet( + FrostFsContainerId containerId, + FrostFsObjectId objectId, + FrostFsRange range, + bool raw = false, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsContainerId ContainerId { get; } = containerId; + + public FrostFsObjectId ObjectId { get; } = objectId; + + public FrostFsRange Range { get; } = range; + + public bool Raw { get; } = raw; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmRangeGet) + return false; + + return Equals((PrmRangeGet)obj); + } + + public readonly bool Equals(PrmRangeGet other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return ContainerId.GetHashCode() + ^ ObjectId.GetHashCode() + ^ Range.GetHashCode() + ^ Raw.GetHashCode(); + } + + public static bool operator ==(PrmRangeGet left, PrmRangeGet right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmRangeGet left, PrmRangeGet right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmRangeHashGet.cs b/src/FrostFS.SDK.Client/Parameters/PrmRangeHashGet.cs new file mode 100644 index 00000000..0d74008a --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmRangeHashGet.cs @@ -0,0 +1,58 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmRangeHashGet( + FrostFsContainerId containerId, + FrostFsObjectId objectId, + FrostFsRange[] ranges, + byte[] salt, + FrostFsSessionToken? sessionToken = null, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsContainerId ContainerId { get; } = containerId; + + public FrostFsObjectId ObjectId { get; } = objectId; + + public FrostFsRange[] Ranges { get; } = ranges; + + public byte[] Salt { get; } = salt; + + /// + public FrostFsSessionToken? SessionToken { get; } = sessionToken; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmRangeHashGet) + return false; + + return Equals((PrmRangeHashGet)obj); + } + + public readonly bool Equals(PrmRangeHashGet other) + { + return GetHashCode() == other.GetHashCode(); + } + + public override readonly int GetHashCode() + { + return ContainerId.GetHashCode() + ^ ObjectId.GetHashCode() + ^ Ranges.GetHashCode() + ^ Salt.GetHashCode() + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()); + } + + public static bool operator ==(PrmRangeHashGet left, PrmRangeHashGet right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmRangeHashGet left, PrmRangeHashGet right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmSessionCreate.cs b/src/FrostFS.SDK.Client/Parameters/PrmSessionCreate.cs new file mode 100644 index 00000000..5a89ebfc --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmSessionCreate.cs @@ -0,0 +1,40 @@ +namespace FrostFS.SDK.Client; + +public readonly struct PrmSessionCreate(ulong expiration, string[]? xheaders = null) : System.IEquatable +{ + public ulong Expiration { get; } = expiration; + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmSessionCreate) + return false; + + return Equals((PrmSessionCreate)obj); + } + + public override readonly int GetHashCode() + { + return Expiration.GetHashCode() ^ XHeaders.GetHashCode(); + } + + public readonly bool Equals(PrmSessionCreate other) + { + return Expiration == other.Expiration + && XHeaders == other.XHeaders; + } + + public static bool operator ==(PrmSessionCreate left, PrmSessionCreate right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmSessionCreate left, PrmSessionCreate right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmSingleObjectPut.cs b/src/FrostFS.SDK.Client/Parameters/PrmSingleObjectPut.cs new file mode 100644 index 00000000..6ca73df4 --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmSingleObjectPut.cs @@ -0,0 +1,46 @@ +namespace FrostFS.SDK.Client; + +public struct PrmSingleObjectPut( + FrostFsObject frostFsObject, + string[]? xheaders = null) : ISessionToken, System.IEquatable +{ + public FrostFsObject FrostFsObject { get; set; } = frostFsObject; + + /// + public FrostFsSessionToken? SessionToken { get; set; } + + /// + /// FrostFS request X-Headers + /// + public string[] XHeaders { get; } = xheaders ?? []; + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmSingleObjectPut) + return false; + + return Equals((PrmSingleObjectPut)obj); + } + + public override readonly int GetHashCode() + { + return FrostFsObject.GetHashCode() + ^ (SessionToken == null ? 0 : SessionToken.GetHashCode()); + } + + public readonly bool Equals(PrmSingleObjectPut other) + { + return FrostFsObject == other.FrostFsObject + && SessionToken == other.SessionToken; + } + + public static bool operator ==(PrmSingleObjectPut left, PrmSingleObjectPut right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmSingleObjectPut left, PrmSingleObjectPut right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PrmWait.cs b/src/FrostFS.SDK.Client/Parameters/PrmWait.cs new file mode 100644 index 00000000..2090ef3d --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PrmWait.cs @@ -0,0 +1,52 @@ +using System; + +namespace FrostFS.SDK.Client; + +public readonly struct PrmWait(TimeSpan timeout, TimeSpan pollInterval) : IEquatable +{ + private static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(120); + private static TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(5); + + public PrmWait(int timeout, int interval) : this(TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(interval)) + { + } + + public static PrmWait DefaultParams { get; } = new PrmWait(DefaultTimeout, DefaultPollInterval); + + public TimeSpan Timeout { get; } = timeout.Ticks == 0 ? DefaultTimeout : timeout; + + public TimeSpan PollInterval { get; } = pollInterval.Ticks == 0 ? DefaultPollInterval : pollInterval; + + public readonly DateTime GetDeadline() + { + return DateTime.UtcNow.AddTicks(Timeout.Ticks); + } + + public override readonly bool Equals(object obj) + { + if (obj == null || obj is not PrmWait) + return false; + + return Equals((PrmWait)obj); + } + + public override readonly int GetHashCode() + { + return DefaultTimeout.GetHashCode() ^ DefaultPollInterval.GetHashCode(); + } + + public readonly bool Equals(PrmWait other) + { + return Timeout == other.Timeout && PollInterval == other.PollInterval; + } + + public static bool operator ==(PrmWait left, PrmWait right) + { + return left.Equals(right); + } + + public static bool operator !=(PrmWait left, PrmWait right) + { + return !(left == right); + } +} diff --git a/src/FrostFS.SDK.Client/Parameters/PutObjectContext.cs b/src/FrostFS.SDK.Client/Parameters/PutObjectContext.cs new file mode 100644 index 00000000..2976ec5e --- /dev/null +++ b/src/FrostFS.SDK.Client/Parameters/PutObjectContext.cs @@ -0,0 +1,10 @@ +namespace FrostFS.SDK.Client; + +internal sealed class PutObjectContext +{ + internal int MaxObjectSizeCache { get; set; } + + internal ulong CurrentStreamPosition { get; set; } + + internal ulong FullLength { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Services/AccountingServiceProvider.cs b/src/FrostFS.SDK.Client/Services/AccountingServiceProvider.cs new file mode 100644 index 00000000..ea45b37d --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/AccountingServiceProvider.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; + +using FrostFS.Accounting; + +namespace FrostFS.SDK.Client; + +internal sealed class AccountingServiceProvider : ContextAccessor +{ + private readonly AccountingService.AccountingServiceClient? _accountingServiceClient; + + internal AccountingServiceProvider( + AccountingService.AccountingServiceClient? accountingServiceClient, + ClientContext context) + : base(context) + { + _accountingServiceClient = accountingServiceClient; + } + + internal async Task GetBallance(CallContext ctx) + { + BalanceRequest request = new() + { + Body = new() + { + OwnerId = ClientContext.Owner.OwnerID + } + }; + + request.AddMetaHeader([]); + request.Sign(ClientContext.Key); + + var response = await _accountingServiceClient!.BalanceAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return response.Body.Balance; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Services/ApeManagerServiceProvider.cs b/src/FrostFS.SDK.Client/Services/ApeManagerServiceProvider.cs new file mode 100644 index 00000000..ebd60de2 --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/ApeManagerServiceProvider.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Frostfs.V2.Apemanager; +using Google.Protobuf; + +namespace FrostFS.SDK.Client.Services; + +internal sealed class ApeManagerServiceProvider : ContextAccessor +{ + private readonly APEManagerService.APEManagerServiceClient? _apeManagerServiceClient; + + internal ApeManagerServiceProvider(APEManagerService.APEManagerServiceClient? apeManagerServiceClient, ClientContext context) + : base(context) + { + _apeManagerServiceClient = apeManagerServiceClient; + } + + internal async Task> AddChainAsync(PrmApeChainAdd args, CallContext ctx) + { + var binary = RuleSerializer.Serialize(args.Chain); + + AddChainRequest request = new() + { + Body = new() + { + Chain = new() { Raw = UnsafeByteOperations.UnsafeWrap(binary) }, + Target = args.Target.GetChainTarget() + } + }; + + request.AddMetaHeader(args.XHeaders); + request.Sign(ClientContext.Key); + + var response = await _apeManagerServiceClient!.AddChainAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return response.Body.ChainId.Memory; + } + + internal async Task RemoveChainAsync(PrmApeChainRemove args, CallContext ctx) + { + RemoveChainRequest request = new() + { + Body = new() + { + ChainId = UnsafeByteOperations.UnsafeWrap(args.ChainId), + Target = args.Target.GetChainTarget() + } + }; + + request.AddMetaHeader(args.XHeaders); + request.Sign(ClientContext.Key); + + var response = await _apeManagerServiceClient!.RemoveChainAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + } + + internal async Task ListChainAsync(PrmApeChainList args, CallContext ctx) + { + ListChainsRequest request = new() + { + Body = new() + { + Target = args.Target.GetChainTarget() + } + }; + + request.AddMetaHeader(args.XHeaders); + request.Sign(ClientContext.Key); + + var response = await _apeManagerServiceClient!.ListChainsAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return [.. response.Body.Chains.Select(c => RuleSerializer.Deserialize([.. c.Raw]))]; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Services/ContainerServiceProvider.cs b/src/FrostFS.SDK.Client/Services/ContainerServiceProvider.cs new file mode 100644 index 00000000..c0f8b1a6 --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/ContainerServiceProvider.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using FrostFS.Container; +using FrostFS.Refs; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; +using FrostFS.Session; + +namespace FrostFS.SDK.Client; + +internal sealed class ContainerServiceProvider(ContainerService.ContainerServiceClient service, ClientContext clientCtx) : ContextAccessor(clientCtx) +{ + private SessionProvider? sessions; + + public async ValueTask GetDefaultSession(ISessionToken args, CallContext ctx) + { + sessions ??= new(ClientContext); + + if (!ClientContext.SessionCache!.TryGetValue(ClientContext.SessionCacheKey, out var token)) + { + var protoToken = await sessions.GetDefaultSession(args, ctx).ConfigureAwait(false); + + token = new FrostFsSessionToken(protoToken); + + ClientContext.SessionCache.SetValue(ClientContext.SessionCacheKey, token); + } + + if (token == null) + { + throw new FrostFsException("Cannot create session"); + } + + return token; + } + + internal async Task GetContainerAsync(PrmContainerGet args, CallContext ctx) + { + GetRequest request = GetContainerRequest(args.Container.GetContainerID(), args.XHeaders, ClientContext.Key); + + var response = await service.GetAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return response.Body.Container.ToModel(); + } + + internal async IAsyncEnumerable ListContainersAsync(PrmContainerGetAll args, CallContext ctx) + { + var request = new ListRequest + { + Body = new() + { + OwnerId = ClientContext.Owner.OwnerID + } + }; + + request.AddMetaHeader(args.XHeaders); + request.Sign(ClientContext.Key); + + var response = await service.ListAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + foreach (var cid in response.Body.ContainerIds) + { + yield return new FrostFsContainerId(Base58.Encode(cid.Value.Span)); + } + } + + internal async Task PutContainerAsync(PrmContainerCreate args, CallContext ctx) + { + var grpcContainer = args.Container.GetContainer(); + + grpcContainer.OwnerId ??= ClientContext.Owner.OwnerID; + grpcContainer.Version ??= ClientContext.Version.VersionID; + + var request = new PutRequest + { + Body = new PutRequest.Types.Body + { + Container = grpcContainer, + Signature = ClientContext.Key.SignRFC6979(grpcContainer) + } + }; + + var sessionToken = (args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false)) ?? throw new FrostFsException("Cannot create session token"); + + var protoToken = sessionToken.CreateContainerToken( + null, + ContainerSessionContext.Types.Verb.Put, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + var response = await service.PutAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + await WaitForContainer(WaitExpects.Exists, response.Body.ContainerId, args.WaitParams, ctx).ConfigureAwait(false); + + return new FrostFsContainerId(response.Body.ContainerId); + } + + internal async Task DeleteContainerAsync(PrmContainerDelete args, CallContext ctx) + { + var request = new DeleteRequest + { + Body = new DeleteRequest.Types.Body + { + ContainerId = args.ContainerId.GetContainerID(), + Signature = ClientContext.Key.SignRFC6979(args.ContainerId.GetContainerID().Value) + } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateContainerToken( + request.Body.ContainerId, + ContainerSessionContext.Types.Verb.Delete, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + var response = await service.DeleteAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + await WaitForContainer(WaitExpects.Removed, request.Body.ContainerId, args.WaitParams, ctx) + .ConfigureAwait(false); + + Verifier.CheckResponse(response); + } + + private static GetRequest GetContainerRequest(ContainerID id, string[] xHeaders, ClientKey key) + { + var request = new GetRequest + { + Body = new GetRequest.Types.Body + { + ContainerId = id + } + }; + + request.AddMetaHeader(xHeaders); + request.Sign(key); + + return request; + } + + private enum WaitExpects + { + Exists, + Removed + } + + private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait waitParams, CallContext ctx) + { + var request = GetContainerRequest(id, [], ClientContext.Key); + + async Task action() + { + var response = await service.GetAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + Verifier.CheckResponse(response); + } + + await WaitFor(action, expect, waitParams).ConfigureAwait(false); + } + + private static async Task WaitFor( + Func action, + WaitExpects expect, + PrmWait waitParams) + { + var deadLine = waitParams.GetDeadline(); + + while (true) + { + try + { + await action().ConfigureAwait(false); + + if (expect == WaitExpects.Exists) + return; + + if (DateTime.UtcNow >= deadLine) + throw new TimeoutException(); + + await Task.Delay(waitParams.PollInterval).ConfigureAwait(false); + } + catch (FrostFsResponseException ex) + { + if (DateTime.UtcNow >= deadLine) + throw new TimeoutException(); + + if (ex.Status?.Code != FrostFsStatusCode.ContainerNotFound) + throw; + + if (expect == WaitExpects.Removed) + return; + + await Task.Delay(waitParams.PollInterval).ConfigureAwait(false); + } + } + } +} diff --git a/src/FrostFS.SDK.Client/Services/NetmapServiceProvider.cs b/src/FrostFS.SDK.Client/Services/NetmapServiceProvider.cs new file mode 100644 index 00000000..672c0107 --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/NetmapServiceProvider.cs @@ -0,0 +1,159 @@ +using System; +using System.Threading.Tasks; + +using FrostFS.Netmap; + +using static FrostFS.Netmap.NetworkConfig.Types; + +namespace FrostFS.SDK.Client; + +internal sealed class NetmapServiceProvider : ContextAccessor +{ + private readonly NetmapService.NetmapServiceClient netmapServiceClient; + + internal NetmapServiceProvider(NetmapService.NetmapServiceClient netmapServiceClient, ClientContext context) + : base(context) + { + this.netmapServiceClient = netmapServiceClient; + } + + internal async Task GetNetworkSettingsAsync(CallContext ctx) + { + if (ClientContext.NetworkSettings != null) + return ClientContext.NetworkSettings; + + var response = await GetNetworkInfoAsync(ctx).ConfigureAwait(false); + + var settings = new NetworkSettings(); + + var info = response.Body.NetworkInfo; + + settings.Epoch = info.CurrentEpoch; + settings.MagicNumber = info.MagicNumber; + settings.MsPerBlock = info.MsPerBlock; + + foreach (var param in info.NetworkConfig.Parameters) + { + SetNetworksParam(param, settings); + } + + ClientContext.NetworkSettings = settings; + + return settings; + } + + internal async Task GetLocalNodeInfoAsync(CallContext ctx) + { + var request = new LocalNodeInfoRequest + { + Body = new LocalNodeInfoRequest.Types.Body { } + }; + + request.AddMetaHeader([]); + request.Sign(ClientContext.Key); + + var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return response.Body.ToModel(); + } + + internal async Task GetNetworkInfoAsync(CallContext ctx) + { + var request = new NetworkInfoRequest(); + + request.AddMetaHeader([]); + request.Sign(ClientContext.Key); + + var response = await netmapServiceClient.NetworkInfoAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken) + .ConfigureAwait(false); + + Verifier.CheckResponse(response); + + return response; + } + + internal async Task GetNetmapSnapshotAsync(CallContext ctx) + { + var request = new NetmapSnapshotRequest(); + + request.AddMetaHeader([]); + request.Sign(ClientContext.Key); + + var response = await netmapServiceClient.NetmapSnapshotAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return response.ToModel(); + } + + private static bool GetBoolValue(ReadOnlySpan bytes) + { + for (int i = bytes.Length - 1; i >= 0; i--) + if (bytes[i] != 0) + return true; + + return false; + } + + private static ulong GetLongValue(ReadOnlySpan bytes) + { + ulong val = 0; + for (var i = bytes.Length - 1; i >= 0; i--) + val = (val << 8) + bytes[i]; + + return val; + } + + private static void SetNetworksParam(Parameter param, NetworkSettings settings) + { + var key = param.Key.ToStringUtf8(); + + var valueBytes = param.Value.Span; + switch (key) + { + case "AuditFee": + settings.AuditFee = GetLongValue(valueBytes); + break; + case "BasicIncomeRate": + settings.BasicIncomeRate = GetLongValue(valueBytes); + break; + case "ContainerFee": + settings.ContainerFee = GetLongValue(valueBytes); + break; + case "ContainerAliasFee": + settings.ContainerAliasFee = GetLongValue(valueBytes); + break; + case "EpochDuration": + settings.EpochDuration = GetLongValue(valueBytes); + break; + case "InnerRingCandidateFee": + settings.InnerRingCandidateFee = GetLongValue(valueBytes); + break; + case "MaxECDataCount": + settings.MaxECDataCount = GetLongValue(valueBytes); + break; + case "MaxECParityCount": + settings.MaxECParityCount = GetLongValue(valueBytes); + break; + case "MaxObjectSize": + settings.MaxObjectSize = GetLongValue(valueBytes); + break; + case "WithdrawFee": + settings.WithdrawFee = GetLongValue(valueBytes); + break; + case "HomomorphicHashingDisabled": + settings.HomomorphicHashingDisabled = GetBoolValue(valueBytes); + break; + case "MaintenanceModeAllowed": + settings.MaintenanceModeAllowed = GetBoolValue(valueBytes); + break; + default: + settings.UnnamedSettings.Add(key, valueBytes.ToArray()); + break; + } + } +} + + diff --git a/src/FrostFS.SDK.Client/Services/ObjectServiceProvider.cs b/src/FrostFS.SDK.Client/Services/ObjectServiceProvider.cs new file mode 100644 index 00000000..b3c1f506 --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/ObjectServiceProvider.cs @@ -0,0 +1,894 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using FrostFS.Object; +using FrostFS.Refs; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Interfaces; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client; + +internal sealed class ObjectServiceProvider(ObjectService.ObjectServiceClient client, ClientContext clientCtx) + : ContextAccessor(clientCtx) +{ + private SessionProvider? sessions; + private readonly ObjectService.ObjectServiceClient client = client; + + public async ValueTask GetDefaultSession(ISessionToken args, CallContext ctx) + { + sessions ??= new(ClientContext); + + if (!ClientContext.SessionCache!.TryGetValue(ClientContext.SessionCacheKey, out var token)) + { + var protoToken = await sessions.GetDefaultSession(args, ctx).ConfigureAwait(false); + + token = new FrostFsSessionToken(protoToken); + + ClientContext.SessionCache.SetValue(ClientContext.SessionCacheKey, token); + } + + if (token == null) + { + throw new FrostFsException("Cannot create session"); + } + + return token; + } + + internal async Task GetObjectHeadAsync(PrmObjectHeadGet args, CallContext ctx) + { + var request = new HeadRequest + { + Body = new HeadRequest.Types.Body + { + Address = new Address + { + ContainerId = args.ContainerId.GetContainerID(), + ObjectId = args.ObjectId.ToMessage() + }, + Raw = args.Raw + } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Head, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + var response = await client!.HeadAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken).ConfigureAwait(false); + + Verifier.CheckResponse(response); + + var result = new FrostFsHeaderResult(); + + if (response.Body.Header != null) + { + result.HeaderInfo = response.Body.Header?.Header.ToModel(); + } + + if (response.Body.SplitInfo != null) + { + result.SplitInfo = new FrostFsSplitInfo(response.Body.SplitInfo); + } + + return result; + } + + internal async Task GetObjectAsync(PrmObjectGet args, CallContext ctx) + { + var request = new GetRequest + { + Body = new GetRequest.Types.Body + { + Address = new Address + { + ContainerId = args.ContainerId.GetContainerID(), + ObjectId = args.ObjectId.ToMessage() + } + } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Get, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + return await GetObject(request, ctx).ConfigureAwait(false); + } + + internal async Task GetRangeAsync(PrmRangeGet args, CallContext ctx) + { + var request = new GetRangeRequest + { + Body = new GetRangeRequest.Types.Body + { + Address = new Address + { + ContainerId = args.ContainerId.GetContainerID(), + ObjectId = args.ObjectId.ToMessage() + }, + Range = new Object.Range + { + Offset = args.Range.Offset, + Length = args.Range.Length + }, + Raw = args.Raw + } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Range, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + var call = client.GetRange(request, null, ctx.GetDeadline(), ctx.CancellationToken); + return new RangeReader(call); + } + + internal async Task[]> GetRangeHashAsync(PrmRangeHashGet args, CallContext ctx) + { + var request = new GetRangeHashRequest + { + Body = new GetRangeHashRequest.Types.Body + { + Address = new Address + { + ContainerId = args.ContainerId.GetContainerID(), + ObjectId = args.ObjectId.ToMessage() + }, + Type = ChecksumType.Sha256, + Salt = ByteString.CopyFrom(args.Salt) // TODO: create a type with calculated cashed ByteString inside + } + }; + + foreach (var range in args.Ranges) + { + request.Body.Ranges.Add(new Object.Range + { + Length = range.Length, + Offset = range.Offset + }); + } + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Rangehash, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + var response = await client.GetRangeHashAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + var hashCollection = response.Body.HashList.Select(h => h.Memory).ToArray(); + + return hashCollection; + } + + internal async Task DeleteObjectAsync(PrmObjectDelete args, CallContext ctx) + { + var request = new DeleteRequest + { + Body = new DeleteRequest.Types.Body + { + Address = new Address + { + ContainerId = args.ContainerId.GetContainerID(), + ObjectId = args.ObjectId.ToMessage() + } + } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Delete, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + request.Sign(ClientContext.Key); + + var response = await client.DeleteAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + } + + internal async IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args, CallContext ctx) + { + var request = new SearchRequest + { + Body = new SearchRequest.Types.Body + { + ContainerId = args.ContainerId.GetContainerID(), + Version = 1 // TODO: clarify this param + } + }; + + request.Body.Filters.AddRange(args.Filters.Select(f => f.ToMessage())); + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + new Address { ContainerId = request.Body.ContainerId }, + ObjectSessionContext.Types.Verb.Search, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + using var stream = GetSearchReader(request, ctx); + + while (true) + { + var ids = await stream.Read(ctx.CancellationToken).ConfigureAwait(false); + + if (ids == null) + yield break; + + foreach (var oid in ids) + { + yield return FrostFsObjectId.FromHash(oid.Value.Span); + } + } + } + + internal async Task PutSingleObjectAsync(PrmSingleObjectPut args, CallContext ctx) + { + var grpcObject = ObjectTools.CreateSingleObject(args.FrostFsObject, ClientContext); + + var request = new PutSingleRequest + { + Body = new() { Object = grpcObject } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + new Address { ContainerId = grpcObject.Header.ContainerId }, + ObjectSessionContext.Types.Verb.Put, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + + request.Sign(ClientContext.Key); + + var response = await client.PutSingleAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken).ConfigureAwait(false); + + Verifier.CheckResponse(response); + + return FrostFsObjectId.FromHash(grpcObject.ObjectId.Value.Span); + } + + internal async Task PatchObjectAsync(PrmObjectPatch args, CallContext ctx) + { + var chunkSize = args.MaxChunkLength; + + var call = client.Patch(null, ctx.GetDeadline(), ctx.CancellationToken); + + var address = new Address + { + ObjectId = args.Address.ObjectId, + ContainerId = args.Address.ContainerId + }; + + if (args.Payload != null && args.Payload.Length > 0) + { + byte[]? chunkBuffer = null; + try + { + chunkBuffer = ArrayPool.Shared.Rent(chunkSize); + + bool isFirstChunk = true; + ulong currentPos = args.Range.Offset; + + while (true) + { + var bytesCount = await args.Payload.ReadAsync(chunkBuffer, 0, chunkSize, ctx.CancellationToken).ConfigureAwait(false); + + if (bytesCount == 0) + { + break; + } + + PatchRequest request; + + if (isFirstChunk) + { + request = await CreateFirstRequest(args, ctx, address).ConfigureAwait(false); + + request.Body.Patch = new PatchRequest.Types.Body.Types.Patch + { + Chunk = UnsafeByteOperations.UnsafeWrap(chunkBuffer.AsMemory(0, bytesCount)), + SourceRange = new Range { Offset = currentPos, Length = (ulong)bytesCount } + }; + + isFirstChunk = false; + } + else + { + request = new PatchRequest() + { + Body = new() + { + Address = address, + Patch = new PatchRequest.Types.Body.Types.Patch + { + Chunk = UnsafeByteOperations.UnsafeWrap(chunkBuffer.AsMemory(0, bytesCount)), + SourceRange = new Range { Offset = currentPos, Length = (ulong)bytesCount } + } + } + }; + + request.AddMetaHeader(args.XHeaders); + } + + request.Sign(ClientContext.Key); + + await call.RequestStream.WriteAsync(request).ConfigureAwait(false); + + currentPos += (ulong)bytesCount; + } + } + finally + { + if (chunkBuffer != null) + { + ArrayPool.Shared.Return(chunkBuffer); + } + } + } + else if (args.NewAttributes != null && args.NewAttributes.Length > 0) + { + PatchRequest request = await CreateFirstRequest(args, ctx, address).ConfigureAwait(false); + + request.Sign(ClientContext.Key); + + await call.RequestStream.WriteAsync(request).ConfigureAwait(false); + } + + await call.RequestStream.CompleteAsync().ConfigureAwait(false); + var response = await call.ResponseAsync.ConfigureAwait(false); + + Verifier.CheckResponse(response); + + return response.Body.ObjectId.ToModel(); + + async Task CreateFirstRequest(PrmObjectPatch args, CallContext ctx, Address address) + { + var body = new PatchRequest.Types.Body() { Address = address }; + + if (args.NewAttributes != null) + { + body.ReplaceAttributes = args.ReplaceAttributes; + + foreach (var attr in args.NewAttributes!) + { + body.NewAttributes.Add(attr.ToMessage()); + } + } + + var request = new PatchRequest() { Body = body }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + address, + ObjectSessionContext.Types.Verb.Patch, + ClientContext.Key); + + request.AddMetaHeader(args.XHeaders, protoToken); + return request; + } + } + + internal async Task PutClientCutObjectAsync(PrmObjectClientCutPut args, CallContext ctx) + { + if (args.Payload == null) + throw new ArgumentException(nameof(args.Payload)); + + if (args.Header == null) + throw new ArgumentException(nameof(args.Header)); + + var networkSettings = await ClientContext.Client.GetNetworkSettingsAsync(ctx).ConfigureAwait(false); + int partSize = (int)networkSettings.MaxObjectSize; + + int chunkSize = args.BufferMaxSize > 0 ? args.BufferMaxSize : Constants.ObjectChunkSize; + + ulong fullLength; + + // Information about the uploaded parts. + var progressInfo = args.Progress; // + var offset = 0L; + + if (progressInfo != null && progressInfo.GetParts().Count > 0) + { + if (!args.Payload.CanSeek) + { + throw new FrostFsException("Cannot resume client cut upload for this stream. Seek must be supported."); + } + + var lastPart = progressInfo.GetLast(); + args.Payload.Position = lastPart.Offset + lastPart.Length; + fullLength = (ulong)(args.Payload.Length - args.Payload.Position); + offset = args.Payload.Position; + } + else + { + if (args.Header.PayloadLength > 0) + fullLength = args.Header.PayloadLength; + else if (args.Payload.CanSeek) + fullLength = (ulong)args.Payload.Length; + else + throw new ArgumentException("The stream does not have a length and payload length is not defined"); + } + + //define collection capacity + var restPart = (fullLength % (ulong)partSize) > 0 ? 1 : 0; + var objectsCount = fullLength > 0 ? (int)(fullLength / (ulong)partSize) + restPart : 0; + + progressInfo ??= new UploadProgressInfo(Guid.NewGuid(), objectsCount); + + var remain = fullLength; + + byte[]? buffer = null; + bool isRentBuffer = false; + + try + { + if (args.CustomBuffer != null) + { + if (args.CustomBuffer.Length < chunkSize) + throw new ArgumentException($"Buffer size is too small. At least {chunkSize} required"); + + buffer = args.CustomBuffer; + } + else + { + buffer = ArrayPool.Shared.Rent(chunkSize); + isRentBuffer = true; + } + + FrostFsObjectId? resultObjectId = null; + FrostFsObjectHeader? parentHeader = null; + + while (remain > 0) + { + var bytesToWrite = Math.Min((ulong)partSize, remain); + var isLastPart = remain <= (ulong)partSize; + + // When the last part of the object is uploaded, all metadata for the object must be added + if (isLastPart && objectsCount > 1) + { + parentHeader = new FrostFsObjectHeader(args.Header.ContainerId, FrostFsObjectType.Regular) + { + Attributes = args.Header.Attributes, + PayloadLength = fullLength + }; + } + + // Uploading the next part of the object. Note: the request must contain a non-null SplitId parameter + var header = objectsCount == 1 ? args.Header : new FrostFsObjectHeader( + args.Header.ContainerId, + FrostFsObjectType.Regular, + [], + new FrostFsSplit(progressInfo.SplitId, progressInfo.GetLast().ObjectId, parentHeader: parentHeader)); + + var prm = new PrmObjectPut(header); + using var stream = await PutStreamObjectAsync(prm, ctx).ConfigureAwait(false); + var uploaded = 0; + + // If an error occurs while uploading a part of the object, there is no need to re-upload the parts + // that were successfully uploaded before. It is sufficient to re-upload only the failed part + + var thisPartRest = (int)Math.Min((ulong)partSize, remain); + while (thisPartRest > 0) + { + var nextChunkSize = Math.Min(thisPartRest, chunkSize); + var size = await args.Payload.ReadAsync(buffer, 0, nextChunkSize).ConfigureAwait(false); + + if (size == 0) + break; + + await stream.WriteAsync(buffer.AsMemory(0, size)).ConfigureAwait(false); + uploaded += size; + thisPartRest -= size; + } + + var objectId = await stream.CompleteAsync().ConfigureAwait(false); + var part = new ObjectPartInfo(offset, uploaded, objectId); + offset += uploaded; + progressInfo.AddPart(part); + + remain -= bytesToWrite; + + if (isLastPart) + { + if (objectsCount == 1) + { + return progressInfo.GetPart(0).ObjectId; + } + + if (parentHeader == null) continue; + + // Once all parts of the object are uploaded, they must be linked into a single entity + var linkObject = new FrostFsLinkObject(header.ContainerId, progressInfo.SplitId, parentHeader, + [.. progressInfo.GetParts().Select(p => p.ObjectId)]); + + await PutSingleObjectAsync(new PrmSingleObjectPut(linkObject), ctx).ConfigureAwait(false); + + // Retrieve the ID of the linked object + resultObjectId = FrostFsObjectId.FromHash(prm.Header!.GetHeader().Split!.Parent.Value.Span); + return resultObjectId; + } + } + + throw new FrostFsException("Unexpected error: cannot send object"); + } + finally + { + if (isRentBuffer && buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + internal async Task PutClientCutSingleObjectAsync(PrmObjectClientCutPut args, CallContext ctx) + { + if (args.Payload == null) + throw new ArgumentException(nameof(args.Payload)); + + if (args.Header == null) + throw new ArgumentException(nameof(args.Header)); + + var networkSettings = await ClientContext.Client.GetNetworkSettingsAsync(ctx).ConfigureAwait(false); + int partSize = (int)networkSettings.MaxObjectSize; + + int chunkSize = args.BufferMaxSize > 0 ? args.BufferMaxSize : Constants.ObjectChunkSize; + + ulong fullLength; + + // Information about the uploaded parts. + var progressInfo = args.Progress; // + var offset = 0L; + + if (progressInfo != null && progressInfo.GetParts().Count > 0) + { + if (!args.Payload.CanSeek) + { + throw new FrostFsException("Cannot resume client cut upload for this stream. Seek must be supported."); + } + + var lastPart = progressInfo.GetLast(); + args.Payload.Position = lastPart.Offset + lastPart.Length; + fullLength = (ulong)(args.Payload.Length - args.Payload.Position); + offset = args.Payload.Position; + } + else + { + if (args.Header.PayloadLength > 0) + fullLength = args.Header.PayloadLength; + else if (args.Payload.CanSeek) + fullLength = (ulong)args.Payload.Length; + else + throw new ArgumentException("The stream does not have a length and payload length is not defined"); + } + + //define collection capacity + var restPart = (fullLength % (ulong)partSize) > 0 ? 1 : 0; + var objectsCount = fullLength > 0 ? (int)(fullLength / (ulong)partSize) + restPart : 0; + + progressInfo ??= new UploadProgressInfo(Guid.NewGuid(), objectsCount); + + // if the object fits one part, it can be loaded as non-complex object, but if it is not upload resuming + if (objectsCount == 1 && progressInfo.GetLast().Length == 0) + { + args.PutObjectContext.MaxObjectSizeCache = partSize; + args.PutObjectContext.FullLength = fullLength; + var singlePartResult = await PutMultipartStreamObjectAsync(args, default).ConfigureAwait(false); + return singlePartResult.ObjectId; + } + + var remain = fullLength; + + byte[]? buffer = null; + bool isRentBuffer = false; + + try + { + if (args.CustomBuffer != null) + { + if (args.CustomBuffer.Length < partSize) + { + throw new ArgumentException($"Buffer size is too small. A buffer with capacity {partSize} is required"); + } + + buffer = args.CustomBuffer; + } + else + { + buffer = ArrayPool.Shared.Rent(partSize); + isRentBuffer = true; + } + + FrostFsObjectHeader? parentHeader = null; + + for (int i = 0; i < objectsCount;) + { + i++; + var bytesToWrite = Math.Min((ulong)partSize, remain); + + var size = await args.Payload.ReadAsync(buffer, 0, (int)bytesToWrite).ConfigureAwait(false); + + if (i == objectsCount) + { + parentHeader = new FrostFsObjectHeader(args.Header.ContainerId, FrostFsObjectType.Regular) + { + PayloadLength = args.PutObjectContext.FullLength, + Attributes = args.Header.Attributes + }; + } + + // Uploading the next part of the object. Note: the request must contain a non-null SplitId parameter + var partHeader = new FrostFsObjectHeader( + args.Header.ContainerId, + FrostFsObjectType.Regular, + [], + new FrostFsSplit(progressInfo.SplitId, progressInfo.GetLast().ObjectId, + parentHeader: parentHeader)) + { + PayloadLength = (ulong)size + }; + + var obj = new FrostFsObject(partHeader) + { + SingleObjectPayload = buffer.AsMemory(0, size) + }; + + var prm = new PrmSingleObjectPut(obj); + + var objectId = await PutSingleObjectAsync(prm, ctx).ConfigureAwait(false); + + var part = new ObjectPartInfo(offset, size, objectId); + progressInfo.AddPart(part); + + offset += size; + + if (i < objectsCount) + { + continue; + } + + // Once all parts of the object are uploaded, they must be linked into a single entity + var linkObject = new FrostFsLinkObject(args.Header.ContainerId, progressInfo.SplitId, parentHeader!, [.. progressInfo.GetParts().Select(p => p.ObjectId)]); + + _ = await PutSingleObjectAsync(new PrmSingleObjectPut(linkObject), ctx).ConfigureAwait(false); + + // Retrieve the ID of the linked object + return partHeader.GetHeader().Split!.Parent.ToModel(); + } + + throw new FrostFsException("Unexpected error"); + } + finally + { + if (isRentBuffer && buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + struct PutObjectResult(FrostFsObjectId objectId, int objectSize) + { + public FrostFsObjectId ObjectId = objectId; + public int ObjectSize = objectSize; + } + + private async Task PutMultipartStreamObjectAsync(PrmObjectClientCutPut args, CallContext ctx) + { + var payload = args.Payload!; + + var chunkSize = args.BufferMaxSize > 0 ? args.BufferMaxSize : Constants.ObjectChunkSize; + var restBytes = args.PutObjectContext.FullLength - args.PutObjectContext.CurrentStreamPosition; + + chunkSize = (int)Math.Min(restBytes, (ulong)chunkSize); + + bool isRentBuffer = false; + byte[]? chunkBuffer = null; + + try + { + // 0 means no limit from client, so server side cut is performed + var objectLimitSize = args.PutObjectContext.MaxObjectSizeCache; + + if (args.CustomBuffer != null) + { + if (args.CustomBuffer.Length < chunkSize) + { + throw new ArgumentException($"Buffer size is too small. At least {chunkSize} required"); + } + + chunkBuffer = args.CustomBuffer; + } + else + { + chunkBuffer = ArrayPool.Shared.Rent(chunkSize); + isRentBuffer = true; + } + + var sentBytes = 0; + + using var stream = await GetUploadStream(args, ctx).ConfigureAwait(false); + + while (objectLimitSize == 0 || sentBytes < objectLimitSize) + { + // send chunks limited to default or user's settings + var bufferSize = objectLimitSize > 0 ? + Math.Min(objectLimitSize - sentBytes, chunkSize) + : chunkSize; + + var bytesCount = await payload.ReadAsync(chunkBuffer, 0, bufferSize, ctx.CancellationToken).ConfigureAwait(false); + + if (bytesCount == 0) + break; + + sentBytes += bytesCount; + + var chunkRequest = new PutRequest + { + Body = new PutRequest.Types.Body + { + Chunk = UnsafeByteOperations.UnsafeWrap(chunkBuffer.AsMemory(0, bytesCount)) + } + }; + + chunkRequest.AddMetaHeader(args.XHeaders); + chunkRequest.Sign(ClientContext.Key); + + await stream.Write(chunkRequest).ConfigureAwait(false); + } + + args.PutObjectContext.CurrentStreamPosition += (ulong)sentBytes; + + var response = await stream.Close().ConfigureAwait(false); + Verifier.CheckResponse(response); + + return new PutObjectResult(FrostFsObjectId.FromHash(response.Body.ObjectId.Value.Span), sentBytes); + } + finally + { + if (isRentBuffer && chunkBuffer != null) + { + ArrayPool.Shared.Return(chunkBuffer); + } + } + } + + internal async Task PutStreamObjectAsync(PrmObjectPutBase args, CallContext ctx) + { + var stream = await GetUploadStream(args, ctx).ConfigureAwait(false); + + return new ObjectWriter(ClientContext, args, stream); + } + + private async Task> GetUploadStream(PrmObjectPutBase args, CallContext ctx) + { + var header = args.Header!; + + header.OwnerId ??= ClientContext.Owner; + header.Version ??= ClientContext.Version; + + var grpcHeader = header.GetHeader(); + + if (header.Split != null) + { + ObjectTools.SetSplitValues(grpcHeader, header.Split, ClientContext.Owner, ClientContext.Version, ClientContext.Key); + } + + var initRequest = new PutRequest + { + Body = new PutRequest.Types.Body + { + Init = new PutRequest.Types.Body.Types.Init + { + Header = grpcHeader, + } + } + }; + + var sessionToken = args.SessionToken ?? await GetDefaultSession(args, ctx).ConfigureAwait(false); + + var protoToken = sessionToken.CreateObjectTokenContext( + new Address { ContainerId = grpcHeader.ContainerId }, + ObjectSessionContext.Types.Verb.Put, + ClientContext.Key); + + initRequest.AddMetaHeader(args.XHeaders, protoToken); + + initRequest.Sign(ClientContext.Key); + + return await PutObjectInit(initRequest, ctx).ConfigureAwait(false); + } + + private async Task> PutObjectInit(PutRequest initRequest, CallContext ctx) + { + if (initRequest is null) + { + throw new ArgumentNullException(nameof(initRequest)); + } + + var call = client.Put(null, ctx.GetDeadline(), ctx.CancellationToken); + + await call.RequestStream.WriteAsync(initRequest).ConfigureAwait(false); + + return new ObjectStreamer(call); + } + + private async Task GetObject(GetRequest request, CallContext ctx) + { + var reader = GetObjectInit(request, ctx); + + var grpcObject = await reader.ReadHeader().ConfigureAwait(false); + var modelObject = grpcObject.ToModel(); + + modelObject.ObjectReader = reader; + + return modelObject; + } + + private ObjectReader GetObjectInit(GetRequest initRequest, CallContext ctx) + { + if (initRequest is null) + throw new ArgumentNullException(nameof(initRequest)); + + var call = client.Get(initRequest, null, ctx.GetDeadline(), ctx.CancellationToken); + + return new ObjectReader(call); + } + + private SearchReader GetSearchReader(SearchRequest initRequest, CallContext ctx) + { + if (initRequest is null) + { + throw new ArgumentNullException(nameof(initRequest)); + } + + var call = client.Search(initRequest, null, ctx.GetDeadline(), ctx.CancellationToken); + + return new SearchReader(call); + } +} diff --git a/src/FrostFS.SDK.Client/Services/SessionServiceProvider.cs b/src/FrostFS.SDK.Client/Services/SessionServiceProvider.cs new file mode 100644 index 00000000..cde2b9eb --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/SessionServiceProvider.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; + +using FrostFS.Session; + +namespace FrostFS.SDK.Client; + +internal sealed class SessionServiceProvider : ContextAccessor +{ + private readonly SessionService.SessionServiceClient? _sessionServiceClient; + + internal SessionServiceProvider(SessionService.SessionServiceClient? sessionServiceClient, ClientContext context) + : base(context) + { + _sessionServiceClient = sessionServiceClient; + } + + internal async Task CreateSessionAsync(PrmSessionCreate args, CallContext ctx) + { + var request = new CreateRequest + { + Body = new CreateRequest.Types.Body + { + OwnerId = ClientContext.Owner.OwnerID, + Expiration = args.Expiration + } + }; + + request.AddMetaHeader(args.XHeaders); + request.Sign(ClientContext.Key); + + return await CreateSession(request, ctx).ConfigureAwait(false); + } + + internal async Task CreateSession(CreateRequest request, CallContext ctx) + { + var response = await _sessionServiceClient!.CreateAsync(request, null, ctx.GetDeadline(), ctx.CancellationToken); + + Verifier.CheckResponse(response); + + return new SessionToken + { + Body = new SessionToken.Types.Body + { + Id = response.Body.Id, + SessionKey = response.Body.SessionKey, + OwnerId = request.Body.OwnerId, + Lifetime = new SessionToken.Types.Body.Types.TokenLifetime + { + Exp = request.Body.Expiration, + Iat = response.MetaHeader.Epoch, + Nbf = response.MetaHeader.Epoch, + } + } + }; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Services/Shared/ContextAccessor.cs b/src/FrostFS.SDK.Client/Services/Shared/ContextAccessor.cs new file mode 100644 index 00000000..e1271468 --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/Shared/ContextAccessor.cs @@ -0,0 +1,6 @@ +namespace FrostFS.SDK.Client; + +internal class ContextAccessor(ClientContext context) +{ + protected ClientContext ClientContext { get; set; } = context; +} diff --git a/src/FrostFS.SDK.Client/Services/Shared/SessionCache.cs b/src/FrostFS.SDK.Client/Services/Shared/SessionCache.cs new file mode 100644 index 00000000..a02436ff --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/Shared/SessionCache.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; + +namespace FrostFS.SDK.Client; + +internal sealed class SessionCache(ulong sessionExpirationDuration) +{ + private ConcurrentDictionary _cache { get; } = []; + + internal ulong CurrentEpoch { get; set; } + + internal ulong TokenDuration { get; set; } = sessionExpirationDuration; + + internal bool Contains(string key) + { + return _cache.ContainsKey(key); + } + + internal bool TryGetValue(string? key, out FrostFsSessionToken? value) + { + if (key == null) + { + value = null; + return false; + } + + var ok = _cache.TryGetValue(key, out value); + + return ok && value != null; + } + + internal void SetValue(string? key, FrostFsSessionToken value) + { + if (key != null) + { + _cache[key] = value; + } + } +} diff --git a/src/FrostFS.SDK.Client/Services/Shared/SessionProvider.cs b/src/FrostFS.SDK.Client/Services/Shared/SessionProvider.cs new file mode 100644 index 00000000..da5a26dd --- /dev/null +++ b/src/FrostFS.SDK.Client/Services/Shared/SessionProvider.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; + +namespace FrostFS.SDK.Client; + +internal interface ISessionProvider +{ + ValueTask GetOrCreateSession(ISessionToken args, CallContext ctx); +} + +internal sealed class SessionProvider(ClientContext envCtx) +{ + public async Task CreateSession(ISessionToken args, CallContext ctx) + { + var token = await GetDefaultSession(args, ctx).ConfigureAwait(false); + + return new FrostFsSessionToken(token); + } + + internal async Task GetDefaultSession(ISessionToken args, CallContext ctx) + { + return await envCtx.Client.CreateSessionInternalAsync(new PrmSessionCreate(uint.MaxValue), ctx).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Tools/ClientContext.cs b/src/FrostFS.SDK.Client/Tools/ClientContext.cs new file mode 100644 index 00000000..104cb3d6 --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/ClientContext.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.ObjectModel; + +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK.Client; + +public class ClientContext(FrostFSClient client, ClientKey key, FrostFsOwner owner, ChannelBase channel, FrostFsVersion version) +{ + private string? sessionKey; + + internal FrostFsOwner Owner { get; } = owner; + + internal string? Address { get; } = channel.Target; + + internal ChannelBase Channel { get; private set; } = channel; + + internal FrostFsVersion Version { get; } = version; + + internal NetworkSettings? NetworkSettings { get; set; } + + internal FrostFSClient Client { get; } = client; + + internal ClientKey Key { get; } = key; + + internal SessionCache? SessionCache { get; set; } + + internal Action? Callback { get; set; } + + internal Collection? Interceptors { get; set; } + + internal Action? PoolErrorHandler { get; set; } + + + internal string? SessionCacheKey + { + get + { + if (sessionKey == null && Key != null && Address != null) + { + sessionKey = $"{Address}{Key}"; + } + + return sessionKey; + } + } +} diff --git a/src/FrostFS.SDK.Client/Tools/NetworkSettings.cs b/src/FrostFS.SDK.Client/Tools/NetworkSettings.cs new file mode 100644 index 00000000..d8f5e12b --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/NetworkSettings.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace FrostFS.SDK.Client; + +public class NetworkSettings +{ + public ulong Epoch { get; internal set; } + public ulong MagicNumber { get; internal set; } + public long MsPerBlock { get; internal set; } + + public ulong AuditFee { get; internal set; } + public ulong BasicIncomeRate { get; internal set; } + public ulong ContainerFee { get; internal set; } + public ulong ContainerAliasFee { get; internal set; } + public ulong EpochDuration { get; internal set; } + public ulong InnerRingCandidateFee { get; internal set; } + public ulong MaxObjectSize { get; internal set; } + public ulong MaxECDataCount { get; internal set; } + public ulong MaxECParityCount { get; internal set; } + public ulong WithdrawFee { get; internal set; } + public bool HomomorphicHashingDisabled { get; internal set; } + public bool MaintenanceModeAllowed { get; internal set; } + + public Dictionary UnnamedSettings { get; } = []; +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Tools/ObjectReader.cs b/src/FrostFS.SDK.Client/Tools/ObjectReader.cs new file mode 100644 index 00000000..7d5dbd7b --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/ObjectReader.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using FrostFS.Object; + +using Grpc.Core; + +namespace FrostFS.SDK.Client; + +public sealed class ObjectReader(AsyncServerStreamingCall call) : IObjectReader +{ + private bool disposed; + + public AsyncServerStreamingCall Call { get; private set; } = call; + + internal async Task ReadHeader() + { + if (!await Call.ResponseStream.MoveNext().ConfigureAwait(false)) + throw new FrostFsStreamException("unexpected end of stream"); + + var response = Call.ResponseStream.Current; + Verifier.CheckResponse(response); + + if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init) + throw new FrostFsStreamException("unexpected message type"); + + return new Object.Object + { + ObjectId = response.Body.Init.ObjectId, + Header = response.Body.Init.Header, + }; + } + + public async ValueTask?> ReadChunk(CancellationToken cancellationToken = default) + { + if (!await Call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false)) + return null; + + var response = Call.ResponseStream.Current; + Verifier.CheckResponse(response); + + if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk) + throw new FrostFsStreamException("unexpected message type"); + + return response.Body.Chunk.Memory; + } + + public void Dispose() + { + if (!disposed) + { + Call?.Dispose(); + GC.SuppressFinalize(this); + + disposed = true; + } + } +} diff --git a/src/FrostFS.SDK.Client/Tools/ObjectStreamer.cs b/src/FrostFS.SDK.Client/Tools/ObjectStreamer.cs new file mode 100644 index 00000000..81a6b3b3 --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/ObjectStreamer.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; + +using Grpc.Core; + +namespace FrostFS.SDK.Client; + +internal sealed class ObjectStreamer(AsyncClientStreamingCall call) : IDisposable +{ + public AsyncClientStreamingCall Call { get; private set; } = call; + + public async Task Write(TRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + await Call.RequestStream.WriteAsync(request).ConfigureAwait(false); + } + + public async Task Close() + { + await Call.RequestStream.CompleteAsync().ConfigureAwait(false); + + return await Call.ResponseAsync.ConfigureAwait(false); + } + + public void Dispose() + { + Call?.Dispose(); + } +} diff --git a/src/FrostFS.SDK.Client/Tools/ObjectTools.cs b/src/FrostFS.SDK.Client/Tools/ObjectTools.cs new file mode 100644 index 00000000..c8c5cafd --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/ObjectTools.cs @@ -0,0 +1,171 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using FrostFS.Object; +using FrostFS.Refs; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client; + +public static class ObjectTools +{ + public static FrostFsObjectId CalculateObjectId( + FrostFsObjectHeader header, + ReadOnlyMemory payloadHash, + FrostFsOwner owner, + FrostFsVersion version, + ClientKey key) + { + if (header is null) + { + throw new ArgumentNullException(nameof(header)); + } + + if (owner is null) + { + throw new ArgumentNullException(nameof(owner)); + } + + if (version is null) + { + throw new ArgumentNullException(nameof(version)); + } + + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + var grpcHeader = CreateHeader(header, payloadHash, owner, version); + + if (header.Split != null) + SetSplitValues(grpcHeader, header.Split, owner, version, key); + + using var sha256 = SHA256.Create(); + using HashStream stream = new(sha256); + grpcHeader.WriteTo(stream); + + return new FrostFsObjectId(Base58.Encode(stream.Hash())); + } + + internal static Object.Object CreateSingleObject(FrostFsObject @object, ClientContext ctx) + { + @object.Header.OwnerId ??= ctx.Owner; + @object.Header.Version ??= ctx.Version; + + var grpcHeader = @object.Header.GetHeader(); + + grpcHeader.PayloadLength = (ulong)@object.SingleObjectPayload.Length; + + if (@object.PayloadHash != null) + { + grpcHeader.PayloadHash = ChecksumFromSha256(@object.PayloadHash); + } + else + { + grpcHeader.PayloadHash = Sha256Checksum(@object.SingleObjectPayload); + } + + var split = @object.Header.Split; + + if (split != null) + { + SetSplitValues(grpcHeader, split, ctx.Owner, ctx.Version, ctx.Key); + } + + var obj = new Object.Object + { + Header = grpcHeader, + ObjectId = new ObjectID { Value = UnsafeByteOperations.UnsafeWrap(grpcHeader.Sha256()) }, + Payload = UnsafeByteOperations.UnsafeWrap(@object.SingleObjectPayload) + }; + + obj.Signature = new Signature + { + Key = ctx.Key.PublicKeyProto, + Sign = ctx.Key.ECDsaKey.SignData(obj.ObjectId.ToByteArray()), + }; + + return obj; + } + + internal static void SetSplitValues( + Header grpcHeader, + FrostFsSplit split, + FrostFsOwner owner, + FrostFsVersion version, + ClientKey key) + { + if (split == null) + { + return; + } + + if (key == null) + { + throw new FrostFsInvalidObjectException(nameof(key)); + } + + grpcHeader.Split = new Header.Types.Split + { + SplitId = split.SplitId?.GetSplitId() + }; + + if (split.Children != null && split.Children.Count != 0) + { + grpcHeader.Split.Children.AddRange(split.Children.Select(id => id.ToMessage())); + } + + if (split.ParentHeader is not null) + { + var grpcParentHeader = CreateHeader(split.ParentHeader, DataHasher.Sha256([]), owner, version); + + grpcHeader.Split.Parent = new ObjectID { Value = UnsafeByteOperations.UnsafeWrap(grpcParentHeader.Sha256()) }; + grpcHeader.Split.ParentHeader = grpcParentHeader; + grpcHeader.Split.ParentSignature = new Signature + { + Key = key.PublicKeyProto, + Sign = key.ECDsaKey.SignData(grpcHeader.Split.Parent.ToByteArray()), + }; + } + + grpcHeader.Split.Previous = split.Previous?.ToMessage(); + } + + internal static Header CreateHeader( + FrostFsObjectHeader header, + ReadOnlyMemory payloadChecksum, + FrostFsOwner owner, + FrostFsVersion version) + { + header.OwnerId ??= owner; + header.Version ??= version; + + var grpcHeader = header.GetHeader(); + + grpcHeader.PayloadHash = ChecksumFromSha256(payloadChecksum); + + return grpcHeader; + } + + internal static Checksum Sha256Checksum(ReadOnlyMemory data) + { + return new Checksum + { + Type = ChecksumType.Sha256, + Sum = UnsafeByteOperations.UnsafeWrap(DataHasher.Sha256(data)) + }; + } + + internal static Checksum ChecksumFromSha256(ReadOnlyMemory dataHash) + { + return new Checksum + { + Type = ChecksumType.Sha256, + Sum = UnsafeByteOperations.UnsafeWrap(dataHash) + }; + } +} diff --git a/src/FrostFS.SDK.Client/Tools/RangeReader.cs b/src/FrostFS.SDK.Client/Tools/RangeReader.cs new file mode 100644 index 00000000..a92d710f --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/RangeReader.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using FrostFS.Object; + +using Grpc.Core; + +namespace FrostFS.SDK.Client; + +public sealed class RangeReader(AsyncServerStreamingCall call) : IObjectReader +{ + private bool disposed; + + public AsyncServerStreamingCall Call { get; private set; } = call; + + public async ValueTask?> ReadChunk(CancellationToken cancellationToken = default) + { + if (!await Call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false)) + return null; + + var response = Call.ResponseStream.Current; + Verifier.CheckResponse(response); + + return response.Body.Chunk.Memory; + } + + public void Dispose() + { + if (!disposed) + { + Call?.Dispose(); + GC.SuppressFinalize(this); + + disposed = true; + } + } +} diff --git a/src/FrostFS.SDK.Client/Tools/RequestConstructor.cs b/src/FrostFS.SDK.Client/Tools/RequestConstructor.cs new file mode 100644 index 00000000..102eeb54 --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/RequestConstructor.cs @@ -0,0 +1,40 @@ +using System; + +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +namespace FrostFS.SDK.Client; + +public static class RequestConstructor +{ + public static void AddMetaHeader(this IRequest request, + string[] xHeaders, + SessionToken? sessionToken = null) + { + if (request is null) + throw new ArgumentNullException(nameof(request)); + + if (request.MetaHeader is not null) + return; + + var metaHeader = MetaHeader.Default(); + metaHeader.Ttl = 2; + + request.MetaHeader = metaHeader.ToMessage(); + + if (sessionToken != null) + request.MetaHeader.SessionToken = sessionToken; + + if (xHeaders != null && xHeaders.Length > 0) + { + if (xHeaders.Length % 2 != 0) + throw new ArgumentException("xHeaders with odd length"); + + for (var i = 0; i < xHeaders.Length; i += 2) + { + request.MetaHeader.XHeaders.Add(new XHeader { Key = xHeaders[i], Value = xHeaders[i + 1] }); + } + } + } +} diff --git a/src/FrostFS.SDK.Client/Tools/RequestSigner.cs b/src/FrostFS.SDK.Client/Tools/RequestSigner.cs new file mode 100644 index 00000000..8df1b7ca --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/RequestSigner.cs @@ -0,0 +1,152 @@ +using System; +using System.Security.Cryptography; + +using FrostFS.Refs; +using FrostFS.SDK.Cryptography; +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; +using Signature = FrostFS.Refs.Signature; + +namespace FrostFS.SDK.Client; + +public static class RequestSigner +{ + internal const int RFC6979SignatureSize = 64; + + internal static ByteString SignRFC6979(this ECDsa key, byte[] data) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + var digest = new Sha256Digest(); + var secp256R1 = SecNamedCurves.GetByName("secp256r1"); + var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N); + var privateKey = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ecParameters); + var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); + + var hash = new byte[digest.GetDigestSize()]; + + digest.BlockUpdate(data, 0, data.Length); + digest.DoFinal(hash, 0); + signer.Init(true, privateKey); + + var rs = signer.GenerateSignature(hash); + + Span signature = stackalloc byte[RFC6979SignatureSize]; + + var rbytes = rs[0].ToByteArrayUnsigned(); + var sbytes = rs[1].ToByteArrayUnsigned(); + var index = RFC6979SignatureSize / 2 - rbytes.Length; + + rbytes.AsSpan().CopyTo(signature.Slice(index)); + index = RFC6979SignatureSize - sbytes.Length; + sbytes.AsSpan().CopyTo(signature.Slice(index)); + + return ByteString.CopyFrom(signature); + } + + internal static SignatureRFC6979 SignRFC6979(this ClientKey key, IMessage message) + { + return new SignatureRFC6979 + { + Key = key.PublicKeyProto, + Sign = key.ECDsaKey.SignRFC6979(message.ToByteArray()), + }; + } + + internal static SignatureRFC6979 SignRFC6979(this ClientKey key, ByteString data) + { + return new SignatureRFC6979 + { + Key = key.PublicKeyProto, + Sign = key.ECDsaKey.SignRFC6979(data.ToByteArray()), + }; + } + + public static ByteString SignData(this ECDsa key, ReadOnlyMemory data) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + Span result = stackalloc byte[65]; + result[0] = 0x04; + + key.SignHash(DataHasher.Sha512(data)).AsSpan().CopyTo(result.Slice(1)); + + return ByteString.CopyFrom(result); + } + + public static ByteString SignDataByHash(this ECDsa key, byte[] hash) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + Span result = stackalloc byte[65]; + result[0] = 0x04; + + key.SignHash(hash).AsSpan().CopyTo(result.Slice(1)); + + return ByteString.CopyFrom(result); + } + + internal static Signature SignMessagePart(this ClientKey key, IMessage? data) + { + if (data is null || data.CalculateSize() == 0) + { + return new Signature + { + Key = key.PublicKeyProto, + Sign = key.ECDsaKey.SignData(ReadOnlyMemory.Empty), + }; + } + + using var sha512 = SHA512.Create(); + using HashStream stream = new(sha512); + data.WriteTo(stream); + + var sig = new Signature + { + Key = key.PublicKeyProto, + Sign = key.ECDsaKey.SignDataByHash(stream.Hash()) + }; + + return sig; + } + + internal static void Sign(this IVerifiableMessage message, ClientKey key) + { + var meta = message.GetMetaHeader(); + IVerificationHeader verify = message switch + { + IRequest => new RequestVerificationHeader(), + IResponse => new ResponseVerificationHeader(), + _ => throw new InvalidOperationException("Unsupported message type") + }; + + var verifyOrigin = message.GetVerificationHeader(); + + if (verifyOrigin is null) + verify.BodySignature = key.SignMessagePart(message.GetBody()); + else + verify.SetOrigin(verifyOrigin); + + verify.MetaSignature = key.SignMessagePart(meta); + verify.OriginSignature = key.SignMessagePart(verifyOrigin); + + message.SetVerificationHeader(verify); + } +} diff --git a/src/FrostFS.SDK.Client/Tools/SearchReader.cs b/src/FrostFS.SDK.Client/Tools/SearchReader.cs new file mode 100644 index 00000000..ba9b1d1d --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/SearchReader.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using FrostFS.Object; +using FrostFS.Refs; + +using Google.Protobuf.Collections; + +using Grpc.Core; + +namespace FrostFS.SDK.Client; + +internal sealed class SearchReader(AsyncServerStreamingCall call) : IDisposable +{ + internal AsyncServerStreamingCall Call { get; private set; } = call; + + internal async Task?> Read(CancellationToken cancellationToken) + { + if (!await Call.ResponseStream.MoveNext(cancellationToken).ConfigureAwait(false)) + return null; + + var response = Call.ResponseStream.Current; + + Verifier.CheckResponse(response); + + return response.Body?.IdList; + } + + public void Dispose() + { + Call?.Dispose(); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Tools/Verifier.cs b/src/FrostFS.SDK.Client/Tools/Verifier.cs new file mode 100644 index 00000000..90eb3cee --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/Verifier.cs @@ -0,0 +1,110 @@ +using System; +using System.Security.Cryptography; + +using FrostFS.Refs; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.SDK.Client; + +public static class Verifier +{ + public const int RFC6979SignatureSize = 64; + + public static bool VerifyData(this ECDsa key, IMessage data, ByteString sig) + { + if (key is null) + throw new ArgumentNullException(nameof(key)); + + if (sig is null) + throw new ArgumentNullException(nameof(sig)); + + var signature = sig.Span.Slice(1).ToArray(); + using var sha = SHA512.Create(); + + if (data is null) + { + return key.VerifyHash(DataHasher.Sha512(new Span([])), signature); + } + + using var stream = new HashStream(sha); + data.WriteTo(stream); + + return key.VerifyHash(stream.Hash(), signature); + } + + public static bool VerifyMessagePart(this Signature sig, IMessage data) + { + if (sig is null || sig.Key is null || sig.Sign is null) + return false; + + using var key = sig.Key.ToByteArray().LoadPublicKey(); + + return key.VerifyData(data, sig.Sign); + } + + internal static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification) + { + if (!verification.MetaSignature.VerifyMessagePart(meta)) + return false; + + var origin = verification.GetOrigin(); + + if (!verification.OriginSignature.VerifyMessagePart(origin)) + return false; + + if (origin is null) + return verification.BodySignature.VerifyMessagePart(body); + + return verification.BodySignature is null && VerifyMatryoskaLevel(body, meta.GetOrigin(), origin); + } + + public static bool Verify(this IVerifiableMessage message) + { + if (message is null) + { + throw new ArgumentNullException(nameof(message)); + } + + return VerifyMatryoskaLevel(message.GetBody(), message.GetMetaHeader(), message.GetVerificationHeader()); + } + + internal static void CheckResponse(IResponse resp) + { + if (!resp.Verify()) + { + throw new FormatException($"invalid response, type={resp.GetType()}"); + } + + if (resp.MetaHeader != null) + { + var status = resp.MetaHeader.Status.ToModel(); + + if (status != null && !status.IsSuccess) + { + throw new FrostFsResponseException(status); + } + } + } + + /// + /// This method is intended for unit tests for request verification. + /// + /// Created by SDK request to gRpc proxy + public static void CheckRequest(IRequest request) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (!request.Verify()) + { + throw new FrostFsResponseException($"invalid response, type={request.GetType()}"); + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Client/Tools/WalletTools.cs b/src/FrostFS.SDK.Client/Tools/WalletTools.cs new file mode 100644 index 00000000..45532a79 --- /dev/null +++ b/src/FrostFS.SDK.Client/Tools/WalletTools.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using System; +using FrostFS.SDK.Client.Wallets; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK.Client; + +public static class WalletTools +{ + public static string GetWifFromWallet(string walletJsonText, byte[] password, int accountIndex = 0) + { + var wallet = JsonSerializer.Deserialize(walletJsonText) ?? throw new ArgumentException("Wrong wallet format"); + + if (wallet.Accounts == null || wallet.Accounts.Length < accountIndex + 1) + { + throw new ArgumentException("Wrong wallet content"); + } + + var encryptedKey = wallet.Accounts[accountIndex].Key; + + if (string.IsNullOrEmpty(encryptedKey)) + { + throw new ArgumentException("Cannot get encrypted WIF"); + } + + var privateKey = CryptoWallet.GetKeyFromEncodedWif(password, encryptedKey!); + var wif = privateKey.GetWIFFromPrivateKey(); + + return wif; + } +} diff --git a/src/FrostFS.SDK.Client/Wallets/Account.cs b/src/FrostFS.SDK.Client/Wallets/Account.cs new file mode 100644 index 00000000..1d5b3eb4 --- /dev/null +++ b/src/FrostFS.SDK.Client/Wallets/Account.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace FrostFS.SDK.Client.Wallets; + +public class Account +{ + [JsonPropertyName("address")] + public string? Address { get; set; } + + [JsonPropertyName("key")] + public string? Key { get; set; } + + [JsonPropertyName("label")] + public string? Label { get; set; } + + [JsonPropertyName("contract")] + public Contract? Contract { get; set; } + + [JsonPropertyName("lock")] + public bool Lock { get; set; } + + [JsonPropertyName("isDefault")] + public bool IsDefault { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Wallets/Contract.cs b/src/FrostFS.SDK.Client/Wallets/Contract.cs new file mode 100644 index 00000000..b15d21d4 --- /dev/null +++ b/src/FrostFS.SDK.Client/Wallets/Contract.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace FrostFS.SDK.Client.Wallets; + +public class Contract +{ + [JsonPropertyName("script")] + public string? Script { get; set; } + + [JsonPropertyName("parameters")] + public Parameter[]? Parameters { get; set; } + + [JsonPropertyName("deployed")] + public bool Deployed { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Wallets/Extra.cs b/src/FrostFS.SDK.Client/Wallets/Extra.cs new file mode 100644 index 00000000..bff8b8dc --- /dev/null +++ b/src/FrostFS.SDK.Client/Wallets/Extra.cs @@ -0,0 +1,6 @@ +namespace FrostFS.SDK.Client.Wallets; + +public class Extra +{ + public string? Tokens { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Wallets/Parameter.cs b/src/FrostFS.SDK.Client/Wallets/Parameter.cs new file mode 100644 index 00000000..c831e36f --- /dev/null +++ b/src/FrostFS.SDK.Client/Wallets/Parameter.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace FrostFS.SDK.Client.Wallets; + +public class Parameter +{ + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("type")] + public string? Type { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Wallets/ScryptValue.cs b/src/FrostFS.SDK.Client/Wallets/ScryptValue.cs new file mode 100644 index 00000000..2548b32c --- /dev/null +++ b/src/FrostFS.SDK.Client/Wallets/ScryptValue.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace FrostFS.SDK.Client.Wallets; + +public class ScryptValue +{ + [JsonPropertyName("n")] + public int N { get; set; } + + [JsonPropertyName("r")] + public int R { get; set; } + + [JsonPropertyName("p")] + public int P { get; set; } +} diff --git a/src/FrostFS.SDK.Client/Wallets/Wallet.cs b/src/FrostFS.SDK.Client/Wallets/Wallet.cs new file mode 100644 index 00000000..e1439e04 --- /dev/null +++ b/src/FrostFS.SDK.Client/Wallets/Wallet.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace FrostFS.SDK.Client.Wallets; + +public class Wallet +{ + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("accounts")] + public Account[]? Accounts { get; set; } + + [JsonPropertyName("scrypt")] + public ScryptValue? Scrypt { get; set; } + + [JsonPropertyName("extra")] + public Extra? Extra { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Client.cs b/src/FrostFS.SDK.ClientV2/Client.cs deleted file mode 100644 index 7cb47d68..00000000 --- a/src/FrostFS.SDK.ClientV2/Client.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Security.Cryptography; -using FrostFS.Container; -using FrostFS.Netmap; -using FrostFS.Object; -using FrostFS.SDK.ClientV2.Interfaces; -using FrostFS.SDK.Cryptography; -using FrostFS.SDK.ModelsV2; -using FrostFS.Session; -using Grpc.Core; -using Grpc.Net.Client; -using Version = FrostFS.SDK.ModelsV2.Version; - -namespace FrostFS.SDK.ClientV2; - -public partial class Client: IFrostFSClient -{ - private GrpcChannel? _channel; - private readonly ECDsa _key; - public readonly OwnerId OwnerId; - public readonly Version Version = new(2, 13); - - private ContainerService.ContainerServiceClient? _containerServiceClient; - private NetmapService.NetmapServiceClient? _netmapServiceClient; - private ObjectService.ObjectServiceClient? _objectServiceClient; - private SessionService.SessionServiceClient? _sessionServiceClient; - - public Client(string key, string host) - { - // TODO: Развязать клиент и реализацию GRPC - _key = key.LoadWif(); - OwnerId = OwnerId.FromKey(_key); - InitGrpcChannel(host); - InitContainerClient(); - InitNetmapClient(); - InitObjectClient(); - InitSessionClient(); - CheckFrostFsVersionSupport(); - } - - private async void CheckFrostFsVersionSupport() - { - var localNodeInfo = await GetLocalNodeInfoAsync(); - if (!localNodeInfo.Version.IsSupported(Version)) - { - var msg = $"FrostFS {localNodeInfo.Version} is not supported."; - Console.WriteLine(msg); - throw new ApplicationException(msg); - } - } - - private void InitGrpcChannel(string host) - { - Uri uri; - try - { - uri = new Uri(host); - } - catch (UriFormatException e) - { - var msg = $"Host '{host}' has invalid format. Error: {e.Message}"; - Console.WriteLine(msg); - throw new ArgumentException(msg); - } - - ChannelCredentials grpcCredentials; - switch (uri.Scheme) - { - case "https": - grpcCredentials = ChannelCredentials.SecureSsl; - break; - case "http": - grpcCredentials = ChannelCredentials.Insecure; - break; - default: - var msg = $"Host '{host}' has invalid URI scheme: '{uri.Scheme}'."; - Console.WriteLine(msg); - throw new ArgumentException(msg); - } - - _channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions { Credentials = grpcCredentials }); - } - - private void InitContainerClient() - { - _containerServiceClient = new ContainerService.ContainerServiceClient(_channel); - } - - private void InitNetmapClient() - { - _netmapServiceClient = new NetmapService.NetmapServiceClient(_channel); - } - - private void InitObjectClient() - { - _objectServiceClient = new ObjectService.ObjectServiceClient(_channel); - } - - private void InitSessionClient() - { - _sessionServiceClient = new SessionService.SessionServiceClient(_channel); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj b/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj deleted file mode 100644 index 02081782..00000000 --- a/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - netstandard2.0 - 12.0 - enable - - - - - - - - - - - - - diff --git a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs deleted file mode 100644 index 8412c6cc..00000000 --- a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -using FrostFS.SDK.ModelsV2; - -namespace FrostFS.SDK.ClientV2.Interfaces; - -public interface IFrostFSClient -{ - Task GetContainerAsync(ContainerId containerId); - IAsyncEnumerable ListContainersAsync(); - Task CreateContainerAsync(ModelsV2.Container container); - Task DeleteContainerAsync(ContainerId containerId); - Task GetObjectHeadAsync(ContainerId containerId, ObjectId objectId); - Task GetObjectAsync(ContainerId containerId, ObjectId objectId); - Task PutObjectAsync(ObjectHeader header, Stream payload); - Task PutObjectAsync(ObjectHeader header, byte[] payload); - Task DeleteObjectAsync(ContainerId containerId, ObjectId objectId); - IAsyncEnumerable SearchObjectsAsync(ContainerId cid, params ObjectFilter[] filters); -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs deleted file mode 100644 index 93eac3fe..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Container.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -using Google.Protobuf; - -using FrostFS.SDK.ClientV2.Mappers.GRPC.Netmap; -using FrostFS.SDK.Cryptography; -using FrostFS.SDK.ModelsV2.Enums; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class ContainerMapper -{ - public static Container.Container ToGrpcMessage(this ModelsV2.Container container) - { - return new Container.Container - { - BasicAcl = (uint)container.BasicAcl, - PlacementPolicy = container.PlacementPolicy.ToGrpcMessage(), - Nonce = ByteString.CopyFrom(container.Nonce.ToBytes()) - }; - } - - public static ModelsV2.Container ToModel(this Container.Container container) - { - var basicAclName = Enum.GetName(typeof(BasicAcl), container.BasicAcl); - if (basicAclName is null) - { - throw new ArgumentException($"Unknown BasicACL rule. Value: '{container.BasicAcl}'."); - } - - return new ModelsV2.Container( - (BasicAcl)Enum.Parse(typeof(BasicAcl), basicAclName), - container.PlacementPolicy.ToModel() - ) - { - Nonce = container.Nonce.ToUuid(), - Version = container.Version.ToModel() - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/ContainerId.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/ContainerId.cs deleted file mode 100644 index 1bfd6146..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/ContainerId.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FrostFS.Refs; -using FrostFS.SDK.ModelsV2; -using Google.Protobuf; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class ContainerIdMapper -{ - public static ContainerID ToGrpcMessage(this ContainerId containerId) - { - return new ContainerID - { - Value = ByteString.CopyFrom(containerId.ToHash()) - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/MetaHeader.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/MetaHeader.cs deleted file mode 100644 index d3bf04fa..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/MetaHeader.cs +++ /dev/null @@ -1,23 +0,0 @@ -using FrostFS.SDK.ModelsV2; -using FrostFS.Session; -using Version = FrostFS.Refs.Version; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class MetaHeaderMapper -{ - public static RequestMetaHeader ToGrpcMessage(this MetaHeader metaHeader) - { - return new RequestMetaHeader - { - Version = new Version - { - Major = (uint)metaHeader.Version.Major, - Minor = (uint)metaHeader.Version.Minor, - - }, - Epoch = (uint)metaHeader.Epoch, - Ttl = (uint)metaHeader.Ttl - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/NodeInfo.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/NodeInfo.cs deleted file mode 100644 index f8f2d56a..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/NodeInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -using FrostFS.Netmap; -using FrostFS.SDK.ModelsV2.Enums; -using NodeInfo = FrostFS.SDK.ModelsV2.Netmap.NodeInfo; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC.Netmap; - -public static class NodeInfoMapper -{ - public static NodeInfo ToModel(this LocalNodeInfoResponse.Types.Body nodeInfo) - { - var nodeStateName = Enum.GetName(typeof(NodeState), nodeInfo.NodeInfo.State); - if (nodeStateName is null) - { - throw new ArgumentException($"Unknown NodeState. Value: '{nodeInfo.NodeInfo.State}'."); - } - return new NodeInfo - { - State = (NodeState)Enum.Parse(typeof(NodeState), nodeStateName), - Version = nodeInfo.Version.ToModel() - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs deleted file mode 100644 index a4cbb0db..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/PlacementPolicy.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Linq; -using FrostFS.Netmap; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC.Netmap; - -public static class PlacementPolicyMapper -{ - public static PlacementPolicy ToGrpcMessage(this ModelsV2.Netmap.PlacementPolicy placementPolicy) - { - var pp = new PlacementPolicy - { - Filters = { }, - Selectors = { }, - Replicas = { }, - Unique = placementPolicy.Unique - }; - foreach (var replica in placementPolicy.Replicas) - { - pp.Replicas.Add(replica.ToGrpcMessage()); - } - - return pp; - } - - public static ModelsV2.Netmap.PlacementPolicy ToModel(this PlacementPolicy placementPolicy) - { - return new ModelsV2.Netmap.PlacementPolicy( - placementPolicy.Unique, - placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray() - ); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/Replica.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/Replica.cs deleted file mode 100644 index 74350857..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Netmap/Replica.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FrostFS.Netmap; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC.Netmap; - -public static class ReplicaMapper -{ - public static Replica ToGrpcMessage(this ModelsV2.Netmap.Replica replica) - { - return new Replica - { - Count = (uint)replica.Count, - Selector = replica.Selector - }; - } - - public static ModelsV2.Netmap.Replica ToModel(this Replica replica) - { - return new ModelsV2.Netmap.Replica((int)replica.Count, replica.Selector); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs deleted file mode 100644 index b6e68f67..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Object.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Linq; - -using FrostFS.Object; -using FrostFS.SDK.ModelsV2; -using MatchType = FrostFS.Object.MatchType; -using ObjectType = FrostFS.Object.ObjectType; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class ObjectAttributeMapper -{ - public static Header.Types.Attribute ToGrpcMessage(this ObjectAttribute attribute) - { - return new Header.Types.Attribute - { - Key = attribute.Key, - Value = attribute.Value - }; - } - - public static ObjectAttribute ToModel(this Header.Types.Attribute attribute) - { - return new ObjectAttribute(attribute.Key, attribute.Value); - } -} - -public static class ObjectFilterMapper -{ - public static SearchRequest.Types.Body.Types.Filter ToGrpcMessage(this ObjectFilter filter) - { - var objMatchTypeName = Enum.GetName(typeof(MatchType), filter.MatchType) - ?? throw new ArgumentException($"Unknown MatchType. Value: '{filter.MatchType}'."); - return new SearchRequest.Types.Body.Types.Filter - { - MatchType = (MatchType)Enum.Parse(typeof(MatchType), objMatchTypeName), - Key = filter.Key, - Value = filter.Value - }; - } -} - -public static class ObjectHeaderMapper -{ - public static Header ToGrpcMessage(this ObjectHeader header) - { - var objTypeName = Enum.GetName(typeof(ObjectType), header.ObjectType) - ?? throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'."); - var head = new Header - { - Attributes = { }, - ContainerId = header.ContainerId.ToGrpcMessage(), - ObjectType = (ObjectType)Enum.Parse(typeof(ObjectType), objTypeName) - }; - - foreach (var attribute in header.Attributes) - { - head.Attributes.Add(attribute.ToGrpcMessage()); - } - - return head; - } - - public static ObjectHeader ToModel(this Header header) - { - var objTypeName = Enum.GetName(typeof(ModelsV2.Enums.ObjectType), header.ObjectType) - ?? throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'."); - return new ObjectHeader( - ContainerId.FromHash(header.ContainerId.Value.ToByteArray()), - (ModelsV2.Enums.ObjectType)Enum.Parse(typeof(ModelsV2.Enums.ObjectType), objTypeName), - header.Attributes.Select(attribute => attribute.ToModel()).ToArray() - ) - { - Size = (long)header.PayloadLength, - Version = header.Version.ToModel() - }; - } -} - -public static class ObjectMapper -{ - public static ModelsV2.Object ToModel(this Object.Object obj) - { - return new ModelsV2.Object - { - Header = obj.Header.ToModel(), - ObjectId = ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()), - Payload = obj.Payload.ToByteArray() - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/ObjectId.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/ObjectId.cs deleted file mode 100644 index 4c280350..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/ObjectId.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FrostFS.Refs; -using FrostFS.SDK.ModelsV2; -using Google.Protobuf; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class ObjectIdMapper -{ - public static ObjectID ToGrpcMessage(this ObjectId objectId) - { - return new ObjectID - { - Value = ByteString.CopyFrom(objectId.ToHash()) - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/OwnerId.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/OwnerId.cs deleted file mode 100644 index 4bea66dd..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/OwnerId.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FrostFS.Refs; -using FrostFS.SDK.ModelsV2; -using Google.Protobuf; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class OwnerIdMapper -{ - public static OwnerID ToGrpcMessage(this OwnerId ownerId) - { - return new OwnerID - { - Value = ByteString.CopyFrom(ownerId.ToHash()) - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Status.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Status.cs deleted file mode 100644 index 9e3d2eb3..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Status.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using FrostFS.SDK.ModelsV2.Enums; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class StatusMapper -{ - public static ModelsV2.Status ToModel(this Status.Status status) - { - if (status is null) return new ModelsV2.Status(StatusCode.Success); - var codeName = Enum.GetName(typeof(StatusCode), status.Code); - - return codeName is null - ? throw new ArgumentException($"Unknown StatusCode. Value: '{status.Code}'.") - : new ModelsV2.Status((StatusCode)Enum.Parse(typeof(StatusCode), codeName), status.Message); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Version.cs b/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Version.cs deleted file mode 100644 index 549ff1ae..00000000 --- a/src/FrostFS.SDK.ClientV2/Mappers/GRPC/Version.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Version = FrostFS.Refs.Version; - -namespace FrostFS.SDK.ClientV2.Mappers.GRPC; - -public static class VersionMapper -{ - public static Version ToGrpcMessage(this ModelsV2.Version version) - { - return new Version - { - Major = (uint)version.Major, - Minor = (uint)version.Minor - }; - } - - public static ModelsV2.Version ToModel(this Version version) - { - return new ModelsV2.Version((int)version.Major, (int)version.Minor); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Range.cs b/src/FrostFS.SDK.ClientV2/Range.cs deleted file mode 100644 index 81583145..00000000 --- a/src/FrostFS.SDK.ClientV2/Range.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace System -{ - /// Represent a type can be used to index a collection either from the start or the end. - /// - /// Index is used by the C# compiler to support the new index syntax - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; - /// int lastElement = someArray[^1]; // lastElement = 5 - /// - /// - internal readonly struct Index : IEquatable - { - private readonly int _value; - - /// Construct an Index using a value and indicating if the index is from the start or from the end. - /// The index value. it has to be zero or positive number. - /// Indicating if the index is from the start or from the end. - /// - /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Index(int value, bool fromEnd = false) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - if (fromEnd) - _value = ~value; - else - _value = value; - } - - // The following private constructors mainly created for perf reason to avoid the checks - private Index(int value) - { - _value = value; - } - - /// Create an Index pointing at first element. - public static Index Start => new Index(0); - - /// Create an Index pointing at beyond last element. - public static Index End => new Index(~0); - - /// Create an Index from the start at the position indicated by the value. - /// The index value from the start. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromStart(int value) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - return new Index(value); - } - - /// Create an Index from the end at the position indicated by the value. - /// The index value from the end. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromEnd(int value) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - return new Index(~value); - } - - /// Returns the index value. - public int Value - { - get - { - if (_value < 0) - { - return ~_value; - } - else - { - return _value; - } - } - } - - /// Indicates whether the index is from the start or the end. - public bool IsFromEnd => _value < 0; - - /// Calculate the offset from the start using the giving collection length. - /// The length of the collection that the Index will be used with. length has to be a positive value - /// - /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. - /// we don't validate either the returned offset is greater than the input length. - /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and - /// then used to index a collection will get out of range exception which will be same affect as the validation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOffset(int length) - { - var offset = _value; - if (IsFromEnd) - { - // offset = length - (~value) - // offset = length + (~(~value) + 1) - // offset = length + value + 1 - - offset += length + 1; - } - return offset; - } - - /// Indicates whether the current Index object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; - - /// Indicates whether the current Index object is equal to another Index object. - /// An object to compare with this object - public bool Equals(Index other) => _value == other._value; - - /// Returns the hash code for this instance. - public override int GetHashCode() => _value; - - /// Converts integer number to an Index. - public static implicit operator Index(int value) => FromStart(value); - - /// Converts the value of the current Index object to its equivalent string representation. - public override string ToString() - { - if (IsFromEnd) - return "^" + ((uint)Value).ToString(); - - return ((uint)Value).ToString(); - } - } - - /// Represent a range has start and end indexes. - /// - /// Range is used by the C# compiler to support the range syntax. - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; - /// int[] subArray1 = someArray[0..2]; // { 1, 2 } - /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } - /// - /// - internal readonly struct Range : IEquatable - { - /// Represent the inclusive start index of the Range. - public Index Start { get; } - - /// Represent the exclusive end index of the Range. - public Index End { get; } - - /// Construct a Range object using the start and end indexes. - /// Represent the inclusive start index of the range. - /// Represent the exclusive end index of the range. - public Range(Index start, Index end) - { - Start = start; - End = end; - } - - /// Indicates whether the current Range object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => - value is Range r && - r.Start.Equals(Start) && - r.End.Equals(End); - - /// Indicates whether the current Range object is equal to another Range object. - /// An object to compare with this object - public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); - - /// Returns the hash code for this instance. - public override int GetHashCode() - { - return Start.GetHashCode() * 31 + End.GetHashCode(); - } - - /// Converts the value of the current Range object to its equivalent string representation. - public override string ToString() - { - return Start + ".." + End; - } - - /// Create a Range object starting from start index to the end of the collection. - public static Range StartAt(Index start) => new Range(start, Index.End); - - /// Create a Range object starting from first element in the collection to the end Index. - public static Range EndAt(Index end) => new Range(Index.Start, end); - - /// Create a Range object starting from first element to the end. - public static Range All => new Range(Index.Start, Index.End); - - /// Calculate the start offset and length of range object using a collection length. - /// The length of the collection that the range will be used with. length has to be a positive value. - /// - /// For performance reason, we don't validate the input length parameter against negative values. - /// It is expected Range will be used with collections which always have non negative length/count. - /// We validate the range is inside the length scope though. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int Offset, int Length) GetOffsetAndLength(int length) - { - int start; - var startIndex = Start; - if (startIndex.IsFromEnd) - start = length - startIndex.Value; - else - start = startIndex.Value; - - int end; - var endIndex = End; - if (endIndex.IsFromEnd) - end = length - endIndex.Value; - else - end = endIndex.Value; - - if ((uint)end > (uint)length || (uint)start > (uint)end) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return (start, end - start); - } - } -} - -namespace System.Runtime.CompilerServices -{ - internal static class RuntimeHelpers - { - /// - /// Slices the specified array using the specified range. - /// - public static T[] GetSubArray(T[] array, Range range) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - (int offset, int length) = range.GetOffsetAndLength(array.Length); - - if (default(T) != null || typeof(T[]) == array.GetType()) - { - // We know the type of the array to be exactly T[]. - - if (length == 0) - { - return Array.Empty(); - } - - var dest = new T[length]; - Array.Copy(array, offset, dest, 0, length); - return dest; - } - else - { - // The array is actually a U[] where U:T. - var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); - Array.Copy(array, offset, dest, 0, length); - return dest; - } - } - } -} diff --git a/src/FrostFS.SDK.ClientV2/RequestConstructor.cs b/src/FrostFS.SDK.ClientV2/RequestConstructor.cs deleted file mode 100644 index fb632eea..00000000 --- a/src/FrostFS.SDK.ClientV2/RequestConstructor.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Security.Cryptography; -using FrostFS.Refs; -using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.SDK.ModelsV2; -using FrostFS.Session; - -namespace FrostFS.SDK.ClientV2; - -public static class RequestConstructor -{ - public static void AddMetaHeader(this IRequest request, RequestMetaHeader? metaHeader = null) - { - if (request.MetaHeader is not null) return; - metaHeader ??= MetaHeader.Default().ToGrpcMessage(); - request.MetaHeader = metaHeader; - } - - public static void AddObjectSessionToken( - this IRequest request, - SessionToken sessionToken, - ContainerID cid, - ObjectID oid, - ObjectSessionContext.Types.Verb verb, - ECDsa key) - { - if (request.MetaHeader.SessionToken is not null) return; - request.MetaHeader.SessionToken = sessionToken; - var ctx = new ObjectSessionContext - { - Target = new ObjectSessionContext.Types.Target - { - Container = cid, - Objects = { oid } - }, - Verb = verb - }; - request.MetaHeader.SessionToken.Body.Object = ctx; - request.MetaHeader.SessionToken.Signature = key.SignMessagePart(request.MetaHeader.SessionToken.Body); - } -} diff --git a/src/FrostFS.SDK.ClientV2/RequestSigner.cs b/src/FrostFS.SDK.ClientV2/RequestSigner.cs deleted file mode 100644 index 3747c921..00000000 --- a/src/FrostFS.SDK.ClientV2/RequestSigner.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Security.Cryptography; - -using Google.Protobuf; - -using Org.BouncyCastle.Asn1.Sec; -using Org.BouncyCastle.Crypto.Digests; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; -using Org.BouncyCastle.Math; - -using FrostFS.Refs; -using FrostFS.SDK.Cryptography; -using FrostFS.Session; - -namespace FrostFS.SDK.ClientV2; - -public static class RequestSigner -{ - public const int RFC6979SignatureSize = 64; - - public static byte[] SignRFC6979(this ECDsa key, byte[] data) - { - var digest = new Sha256Digest(); - var secp256R1 = SecNamedCurves.GetByName("secp256r1"); - var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N); - var privateKey = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ecParameters); - var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); - var hash = new byte[digest.GetDigestSize()]; - - digest.BlockUpdate(data, 0, data.Length); - digest.DoFinal(hash, 0); - signer.Init(true, privateKey); - - var rs = signer.GenerateSignature(hash); - var signature = new byte[RFC6979SignatureSize]; - var rbytes = rs[0].ToByteArrayUnsigned(); - var sbytes = rs[1].ToByteArrayUnsigned(); - var index = RFC6979SignatureSize / 2 - rbytes.Length; - - rbytes.CopyTo(signature, index); - index = RFC6979SignatureSize - sbytes.Length; - sbytes.CopyTo(signature, index); - - return signature; - } - - public static SignatureRFC6979 SignRFC6979(this ECDsa key, IMessage message) - { - return new SignatureRFC6979 - { - Key = ByteString.CopyFrom(key.PublicKey()), - Sign = ByteString.CopyFrom(key.SignRFC6979(message.ToByteArray())), - }; - } - - public static SignatureRFC6979 SignRFC6979(this ECDsa key, ByteString data) - { - return new SignatureRFC6979 - { - Key = ByteString.CopyFrom(key.PublicKey()), - Sign = ByteString.CopyFrom(key.SignRFC6979(data.ToByteArray())), - }; - } - - public static byte[] SignData(this ECDsa key, byte[] data) - { - var hash = new byte[65]; - hash[0] = 0x04; - key - .SignHash(SHA512.Create().ComputeHash(data)) - .CopyTo(hash, 1); - return hash; - } - - public static Signature SignMessagePart(this ECDsa key, IMessage? data) - { - var data2Sign = data is null ? Array.Empty() : data.ToByteArray(); - var sig = new Signature - { - Key = ByteString.CopyFrom(key.PublicKey()), - Sign = ByteString.CopyFrom(key.SignData(data2Sign)), - }; - return sig; - } - - public static void Sign(this IVerificableMessage message, ECDsa key) - { - var meta = message.GetMetaHeader(); - IVerificationHeader verify = message switch - { - IRequest => new RequestVerificationHeader(), - IResponse => new ResponseVerificationHeader(), - _ => throw new InvalidOperationException("Unsopported message type") - }; - - var verifyOrigin = message.GetVerificationHeader(); - if (verifyOrigin is null) - verify.BodySignature = key.SignMessagePart(message.GetBody()); - - verify.MetaSignature = key.SignMessagePart(meta); - verify.OriginSignature = key.SignMessagePart(verifyOrigin); - verify.SetOrigin(verifyOrigin); - message.SetVerificationHeader(verify); - } -} diff --git a/src/FrostFS.SDK.ClientV2/Services/Container.cs b/src/FrostFS.SDK.ClientV2/Services/Container.cs deleted file mode 100644 index 886768ca..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/Container.cs +++ /dev/null @@ -1,81 +0,0 @@ -using FrostFS.Container; -using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.SDK.ModelsV2; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace FrostFS.SDK.ClientV2; - -public partial class Client -{ - public async Task GetContainerAsync(ContainerId cid) - { - var request = new GetRequest - { - Body = new GetRequest.Types.Body - { - ContainerId = cid.ToGrpcMessage() - }, - }; - request.AddMetaHeader(); - request.Sign(_key); - var response = await _containerServiceClient.GetAsync(request); - Verifier.CheckResponse(response); - return response.Body.Container.ToModel(); - } - - public async IAsyncEnumerable ListContainersAsync() - { - var request = new ListRequest - { - Body = new ListRequest.Types.Body - { - OwnerId = OwnerId.ToGrpcMessage() - } - }; - request.AddMetaHeader(); - request.Sign(_key); - var response = await _containerServiceClient.ListAsync(request); - Verifier.CheckResponse(response); - foreach (var cid in response.Body.ContainerIds) - { - yield return ContainerId.FromHash(cid.Value.ToByteArray()); - } - } - - public async Task CreateContainerAsync(ModelsV2.Container container) - { - var cntnr = container.ToGrpcMessage(); - cntnr.OwnerId = OwnerId.ToGrpcMessage(); - cntnr.Version = Version.ToGrpcMessage(); - var request = new PutRequest - { - Body = new PutRequest.Types.Body - { - Container = cntnr, - Signature = _key.SignRFC6979(cntnr), - } - }; - request.AddMetaHeader(); - request.Sign(_key); - var response = await _containerServiceClient.PutAsync(request); - Verifier.CheckResponse(response); - return ContainerId.FromHash(response.Body.ContainerId.Value.ToByteArray()); - } - - public async Task DeleteContainerAsync(ContainerId cid) - { - var request = new DeleteRequest - { - Body = new DeleteRequest.Types.Body - { - ContainerId = cid.ToGrpcMessage(), - Signature = _key.SignRFC6979(cid.ToGrpcMessage().Value) - } - }; - request.AddMetaHeader(); - request.Sign(_key); - var response = await _containerServiceClient.DeleteAsync(request); - Verifier.CheckResponse(response); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Services/Netmap.cs b/src/FrostFS.SDK.ClientV2/Services/Netmap.cs deleted file mode 100644 index ce439ccb..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/Netmap.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Threading.Tasks; - -using FrostFS.Netmap; -using FrostFS.SDK.ClientV2.Mappers.GRPC.Netmap; - -using NodeInfo = FrostFS.SDK.ModelsV2.Netmap.NodeInfo; - -namespace FrostFS.SDK.ClientV2; - -public partial class Client -{ - public async Task GetLocalNodeInfoAsync() - { - var request = new LocalNodeInfoRequest - { - Body = new LocalNodeInfoRequest.Types.Body { } - }; - - request.AddMetaHeader(); - request.Sign(_key); - var response = await _netmapServiceClient.LocalNodeInfoAsync(request); - - return response.Body.ToModel(); - } - - public async Task GetNetworkInfoAsync() - { - var request = new NetworkInfoRequest - { - Body = new NetworkInfoRequest.Types.Body { } - }; - - request.AddMetaHeader(); - request.Sign(_key); - - return await _netmapServiceClient.NetworkInfoAsync(request); - } -} diff --git a/src/FrostFS.SDK.ClientV2/Services/Object.cs b/src/FrostFS.SDK.ClientV2/Services/Object.cs deleted file mode 100644 index 180e3513..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/Object.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -using Google.Protobuf; - -using FrostFS.Object; -using FrostFS.Refs; -using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.SDK.Cryptography; -using FrostFS.SDK.ModelsV2; -using FrostFS.Session; - -namespace FrostFS.SDK.ClientV2; - -public partial class Client -{ - public async Task GetObjectHeadAsync(ContainerId cid, ObjectId oid) - { - var request = new HeadRequest - { - Body = new HeadRequest.Types.Body - { - Address = new Address - { - ContainerId = cid.ToGrpcMessage(), - ObjectId = oid.ToGrpcMessage() - } - } - }; - - request.AddMetaHeader(); - request.Sign(_key); - var response = await _objectServiceClient.HeadAsync(request); - Verifier.CheckResponse(response); - - return response.Body.Header.Header.ToModel(); - } - - public async Task GetObjectAsync(ContainerId cid, ObjectId oid) - { - var sessionToken = await CreateSessionAsync(uint.MaxValue); - var request = new GetRequest - { - Body = new GetRequest.Types.Body - { - Raw = false, - Address = new Address - { - ContainerId = cid.ToGrpcMessage(), - ObjectId = oid.ToGrpcMessage() - } - } - }; - - request.AddMetaHeader(); - request.AddObjectSessionToken( - sessionToken, - cid.ToGrpcMessage(), - oid.ToGrpcMessage(), - ObjectSessionContext.Types.Verb.Get, - _key - ); - - request.Sign(_key); - var obj = await GetObject(request); - - return obj.ToModel(); - } - - public async Task PutObjectAsync(ObjectHeader header, Stream payload) - { - return await PutObject(header, payload); - } - - public async Task PutObjectAsync(ObjectHeader header, byte[] payload) - { - return await PutObject(header, new MemoryStream(payload)); - } - - public async Task DeleteObjectAsync(ContainerId cid, ObjectId oid) - { - var request = new DeleteRequest - { - Body = new DeleteRequest.Types.Body - { - Address = new Address - { - ContainerId = cid.ToGrpcMessage(), - ObjectId = oid.ToGrpcMessage() - } - } - }; - - request.AddMetaHeader(); - request.Sign(_key); - var response = await _objectServiceClient.DeleteAsync(request); - Verifier.CheckResponse(response); - } - - public async IAsyncEnumerable SearchObjectsAsync(ContainerId cid, params ObjectFilter[] filters) - { - var request = new SearchRequest - { - Body = new SearchRequest.Types.Body - { - ContainerId = cid.ToGrpcMessage(), - Filters = { }, - Version = 1 - } - }; - - foreach (var filter in filters) - { - request.Body.Filters.Add(filter.ToGrpcMessage()); - } - - request.AddMetaHeader(); - request.Sign(_key); - var objectsIds = SearchObjects(request); - - await foreach (var oid in objectsIds) - { - yield return ObjectId.FromHash(oid.Value.ToByteArray()); - } - } - - private async Task GetObject(GetRequest request) - { - using var stream = GetObjectInit(request); - var obj = await stream.ReadHeader(); - var payload = new byte[obj.Header.PayloadLength]; - var offset = 0; - var chunk = await stream.ReadChunk(); - - while (chunk is not null) - { - chunk.CopyTo(payload, offset); - offset += chunk.Length; - chunk = await stream.ReadChunk(); - } - - obj.Payload = ByteString.CopyFrom(payload); - - return obj; - } - - private ObjectReader GetObjectInit(GetRequest initRequest) - { - if (initRequest is null) - throw new ArgumentNullException(nameof(initRequest)); - - return new ObjectReader - { - Call = _objectServiceClient.Get(initRequest) - }; - } - - private async Task PutObject(ObjectHeader header, Stream payload) - { - var sessionToken = await CreateSessionAsync(uint.MaxValue); - var hdr = header.ToGrpcMessage(); - hdr.OwnerId = OwnerId.ToGrpcMessage(); - hdr.Version = Version.ToGrpcMessage(); - - var oid = new ObjectID - { - Value = hdr.Sha256() - }; - - var request = new PutRequest - { - Body = new PutRequest.Types.Body - { - Init = new PutRequest.Types.Body.Types.Init - { - Header = hdr - }, - } - }; - - request.AddMetaHeader(); - request.AddObjectSessionToken( - sessionToken, - hdr.ContainerId, - oid, - ObjectSessionContext.Types.Verb.Put, - _key - ); - - request.Sign(_key); - - using var stream = await PutObjectInit(request); - var buffer = new byte[Constants.ObjectChunkSize]; - var bufferLength = await payload.ReadAsync(buffer, 0, Constants.ObjectChunkSize); - - while (bufferLength > 0) - { - request.Body = new PutRequest.Types.Body - { - Chunk = ByteString.CopyFrom(buffer[..bufferLength]), - }; - request.VerifyHeader = null; - request.Sign(_key); - await stream.Write(request); - bufferLength = await payload.ReadAsync(buffer, 0, Constants.ObjectChunkSize); - } - - var response = await stream.Close(); - Verifier.CheckResponse(response); - - return ObjectId.FromHash(response.Body.ObjectId.Value.ToByteArray()); - } - - private async Task PutObjectInit(PutRequest initRequest) - { - if (initRequest is null) - { - throw new ArgumentNullException(nameof(initRequest)); - } - - var call = _objectServiceClient.Put(); - await call.RequestStream.WriteAsync(initRequest); - - return new ObjectStreamer(call); - } - - private async IAsyncEnumerable SearchObjects(SearchRequest request) - { - using var stream = GetSearchReader(request); - var ids = await stream.Read(); - while (ids is not null) - { - foreach (var oid in ids) - { - yield return oid; - } - - ids = await stream.Read(); - } - } - - private SearchReader GetSearchReader(SearchRequest initRequest) - { - if (initRequest is null) - { - throw new ArgumentNullException(nameof(initRequest)); - } - - return new SearchReader(_objectServiceClient.Search(initRequest)); - } -} diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectReader.cs b/src/FrostFS.SDK.ClientV2/Services/ObjectReader.cs deleted file mode 100644 index 415c0014..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/ObjectReader.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; - -using Grpc.Core; - -using FrostFS.Object; - -namespace FrostFS.SDK.ClientV2; - -internal class ObjectReader : IDisposable -{ - public AsyncServerStreamingCall Call { get; set; } - - public async Task ReadHeader() - { - if (!await Call.ResponseStream.MoveNext()) - throw new InvalidOperationException("unexpect end of stream"); - - var response = Call.ResponseStream.Current; - Verifier.CheckResponse(response); - - if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init) - throw new InvalidOperationException("unexpect message type"); - - return new Object.Object - { - ObjectId = response.Body.Init.ObjectId, - Header = response.Body.Init.Header, - }; - } - - public async Task ReadChunk() - { - if (!await Call.ResponseStream.MoveNext()) - return null; - - var response = Call.ResponseStream.Current; - Verifier.CheckResponse(response); - - if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk) - throw new InvalidOperationException("unexpect message type"); - - return response.Body.Chunk.ToByteArray(); - } - - public void Dispose() - { - Call.Dispose(); - } -} diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectStreamer.cs b/src/FrostFS.SDK.ClientV2/Services/ObjectStreamer.cs deleted file mode 100644 index 11858c37..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/ObjectStreamer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading.Tasks; - -using Grpc.Core; - -using FrostFS.Object; - -namespace FrostFS.SDK.ClientV2; - -internal class ObjectStreamer(AsyncClientStreamingCall call) : IDisposable -{ - public AsyncClientStreamingCall Call { get; private set; } = call; - - public async Task Write(PutRequest request) - { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - await Call.RequestStream.WriteAsync(request); - } - - public async Task Close() - { - await Call.RequestStream.CompleteAsync(); - - return await Call.ResponseAsync; - } - - public void Dispose() - { - Call.Dispose(); - } -} diff --git a/src/FrostFS.SDK.ClientV2/Services/SearchReader.cs b/src/FrostFS.SDK.ClientV2/Services/SearchReader.cs deleted file mode 100644 index c3d425ad..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/SearchReader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using Grpc.Core; - -using FrostFS.Object; -using FrostFS.Refs; - -namespace FrostFS.SDK.ClientV2; - -internal class SearchReader(AsyncServerStreamingCall call) : IDisposable -{ - public AsyncServerStreamingCall Call { get; private set; } = call; - - public async Task?> Read() - { - if (!await Call.ResponseStream.MoveNext()) - { - return null; - } - - var response = Call.ResponseStream.Current; - Verifier.CheckResponse(response); - - return response.Body?.IdList.ToList(); - } - - public void Dispose() - { - Call.Dispose(); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Services/Session.cs b/src/FrostFS.SDK.ClientV2/Services/Session.cs deleted file mode 100644 index 3dc52a16..00000000 --- a/src/FrostFS.SDK.ClientV2/Services/Session.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Threading.Tasks; - -using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.Session; - -namespace FrostFS.SDK.ClientV2; - -public partial class Client -{ - private async Task CreateSessionAsync(ulong expiration) - { - var request = new CreateRequest - { - Body = new CreateRequest.Types.Body - { - OwnerId = OwnerId.ToGrpcMessage(), - Expiration = expiration, - } - }; - - request.AddMetaHeader(); - request.Sign(_key); - - return await CreateSession(request); - } - - private async Task CreateSession(CreateRequest request) - { - var resp = await _sessionServiceClient.CreateAsync(request); - - return new SessionToken - { - Body = new SessionToken.Types.Body - { - Id = resp.Body.Id, - SessionKey = resp.Body.SessionKey, - OwnerId = request.Body.OwnerId, - Lifetime = new SessionToken.Types.Body.Types.TokenLifetime - { - Exp = request.Body.Expiration, - Iat = resp.MetaHeader.Epoch, - Nbf = resp.MetaHeader.Epoch, - } - } - }; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Verifier.cs b/src/FrostFS.SDK.ClientV2/Verifier.cs deleted file mode 100644 index ad6b3e6a..00000000 --- a/src/FrostFS.SDK.ClientV2/Verifier.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Security.Cryptography; - -using Google.Protobuf; - -using Org.BouncyCastle.Asn1.Sec; -using Org.BouncyCastle.Crypto.Digests; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Crypto.Signers; -using Org.BouncyCastle.Math; - -using FrostFS.Refs; -using FrostFS.SDK.ClientV2.Mappers.GRPC; -using FrostFS.SDK.Cryptography; -using FrostFS.Session; - -namespace FrostFS.SDK.ClientV2; - -public static class Verifier -{ - public const int RFC6979SignatureSize = 64; - - private static BigInteger[] DecodeSignature(byte[] sig) - { - if (sig.Length != RFC6979SignatureSize) - throw new FormatException($"Wrong signature size, expect={RFC6979SignatureSize}, actual={sig.Length}"); - - var rs = new BigInteger[2]; - rs[0] = new BigInteger(1, sig[..32]); - rs[1] = new BigInteger(1, sig[32..]); - - return rs; - } - - public static bool VerifyRFC6979(this byte[] publicKey, byte[] data, byte[] sig) - { - if (publicKey is null || data is null || sig is null) - return false; - - var rs = DecodeSignature(sig); - var digest = new Sha256Digest(); - var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); - var secp256R1 = SecNamedCurves.GetByName("secp256r1"); - var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N); - var bcPublicKey = new ECPublicKeyParameters(secp256R1.Curve.DecodePoint(publicKey), ecParameters); - var hash = new byte[digest.GetDigestSize()]; - - digest.BlockUpdate(data, 0, data.Length); - digest.DoFinal(hash, 0); - signer.Init(false, bcPublicKey); - - return signer.VerifySignature(hash, rs[0], rs[1]); - } - - public static bool VerifyRFC6979(this SignatureRFC6979 signature, IMessage message) - { - return signature.Key.ToByteArray().VerifyRFC6979(message.ToByteArray(), signature.Sign.ToByteArray()); - } - - public static bool VerifyData(this ECDsa key, byte[] data, byte[] sig) - { - return key.VerifyHash(SHA512.Create().ComputeHash(data), sig[1..]); - } - - public static bool VerifyMessagePart(this Signature sig, IMessage data) - { - if (sig is null || sig.Key is null || sig.Sign is null) - return false; - - using var key = sig.Key.ToByteArray().LoadPublicKey(); - var data2Verify = data is null ? Array.Empty() : data.ToByteArray(); - - return key.VerifyData(data2Verify, sig.Sign.ToByteArray()); - } - - public static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification) - { - if (!verification.MetaSignature.VerifyMessagePart(meta)) - return false; - - var origin = verification.GetOrigin(); - - if (!verification.OriginSignature.VerifyMessagePart(origin)) - return false; - - if (origin is null) - return verification.BodySignature.VerifyMessagePart(body); - - return verification.BodySignature is null && VerifyMatryoskaLevel(body, meta.GetOrigin(), origin); - } - - public static bool Verify(this IVerificableMessage message) - { - return VerifyMatryoskaLevel(message.GetBody(), message.GetMetaHeader(), message.GetVerificationHeader()); - } - - public static void CheckResponse(IResponse resp) - { - if (!resp.Verify()) - throw new FormatException($"invalid response, type={resp.GetType()}"); - - var status = resp.MetaHeader.Status.ToModel(); - if (!status.IsSuccess()) - throw new ApplicationException(status.ToString()); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.Cryptography/ArrayHelper.cs b/src/FrostFS.SDK.Cryptography/ArrayHelper.cs index 00c356db..c88134d8 100644 --- a/src/FrostFS.SDK.Cryptography/ArrayHelper.cs +++ b/src/FrostFS.SDK.Cryptography/ArrayHelper.cs @@ -21,4 +21,18 @@ internal static class ArrayHelper return dst; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void GetRevertedArray(ReadOnlySpan source, byte[] data) + { + if (source.Length != 0) + { + int i = 0; + int j = source.Length - 1; + while (i < source.Length) + { + data[i++] = source[j--]; + } + } + } } \ No newline at end of file diff --git a/src/FrostFS.SDK.Cryptography/AssemblyInfo.cs b/src/FrostFS.SDK.Cryptography/AssemblyInfo.cs index a593c3a0..64bc8e9d 100644 --- a/src/FrostFS.SDK.Cryptography/AssemblyInfo.cs +++ b/src/FrostFS.SDK.Cryptography/AssemblyInfo.cs @@ -1,20 +1,7 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Reflection; -// In SDK-style projects such as this one, several assembly attributes that were historically -// defined in this file are now automatically added during build and populated with -// values defined in project properties. For details of which attributes are included -// and how to customise this process see: https://aka.ms/assembly-info-properties - - -// Setting ComVisible to false makes the types in this assembly not visible to COM -// components. If you need to access a type in this assembly from COM, set the ComVisible -// attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM. - -[assembly: Guid("08a8487e-39ce-41fb-9c24-13f73ff2bde0")] - -[assembly: InternalsVisibleToAttribute("FrostFS.SDK.Cryptography.Test")] \ No newline at end of file +[assembly: AssemblyCompany("TrueCloudLab")] +[assembly: AssemblyFileVersion("1.0.7.0")] +[assembly: AssemblyProduct("FrostFS.SDK.Cryptography")] +[assembly: AssemblyTitle("FrostFS.SDK.Cryptography")] +[assembly: AssemblyVersion("1.0.7.0")] diff --git a/src/FrostFS.SDK.Cryptography/Base58.cs b/src/FrostFS.SDK.Cryptography/Base58.cs index 36711c07..4dc61875 100644 --- a/src/FrostFS.SDK.Cryptography/Base58.cs +++ b/src/FrostFS.SDK.Cryptography/Base58.cs @@ -3,8 +3,6 @@ using System.Linq; using System.Numerics; using System.Text; -using Org.BouncyCastle.Security.Certificates; - namespace FrostFS.SDK.Cryptography; public static class Base58 @@ -15,35 +13,41 @@ public static class Base58 { if (input is null) throw new ArgumentNullException(nameof(input)); - + byte[] buffer = Decode(input); - if (buffer.Length < 4) + if (buffer.Length < 4) throw new FormatException(); - - byte[] checksum = buffer[0..(buffer.Length - 4)].Sha256().Sha256(); - if (!buffer.AsSpan(buffer.Length - 4).SequenceEqual(checksum[..4].AsSpan())) + var check = buffer.AsSpan(0, buffer.Length - 4); + byte[] checksum = DataHasher.Sha256(DataHasher.Sha256(check).AsSpan()); + + if (!buffer.AsSpan(buffer.Length - 4).SequenceEqual(checksum.AsSpan(0, 4))) throw new FormatException(); - - var ret = buffer[..^4]; + + var result = check.ToArray(); Array.Clear(buffer, 0, buffer.Length); - return ret; + return result; } - public static string Base58CheckEncode(this ReadOnlySpan data) + public static string Base58CheckEncode(this Span data) { - byte[] checksum = data.ToArray().Sha256().Sha256(); + byte[] checksum = DataHasher.Sha256(DataHasher.Sha256(data).AsSpan()); Span buffer = stackalloc byte[data.Length + 4]; data.CopyTo(buffer); - checksum[..4].AsSpan().CopyTo(buffer[data.Length..]); + + checksum.AsSpan(0, 4).CopyTo(buffer.Slice(data.Length)); var ret = Encode(buffer); buffer.Clear(); + return ret; } public static byte[] Decode(string input) { + if (input == null) + throw new ArgumentNullException(nameof(input)); + // Decode Base58 string to BigInteger var bi = BigInteger.Zero; for (int i = 0; i < input.Length; i++) @@ -51,31 +55,44 @@ public static class Base58 int digit = Alphabet.IndexOf(input[i]); if (digit < 0) throw new FormatException($"Invalid Base58 character '{input[i]}' at position {i}"); - + bi = bi * Alphabet.Length + digit; } int leadingZeroCount = input.TakeWhile(c => c == Alphabet[0]).Count(); - var leadingZeros = new byte[leadingZeroCount]; - - if (bi.IsZero) - return leadingZeros; - var bytesBigEndian = bi.ToByteArray().Reverse(); + if (bi.IsZero) + return new byte[leadingZeroCount]; + + var bytesBigEndian = bi.ToByteArray().Reverse().ToArray(); var firstNonZeroIndex = 0; - while(bytesBigEndian.ElementAt(firstNonZeroIndex) == 0x0) + while (bytesBigEndian.ElementAt(firstNonZeroIndex) == 0x0) firstNonZeroIndex++; - + var bytesWithoutLeadingZeros = bytesBigEndian.Skip(firstNonZeroIndex).ToArray(); - return ArrayHelper.Concat(leadingZeros, bytesWithoutLeadingZeros); + var result = new byte[leadingZeroCount + bytesBigEndian.Length - firstNonZeroIndex]; + + int p = 0; + while (p < leadingZeroCount) + result[p++] = 0; + + for (int j = firstNonZeroIndex; j < bytesBigEndian.Length; j++) + result[p++] = bytesBigEndian[j]; + + return result; } public static string Encode(ReadOnlySpan input) { - var data = input.ToArray().Reverse().Concat(new byte[] { 0 }).ToArray(); + var data = new byte[input.Length + 1]; + + ArrayHelper.GetRevertedArray(input, data); + + data[input.Length] = 0; + BigInteger value = new(data); // Encode BigInteger to Base58 string diff --git a/src/FrostFS.SDK.Cryptography/DataHasher.cs b/src/FrostFS.SDK.Cryptography/DataHasher.cs new file mode 100644 index 00000000..fceeeaae --- /dev/null +++ b/src/FrostFS.SDK.Cryptography/DataHasher.cs @@ -0,0 +1,121 @@ +using System; +using System.Buffers; +using System.Security.Cryptography; + +namespace FrostFS.SDK.Cryptography; + +public static class DataHasher +{ + private const int LargeBlockSize = 1024 * 1024; + private const int SmallBlockSize = 256; + + public static byte[] Hash(this ReadOnlyMemory bytes, HashAlgorithm algorithm) + { + if (algorithm is null) + { + throw new ArgumentNullException(nameof(algorithm)); + } + + if (bytes.Length == 0) + { + return algorithm.ComputeHash([]); + } + + int rest, pos = 0; + + var blockSize = bytes.Length <= SmallBlockSize ? SmallBlockSize : LargeBlockSize; + + byte[] buffer = ArrayPool.Shared.Rent(blockSize); + + try + { + while ((rest = bytes.Length - pos) > 0) + { + var size = Math.Min(rest, blockSize); + + bytes.Slice(pos, size).CopyTo(buffer); + + algorithm.TransformBlock(buffer, 0, size, buffer, 0); + + pos += size; + } + + algorithm.TransformFinalBlock([], 0, 0); + return algorithm.Hash; + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + public static byte[] Hash(this ReadOnlySpan bytes, HashAlgorithm algorithm) + { + if (algorithm is null) + { + throw new ArgumentNullException(nameof(algorithm)); + } + + if (bytes.Length == 0) + { + return algorithm.ComputeHash([]); + } + + int rest, pos = 0; + + var blockSize = bytes.Length <= SmallBlockSize ? SmallBlockSize : LargeBlockSize; + + byte[] buffer = ArrayPool.Shared.Rent(blockSize); + + try + { + while ((rest = bytes.Length - pos) > 0) + { + var size = Math.Min(rest, blockSize); + + bytes.Slice(pos, size).CopyTo(buffer); + + algorithm.TransformBlock(buffer, 0, size, buffer, 0); + + pos += size; + } + + algorithm.TransformFinalBlock([], 0, 0); + return algorithm.Hash; + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + public static byte[] Sha256(ReadOnlyMemory value) + { + using SHA256 sha = SHA256.Create(); + return Hash(value, sha); + } + + public static byte[] Sha256(ReadOnlySpan value) + { + using SHA256 sha = SHA256.Create(); + return Hash(value, sha); + } + + public static byte[] Sha512(ReadOnlyMemory value) + { + using SHA512 sha = SHA512.Create(); + return Hash(value, sha); + } + + public static byte[] Sha512(ReadOnlySpan value) + { + using SHA512 sha = SHA512.Create(); + return Hash(value, sha); + } +} diff --git a/src/FrostFS.SDK.Cryptography/Extentions.cs b/src/FrostFS.SDK.Cryptography/Extentions.cs new file mode 100644 index 00000000..5f4fdd9a --- /dev/null +++ b/src/FrostFS.SDK.Cryptography/Extentions.cs @@ -0,0 +1,16 @@ +using Org.BouncyCastle.Crypto.Digests; + +namespace FrostFS.SDK.Cryptography; + +public static class Extentions +{ + internal static byte[] RIPEMD160(this byte[] value) + { + var hash = new byte[20]; + + var digest = new RipeMD160Digest(); + digest.BlockUpdate(value, 0, value.Length); + digest.DoFinal(hash, 0); + return hash; + } +} diff --git a/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj b/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj index 05347202..faeabb12 100644 --- a/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj +++ b/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj @@ -4,12 +4,40 @@ netstandard2.0 12.0 enable + FrostFS.SDK.Cryptography + 1.0.7 + + Cryptography tools for C# SDK + + true + + + + true + + + + <_SkipUpgradeNetAnalyzersNuGetWarning>true + + + + true + + + + false + True + .\\..\\..\\keyfile.snk - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/FrostFS.SDK.Cryptography/HashStream.cs b/src/FrostFS.SDK.Cryptography/HashStream.cs new file mode 100644 index 00000000..2893f260 --- /dev/null +++ b/src/FrostFS.SDK.Cryptography/HashStream.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Security.Cryptography; + +namespace FrostFS.SDK.Cryptography; + +public sealed class HashStream(HashAlgorithm algorithm) : Stream +{ + private long position; + + private readonly HashAlgorithm _hash = algorithm; + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length => long.MaxValue; + + public override long Position + { + get { return position; } + set { position = value; } + } + + public override void Flush() + { } + + public override int Read(byte[] buffer, int offset, int count) + { + return 0; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return 0; + } + + public override void SetLength(long value) + { } + + public override void Write(byte[] buffer, int offset, int count) + { + _hash.TransformBlock(buffer, offset, count, buffer, offset); + } + + public byte[] Hash() + { + _hash.TransformFinalBlock([], 0, 0); + return _hash.Hash; + } + + protected override void Dispose(bool disposing) + { + _hash?.Dispose(); + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Cryptography/Helper.cs b/src/FrostFS.SDK.Cryptography/Helper.cs deleted file mode 100644 index 93058d94..00000000 --- a/src/FrostFS.SDK.Cryptography/Helper.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Google.Protobuf; -using Org.BouncyCastle.Crypto.Digests; -using System; -using System.Buffers.Binary; -using System.Security.Cryptography; - -namespace FrostFS.SDK.Cryptography; - -public static class Helper -{ - internal static byte[] RIPEMD160(this byte[] value) - { - var hash = new byte[20]; - var digest = new RipeMD160Digest(); - digest.BlockUpdate(value, 0, value.Length); - digest.DoFinal(hash, 0); - return hash; - } - - public static byte[] Sha256(this byte[] value) - { - using var sha256 = SHA256.Create(); - return sha256.ComputeHash(value); - } - - internal static byte[] Sha256(this byte[] value, int offset, int count) - { - using var sha256 = SHA256.Create(); - return sha256.ComputeHash(value, offset, count); - } - - internal static byte[] Sha256(this ReadOnlySpan value) - { - using var sha256 = SHA256.Create(); - return sha256.ComputeHash(value.ToArray()); - } - - public static ByteString Sha256(this IMessage data) - { - return ByteString.CopyFrom(data.ToByteArray().Sha256()); - } - - public static ByteString Sha256(this ByteString data) - { - return ByteString.CopyFrom(data.ToByteArray().Sha256()); - } - - - public static ulong Murmur64(this byte[] value, uint seed) - { - using var murmur = new Murmur3_128(seed); - return BinaryPrimitives.ReadUInt64LittleEndian(murmur.ComputeHash(value)); - } -} diff --git a/src/FrostFS.SDK.Cryptography/Key.cs b/src/FrostFS.SDK.Cryptography/Key.cs index 56f71cb1..9eca1046 100644 --- a/src/FrostFS.SDK.Cryptography/Key.cs +++ b/src/FrostFS.SDK.Cryptography/Key.cs @@ -16,10 +16,13 @@ public static class KeyExtension private const int UncompressedPublicKeyLength = 65; private static readonly uint CheckSigDescriptor = - BinaryPrimitives.ReadUInt32LittleEndian(Encoding.ASCII.GetBytes("System.Crypto.CheckSig").Sha256()); + BinaryPrimitives.ReadUInt32LittleEndian(DataHasher.Sha256(Encoding.ASCII.GetBytes("System.Crypto.CheckSig").AsSpan())); public static byte[] Compress(this byte[] publicKey) { + if (publicKey == null) + throw new ArgumentNullException(nameof(publicKey)); + if (publicKey.Length != UncompressedPublicKeyLength) throw new FormatException( $"{nameof(Compress)} argument isn't uncompressed public key. " + @@ -34,6 +37,9 @@ public static class KeyExtension public static byte[] Decompress(this byte[] publicKey) { + if (publicKey == null) + throw new ArgumentNullException(nameof(publicKey)); + if (publicKey.Length != CompressedPublicKeyLength) throw new FormatException( $"{nameof(Decompress)} argument isn't compressed public key. " + @@ -54,52 +60,77 @@ public static class KeyExtension $"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}" ); - var script = new byte[] { 0x0c, CompressedPublicKeyLength }; //PUSHDATA1 33 - script = ArrayHelper.Concat(script, publicKey); - script = ArrayHelper.Concat(script, new byte[] { 0x41 }); //SYSCALL - script = ArrayHelper.Concat(script, BitConverter.GetBytes(CheckSigDescriptor)); //Neo_Crypto_CheckSig + var signDescriptor = BitConverter.GetBytes(CheckSigDescriptor); + + var script = new byte[3 + publicKey.Length + signDescriptor.Length]; + + script[0] = 0x0c; + script[1] = CompressedPublicKeyLength; //PUSHDATA1 33 + Buffer.BlockCopy(publicKey, 0, script, 2, publicKey.Length); + script[publicKey.Length + 2] = 0x41; //SYSCALL + Buffer.BlockCopy(signDescriptor, 0, script, publicKey.Length + 3, signDescriptor.Length); return script; } public static byte[] GetScriptHash(this byte[] publicKey) { + if (publicKey == null) + throw new ArgumentNullException(nameof(publicKey)); + var script = publicKey.CreateSignatureRedeemScript(); - return script.Sha256().RIPEMD160(); + return DataHasher.Sha256(script.AsSpan()).RIPEMD160(); + } + + public static string GetWIFFromPrivateKey(this byte[] privateKey) + { + if (privateKey == null || privateKey.Length != 32) + { + throw new ArgumentNullException(nameof(privateKey)); + } + + Span wifSpan = stackalloc byte[34]; + + wifSpan[0] = 0x80; + wifSpan[33] = 0x01; + + privateKey.AsSpan().CopyTo(wifSpan.Slice(1)); + + var wif = Base58.Base58CheckEncode(wifSpan); + + return wif; } private static string ToAddress(this byte[] scriptHash, byte version) { Span data = stackalloc byte[21]; data[0] = version; - scriptHash.CopyTo(data[1..]); + scriptHash.CopyTo(data.Slice(1)); return Base58.Base58CheckEncode(data); } private static byte[] GetPrivateKeyFromWIF(string wif) { - if (wif == null) - throw new ArgumentNullException(); - + if (wif == null) + throw new ArgumentNullException(nameof(wif)); + var data = wif.Base58CheckDecode(); - + if (data.Length != 34 || data[0] != 0x80 || data[33] != 0x01) throw new FormatException(); - + var privateKey = new byte[32]; Buffer.BlockCopy(data, 1, privateKey, 0, privateKey.Length); Array.Clear(data, 0, data.Length); return privateKey; } - public static string Address(this ECDsa key) - { - return key.PublicKey().PublicKeyToAddress(); - } - public static string PublicKeyToAddress(this byte[] publicKey) { + if (publicKey == null) + throw new ArgumentNullException(nameof(publicKey)); + if (publicKey.Length != CompressedPublicKeyLength) throw new FormatException( nameof(publicKey) + @@ -112,37 +143,48 @@ public static class KeyExtension public static byte[] PublicKey(this ECDsa key) { + if (key == null) + throw new ArgumentNullException(nameof(key)); + var param = key.ExportParameters(false); var pubkey = new byte[33]; var pos = 33 - param.Q.X.Length; param.Q.X.CopyTo(pubkey, pos); - if (new BigInteger(param.Q.Y.Reverse().Concat(new byte[] { 0x00 }).ToArray()).IsEven) - pubkey[0] = 0x2; - else - pubkey[0] = 0x3; + + var y = new byte[33]; + ArrayHelper.GetRevertedArray(param.Q.Y, y); + y[32] = 0; + + pubkey[0] = new BigInteger(y).IsEven ? (byte)0x2 : (byte)0x3; return pubkey; } public static byte[] PrivateKey(this ECDsa key) { + if (key == null) + throw new ArgumentNullException(nameof(key)); + return key.ExportParameters(true).D; } public static ECDsa LoadPrivateKey(this byte[] privateKey) { var secp256R1 = SecNamedCurves.GetByName("secp256r1"); - var publicKey = - secp256R1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, privateKey)).GetEncoded(false)[1..]; + var publicKey = secp256R1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, privateKey)) + .GetEncoded(false) + .Skip(1) + .ToArray(); + var key = ECDsa.Create(new ECParameters { Curve = ECCurve.NamedCurves.nistP256, D = privateKey, Q = new ECPoint { - X = publicKey[..32], - Y = publicKey[32..] + X = publicKey.Take(32).ToArray(), + Y = publicKey.Skip(32).ToArray() } }); @@ -152,20 +194,20 @@ public static class KeyExtension public static ECDsa LoadWif(this string wif) { var privateKey = GetPrivateKeyFromWIF(wif); - + return LoadPrivateKey(privateKey); } public static ECDsa LoadPublicKey(this byte[] publicKey) { - var publicKeyFull = publicKey.Decompress()[1..]; + var publicKeyFull = publicKey.Decompress().Skip(1).ToArray(); var key = ECDsa.Create(new ECParameters { Curve = ECCurve.NamedCurves.nistP256, Q = new ECPoint { - X = publicKeyFull[..32], - Y = publicKeyFull[32..] + X = publicKeyFull.Take(32).ToArray(), + Y = publicKeyFull.Skip(32).ToArray() } }); diff --git a/src/FrostFS.SDK.Cryptography/Murmur3_128.cs b/src/FrostFS.SDK.Cryptography/Murmur3_128.cs index 1a846ab0..3e0ae5ff 100644 --- a/src/FrostFS.SDK.Cryptography/Murmur3_128.cs +++ b/src/FrostFS.SDK.Cryptography/Murmur3_128.cs @@ -4,7 +4,7 @@ using System.Security.Cryptography; namespace FrostFS.SDK.Cryptography; -internal class Murmur3_128 : HashAlgorithm +public class Murmur3 : HashAlgorithm { private const ulong c1 = 0x87c37b91114253d5; private const ulong c2 = 0x4cf5ad432745937f; @@ -17,18 +17,35 @@ internal class Murmur3_128 : HashAlgorithm private readonly uint seed; private int length; - public Murmur3_128(uint seed) + public Murmur3(uint seed) { this.seed = seed; Initialize(); } + public ulong GetCheckSum64(byte[] bytes) + { + if (bytes is null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + Initialize(); + HashCore(bytes, 0, bytes.Length); + return HashFinalUlong(); + } + protected override void HashCore(byte[] array, int ibStart, int cbSize) { + if (array is null) + { + throw new ArgumentNullException(nameof(array)); + } + length += cbSize; int remainder = cbSize & 15; int alignedLength = ibStart + (cbSize - remainder); - + for (int i = ibStart; i < alignedLength; i += 16) { ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(array.AsSpan(i)); @@ -50,7 +67,7 @@ internal class Murmur3_128 : HashAlgorithm h2 += h1; h2 = h2 * m + n2; } - + if (remainder > 0) { ulong k1 = 0, k2 = 0; @@ -92,6 +109,11 @@ internal class Murmur3_128 : HashAlgorithm } protected override byte[] HashFinal() + { + return BitConverter.GetBytes(HashFinalUlong()); + } + + protected ulong HashFinalUlong() { h1 ^= (ulong)length; h2 ^= (ulong)length; @@ -101,8 +123,8 @@ internal class Murmur3_128 : HashAlgorithm h2 = Fimix64(h2); h1 += h2; h2 += h1; - - return BitConverter.GetBytes(h1); + + return h1; } public override void Initialize() @@ -112,7 +134,7 @@ internal class Murmur3_128 : HashAlgorithm h2 = seed; } - private ulong Fimix64(ulong k) + private static ulong Fimix64(ulong k) { k ^= k >> 33; k *= 0xff51afd7ed558ccd; diff --git a/src/FrostFS.SDK.Cryptography/Range.cs b/src/FrostFS.SDK.Cryptography/Range.cs deleted file mode 100644 index 81583145..00000000 --- a/src/FrostFS.SDK.Cryptography/Range.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace System -{ - /// Represent a type can be used to index a collection either from the start or the end. - /// - /// Index is used by the C# compiler to support the new index syntax - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; - /// int lastElement = someArray[^1]; // lastElement = 5 - /// - /// - internal readonly struct Index : IEquatable - { - private readonly int _value; - - /// Construct an Index using a value and indicating if the index is from the start or from the end. - /// The index value. it has to be zero or positive number. - /// Indicating if the index is from the start or from the end. - /// - /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Index(int value, bool fromEnd = false) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - if (fromEnd) - _value = ~value; - else - _value = value; - } - - // The following private constructors mainly created for perf reason to avoid the checks - private Index(int value) - { - _value = value; - } - - /// Create an Index pointing at first element. - public static Index Start => new Index(0); - - /// Create an Index pointing at beyond last element. - public static Index End => new Index(~0); - - /// Create an Index from the start at the position indicated by the value. - /// The index value from the start. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromStart(int value) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - return new Index(value); - } - - /// Create an Index from the end at the position indicated by the value. - /// The index value from the end. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Index FromEnd(int value) - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); - } - - return new Index(~value); - } - - /// Returns the index value. - public int Value - { - get - { - if (_value < 0) - { - return ~_value; - } - else - { - return _value; - } - } - } - - /// Indicates whether the index is from the start or the end. - public bool IsFromEnd => _value < 0; - - /// Calculate the offset from the start using the giving collection length. - /// The length of the collection that the Index will be used with. length has to be a positive value - /// - /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. - /// we don't validate either the returned offset is greater than the input length. - /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and - /// then used to index a collection will get out of range exception which will be same affect as the validation. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetOffset(int length) - { - var offset = _value; - if (IsFromEnd) - { - // offset = length - (~value) - // offset = length + (~(~value) + 1) - // offset = length + value + 1 - - offset += length + 1; - } - return offset; - } - - /// Indicates whether the current Index object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; - - /// Indicates whether the current Index object is equal to another Index object. - /// An object to compare with this object - public bool Equals(Index other) => _value == other._value; - - /// Returns the hash code for this instance. - public override int GetHashCode() => _value; - - /// Converts integer number to an Index. - public static implicit operator Index(int value) => FromStart(value); - - /// Converts the value of the current Index object to its equivalent string representation. - public override string ToString() - { - if (IsFromEnd) - return "^" + ((uint)Value).ToString(); - - return ((uint)Value).ToString(); - } - } - - /// Represent a range has start and end indexes. - /// - /// Range is used by the C# compiler to support the range syntax. - /// - /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; - /// int[] subArray1 = someArray[0..2]; // { 1, 2 } - /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } - /// - /// - internal readonly struct Range : IEquatable - { - /// Represent the inclusive start index of the Range. - public Index Start { get; } - - /// Represent the exclusive end index of the Range. - public Index End { get; } - - /// Construct a Range object using the start and end indexes. - /// Represent the inclusive start index of the range. - /// Represent the exclusive end index of the range. - public Range(Index start, Index end) - { - Start = start; - End = end; - } - - /// Indicates whether the current Range object is equal to another object of the same type. - /// An object to compare with this object - public override bool Equals(object? value) => - value is Range r && - r.Start.Equals(Start) && - r.End.Equals(End); - - /// Indicates whether the current Range object is equal to another Range object. - /// An object to compare with this object - public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); - - /// Returns the hash code for this instance. - public override int GetHashCode() - { - return Start.GetHashCode() * 31 + End.GetHashCode(); - } - - /// Converts the value of the current Range object to its equivalent string representation. - public override string ToString() - { - return Start + ".." + End; - } - - /// Create a Range object starting from start index to the end of the collection. - public static Range StartAt(Index start) => new Range(start, Index.End); - - /// Create a Range object starting from first element in the collection to the end Index. - public static Range EndAt(Index end) => new Range(Index.Start, end); - - /// Create a Range object starting from first element to the end. - public static Range All => new Range(Index.Start, Index.End); - - /// Calculate the start offset and length of range object using a collection length. - /// The length of the collection that the range will be used with. length has to be a positive value. - /// - /// For performance reason, we don't validate the input length parameter against negative values. - /// It is expected Range will be used with collections which always have non negative length/count. - /// We validate the range is inside the length scope though. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int Offset, int Length) GetOffsetAndLength(int length) - { - int start; - var startIndex = Start; - if (startIndex.IsFromEnd) - start = length - startIndex.Value; - else - start = startIndex.Value; - - int end; - var endIndex = End; - if (endIndex.IsFromEnd) - end = length - endIndex.Value; - else - end = endIndex.Value; - - if ((uint)end > (uint)length || (uint)start > (uint)end) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return (start, end - start); - } - } -} - -namespace System.Runtime.CompilerServices -{ - internal static class RuntimeHelpers - { - /// - /// Slices the specified array using the specified range. - /// - public static T[] GetSubArray(T[] array, Range range) - { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } - - (int offset, int length) = range.GetOffsetAndLength(array.Length); - - if (default(T) != null || typeof(T[]) == array.GetType()) - { - // We know the type of the array to be exactly T[]. - - if (length == 0) - { - return Array.Empty(); - } - - var dest = new T[length]; - Array.Copy(array, offset, dest, 0, length); - return dest; - } - else - { - // The array is actually a U[] where U:T. - var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); - Array.Copy(array, offset, dest, 0, length); - return dest; - } - } - } -} diff --git a/src/FrostFS.SDK.Cryptography/Tz/GF127.cs b/src/FrostFS.SDK.Cryptography/Tz/GF127.cs deleted file mode 100644 index d1abdcf6..00000000 --- a/src/FrostFS.SDK.Cryptography/Tz/GF127.cs +++ /dev/null @@ -1,253 +0,0 @@ -using System; -using System.Security.Cryptography; - -namespace FrostFS.SDK.Cryptography.Tz; - -// GF127 represents element of GF(2^127) -public class GF127 : IEquatable -{ - public const int ByteSize = 16; - public const ulong MSB64 = (ulong)1 << 63; // 2^63 - public static readonly GF127 Zero = new(0, 0); - public static readonly GF127 One = new(1, 0); - public static readonly GF127 X127X631 = new(MSB64 + 1, MSB64); // x^127+x^63+1 - - private readonly ulong[] _data; - - public ulong this[int index] - { - get { return _data[index]; } - set { _data[index] = value; } - } - - public GF127(ulong[] value) - { - if (value is null || value.Length != 2) - throw new ArgumentException(nameof(value) + "is invalid"); - _data = value; - } - - // Constructs new element of GF(2^127) as u1*x^64 + u0. - // It is assumed that u1 has zero MSB. - public GF127(ulong u0, ulong u1) : this(new ulong[] { u0, u1 }) - { - } - - public GF127() : this(0, 0) - { - } - - public override bool Equals(object obj) - { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj is GF127 b) - return Equals(b); - return false; - } - - public override int GetHashCode() - { - return this[0].GetHashCode() + this[1].GetHashCode(); - } - - public bool Equals(GF127 other) - { - if (other is null) - return false; - if (ReferenceEquals(this, other)) - return true; - return this[0] == other[0] && this[1] == other[1]; - } - - // return the index of MSB - private int IndexOfMSB() - { - int i = Helper.GetLeadingZeros(this[1]); - if (i == 64) - i += Helper.GetLeadingZeros(this[0]); - return 127 - i; - } - - // Set index n to 1 - public static GF127 SetN(int n) - { - if (n < 64) - return new GF127((ulong)1 << n, 0); - return new GF127(0, (ulong)1 << (n - 64)); - } - - // Add - public static GF127 operator +(GF127 a, GF127 b) - { - return new GF127(a[0] ^ b[0], a[1] ^ b[1]); - } - - // Bitwise-and - public static GF127 operator &(GF127 a, GF127 b) - { - return new GF127(a[0] & b[0], a[1] & b[1]); - } - - // Multiply - public static GF127 operator *(GF127 a, GF127 b) // 2^63 * 2, 10 - { - GF127 r = new(); - GF127 c = a; - - if (b[1] == 0) - { - for (int i = 0; i < b[0].GetNonZeroLength(); i++) - { - if ((b[0] & ((ulong)1 << i)) != 0) - r += c; - c = Mul10(c); // c = c * 2 - } - } - else - { - for (int i = 0; i < 64; i++) - { - if ((b[0] & ((ulong)1 << i)) != 0) - r += c; - c = Mul10(c); // c = c * 2 - } - - for (int i = 0; i < b[1].GetNonZeroLength(); i++) - { - if ((b[1] & ((ulong)1 << i)) != 0) - r += c; - c = Mul10(c); - } - } - - return r; - } - - // Inverse, returns a^-1 - // Extended Euclidean Algorithm - // https://link.springer.com/content/pdf/10.1007/3-540-44499-8_1.pdf - public static GF127 Inv(GF127 a) - { - GF127 v = X127X631, - u = a, - c = new(1, 0), - d = new(0, 0), - t, - x; - - int du = u.IndexOfMSB(); - int dv = v.IndexOfMSB(); - // degree of polynomial is a position of most significant bit - while (du != 0) - { - if (du < dv) - { - (v, u) = (u, v); - (dv, du) = (du, dv); - (d, c) = (c, d); - } - - x = SetN(du - dv); - t = x * v; - u += t; - // because * performs reduction on t, manually reduce u at first step - if (u.IndexOfMSB() == 127) - u += X127X631; - - t = x * d; - c += t; - - du = u.IndexOfMSB(); - dv = v.IndexOfMSB(); - } - - return c; - } - - // Mul10 returns a*x - public static GF127 Mul10(GF127 a) - { - GF127 b = new(); - var c = (a[0] & MSB64) >> 63; - b[0] = a[0] << 1; - b[1] = (a[1] << 1) ^ c; - if ((b[1] & MSB64) != 0) - { - b[0] ^= X127X631[0]; - b[1] ^= X127X631[1]; - } - - return b; - } - - // Mul11 returns a*(x+1) - public static GF127 Mul11(GF127 a) - { - GF127 b = new(); - var c = (a[0] & MSB64) >> 63; - b[0] = a[0] ^ (a[0] << 1); - b[1] = a[1] ^ (a[1] << 1) ^ c; - if ((b[1] & MSB64) == 0) return b; - b[0] ^= X127X631[0]; - b[1] ^= X127X631[1]; - return b; - } - - // Random returns random element from GF(2^127). - // Is used mostly for testing. - public static GF127 Random() - { - using RandomNumberGenerator rng = RandomNumberGenerator.Create(); - return new GF127(rng.NextUlong(), rng.NextUlong() >> 1); - } - - // FromByteArray does the deserialization stuff - public GF127 FromByteArray(byte[] data) - { - if (data.Length != ByteSize) - throw new ArgumentException( - nameof(data) + $" wrong data lenght, {nameof(GF127)} expect={ByteSize}, actual={data.Length}" - ); - var t0 = new byte[8]; - var t1 = new byte[8]; - Array.Copy(data, 0, t1, 0, 8); - Array.Copy(data, 8, t0, 0, 8); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(t0); - Array.Reverse(t1); - } - - _data[0] = BitConverter.ToUInt64(t0, 0); - _data[1] = BitConverter.ToUInt64(t1, 0); - if ((_data[1] & MSB64) != 0) - throw new ArgumentException(nameof(data) + " invalid data"); - return this; - } - - // ToArray() represents element of GF(2^127) as byte array of length 16. - public byte[] ToByteArray() - { - var buff = new byte[16]; - var b0 = BitConverter.GetBytes(_data[0]); - var b1 = BitConverter.GetBytes(_data[1]); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(b0); - Array.Reverse(b1); - } - - Array.Copy(b1, 0, buff, 0, 8); - Array.Copy(b0, 0, buff, 8, 8); - return buff; - } - - // ToString() returns hex-encoded representation, starting with MSB. - public override string ToString() - { - return BitConverter.ToString(ToByteArray()).Replace("-", ""); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.Cryptography/Tz/Helper.cs b/src/FrostFS.SDK.Cryptography/Tz/Helper.cs deleted file mode 100644 index 807870e9..00000000 --- a/src/FrostFS.SDK.Cryptography/Tz/Helper.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Security.Cryptography; - -namespace FrostFS.SDK.Cryptography.Tz; - -public static class Helper -{ - public static ulong NextUlong(this RandomNumberGenerator rng) - { - var buff = new byte[8]; - rng.GetBytes(buff); - return BitConverter.ToUInt64(buff, 0); - } - - public static int GetLeadingZeros(ulong value) - { - var i = 64; - while (value != 0) - { - value >>= 1; - i--; - } - return i; - } - - public static int GetNonZeroLength(this ulong value) - { - return 64 - GetLeadingZeros(value); - } -} diff --git a/src/FrostFS.SDK.Cryptography/Tz/SL2.cs b/src/FrostFS.SDK.Cryptography/Tz/SL2.cs deleted file mode 100644 index 68025011..00000000 --- a/src/FrostFS.SDK.Cryptography/Tz/SL2.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Linq; - -namespace FrostFS.SDK.Cryptography.Tz; - -public class SL2 : IEquatable -{ - // 2x2 matrix - private readonly GF127[][] data; - - public static readonly SL2 ID = new( - new GF127(1, 0), new GF127(0, 0), new GF127(0, 0), new GF127(1, 0)); - - public static readonly SL2 A = new( - new GF127(2, 0), new GF127(1, 0), new GF127(1, 0), new GF127(0, 0)); - - public static readonly SL2 B = new( - new GF127(2, 0), new GF127(3, 0), new GF127(1, 0), new GF127(1, 0)); - - // Indexer - public GF127[] this[int i] - { - get { return data[i]; } - set { data[i] = value; } - } - - public SL2(GF127[][] value) - { - if (value is null || value.Length != 2 || !value.All(p => p.Length == 2)) - throw new ArgumentException(nameof(value) + $" invalid {nameof(GF127)} matrics"); - data = value; - } - - public SL2(GF127 g00, GF127 g01, GF127 g10, GF127 g11) - : this([[g00, g01], [g10, g11]]) - { - } - - public SL2() : this(GF127.One, GF127.Zero, GF127.Zero, GF127.One) - { - } - - public override bool Equals(object obj) - { - if (obj is null) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj is SL2 b) - return Equals(b); - return false; - } - - public override int GetHashCode() - { - return this[0][0].GetHashCode() + - this[0][1].GetHashCode() + - this[1][0].GetHashCode() + - this[1][1].GetHashCode(); - } - - public bool Equals(SL2 other) - { - if (other is null) - return false; - if (ReferenceEquals(this, other)) - return true; - return this[0][0].Equals(other[0][0]) && - this[0][1].Equals(other[0][1]) && - this[1][0].Equals(other[1][0]) && - this[1][1].Equals(other[1][1]); - } - - // 2X2 matrix multiplication - public static SL2 operator *(SL2 a, SL2 b) - { - return new SL2( - a[0][0] * b[0][0] + a[0][1] * b[1][0], - a[0][0] * b[0][1] + a[0][1] * b[1][1], - a[1][0] * b[0][0] + a[1][1] * b[1][0], - a[1][0] * b[0][1] + a[1][1] * b[1][1]); - } - - // Multiplication using strassen algorithm - public static SL2 MulStrassen(SL2 a, SL2 b) - { - GF127[] t = - [ - (a[0][0] + a[1][1]) * (b[0][0] + b[1][1]), // t[0] == (a11 + a22) * (b11 + b22) - (a[1][0] + a[1][1]) * b[0][0], // t[1] == (a21 + a22) * b11 - (b[0][1] + b[1][1]) * a[0][0], // t[2] == (b12 + b22) * a11 - (b[1][0] + b[0][0]) * a[1][1], // t[3] == (b21 + b11) * a22 - (a[0][0] + a[0][1]) * b[1][1], // t[4] == (a11 + a12) * b22 - (a[1][0] + a[0][0]) * (b[0][0] + b[0][1]), // t[5] == (a21 + a11) * (b11 + b12) - (a[0][1] + a[1][1]) * (b[1][0] + b[1][1]), // t[6] == (a12 + a22) * (b21 + b22) - ]; - - SL2 r = new(); - r[0][1] = t[2] + t[4]; // r12 == a11*b12 + a11*b22 + a11*b22 + a12*b22 == a11*b12 + a12*b22 - r[1][0] = t[1] + t[3]; // r21 == a21*b11 + a22*b11 + a22*b21 + a22*b11 == a21*b11 + a22*b21 - // r11 == (a11*b11 + a22*b11` + a11*b22` + a22*b22`) + (a22*b21` + a22*b11`) + (a11*b22` + a12*b22`) + - // (a12*b21 + a22*b21` + a12*b22` + a22*b22`) == a11*b11 + a12*b21 - r[0][0] = t[0] + t[3] + t[4] + t[6]; - // r22 == (a11*b11` + a22*b11` + a11*b22` + a22*b22) + (a21*b11` + a22*b11`) + (a11*b12` + a11*b22`) + - // (a21*b11` + a11*b11` + a21*b12 + a11*b12`) == a21*b12 + a22*b22 - r[1][1] = t[0] + t[1] + t[2] + t[5]; - - return r; - } - - // Inv() returns inverse of a in SL2(GF(2^127)) - public static SL2 Inv(SL2 a) - { - GF127[] t = new GF127[2]; - t[0] = a[0][0] * a[1][1] + a[0][1] * a[1][0]; - t[1] = GF127.Inv(t[0]); - - SL2 r = new(); - r[1][1] = t[1] * a[0][0]; - r[0][1] = t[1] * a[0][1]; - r[1][0] = t[1] * a[1][0]; - r[0][0] = t[1] * a[1][1]; - - return r; - } - - // MulA() returns this*A, A = {{x, 1}, {1, 0}} - public SL2 MulA() - { - var r = new SL2(); - r[0][0] = GF127.Mul10(this[0][0]) + this[0][1]; // r11 == t11*x + t12 - r[0][1] = this[0][0]; // r12 == t11 - - r[1][0] = GF127.Mul10(this[1][0]) + this[1][1]; // r21 == t21*x + t22 - r[1][1] = this[1][0]; // r22 == t21 - - return r; - } - - // MulB() returns this*B, B = {{x, x+1}, {1, 1}} - public SL2 MulB() - { - var r = new SL2(); - r[0][0] = GF127.Mul10(this[0][0]) + this[0][1]; // r11 == t11*x + t12 - r[0][1] = GF127.Mul10(this[0][0]) + this[0][0] + this[0][1]; // r12 == t11*x + t11 + t12 - - r[1][0] = GF127.Mul10(this[1][0]) + this[1][1]; // r21 == t21*x + t22 - r[1][1] = GF127.Mul10(this[1][0]) + this[1][0] + this[1][1]; // r22 == t21*x + t21 + t22 - - return r; - } - - public SL2 FromByteArray(byte[] data) - { - if (data.Length != 64) - throw new ArgumentException(nameof(SL2) + $" invalid data, exect={64}, ecatual={data.Length}"); - this[0][0] = new GF127().FromByteArray(data[0..16]); - this[0][1] = new GF127().FromByteArray(data[16..32]); - this[1][0] = new GF127().FromByteArray(data[32..48]); - this[1][1] = new GF127().FromByteArray(data[48..64]); - return this; - } - - public byte[] ToByteArray() - { - var buff = new byte[64]; - Array.Copy(this[0][0].ToByteArray(), 0, buff, 0, 16); - Array.Copy(this[0][1].ToByteArray(), 0, buff, 16, 16); - Array.Copy(this[1][0].ToByteArray(), 0, buff, 32, 16); - Array.Copy(this[1][1].ToByteArray(), 0, buff, 48, 16); - return buff; - } - - public override string ToString() - { - return this[0][0].ToString() + this[0][1].ToString() + - this[1][0].ToString() + this[1][1].ToString(); - } -} diff --git a/src/FrostFS.SDK.Cryptography/Tz/TzHash.cs b/src/FrostFS.SDK.Cryptography/Tz/TzHash.cs deleted file mode 100644 index 7114bede..00000000 --- a/src/FrostFS.SDK.Cryptography/Tz/TzHash.cs +++ /dev/null @@ -1,140 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.Security; -//using System.Security.Cryptography; - -//namespace FrostFS.SDK.Cryptography.Tz; - -//public class TzHash : HashAlgorithm -//{ -// private const int TzHashLength = 64; -// private GF127[] x; -// public override int HashSize => TzHashLength; - -// public TzHash() -// { -// Initialize(); -// } - -// public override void Initialize() -// { -// x = new GF127[4]; -// Reset(); -// HashValue = null; -// } - -// public void Reset() -// { -// x[0] = new GF127(1, 0); -// x[1] = new GF127(0, 0); -// x[2] = new GF127(0, 0); -// x[3] = new GF127(1, 0); -// } - -// public byte[] ToByteArray() -// { -// var buff = new byte[HashSize]; -// for (int i = 0; i < 4; i++) -// { -// Array.Copy(x[i].ToByteArray(), 0, buff, i * 16, 16); -// } -// return buff; -// } - -// [SecurityCritical] -// protected override void HashCore(byte[] array, int ibStart, int cbSize) -// { -// _ = HashData(array[ibStart..(ibStart + cbSize)]); -// } - -// [SecurityCritical] -// protected override byte[] HashFinal() -// { -// return HashValue = ToByteArray(); -// } - -// [SecurityCritical] -// private int HashData(byte[] data) -// { -// var n = data.Length; -// for (int i = 0; i < n; i++) -// { -// for (int j = 7; j >= 0; j--) -// { -// MulBitRight(ref x[0], ref x[1], ref x[2], ref x[3], (data[i] & (1 << j)) != 0); -// } -// } -// return n; -// } - -// // MulBitRight() multiply A (if the bit is 0) or B (if the bit is 1) on the right side -// private void MulBitRight(ref GF127 c00, ref GF127 c01, ref GF127 c10, ref GF127 c11, bool bit) -// { -// // plan 1 -// GF127 t; -// if (bit) -// { // MulB -// t = c00; -// c00 = GF127.Mul10(c00) + c01; // c00 = c00 * x + c01 -// c01 = GF127.Mul11(t) + c01; // c01 = c00 * (x+1) + c01 - -// t = c10; -// c10 = GF127.Mul10(c10) + c11; // c10 = c10 * x + c11 -// c11 = GF127.Mul11(t) + c11; // c11 = c10 * (x+1) + c11 -// } -// else -// { // MulA -// t = c00; -// c00 = GF127.Mul10(c00) + c01; // c00 = c00 * x + c01 -// c01 = t; // c01 = c00 - -// t = c10; -// c10 = GF127.Mul10(c10) + c11; // c10 = c10 * x + c11 -// c11 = t; // c11 = c10; -// } - -// //// plan 2 -// //var r = new SL2(c00, c01, c10, c11); -// //if (bit) -// // r.MulB(); -// //else -// // r.MulA(); -// } - -// // Concat() performs combining of hashes based on homomorphic characteristic. -// public static byte[] Concat(List hs) -// { -// var r = SL2.ID; -// foreach (var h in hs) -// { -// r *= new SL2().FromByteArray(h); -// } -// return r.ToByteArray(); -// } - -// // Validate() checks if hashes in hs combined are equal to h. -// public static bool Validate(byte[] h, List hs) -// { -// var expected = new SL2().FromByteArray(h); -// var actual = new SL2().FromByteArray(Concat(hs)); -// return expected.Equals(actual); -// } - -// // SubtractR() returns hash a, such that Concat(a, b) == c -// public static byte[] SubstractR(byte[] b, byte[] c) -// { -// var t1 = new SL2().FromByteArray(b); -// var t2 = new SL2().FromByteArray(c); -// var r = t2 * SL2.Inv(t1); -// return r.ToByteArray(); -// } - -// // SubtractL() returns hash b, such that Concat(a, b) == c -// public static byte[] SubstractL(byte[] a, byte[] c) -// { -// var t1 = new SL2().FromByteArray(a); -// var t2 = new SL2().FromByteArray(c); -// var r = SL2.Inv(t1) * t2; -// return r.ToByteArray(); -// } -//} diff --git a/src/FrostFS.SDK.Cryptography/UUID.cs b/src/FrostFS.SDK.Cryptography/UUID.cs deleted file mode 100644 index 656acaf8..00000000 --- a/src/FrostFS.SDK.Cryptography/UUID.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Google.Protobuf; -using System; - -namespace FrostFS.SDK.Cryptography; - -public static class UUIDExtension -{ - public static Guid ToUuid(this ByteString id) - { - return Guid.Parse(BitConverter.ToString(id.ToByteArray()).Replace("-", "")); - } - - public static byte[] ToBytes(this Guid id) - { - var str = id.ToString("N"); - var len = str.Length; - var bytes = new byte[len/2]; - - for (int i = 0; i < len; i += 2) - bytes[i/2] = Convert.ToByte(str.Substring(i, 2), 16); - - return bytes; - } -} diff --git a/src/FrostFS.SDK.Cryptography/WalletExtractor.cs b/src/FrostFS.SDK.Cryptography/WalletExtractor.cs new file mode 100644 index 00000000..3e940f57 --- /dev/null +++ b/src/FrostFS.SDK.Cryptography/WalletExtractor.cs @@ -0,0 +1,98 @@ +using System; +using System.Text; +using System.Linq; +using System.Security.Cryptography; +using Org.BouncyCastle.Crypto.Generators; + +namespace FrostFS.SDK.Cryptography; + +public static class CryptoWallet +{ + private static int N = 16384; + private static int R = 8; + private static int P = 8; + + private enum CipherAction + { + Encrypt, + Decrypt + } + + public static byte[] GetKeyFromEncodedWif(byte[] password, string encryptedWIF) + { + var nep2Data = Base58.Base58CheckDecode(encryptedWIF); + + if (nep2Data.Length == 39 && nep2Data[0] == 1 && nep2Data[1] == 66 && nep2Data[2] == 224) + { + var addressHash = nep2Data.AsSpan(3, 4).ToArray(); + var derivedKey = SCrypt.Generate(password, addressHash, N, R, P, 64); + var derivedKeyHalf1 = derivedKey.Take(32).ToArray(); + var derivedKeyHalf2 = derivedKey.Skip(32).ToArray(); + var encrypted = nep2Data.AsSpan(7, 32).ToArray(); + var decrypted = Aes(encrypted, derivedKeyHalf2, CipherAction.Decrypt); + var plainPrivateKey = XorRange(decrypted, derivedKeyHalf1, 0, decrypted.Length); + + return plainPrivateKey; + } + else + { + throw new ArgumentException("Not valid NEP2 prefix."); + } + } + + public static string EncryptWif(string plainText, ECDsa key) + { + var addressHash = GetAddressHash(key); + var derivedKey = SCrypt.Generate(Encoding.UTF8.GetBytes(plainText), addressHash, N, R, P, 64); + var derivedHalf1 = derivedKey.Take(32).ToArray(); + var derivedHalf2 = derivedKey.Skip(32).ToArray(); + var encryptedHalf1 = Aes(XorRange(key.PrivateKey(), derivedHalf1, 0, 16), derivedHalf2, CipherAction.Encrypt); + var encryptedHalf2 = Aes(XorRange(key.PrivateKey(), derivedHalf1, 16, 32), derivedHalf2, CipherAction.Encrypt); + var prefixes = new byte[] { 1, 66, 224 }; + var concatenation = ArrayHelper.Concat([prefixes, addressHash, encryptedHalf1, encryptedHalf2]); + + return Base58.Base58CheckEncode(concatenation); + } + + public static byte[] GetAddressHash(ECDsa key) + { + string address = key.PublicKey().PublicKeyToAddress(); + + using SHA256 sha256 = SHA256.Create(); + byte[] addressHashed = sha256.ComputeHash(Encoding.UTF8.GetBytes(address)); + return [.. addressHashed.Take(4)]; + } + + private static byte[] XorRange(byte[] arr1, byte[] arr2, int from, int length) + { + byte[] result = new byte[length]; + + var j = 0; + for (var i = from; i < length; ++i) + { + result[j++] = (byte)(arr1[i] ^ arr2[i]); + } + + return result; + } + + private static byte[] Aes(byte[] data, byte[] key, CipherAction action) + { + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + + if (action == CipherAction.Encrypt) + { + return aes.CreateEncryptor().TransformFinalBlock(data, 0, data.Length); + } + + if (action == CipherAction.Decrypt) + { + return aes.CreateDecryptor().TransformFinalBlock(data, 0, data.Length); + } + + throw new ArgumentException("Wrong cippher action", nameof(action)); + } +} diff --git a/src/FrostFS.SDK.ModelsV2/Constants.cs b/src/FrostFS.SDK.ModelsV2/Constants.cs deleted file mode 100644 index eeb2b677..00000000 --- a/src/FrostFS.SDK.ModelsV2/Constants.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FrostFS.SDK.ModelsV2; - -public class Constants -{ - public const int ObjectChunkSize = 3 * (1 << 20); - public const int Sha256HashLength = 32; -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Container.cs b/src/FrostFS.SDK.ModelsV2/Container.cs deleted file mode 100644 index d32ff8f7..00000000 --- a/src/FrostFS.SDK.ModelsV2/Container.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; - -using FrostFS.SDK.ModelsV2.Enums; -using FrostFS.SDK.ModelsV2.Netmap; - -namespace FrostFS.SDK.ModelsV2; - -public class Container -{ - public Guid Nonce { get; set; } - public BasicAcl BasicAcl { get; set; } - public PlacementPolicy PlacementPolicy { get; set; } - public Version Version { get; set; } - - public Container(BasicAcl basicAcl, PlacementPolicy placementPolicy) - { - Nonce = Guid.NewGuid(); - BasicAcl = basicAcl; - PlacementPolicy = placementPolicy; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/ContainerId.cs b/src/FrostFS.SDK.ModelsV2/ContainerId.cs deleted file mode 100644 index 8bfbc54c..00000000 --- a/src/FrostFS.SDK.ModelsV2/ContainerId.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -using FrostFS.SDK.Cryptography; - -namespace FrostFS.SDK.ModelsV2; - -public class ContainerId -{ - public string Value { get; set; } - - public ContainerId(string id) - { - Value = id; - } - - public static ContainerId FromHash(byte[] hash) - { - if (hash.Length != Constants.Sha256HashLength) - throw new FormatException("ContainerID must be a sha256 hash."); - - return new ContainerId(Base58.Encode(hash)); - } - - public byte[] ToHash() - { - return Base58.Decode(Value); - } - - public override string ToString() - { - return Value; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Enums/BasicAcl.cs b/src/FrostFS.SDK.ModelsV2/Enums/BasicAcl.cs deleted file mode 100644 index 5c222fc2..00000000 --- a/src/FrostFS.SDK.ModelsV2/Enums/BasicAcl.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.ComponentModel; - -namespace FrostFS.SDK.ModelsV2.Enums; - -public enum BasicAcl -{ - [Description("Basic ACL for private container")] - Private = 0x1C8C8CCC, - - [Description("Basic ACL for public RO container")] - PublicRO = 0x1FBF8CFF, - - [Description("Basic ACL for public RW container")] - PublicRW = 0x1FBFBFFF, - - [Description("Basic ACL for public append container")] - PublicAppend = 0x1FBF9FFF, -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Enums/ObjectType.cs b/src/FrostFS.SDK.ModelsV2/Enums/ObjectType.cs deleted file mode 100644 index 6f8a905f..00000000 --- a/src/FrostFS.SDK.ModelsV2/Enums/ObjectType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace FrostFS.SDK.ModelsV2.Enums; - -public enum ObjectType -{ - Regular = 0, - Tombstone = 1, - Lock = 3 -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj b/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj deleted file mode 100644 index 96a477c5..00000000 --- a/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - netstandard2.0 - 12.0 - enable - - - - - - - diff --git a/src/FrostFS.SDK.ModelsV2/MetaHeader.cs b/src/FrostFS.SDK.ModelsV2/MetaHeader.cs deleted file mode 100644 index 68abbb38..00000000 --- a/src/FrostFS.SDK.ModelsV2/MetaHeader.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace FrostFS.SDK.ModelsV2; - -public class MetaHeader -{ - public Version Version { get; set; } - public int Epoch { get; set; } - public int Ttl { get; set; } - - public MetaHeader(Version version, int epoch, int ttl) - { - Version = version; - Epoch = epoch; - Ttl = ttl; - } - - public static MetaHeader Default() - { - return new MetaHeader( - new Version( - major: 2, - minor: 13 - ), - epoch: 0, - ttl: 2 - ); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Netmap/NodeInfo.cs b/src/FrostFS.SDK.ModelsV2/Netmap/NodeInfo.cs deleted file mode 100644 index 7e86912b..00000000 --- a/src/FrostFS.SDK.ModelsV2/Netmap/NodeInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -using FrostFS.SDK.ModelsV2.Enums; - -namespace FrostFS.SDK.ModelsV2.Netmap; - -public class NodeInfo -{ - public NodeState State { get; set; } - public Version Version { get; set; } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Netmap/PlacementPolicy.cs b/src/FrostFS.SDK.ModelsV2/Netmap/PlacementPolicy.cs deleted file mode 100644 index b638bb63..00000000 --- a/src/FrostFS.SDK.ModelsV2/Netmap/PlacementPolicy.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace FrostFS.SDK.ModelsV2.Netmap; - -public class PlacementPolicy -{ - public Replica[] Replicas { get; private set; } - public bool Unique { get; private set; } - - public PlacementPolicy(bool unique, params Replica[] replicas) - { - Replicas = replicas; - Unique = unique; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Netmap/Replica.cs b/src/FrostFS.SDK.ModelsV2/Netmap/Replica.cs deleted file mode 100644 index a6aeac9b..00000000 --- a/src/FrostFS.SDK.ModelsV2/Netmap/Replica.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace FrostFS.SDK.ModelsV2.Netmap; - -public class Replica -{ - public int Count { get; set; } - public string Selector { get; set; } - - public Replica(int count, string? selector = null) - { - selector ??= string.Empty; - - Count = count; - Selector = selector; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Object.cs b/src/FrostFS.SDK.ModelsV2/Object.cs deleted file mode 100644 index 7db6289c..00000000 --- a/src/FrostFS.SDK.ModelsV2/Object.cs +++ /dev/null @@ -1,77 +0,0 @@ -using FrostFS.SDK.ModelsV2.Enums; - -namespace FrostFS.SDK.ModelsV2; - -public class ObjectAttribute -{ - public string Key { get; set; } - public string Value { get; set; } - - public ObjectAttribute(string key, string value) - { - Key = key; - Value = value; - } -} - -public class ObjectFilter -{ - private const string HeaderPrefix = "$Object:"; - public ObjectMatchType MatchType { get; set; } - public string Key { get; set; } - public string Value { get; set; } - - public ObjectFilter(ObjectMatchType matchType, string key, string value) - { - MatchType = matchType; - Key = key; - Value = value; - } - - public static ObjectFilter ObjectIdFilter(ObjectMatchType matchType, ObjectId objectId) - { - return new ObjectFilter(matchType, HeaderPrefix + "objectID", objectId.Value); - } - - public static ObjectFilter OwnerFilter(ObjectMatchType matchType, OwnerId ownerId) - { - return new ObjectFilter(matchType, HeaderPrefix + "ownerID", ownerId.Value); - } - - public static ObjectFilter RootFilter() - { - return new ObjectFilter(ObjectMatchType.Unspecified, HeaderPrefix + "ROOT", ""); - } - - public static ObjectFilter VersionFilter(ObjectMatchType matchType, Version version) - { - return new ObjectFilter(matchType, HeaderPrefix + "version", version.ToString()); - } -} - -public class ObjectHeader -{ - public ObjectAttribute[] Attributes { get; set; } - public ContainerId ContainerId { get; set; } - public long Size { get; set; } - public ObjectType ObjectType { get; set; } - public Version Version { get; set; } - - public ObjectHeader( - ContainerId containerId, - ObjectType type = ObjectType.Regular, - params ObjectAttribute[] attributes - ) - { - Attributes = attributes; - ContainerId = containerId; - ObjectType = type; - } -} - -public class Object -{ - public ObjectHeader Header { get; set; } - public ObjectId ObjectId { get; set; } - public byte[] Payload { get; set; } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/OwnerId.cs b/src/FrostFS.SDK.ModelsV2/OwnerId.cs deleted file mode 100644 index 85cf2391..00000000 --- a/src/FrostFS.SDK.ModelsV2/OwnerId.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Security.Cryptography; - -using FrostFS.SDK.Cryptography; - -namespace FrostFS.SDK.ModelsV2; - -public class OwnerId -{ - public string Value { get; } - - public OwnerId(string id) - { - Value = id; - } - - public static OwnerId FromKey(ECDsa key) - { - return new OwnerId(key.PublicKey().PublicKeyToAddress()); - } - - public byte[] ToHash() - { - return Base58.Decode(Value); - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Status.cs b/src/FrostFS.SDK.ModelsV2/Status.cs deleted file mode 100644 index 2d564418..00000000 --- a/src/FrostFS.SDK.ModelsV2/Status.cs +++ /dev/null @@ -1,25 +0,0 @@ -using FrostFS.SDK.ModelsV2.Enums; - -namespace FrostFS.SDK.ModelsV2; - -public class Status -{ - public StatusCode Code { get; set; } - public string Message { get; set; } - - public Status(StatusCode code, string? message = null) - { - Code = code; - Message = message ?? string.Empty; - } - - public bool IsSuccess() - { - return Code == StatusCode.Success; - } - - public override string ToString() - { - return $"Response status: {Code}. Message: {Message}."; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/Version.cs b/src/FrostFS.SDK.ModelsV2/Version.cs deleted file mode 100644 index 3d852fb6..00000000 --- a/src/FrostFS.SDK.ModelsV2/Version.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace FrostFS.SDK.ModelsV2; - -public class Version -{ - public int Major { get; set; } - public int Minor { get; set; } - - public Version(int major, int minor) - { - Major = major; - Minor = minor; - } - - public bool IsSupported(Version version) - { - return Major == version.Major; - } - - public override string ToString() - { - return $"v{Major}.{Minor}"; - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.Protos/AssemblyInfo.cs b/src/FrostFS.SDK.Protos/AssemblyInfo.cs new file mode 100644 index 00000000..d7fab057 --- /dev/null +++ b/src/FrostFS.SDK.Protos/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; + +[assembly: AssemblyCompany("TrueCloudLab")] +[assembly: AssemblyFileVersion("1.0.7.0")] +[assembly: AssemblyProduct("FrostFS.SDK.Protos")] +[assembly: AssemblyTitle("FrostFS.SDK.Protos")] +[assembly: AssemblyVersion("1.0.7.0")] diff --git a/src/FrostFS.SDK.ProtosV2/FrostFS.SDK.ProtosV2.csproj b/src/FrostFS.SDK.Protos/FrostFS.SDK.Protos.csproj similarity index 53% rename from src/FrostFS.SDK.ProtosV2/FrostFS.SDK.ProtosV2.csproj rename to src/FrostFS.SDK.Protos/FrostFS.SDK.Protos.csproj index 746a8b49..222acd19 100644 --- a/src/FrostFS.SDK.ProtosV2/FrostFS.SDK.ProtosV2.csproj +++ b/src/FrostFS.SDK.Protos/FrostFS.SDK.Protos.csproj @@ -4,21 +4,45 @@ netstandard2.0 12.0 enable + FrostFS.SDK.Protos + 1.0.7 + + Protobuf client for C# SDK + + true + + + + true + + + + <_SkipUpgradeNetAnalyzersNuGetWarning>true + + + + true + + + + false + True + .\\..\\..\\keyfile.snk - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/FrostFS.SDK.ProtosV2/Interfaces/IMetaHeader.cs b/src/FrostFS.SDK.Protos/Interfaces/IMetaHeader.cs similarity index 100% rename from src/FrostFS.SDK.ProtosV2/Interfaces/IMetaHeader.cs rename to src/FrostFS.SDK.Protos/Interfaces/IMetaHeader.cs diff --git a/src/FrostFS.SDK.Protos/Interfaces/IRequest.cs b/src/FrostFS.SDK.Protos/Interfaces/IRequest.cs new file mode 100644 index 00000000..a3a269ff --- /dev/null +++ b/src/FrostFS.SDK.Protos/Interfaces/IRequest.cs @@ -0,0 +1,9 @@ +using FrostFS.Session; + +namespace FrostFS.SDK.Proto.Interfaces; + +public interface IRequest : IVerifiableMessage +{ + RequestMetaHeader MetaHeader { get; set; } + RequestVerificationHeader VerifyHeader { get; set; } +} diff --git a/src/FrostFS.SDK.ProtosV2/Interfaces/IResponse.cs b/src/FrostFS.SDK.Protos/Interfaces/IResponse.cs similarity index 73% rename from src/FrostFS.SDK.ProtosV2/Interfaces/IResponse.cs rename to src/FrostFS.SDK.Protos/Interfaces/IResponse.cs index 609be8e3..ac6382ae 100644 --- a/src/FrostFS.SDK.ProtosV2/Interfaces/IResponse.cs +++ b/src/FrostFS.SDK.Protos/Interfaces/IResponse.cs @@ -1,6 +1,6 @@ namespace FrostFS.Session; -public interface IResponse : IVerificableMessage +public interface IResponse : IVerifiableMessage { ResponseMetaHeader MetaHeader { get; set; } ResponseVerificationHeader VerifyHeader { get; set; } diff --git a/src/FrostFS.SDK.ProtosV2/Interfaces/IVerifiableMessage.cs b/src/FrostFS.SDK.Protos/Interfaces/IVerifiableMessage.cs similarity index 85% rename from src/FrostFS.SDK.ProtosV2/Interfaces/IVerifiableMessage.cs rename to src/FrostFS.SDK.Protos/Interfaces/IVerifiableMessage.cs index 39037578..57fce64f 100644 --- a/src/FrostFS.SDK.ProtosV2/Interfaces/IVerifiableMessage.cs +++ b/src/FrostFS.SDK.Protos/Interfaces/IVerifiableMessage.cs @@ -2,7 +2,7 @@ using Google.Protobuf; namespace FrostFS.Session; -public interface IVerificableMessage : IMessage +public interface IVerifiableMessage : IMessage { IMetaHeader GetMetaHeader(); void SetMetaHeader(IMetaHeader metaHeader); diff --git a/src/FrostFS.SDK.ProtosV2/Interfaces/IVerificationHeader.cs b/src/FrostFS.SDK.Protos/Interfaces/IVerificationHeader.cs similarity index 99% rename from src/FrostFS.SDK.ProtosV2/Interfaces/IVerificationHeader.cs rename to src/FrostFS.SDK.Protos/Interfaces/IVerificationHeader.cs index 6a637c3e..32ba5d41 100644 --- a/src/FrostFS.SDK.ProtosV2/Interfaces/IVerificationHeader.cs +++ b/src/FrostFS.SDK.Protos/Interfaces/IVerificationHeader.cs @@ -1,4 +1,5 @@ using FrostFS.Refs; + using Google.Protobuf; namespace FrostFS.Session; diff --git a/src/FrostFS.SDK.Protos/accounting/Extension.Message.cs b/src/FrostFS.SDK.Protos/accounting/Extension.Message.cs new file mode 100644 index 00000000..82b7c78e --- /dev/null +++ b/src/FrostFS.SDK.Protos/accounting/Extension.Message.cs @@ -0,0 +1,62 @@ +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.Accounting; + +public partial class BalanceRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class BalanceResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ProtosV2/accounting/service.proto b/src/FrostFS.SDK.Protos/accounting/service.proto similarity index 79% rename from src/FrostFS.SDK.ProtosV2/accounting/service.proto rename to src/FrostFS.SDK.Protos/accounting/service.proto index 715ef63e..6049e0f5 100644 --- a/src/FrostFS.SDK.ProtosV2/accounting/service.proto +++ b/src/FrostFS.SDK.Protos/accounting/service.proto @@ -9,13 +9,13 @@ import "accounting/types.proto"; import "refs/types.proto"; import "session/types.proto"; -// Accounting service provides methods for interaction with NeoFS sidechain via -// other NeoFS nodes to get information about the account balance. Deposit and -// Withdraw operations can't be implemented here, as they require Mainnet NeoFS -// smart contract invocation. Transfer operations between internal NeoFS -// accounts are possible if both use the same token type. +// Accounting service provides methods for interaction with FrostFS sidechain +// via other FrostFS nodes to get information about the account balance. Deposit +// and Withdraw operations can't be implemented here, as they require Mainnet +// FrostFS smart contract invocation. Transfer operations between internal +// FrostFS accounts are possible if both use the same token type. service AccountingService { - // Returns the amount of funds in GAS token for the requested NeoFS account. + // Returns the amount of funds in GAS token for the requested FrostFS account. // // Statuses: // - **OK** (0, SECTION_SUCCESS): @@ -27,9 +27,9 @@ service AccountingService { // BalanceRequest message message BalanceRequest { // To indicate the account for which the balance is requested, its identifier - // is used. It can be any existing account in NeoFS sidechain `Balance` smart - // contract. If omitted, client implementation MUST set it to the request's - // signer `OwnerID`. + // is used. It can be any existing account in FrostFS sidechain `Balance` + // smart contract. If omitted, client implementation MUST set it to the + // request's signer `OwnerID`. message Body { // Valid user identifier in `OwnerID` format for which the balance is // requested. Required field. diff --git a/src/FrostFS.SDK.ProtosV2/accounting/types.proto b/src/FrostFS.SDK.Protos/accounting/types.proto similarity index 100% rename from src/FrostFS.SDK.ProtosV2/accounting/types.proto rename to src/FrostFS.SDK.Protos/accounting/types.proto diff --git a/src/FrostFS.SDK.ProtosV2/acl/types.proto b/src/FrostFS.SDK.Protos/acl/types.proto similarity index 86% rename from src/FrostFS.SDK.ProtosV2/acl/types.proto rename to src/FrostFS.SDK.Protos/acl/types.proto index 186f08fa..a1d9ae27 100644 --- a/src/FrostFS.SDK.ProtosV2/acl/types.proto +++ b/src/FrostFS.SDK.Protos/acl/types.proto @@ -6,6 +6,7 @@ option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl/grpc;ac option csharp_namespace = "FrostFS.Acl"; import "refs/types.proto"; +import "ape/types.proto"; // Target role of the access control rule in access control list. enum Role { @@ -88,14 +89,14 @@ enum HeaderType { // Filter object headers OBJECT = 2; - // Filter service headers. These are not processed by NeoFS nodes and + // Filter service headers. These are not processed by FrostFS nodes and // exist for service use only. SERVICE = 3; } // Describes a single eACL rule. message EACLRecord { - // NeoFS request Verb to match + // FrostFS request Verb to match Operation operation = 1 [ json_name = "operation" ]; // Rule execution result. Either allows or denies access if filters match. @@ -164,7 +165,7 @@ message EACLRecord { // Extended ACL rules table. A list of ACL rules defined additionally to Basic // ACL. Extended ACL rules can be attached to a container and can be updated // or may be defined in `BearerToken` structure. Please see the corresponding -// NeoFS Technical Specification section for detailed description. +// FrostFS Technical Specification section for detailed description. message EACLTable { // eACL format version. Effectively, the version of API library used to create // eACL Table. @@ -194,6 +195,9 @@ message BearerToken { // container. If it contains `container_id` field, bearer token is only // valid for this specific container. Otherwise, any container of the same // owner is allowed. + // + // Deprecated: eACL tables are no longer relevant - `APEOverrides` should be + // used instead. EACLTable eacl_table = 1 [ json_name = "eaclTable" ]; // `OwnerID` defines to whom the token was issued. It must match the request @@ -218,6 +222,24 @@ message BearerToken { // AllowImpersonate flag to consider token signer as request owner. // If this field is true extended ACL table in token body isn't processed. bool allow_impersonate = 4 [ json_name = "allowImpersonate" ]; + + // APEOverride is the list of APE chains defined for a target. + // These chains are meant to serve as overrides to the already defined (or + // even undefined) APE chains for the target (see contract `Policy`). + // + // The server-side processing of the bearer token with set APE overrides + // must verify if a client is permitted to override chains for the target, + // preventing unauthorized access through the APE mechanism. + message APEOverride { + // Target for which chains are applied. + frostfs.v2.ape.ChainTarget target = 1 [ json_name = "target" ]; + + // The list of APE chains. + repeated frostfs.v2.ape.Chain chains = 2 [ json_name = "chains" ]; + } + + // APE override for the target. + APEOverride ape_override = 5 [ json_name = "apeOverride" ]; } // Bearer Token body Body body = 1 [ json_name = "body" ]; diff --git a/src/FrostFS.SDK.ProtosV2/apemanager/types.proto b/src/FrostFS.SDK.Protos/ape/types.proto similarity index 86% rename from src/FrostFS.SDK.ProtosV2/apemanager/types.proto rename to src/FrostFS.SDK.Protos/ape/types.proto index c0646270..71468c38 100644 --- a/src/FrostFS.SDK.ProtosV2/apemanager/types.proto +++ b/src/FrostFS.SDK.Protos/ape/types.proto @@ -1,8 +1,8 @@ -syntax = "proto3"; +syntax = "proto3"; -package frostfs.v2.apemanager; +package frostfs.v2.ape; -option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager/grpc;apemanager"; +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape/grpc;ape"; // TargetType is a type target to which a rule chain is defined. enum TargetType { diff --git a/src/FrostFS.SDK.Protos/apemanager/Extension.Message.cs b/src/FrostFS.SDK.Protos/apemanager/Extension.Message.cs new file mode 100644 index 00000000..53b7fdb2 --- /dev/null +++ b/src/FrostFS.SDK.Protos/apemanager/Extension.Message.cs @@ -0,0 +1,174 @@ +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace Frostfs.V2.Apemanager; + +public partial class AddChainRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class AddChainResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class RemoveChainRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class RemoveChainResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class ListChainsRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class ListChainsResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} diff --git a/src/FrostFS.SDK.ProtosV2/apemanager/service.proto b/src/FrostFS.SDK.Protos/apemanager/service.proto similarity index 95% rename from src/FrostFS.SDK.ProtosV2/apemanager/service.proto rename to src/FrostFS.SDK.Protos/apemanager/service.proto index 6b9da608..166ba4dd 100644 --- a/src/FrostFS.SDK.ProtosV2/apemanager/service.proto +++ b/src/FrostFS.SDK.Protos/apemanager/service.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package frostfs.v2.apemanager; -import "apemanager/types.proto"; +import "ape/types.proto"; import "session/types.proto"; option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager/grpc;apemanager"; @@ -52,10 +52,10 @@ service APEManagerService { message AddChainRequest { message Body { // A target for which a rule chain is added. - ChainTarget target = 1; + frostfs.v2.ape.ChainTarget target = 1; // The chain to set for the target. - Chain chain = 2; + frostfs.v2.ape.Chain chain = 2; } // The request's body. @@ -95,7 +95,7 @@ message AddChainResponse { message RemoveChainRequest { message Body { // Target for which a rule chain is removed. - ChainTarget target = 1; + frostfs.v2.ape.ChainTarget target = 1; // Chain ID assigned for the rule chain. bytes chain_id = 2; @@ -135,7 +135,7 @@ message RemoveChainResponse { message ListChainsRequest { message Body { // Target for which rule chains are listed. - ChainTarget target = 1; + frostfs.v2.ape.ChainTarget target = 1; } // The request's body. @@ -154,7 +154,7 @@ message ListChainsRequest { message ListChainsResponse { message Body { // The list of chains defined for the reqeusted target. - repeated Chain chains = 1; + repeated frostfs.v2.ape.Chain chains = 1; } // The response's body. @@ -168,4 +168,4 @@ message ListChainsResponse { // authenticate the nodes of the message route and check the correctness of // transmission. neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; -} \ No newline at end of file +} diff --git a/src/FrostFS.SDK.Protos/container/Extension.Message.cs b/src/FrostFS.SDK.Protos/container/Extension.Message.cs new file mode 100644 index 00000000..7cb69a8f --- /dev/null +++ b/src/FrostFS.SDK.Protos/container/Extension.Message.cs @@ -0,0 +1,230 @@ +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.Container; + +public partial class GetRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class GetResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class PutRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class PutResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class DeleteRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class DeleteResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class ListRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class ListResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} diff --git a/src/FrostFS.SDK.ProtosV2/container/service.proto b/src/FrostFS.SDK.Protos/container/service.proto similarity index 55% rename from src/FrostFS.SDK.ProtosV2/container/service.proto rename to src/FrostFS.SDK.Protos/container/service.proto index 4a5cc028..abf3e9d7 100644 --- a/src/FrostFS.SDK.ProtosV2/container/service.proto +++ b/src/FrostFS.SDK.Protos/container/service.proto @@ -11,8 +11,8 @@ import "refs/types.proto"; import "session/types.proto"; // `ContainerService` provides API to interact with `Container` smart contract -// in NeoFS sidechain via other NeoFS nodes. All of those actions can be done -// equivalently by directly issuing transactions and RPC calls to sidechain +// in FrostFS sidechain via other FrostFS nodes. All of those actions can be +// done equivalently by directly issuing transactions and RPC calls to sidechain // nodes. service ContainerService { // `Put` invokes `Container` smart contract's `Put` method and returns @@ -25,7 +25,7 @@ service ContainerService { // request to save the container has been sent to the sidechain; // - Common failures (SECTION_FAILURE_COMMON); // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ - // container create access denied. + // container create access denied. rpc Put(PutRequest) returns (PutResponse); // `Delete` invokes `Container` smart contract's `Delete` method and returns @@ -38,7 +38,7 @@ service ContainerService { // request to remove the container has been sent to the sidechain; // - Common failures (SECTION_FAILURE_COMMON); // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ - // container delete access denied. + // container delete access denied. rpc Delete(DeleteRequest) returns (DeleteResponse); // Returns container structure from `Container` smart contract storage. @@ -50,7 +50,7 @@ service ContainerService { // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ // requested container not found; // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ - // access to container is denied. + // access to container is denied. rpc Get(GetRequest) returns (GetResponse); // Returns all owner's containers from 'Container` smart contract' storage. @@ -60,47 +60,11 @@ service ContainerService { // container list has been successfully read; // - Common failures (SECTION_FAILURE_COMMON); // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ - // container list access denied. + // container list access denied. rpc List(ListRequest) returns (ListResponse); - - // Invokes 'SetEACL' method of 'Container` smart contract and returns response - // immediately. After one more block in sidechain, changes in an Extended ACL - // are added into smart contract storage. - // - // Statuses: - // - **OK** (0, SECTION_SUCCESS): \ - // request to save container eACL has been sent to the sidechain; - // - Common failures (SECTION_FAILURE_COMMON); - // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ - // set container eACL access denied. - rpc SetExtendedACL(SetExtendedACLRequest) returns (SetExtendedACLResponse); - - // Returns Extended ACL table and signature from `Container` smart contract - // storage. - // - // Statuses: - // - **OK** (0, SECTION_SUCCESS): \ - // container eACL has been successfully read; - // - Common failures (SECTION_FAILURE_COMMON); - // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ - // container not found; - // - **EACL_NOT_FOUND** (3073, SECTION_CONTAINER): \ - // eACL table not found; - // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ - // access to container eACL is denied. - rpc GetExtendedACL(GetExtendedACLRequest) returns (GetExtendedACLResponse); - - // Announces the space values used by the container for P2P synchronization. - // - // Statuses: - // - **OK** (0, SECTION_SUCCESS): \ - // estimation of used space has been successfully announced; - // - Common failures (SECTION_FAILURE_COMMON). - rpc AnnounceUsedSpace(AnnounceUsedSpaceRequest) - returns (AnnounceUsedSpaceResponse); } -// New NeoFS Container creation request +// New FrostFS Container creation request message PutRequest { // Container creation request has container structure's signature as a // separate field. It's not stored in sidechain, just verified on container @@ -108,7 +72,7 @@ message PutRequest { // the stable-marshalled container strucutre, hence there is no need for // additional signature checks. message Body { - // Container structure to register in NeoFS + // Container structure to register in FrostFS container.Container container = 1; // Signature of a stable-marshalled container according to RFC-6979. @@ -127,7 +91,7 @@ message PutRequest { neo.fs.v2.session.RequestVerificationHeader verify_header = 3; } -// New NeoFS Container creation response +// New FrostFS Container creation response message PutResponse { // Container put response body contains information about the newly registered // container as seen by `Container` smart contract. `ContainerID` can be @@ -156,7 +120,7 @@ message DeleteRequest { // the container owner's intent. The signature will be verified by `Container` // smart contract, so signing algorithm must be supported by NeoVM. message Body { - // Identifier of the container to delete from NeoFS + // Identifier of the container to delete from FrostFS neo.fs.v2.refs.ContainerID container_id = 1; // `ContainerID` signed with the container owner's key according to @@ -282,150 +246,3 @@ message ListResponse { // transmission. neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; } - -// Set Extended ACL -message SetExtendedACLRequest { - // Set Extended ACL request body does not have separate `ContainerID` - // reference. It will be taken from `EACLTable.container_id` field. - message Body { - // Extended ACL table to set for the container - neo.fs.v2.acl.EACLTable eacl = 1; - - // Signature of stable-marshalled Extended ACL table according to RFC-6979. - neo.fs.v2.refs.SignatureRFC6979 signature = 2; - } - // Body of set extended acl request message. - Body body = 1; - - // Carries request meta information. Header data is used only to regulate - // message transport and does not affect request execution. - neo.fs.v2.session.RequestMetaHeader meta_header = 2; - - // Carries request verification information. This header is used to - // authenticate the nodes of the message route and check the correctness of - // transmission. - neo.fs.v2.session.RequestVerificationHeader verify_header = 3; -} - -// Set Extended ACL -message SetExtendedACLResponse { - // `SetExtendedACLResponse` has an empty body because the operation is - // asynchronous and the update should be reflected in `Container` smart - // contract's storage after next block is issued in sidechain. - message Body {} - - // Body of set extended acl response message. - Body body = 1; - - // Carries response meta information. Header data is used only to regulate - // message transport and does not affect request execution. - neo.fs.v2.session.ResponseMetaHeader meta_header = 2; - - // Carries response verification information. This header is used to - // authenticate the nodes of the message route and check the correctness of - // transmission. - neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; -} - -// Get Extended ACL -message GetExtendedACLRequest { - // Get Extended ACL request body - message Body { - // Identifier of the container having Extended ACL - neo.fs.v2.refs.ContainerID container_id = 1; - } - - // Body of get extended acl request message. - Body body = 1; - - // Carries request meta information. Header data is used only to regulate - // message transport and does not affect request execution. - neo.fs.v2.session.RequestMetaHeader meta_header = 2; - - // Carries request verification information. This header is used to - // authenticate the nodes of the message route and check the correctness of - // transmission. - neo.fs.v2.session.RequestVerificationHeader verify_header = 3; -} - -// Get Extended ACL -message GetExtendedACLResponse { - // Get Extended ACL Response body can be empty if the requested container does - // not have Extended ACL Table attached or Extended ACL has not been allowed - // at the time of container creation. - message Body { - // Extended ACL requested, if available - neo.fs.v2.acl.EACLTable eacl = 1; - - // Signature of stable-marshalled Extended ACL according to RFC-6979. - neo.fs.v2.refs.SignatureRFC6979 signature = 2; - - // Session token if Extended ACL was set within a session - neo.fs.v2.session.SessionToken session_token = 3; - } - // Body of get extended acl response message. - Body body = 1; - - // Carries response meta information. Header data is used only to regulate - // message transport and does not affect request execution. - neo.fs.v2.session.ResponseMetaHeader meta_header = 2; - - // Carries response verification information. This header is used to - // authenticate the nodes of the message route and check the correctness of - // transmission. - neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; -} - -// Announce container used space -message AnnounceUsedSpaceRequest { - // Container used space announcement body. - message Body { - // Announcement contains used space information for a single container. - message Announcement { - // Epoch number for which the container size estimation was produced. - uint64 epoch = 1; - - // Identifier of the container. - neo.fs.v2.refs.ContainerID container_id = 2; - - // Used space is a sum of object payload sizes of a specified - // container, stored in the node. It must not include inhumed objects. - uint64 used_space = 3; - } - - // List of announcements. If nodes share several containers, - // announcements are transferred in a batch. - repeated Announcement announcements = 1; - } - - // Body of announce used space request message. - Body body = 1; - - // Carries request meta information. Header data is used only to regulate - // message transport and does not affect request execution. - neo.fs.v2.session.RequestMetaHeader meta_header = 2; - - // Carries request verification information. This header is used to - // authenticate the nodes of the message route and check the correctness of - // transmission. - neo.fs.v2.session.RequestVerificationHeader verify_header = 3; -} - -// Announce container used space -message AnnounceUsedSpaceResponse { - // `AnnounceUsedSpaceResponse` has an empty body because announcements are - // one way communication. - message Body {} - - // Body of announce used space response message. - Body body = 1; - - // Carries response meta information. Header data is used only to regulate - // message transport and does not affect request execution. - neo.fs.v2.session.ResponseMetaHeader meta_header = 2; - - // Carries response verification information. This header is used to - // authenticate the nodes of the message route and check the correctness of - // transmission. - neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; -} diff --git a/src/FrostFS.SDK.ProtosV2/container/types.proto b/src/FrostFS.SDK.Protos/container/types.proto similarity index 97% rename from src/FrostFS.SDK.ProtosV2/container/types.proto rename to src/FrostFS.SDK.Protos/container/types.proto index 075081a1..c657d1c6 100644 --- a/src/FrostFS.SDK.ProtosV2/container/types.proto +++ b/src/FrostFS.SDK.Protos/container/types.proto @@ -50,7 +50,7 @@ message Container { // (`__NEOFS__DISABLE_HOMOMORPHIC_HASHING` is deprecated) \ // Disables homomorphic hashing for the container if the value equals "true" // string. Any other values are interpreted as missing attribute. Container - // could be accepted in a NeoFS network only if the global network hashing + // could be accepted in a FrostFS network only if the global network hashing // configuration value corresponds with that attribute's value. After // container inclusion, network setting is ignored. // diff --git a/src/FrostFS.SDK.ProtosV2/lock/types.proto b/src/FrostFS.SDK.Protos/lock/types.proto similarity index 100% rename from src/FrostFS.SDK.ProtosV2/lock/types.proto rename to src/FrostFS.SDK.Protos/lock/types.proto diff --git a/src/FrostFS.SDK.Protos/netmap/Extension.Message.cs b/src/FrostFS.SDK.Protos/netmap/Extension.Message.cs new file mode 100644 index 00000000..8579baf9 --- /dev/null +++ b/src/FrostFS.SDK.Protos/netmap/Extension.Message.cs @@ -0,0 +1,175 @@ +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.Netmap; + +public partial class LocalNodeInfoRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class LocalNodeInfoResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class NetworkInfoRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class NetworkInfoResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + + +public partial class NetmapSnapshotRequest : IRequest +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} + +public partial class NetmapSnapshotResponse : IResponse +{ + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ProtosV2/netmap/service.proto b/src/FrostFS.SDK.Protos/netmap/service.proto similarity index 96% rename from src/FrostFS.SDK.ProtosV2/netmap/service.proto rename to src/FrostFS.SDK.Protos/netmap/service.proto index 8611d9a1..11f8f969 100644 --- a/src/FrostFS.SDK.ProtosV2/netmap/service.proto +++ b/src/FrostFS.SDK.Protos/netmap/service.proto @@ -12,7 +12,7 @@ import "session/types.proto"; // `NetmapService` provides methods to work with `Network Map` and the // information required to build it. The resulting `Network Map` is stored in // sidechain `Netmap` smart contract, while related information can be obtained -// from other NeoFS nodes. +// from other FrostFS nodes. service NetmapService { // Get NodeInfo structure from the particular node directly. // Node information can be taken from `Netmap` smart contract. In some cases, @@ -27,7 +27,7 @@ service NetmapService { // - Common failures (SECTION_FAILURE_COMMON). rpc LocalNodeInfo(LocalNodeInfoRequest) returns (LocalNodeInfoResponse); - // Read recent information about the NeoFS network. + // Read recent information about the FrostFS network. // // Statuses: // - **OK** (0, SECTION_SUCCESS): @@ -35,7 +35,7 @@ service NetmapService { // - Common failures (SECTION_FAILURE_COMMON). rpc NetworkInfo(NetworkInfoRequest) returns (NetworkInfoResponse); - // Returns network map snapshot of the current NeoFS epoch. + // Returns network map snapshot of the current FrostFS epoch. // // Statuses: // - **OK** (0, SECTION_SUCCESS): @@ -65,7 +65,7 @@ message LocalNodeInfoRequest { message LocalNodeInfoResponse { // Local Node Info, including API Version in use. message Body { - // Latest NeoFS API version in use + // Latest FrostFS API version in use neo.fs.v2.refs.Version version = 1; // NodeInfo structure with recent information from node itself diff --git a/src/FrostFS.SDK.ProtosV2/netmap/types.proto b/src/FrostFS.SDK.Protos/netmap/types.proto similarity index 82% rename from src/FrostFS.SDK.ProtosV2/netmap/types.proto rename to src/FrostFS.SDK.Protos/netmap/types.proto index baaca041..5f0e93e9 100644 --- a/src/FrostFS.SDK.ProtosV2/netmap/types.proto +++ b/src/FrostFS.SDK.Protos/netmap/types.proto @@ -36,6 +36,9 @@ enum Operation { // Logical negation NOT = 9; + + // Matches pattern + LIKE = 10; } // Selector modifier shows how the node set will be formed. By default selector @@ -119,7 +122,7 @@ message PlacementPolicy { // bucket repeated Replica replicas = 1 [ json_name = "replicas" ]; - // Container backup factor controls how deep NeoFS will search for nodes + // Container backup factor controls how deep FrostFS will search for nodes // alternatives to include into container's nodes subset uint32 container_backup_factor = 2 [ json_name = "containerBackupFactor" ]; @@ -133,25 +136,25 @@ message PlacementPolicy { bool unique = 5 [ json_name = "unique" ]; } -// NeoFS node description +// FrostFS node description message NodeInfo { - // Public key of the NeoFS node in a binary format + // Public key of the FrostFS node in a binary format bytes public_key = 1 [ json_name = "publicKey" ]; // Ways to connect to a node repeated string addresses = 2 [ json_name = "addresses" ]; - // Administrator-defined Attributes of the NeoFS Storage Node. + // Administrator-defined Attributes of the FrostFS Storage Node. // // `Attribute` is a Key-Value metadata pair. Key name must be a valid UTF-8 // string. Value can't be empty. // // Attributes can be constructed into a chain of attributes: any attribute can // have a parent attribute and a child attribute (except the first and the - // last one). A string representation of the chain of attributes in NeoFS + // last one). A string representation of the chain of attributes in FrostFS // Storage Node configuration uses ":" and "/" symbols, e.g.: // - // `NEOFS_NODE_ATTRIBUTE_1=key1:val1/key2:val2` + // `FrostFS_NODE_ATTRIBUTE_1=key1:val1/key2:val2` // // Therefore the string attribute representation in the Node configuration // must use "\:", "\/" and "\\" escaped symbols if any of them appears in an @@ -198,8 +201,8 @@ message NodeInfo { // [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2). Calculated // automatically from `UN-LOCODE` attribute. // * Continent \ - // Node's continent name according to the [Seven-Continent model] - // (https://en.wikipedia.org/wiki/Continent#Number). Calculated + // Node's continent name according to the [Seven-Continent + // model](https://en.wikipedia.org/wiki/Continent#Number). Calculated // automatically from `UN-LOCODE` attribute. // * ExternalAddr // Node's preferred way for communications with external clients. @@ -207,7 +210,7 @@ message NodeInfo { // Must contain a comma-separated list of multi-addresses. // // For detailed description of each well-known attribute please see the - // corresponding section in NeoFS Technical Specification. + // corresponding section in FrostFS Technical Specification. message Attribute { // Key of the node attribute string key = 1 [ json_name = "key" ]; @@ -219,13 +222,13 @@ message NodeInfo { // `Country`. repeated string parents = 3 [ json_name = "parents" ]; } - // Carries list of the NeoFS node attributes in a key-value form. Key name + // Carries list of the FrostFS node attributes in a key-value form. Key name // must be a node-unique valid UTF-8 string. Value can't be empty. NodeInfo // structures with duplicated attribute names or attributes with empty values // will be considered invalid. repeated Attribute attributes = 3 [ json_name = "attributes" ]; - // Represents the enumeration of various states of the NeoFS node. + // Represents the enumeration of various states of the FrostFS node. enum State { // Unknown state UNSPECIFIED = 0; @@ -240,7 +243,7 @@ message NodeInfo { MAINTENANCE = 3; } - // Carries state of the NeoFS node + // Carries state of the FrostFS node State state = 4 [ json_name = "state" ]; } @@ -253,7 +256,7 @@ message Netmap { repeated NodeInfo nodes = 2 [ json_name = "nodes" ]; } -// NeoFS network configuration +// FrostFS network configuration message NetworkConfig { // Single configuration parameter. Key MUST be network-unique. // @@ -272,7 +275,7 @@ message NetworkConfig { // Fee paid for container creation by the container owner. // Value: little-endian integer. Default: 0. // - **EpochDuration** \ - // NeoFS epoch duration measured in Sidechain blocks. + // FrostFS epoch duration measured in Sidechain blocks. // Value: little-endian integer. Default: 0. // - **HomomorphicHashingDisabled** \ // Flag of disabling the homomorphic hashing of objects' payload. @@ -284,8 +287,39 @@ message NetworkConfig { // Flag allowing setting the MAINTENANCE state to storage nodes. // Value: true if any byte != 0. Default: false. // - **MaxObjectSize** \ - // Maximum size of physically stored NeoFS object measured in bytes. + // Maximum size of physically stored FrostFS object measured in bytes. // Value: little-endian integer. Default: 0. + // + // This value refers to the maximum size of a **physically** stored object + // in FrostFS. However, from a user's perspective, the **logical** size of a + // stored object can be significantly larger. The relationship between the + // physical and logical object sizes is governed by the following formula + // + // ```math + // \mathrm{Stored\ Object\ Size} \le + // \frac{ + // \left(\mathrm{Max\ Object\ Size}\right)^2 + // }{ + // \mathrm{Object\ ID\ Size} + // } + // ``` + // + // This arises from the fact that a tombstone, also being an object, stores + // the IDs of inhumed objects and cannot be divided into smaller objects, + // thus having an upper limit for its size. + // + // For example, if: + // * Max Object Size Size = 64 MiB; + // * Object ID Size = 32 B; + // + // then: + // ```math + // \mathrm{Stored\ Object\ Size} \le + // \frac{\left(64\ \mathrm{MiB}\right)^2}{32\ \mathrm{B}} = + // \frac{2^{52}}{2^5}\ \mathrm{B} = + // 2^{47}\ \mathrm{B} = + // 128\ \mathrm{TiB} + // ``` // - **WithdrawFee** \ // Fee paid for withdrawal of funds paid by the account owner. // Value: little-endian integer. Default: 0. @@ -306,18 +340,18 @@ message NetworkConfig { repeated Parameter parameters = 1 [ json_name = "parameters" ]; } -// Information about NeoFS network +// Information about FrostFS network message NetworkInfo { - // Number of the current epoch in the NeoFS network + // Number of the current epoch in the FrostFS network uint64 current_epoch = 1 [ json_name = "currentEpoch" ]; - // Magic number of the sidechain of the NeoFS network + // Magic number of the sidechain of the FrostFS network uint64 magic_number = 2 [ json_name = "magicNumber" ]; - // MillisecondsPerBlock network parameter of the sidechain of the NeoFS + // MillisecondsPerBlock network parameter of the sidechain of the FrostFS // network int64 ms_per_block = 3 [ json_name = "msPerBlock" ]; - // NeoFS network configuration + // FrostFS network configuration NetworkConfig network_config = 4 [ json_name = "networkConfig" ]; } diff --git a/src/FrostFS.SDK.Protos/object/Extension.Message.cs b/src/FrostFS.SDK.Protos/object/Extension.Message.cs new file mode 100644 index 00000000..f9348ffc --- /dev/null +++ b/src/FrostFS.SDK.Protos/object/Extension.Message.cs @@ -0,0 +1,517 @@ +using System.Diagnostics; + +using FrostFS.SDK.Proto.Interfaces; +using FrostFS.Session; + +using Google.Protobuf; + +namespace FrostFS.Object +{ + public partial class GetRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PutRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PutResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PutSingleRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PutSingleResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class DeleteRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class DeleteResponse : IResponse + { + [DebuggerStepThrough] + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + [DebuggerStepThrough] + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + [DebuggerStepThrough] + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + [DebuggerStepThrough] + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + [DebuggerStepThrough] + public IMessage GetBody() + { + return Body; + } + } + + public partial class HeadRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class HeadResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + public partial class SearchRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class SearchResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetRangeRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetRangeResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetRangeHashRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetRangeHashResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PatchRequest : IRequest + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PatchResponse : IResponse + { + IMetaHeader IVerifiableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerifiableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ProtosV2/object/service.proto b/src/FrostFS.SDK.Protos/object/service.proto similarity index 85% rename from src/FrostFS.SDK.ProtosV2/object/service.proto rename to src/FrostFS.SDK.Protos/object/service.proto index 383e83b0..2b8042bb 100644 --- a/src/FrostFS.SDK.ProtosV2/object/service.proto +++ b/src/FrostFS.SDK.Protos/object/service.proto @@ -151,7 +151,7 @@ service ObjectService { rpc Head(HeadRequest) returns (HeadResponse); // Search objects in container. Search query allows to match by Object - // Header's filed values. Please see the corresponding NeoFS Technical + // Header's filed values. Please see the corresponding FrostFS Technical // Specification section for more details. // // Extended headers can change `Search` behaviour: @@ -283,6 +283,55 @@ service ObjectService { // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ // provided session token has expired. rpc PutSingle(PutSingleRequest) returns (PutSingleResponse); + + // Patch the object. Request uses gRPC stream. First message must set + // the address of the object that is going to get patched. If the object's + // attributes are patched, then these attrubutes must be set only within the + // first stream message. + // + // If the patch request is performed by NOT the object's owner but if the + // actor has the permission to perform the patch, then `OwnerID` of the object + // is changed. In this case the object's owner loses the object's ownership + // after the patch request is successfully done. + // + // As objects are content-addressable the patching causes new object ID + // generation for the patched object. This object id is set witihn + // `PatchResponse`. But the object id may remain unchanged in such cases: + // 1. The chunk of the applying patch contains the same value as the object's + // payload within the same range; + // 2. The patch that reverts the changes applied by preceding patch; + // 3. The application of the same patches for the object a few times. + // + // Extended headers can change `Patch` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requsted version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully patched and saved in the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // write access to the container is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted. + // - **OUT_OF_RANGE** (2053, SECTION_OBJECT): \ + // the requested range is out of bounds; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object storage container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_NOT_FOUND** (4096, SECTION_SESSION): \ + // (for trusted object preparation) session private key does not exist or + // has been deleted; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Patch(stream PatchRequest) returns (PatchResponse); } // GET object request @@ -583,6 +632,9 @@ message SearchRequest { // object_id of parent // * $Object:split.splitID \ // 16 byte UUIDv4 used to identify the split object hierarchy parts + // * $Object:ec.parent \ + // If the object is stored according to EC policy, then ec_parent + // attribute is set to return an id list of all related EC chunks. // // There are some well-known filter aliases to match objects by certain // properties: @@ -813,4 +865,75 @@ message PutSingleResponse { // authenticate the nodes of the message route and check the correctness of // transmission. neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; -} \ No newline at end of file +} + +// Object PATCH request +message PatchRequest { + // PATCH request body + message Body { + // The address of the object that is requested to get patched. + neo.fs.v2.refs.Address address = 1; + + // New attributes for the object. See `replace_attributes` flag usage to + // define how new attributes should be set. + repeated neo.fs.v2.object.Header.Attribute new_attributes = 2; + + // If this flag is set, then the object's attributes will be entirely + // replaced by `new_attributes` list. The empty `new_attributes` list with + // `replace_attributes = true` just resets attributes list for the object. + // + // Default `false` value for this flag means the attributes will be just + // merged. If the incoming `new_attributes` list contains already existing + // key, then it just replaces it while merging the lists. + bool replace_attributes = 3; + + // The patch for the object's payload. + message Patch { + // The range of the source object for which the payload is replaced by the + // patch's chunk. If the range's `length = 0`, then the patch's chunk is + // just appended to the original payload starting from the `offest` + // without any replace. + Range source_range = 1; + + // The chunk that is being appended to or that replaces the original + // payload on the given range. + bytes chunk = 2; + } + + // The patch that is applied for the object. + Patch patch = 4; + } + + // Body for patch request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Object PATCH response +message PatchResponse { + // PATCH response body + message Body { + // The object ID of the saved patched object. + neo.fs.v2.refs.ObjectID object_id = 1; + } + + // Body for patch response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/src/FrostFS.SDK.ProtosV2/object/types.proto b/src/FrostFS.SDK.Protos/object/types.proto similarity index 92% rename from src/FrostFS.SDK.ProtosV2/object/types.proto rename to src/FrostFS.SDK.Protos/object/types.proto index 6e62b865..b838c8ea 100644 --- a/src/FrostFS.SDK.ProtosV2/object/types.proto +++ b/src/FrostFS.SDK.Protos/object/types.proto @@ -155,7 +155,7 @@ message Header { // MIME Content Type of object's payload // // For detailed description of each well-known attribute please see the - // corresponding section in NeoFS Technical Specification. + // corresponding section in FrostFS Technical Specification. message Attribute { // string key to the object attribute string key = 1 [ json_name = "key" ]; @@ -208,6 +208,18 @@ message Header { uint32 header_length = 4 [ json_name = "headerLength" ]; // Chunk of a parent header. bytes header = 5 [ json_name = "header" ]; + // As the origin object is EC-splitted its identifier is known to all + // chunks as parent. But parent itself can be a part of Split (does not + // relate to EC-split). In this case parent_split_id should be set. + bytes parent_split_id = 6 [ json_name = "parentSplitID" ]; + // EC-parent's parent ID. parent_split_parent_id is set if EC-parent, + // itself, is a part of Split and if an object ID of its parent is + // presented. The field allows to determine how EC-chunk is placed in Split + // hierarchy. + neo.fs.v2.refs.ObjectID parent_split_parent_id = 7 + [ json_name = "parentSplitParentID" ]; + // EC parent's attributes. + repeated Attribute parent_attributes = 8 [ json_name = "parentAttributes" ]; } // Erasure code chunk information. EC ec = 12 [ json_name = "ec" ]; diff --git a/src/FrostFS.SDK.ProtosV2/refs/types.proto b/src/FrostFS.SDK.Protos/refs/types.proto similarity index 93% rename from src/FrostFS.SDK.ProtosV2/refs/types.proto rename to src/FrostFS.SDK.Protos/refs/types.proto index 15d32c1c..014c7360 100644 --- a/src/FrostFS.SDK.ProtosV2/refs/types.proto +++ b/src/FrostFS.SDK.Protos/refs/types.proto @@ -5,7 +5,7 @@ package neo.fs.v2.refs; option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs/grpc;refs"; option csharp_namespace = "FrostFS.Refs"; -// Objects in NeoFS are addressed by their ContainerID and ObjectID. +// Objects in FrostFS are addressed by their ContainerID and ObjectID. // // String presentation of `Address` is a concatenation of string encoded // `ContainerID` and `ObjectID` delimited by '/' character. @@ -16,8 +16,9 @@ message Address { ObjectID object_id = 2 [ json_name = "objectID" ]; } -// NeoFS Object unique identifier. Objects are immutable and content-addressed. -// It means `ObjectID` will change if the `header` or the `payload` changes. +// FrostFS Object unique identifier. Objects are immutable and +// content-addressed. It means `ObjectID` will change if the `header` or the +// `payload` changes. // // `ObjectID` is a 32 byte long // [SHA256](https://csrc.nist.gov/publications/detail/fips/180/4/final) hash of @@ -37,7 +38,7 @@ message ObjectID { bytes value = 1 [ json_name = "value" ]; } -// NeoFS container identifier. Container structures are immutable and +// FrostFS container identifier. Container structures are immutable and // content-addressed. // // `ContainerID` is a 32 byte long @@ -90,7 +91,7 @@ message Version { uint32 minor = 2 [ json_name = "minor" ]; } -// Signature of something in NeoFS. +// Signature of something in FrostFS. message Signature { // Public key used for signing bytes key = 1 [ json_name = "key" ]; diff --git a/src/FrostFS.SDK.ProtosV2/session/Extension.Message.cs b/src/FrostFS.SDK.Protos/session/Extension.Message.cs similarity index 54% rename from src/FrostFS.SDK.ProtosV2/session/Extension.Message.cs rename to src/FrostFS.SDK.Protos/session/Extension.Message.cs index fd78adcf..b7ca64e5 100644 --- a/src/FrostFS.SDK.ProtosV2/session/Extension.Message.cs +++ b/src/FrostFS.SDK.Protos/session/Extension.Message.cs @@ -1,25 +1,27 @@ -using Google.Protobuf; +using FrostFS.SDK.Proto.Interfaces; + +using Google.Protobuf; namespace FrostFS.Session; public partial class CreateResponse : IResponse { - IMetaHeader IVerificableMessage.GetMetaHeader() + IMetaHeader IVerifiableMessage.GetMetaHeader() { return MetaHeader; } - IVerificationHeader IVerificableMessage.GetVerificationHeader() + IVerificationHeader IVerifiableMessage.GetVerificationHeader() { return VerifyHeader; } - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) { MetaHeader = (ResponseMetaHeader)metaHeader; } - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) { VerifyHeader = (ResponseVerificationHeader)verificationHeader; } @@ -32,22 +34,22 @@ public partial class CreateResponse : IResponse public partial class CreateRequest : IRequest { - IMetaHeader IVerificableMessage.GetMetaHeader() + IMetaHeader IVerifiableMessage.GetMetaHeader() { return MetaHeader; } - IVerificationHeader IVerificableMessage.GetVerificationHeader() + IVerificationHeader IVerifiableMessage.GetVerificationHeader() { return VerifyHeader; } - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + void IVerifiableMessage.SetMetaHeader(IMetaHeader metaHeader) { MetaHeader = (RequestMetaHeader)metaHeader; } - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + void IVerifiableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) { VerifyHeader = (RequestVerificationHeader)verificationHeader; } diff --git a/src/FrostFS.SDK.ProtosV2/session/Extension.MetaHeader.cs b/src/FrostFS.SDK.Protos/session/Extension.MetaHeader.cs similarity index 100% rename from src/FrostFS.SDK.ProtosV2/session/Extension.MetaHeader.cs rename to src/FrostFS.SDK.Protos/session/Extension.MetaHeader.cs diff --git a/src/FrostFS.SDK.ProtosV2/session/Extension.VerificationHeader.cs b/src/FrostFS.SDK.Protos/session/Extension.VerificationHeader.cs similarity index 100% rename from src/FrostFS.SDK.ProtosV2/session/Extension.VerificationHeader.cs rename to src/FrostFS.SDK.Protos/session/Extension.VerificationHeader.cs diff --git a/src/FrostFS.SDK.ProtosV2/session/Extension.XHeader.cs b/src/FrostFS.SDK.Protos/session/Extension.XHeader.cs similarity index 79% rename from src/FrostFS.SDK.ProtosV2/session/Extension.XHeader.cs rename to src/FrostFS.SDK.Protos/session/Extension.XHeader.cs index f8f26958..c0c5b253 100644 --- a/src/FrostFS.SDK.ProtosV2/session/Extension.XHeader.cs +++ b/src/FrostFS.SDK.Protos/session/Extension.XHeader.cs @@ -2,7 +2,7 @@ public partial class XHeader { - public const string ReservedXHeaderPrefix = "__NEOFS__"; + public const string ReservedXHeaderPrefix = "__SYSTEM__"; public const string XHeaderNetmapEpoch = ReservedXHeaderPrefix + "NETMAP_EPOCH"; public const string XHeaderNetmapLookupDepth = ReservedXHeaderPrefix + "NETMAP_LOOKUP_DEPTH"; } diff --git a/src/FrostFS.SDK.ProtosV2/session/service.proto b/src/FrostFS.SDK.Protos/session/service.proto similarity index 97% rename from src/FrostFS.SDK.ProtosV2/session/service.proto rename to src/FrostFS.SDK.Protos/session/service.proto index 6f48e3a1..6511f3bd 100644 --- a/src/FrostFS.SDK.ProtosV2/session/service.proto +++ b/src/FrostFS.SDK.Protos/session/service.proto @@ -11,7 +11,7 @@ import "session/types.proto"; // `SessionService` allows to establish a temporary trust relationship between // two peer nodes and generate a `SessionToken` as the proof of trust to be // attached in requests for further verification. Please see corresponding -// section of NeoFS Technical Specification for details. +// section of FrostFS Technical Specification for details. service SessionService { // Open a new session between two peers. // diff --git a/src/FrostFS.SDK.ProtosV2/session/types.proto b/src/FrostFS.SDK.Protos/session/types.proto similarity index 96% rename from src/FrostFS.SDK.ProtosV2/session/types.proto rename to src/FrostFS.SDK.Protos/session/types.proto index d1a9ef13..bc0d7f12 100644 --- a/src/FrostFS.SDK.ProtosV2/session/types.proto +++ b/src/FrostFS.SDK.Protos/session/types.proto @@ -36,6 +36,9 @@ message ObjectSessionContext { // Refers to object.GetRangeHash RPC call RANGEHASH = 7; + + // Refers to object.Patch RPC call + PATCH = 8; } // Type of request for which the token is issued Verb verb = 1 [ json_name = "verb" ]; @@ -47,7 +50,7 @@ message ObjectSessionContext { refs.ContainerID container = 1 [ json_name = "container" ]; // Indicates which objects the session is spread to. Objects are expected - // to be stored in the NeoFS container referenced by `container` field. + // to be stored in the FrostFS container referenced by `container` field. // Each element MUST have correct format. repeated refs.ObjectID objects = 2 [ json_name = "objects" ]; } @@ -85,7 +88,7 @@ message ContainerSessionContext { refs.ContainerID container_id = 3 [ json_name = "containerID" ]; } -// NeoFS Session Token. +// FrostFS Session Token. message SessionToken { // Session Token body message Body { @@ -123,7 +126,7 @@ message SessionToken { } // Session Token contains the proof of trust between peers to be attached in // requests for further verification. Please see corresponding section of - // NeoFS Technical Specification for details. + // FrostFS Technical Specification for details. Body body = 1 [ json_name = "body" ]; // Signature of `SessionToken` information @@ -183,7 +186,7 @@ message RequestMetaHeader { // `RequestMetaHeader` of the origin request RequestMetaHeader origin = 7 [ json_name = "origin" ]; - // NeoFS network magic. Must match the value for the network + // FrostFS network magic. Must match the value for the network // that the server belongs to. uint64 magic_number = 8 [ json_name = "magicNumber" ]; } diff --git a/src/FrostFS.SDK.ProtosV2/status/types.proto b/src/FrostFS.SDK.Protos/status/types.proto similarity index 86% rename from src/FrostFS.SDK.ProtosV2/status/types.proto rename to src/FrostFS.SDK.Protos/status/types.proto index 8ab2f400..694f9697 100644 --- a/src/FrostFS.SDK.ProtosV2/status/types.proto +++ b/src/FrostFS.SDK.Protos/status/types.proto @@ -5,12 +5,12 @@ package neo.fs.v2.status; option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status/grpc;status"; option csharp_namespace = "FrostFS.Status"; -// Declares the general format of the status returns of the NeoFS RPC protocol. -// Status is present in all response messages. Each RPC of NeoFS protocol -// describes the possible outcomes and details of the operation. +// Declares the general format of the status returns of the FrostFS RPC +// protocol. Status is present in all response messages. Each RPC of FrostFS +// protocol describes the possible outcomes and details of the operation. // // Each status is assigned a one-to-one numeric code. Any unique result of an -// operation in NeoFS is unambiguously associated with the code value. +// operation in FrostFS is unambiguously associated with the code value. // // Numerical set of codes is split into 1024-element sections. An enumeration // is defined for each section. Values can be referred to in the following ways: @@ -78,7 +78,7 @@ enum Section { SECTION_APE_MANAGER = 5; } -// Section of NeoFS successful return codes. +// Section of FrostFS successful return codes. enum Success { // [**0**] Default success. Not detailed. // If the server cannot match successful outcome to the code, it should @@ -93,9 +93,9 @@ enum CommonFail { // use this code. INTERNAL = 0; - // [**1025**] Wrong magic of the NeoFS network. + // [**1025**] Wrong magic of the FrostFS network. // Details: - // - [**0**] Magic number of the served NeoFS network (big-endian 64-bit + // - [**0**] Magic number of the served FrostFS network (big-endian 64-bit // unsigned integer). WRONG_MAGIC_NUMBER = 1; @@ -104,6 +104,11 @@ enum CommonFail { // [**1027**] Node is under maintenance. NODE_UNDER_MAINTENANCE = 3; + + // [**1028**] Invalid argument error. If the server fails on validation of a + // request parameter as the client sent it incorrectly, then this code should + // be used. + INVALID_ARGUMENT = 4; } // Section of statuses for object-related operations. diff --git a/src/FrostFS.SDK.ProtosV2/tombstone/types.proto b/src/FrostFS.SDK.Protos/tombstone/types.proto similarity index 83% rename from src/FrostFS.SDK.ProtosV2/tombstone/types.proto rename to src/FrostFS.SDK.Protos/tombstone/types.proto index 739bef4d..87803172 100644 --- a/src/FrostFS.SDK.ProtosV2/tombstone/types.proto +++ b/src/FrostFS.SDK.Protos/tombstone/types.proto @@ -8,10 +8,10 @@ option csharp_namespace = "FrostFS.Tombstone"; import "refs/types.proto"; // Tombstone keeps record of deleted objects for a few epochs until they are -// purged from the NeoFS network. +// purged from the FrostFS network. message Tombstone { - // Last NeoFS epoch number of the tombstone lifetime. It's set by the - // tombstone creator depending on the current NeoFS network settings. A + // Last FrostFS epoch number of the tombstone lifetime. It's set by the + // tombstone creator depending on the current FrostFS network settings. A // tombstone object must have the same expiration epoch value in // `__SYSTEM__EXPIRATION_EPOCH` (`__NEOFS__EXPIRATION_EPOCH` is deprecated) // attribute. Otherwise, the tombstone will be rejected by a storage node. diff --git a/src/FrostFS.SDK.ProtosV2/Interfaces/IRequest.cs b/src/FrostFS.SDK.ProtosV2/Interfaces/IRequest.cs deleted file mode 100644 index f75db43b..00000000 --- a/src/FrostFS.SDK.ProtosV2/Interfaces/IRequest.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FrostFS.Session; - -public interface IRequest : IVerificableMessage -{ - RequestMetaHeader MetaHeader { get; set; } - RequestVerificationHeader VerifyHeader { get; set; } -} diff --git a/src/FrostFS.SDK.ProtosV2/container/Extension.Message.cs b/src/FrostFS.SDK.ProtosV2/container/Extension.Message.cs deleted file mode 100644 index a46ddc20..00000000 --- a/src/FrostFS.SDK.ProtosV2/container/Extension.Message.cs +++ /dev/null @@ -1,397 +0,0 @@ -using Google.Protobuf; - -using FrostFS.Session; - -namespace FrostFS.Container; - -public partial class AnnounceUsedSpaceRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class AnnounceUsedSpaceResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class GetRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class GetResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class PutRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class PutResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class DeleteRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class DeleteResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class ListRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class ListResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class SetExtendedACLRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class SetExtendedACLResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class GetExtendedACLRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class GetExtendedACLResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} diff --git a/src/FrostFS.SDK.ProtosV2/netmap/Extension.Message.cs b/src/FrostFS.SDK.ProtosV2/netmap/Extension.Message.cs deleted file mode 100644 index 207568d6..00000000 --- a/src/FrostFS.SDK.ProtosV2/netmap/Extension.Message.cs +++ /dev/null @@ -1,116 +0,0 @@ -using FrostFS.Session; -using Google.Protobuf; - -namespace FrostFS.Netmap; - -public partial class LocalNodeInfoRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class LocalNodeInfoResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class NetworkInfoRequest : IRequest -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} - -public partial class NetworkInfoResponse : IResponse -{ - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } -} diff --git a/src/FrostFS.SDK.ProtosV2/object/Extension.Message.cs b/src/FrostFS.SDK.ProtosV2/object/Extension.Message.cs deleted file mode 100644 index 243ab006..00000000 --- a/src/FrostFS.SDK.ProtosV2/object/Extension.Message.cs +++ /dev/null @@ -1,396 +0,0 @@ -using FrostFS.Session; -using Google.Protobuf; - -namespace FrostFS.Object -{ - public partial class GetRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class GetResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class PutRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class PutResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class DeleteRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class DeleteResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class HeadRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class HeadResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - public partial class SearchRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class SearchResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class GetRangeRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class GetRangeResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class GetRangeHashRequest : IRequest - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (RequestMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (RequestVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } - - public partial class GetRangeHashResponse : IResponse - { - IMetaHeader IVerificableMessage.GetMetaHeader() - { - return MetaHeader; - } - - IVerificationHeader IVerificableMessage.GetVerificationHeader() - { - return VerifyHeader; - } - - void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) - { - MetaHeader = (ResponseMetaHeader)metaHeader; - } - - void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) - { - VerifyHeader = (ResponseVerificationHeader)verificationHeader; - } - - public IMessage GetBody() - { - return Body; - } - } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/CallbackInterceptor.cs b/src/FrostFS.SDK.Tests/CallbackInterceptor.cs new file mode 100644 index 00000000..c4eb7268 --- /dev/null +++ b/src/FrostFS.SDK.Tests/CallbackInterceptor.cs @@ -0,0 +1,33 @@ +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace FrostFS.SDK.SmokeTests; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", + Justification = "parameters are provided by GRPC infrastructure")] +public class CallbackInterceptor(Action? callback = null) : Interceptor +{ + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + var call = continuation(request, context); + + return new AsyncUnaryCall( + HandleUnaryResponse(call), + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose); + } + + private async Task HandleUnaryResponse(AsyncUnaryCall call) + { + var response = await call; + + callback?.Invoke($"elapsed"); + + return response; + } +} diff --git a/src/FrostFS.SDK.Tests/FrostFS.SDK.Tests.csproj b/src/FrostFS.SDK.Tests/FrostFS.SDK.Tests.csproj new file mode 100644 index 00000000..37bbace2 --- /dev/null +++ b/src/FrostFS.SDK.Tests/FrostFS.SDK.Tests.csproj @@ -0,0 +1,50 @@ + + + + net8.0 + enable + enable + + false + true + + + + true + + + + true + + + + True + .\\..\\..\\keyfile.snk + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/src/FrostFS.SDK.Tests/Mocks/AsyncStreamRangeReaderMock.cs b/src/FrostFS.SDK.Tests/Mocks/AsyncStreamRangeReaderMock.cs new file mode 100644 index 00000000..bd9d8583 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/AsyncStreamRangeReaderMock.cs @@ -0,0 +1,38 @@ +using FrostFS.Object; +using FrostFS.Session; + +using Google.Protobuf; + +using Grpc.Core; + +namespace FrostFS.SDK.Tests; + +public class AsyncStreamRangeReaderMock(string key, byte[] response) : ServiceBase(key), IAsyncStreamReader +{ + private readonly byte[] _response = response; + + public GetRangeResponse Current + { + get + { + var response = new GetRangeResponse + { + Body = new GetRangeResponse.Types.Body + { + Chunk = ByteString.CopyFrom(_response) + }, + MetaHeader = new ResponseMetaHeader() + }; + + response.VerifyHeader = GetResponseVerificationHeader(response); + + return response; + } + } + + public Task MoveNext(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } +} + diff --git a/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs b/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs new file mode 100644 index 00000000..58a9d40d --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/AsyncStreamReaderMock.cs @@ -0,0 +1,63 @@ +using System.Security.Cryptography; + +using FrostFS.Object; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.Session; + +using Google.Protobuf; + +using Grpc.Core; + +namespace FrostFS.SDK.Tests; + +public class AsyncStreamReaderMock(string key, FrostFsObjectHeader objectHeader) : ServiceBase(key), IAsyncStreamReader +{ + public GetResponse Current + { + get + { + var header = new Header + { + ContainerId = objectHeader.ContainerId.ToMessage(), + PayloadLength = objectHeader.PayloadLength, + Version = objectHeader.Version!.ToMessage(), + OwnerId = objectHeader.OwnerId!.ToMessage() + }; + + if (objectHeader.Attributes != null) + { + foreach (var attr in objectHeader.Attributes) + header.Attributes.Add(attr.ToMessage()); + } + + var response = new GetResponse + { + Body = new GetResponse.Types.Body + { + Init = new GetResponse.Types.Body.Types.Init + { + Header = header, + ObjectId = new Refs.ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Array.Empty())) }, + Signature = new Refs.Signature + { + Key = Key.PublicKeyProto, + Sign = Key.ECDsaKey.SignData(header.ToByteArray()), + } + } + }, + MetaHeader = new ResponseMetaHeader() + }; + + response.VerifyHeader = GetResponseVerificationHeader(response); + + return response; + } + } + + public Task MoveNext(CancellationToken cancellationToken) + { + return Task.FromResult(true); + } +} + diff --git a/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs b/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs new file mode 100644 index 00000000..7fba4f67 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/ClientStreamWriter.cs @@ -0,0 +1,35 @@ +using System.Collections.ObjectModel; + +using FrostFS.SDK.Proto.Interfaces; + +using Grpc.Core; + +namespace FrostFS.SDK.Tests; + +public class ClientStreamWriter : IClientStreamWriter +{ + private WriteOptions? _options; + + public Collection Messages { get; } = []; + public bool CompletedTask { get; private set; } + + public WriteOptions? WriteOptions + { + get => _options; + set => _options = value; + } + + public Task CompleteAsync() + { + CompletedTask = true; + return Task.CompletedTask; + } + + public Task WriteAsync(IRequest message) + { + Object.PutRequest pr = new((Object.PutRequest)message); + Messages.Add(pr); + return Task.CompletedTask; + } +} + diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs new file mode 100644 index 00000000..eafdda54 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerServiceBase.cs @@ -0,0 +1,86 @@ +using FrostFS.Container; +using FrostFS.Object; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; +using FrostFS.Session; + +using Google.Protobuf; + +using Grpc.Core; + +using Moq; + +namespace FrostFS.SDK.Tests; + +public abstract class ServiceBase(string key) +{ + public string StringKey { get; private set; } = key; + public ClientKey Key { get; private set; } = new ClientKey(key.LoadWif()); + public FrostFsVersion Version { get; set; } = DefaultVersion; + public FrostFsPlacementPolicy PlacementPolicy { get; set; } = DefaultPlacementPolicy; + + public static FrostFsVersion DefaultVersion { get; } = new(2, 13); + public static FrostFsPlacementPolicy DefaultPlacementPolicy { get; } = new FrostFsPlacementPolicy(true, 1, [], [], new FrostFsReplica(1)); + +#pragma warning disable CA2227 // this is specific object, should be treated as is + public Metadata? Metadata { get; set; } +#pragma warning restore CA2227 + + public DateTime? DateTime { get; protected set; } + + public CancellationToken CancellationToken { get; protected set; } + public CancellationTokenSource CancellationTokenSource { get; protected set; } = new CancellationTokenSource(); + + public static Metadata ResponseMetaData => []; + + protected ResponseVerificationHeader GetResponseVerificationHeader(IResponse response) + { + ArgumentNullException.ThrowIfNull(response); + + var verifyHeader = new ResponseVerificationHeader + { + MetaSignature = new Refs.Signature + { + Key = Key.PublicKeyProto, + Scheme = Refs.SignatureScheme.EcdsaRfc6979Sha256, + Sign = Key.ECDsaKey.SignData(response.MetaHeader.ToByteArray()) + }, + BodySignature = new Refs.Signature + { + Key = Key.PublicKeyProto, + Scheme = Refs.SignatureScheme.EcdsaRfc6979Sha256, + Sign = Key.ECDsaKey.SignData(response.GetBody().ToByteArray()) + }, + OriginSignature = new Refs.Signature + { + Key = Key.PublicKeyProto, + Scheme = Refs.SignatureScheme.EcdsaRfc6979Sha256, + Sign = Key.ECDsaKey.SignData(ReadOnlyMemory.Empty) + } + }; + + return verifyHeader; + } + + public ResponseMetaHeader ResponseMetaHeader => new() + { + Version = Version.ToMessage(), + Epoch = 100, + Ttl = 1 + }; +} + +public abstract class ContainerServiceBase(string key) : ServiceBase(key) +{ + public Guid ContainerGuid { get; set; } = Guid.NewGuid(); + + public abstract Mock GetMock(); +} + +public abstract class ObjectServiceBase(string key) : ServiceBase(key) +{ + public abstract Mock GetMock(); + + public Guid ContainerGuid { get; set; } = Guid.NewGuid(); +} diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerStub.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerStub.cs new file mode 100644 index 00000000..2734a265 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/ContainerStub.cs @@ -0,0 +1,13 @@ +using FrostFS.Container; + +using Moq; + +namespace FrostFS.SDK.Tests; + +public class ContainerStub(string key) : ContainerServiceBase(key) +{ + public override Mock GetMock() + { + return new Mock(); + } +} diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs new file mode 100644 index 00000000..74919509 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs @@ -0,0 +1,191 @@ +using System.Collections.ObjectModel; + +using FrostFS.Container; +using FrostFS.Refs; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; +using FrostFS.Session; + +using Google.Protobuf; + +using Grpc.Core; + +using Moq; + +namespace FrostFS.SDK.Tests; + +public class ContainerMocker(string key) : ContainerServiceBase(key) +{ + public override Mock GetMock() + { + var mock = new Mock(); + + var grpcVersion = Version.ToMessage(); + + Span ContainerGuidSpan = stackalloc byte[16]; + ContainerGuid.ToBytes(ContainerGuidSpan); + + PutResponse putResponse = new() + { + Body = new PutResponse.Types.Body + { + ContainerId = new ContainerID + { + Value = ByteString.CopyFrom(ContainerGuidSpan) + } + }, + MetaHeader = new ResponseMetaHeader + { + Version = (Version is null ? DefaultVersion : Version).ToMessage(), + Epoch = 100, + Ttl = 1 + } + }; + + putResponse.VerifyHeader = GetResponseVerificationHeader(putResponse); + + var metadata = new Metadata(); + using var putContainerResponse = new AsyncUnaryCall( + Task.FromResult(putResponse), + Task.FromResult(metadata), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => metadata, + () => { }); + + mock.Setup(x => x.PutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((PutRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + return putContainerResponse; + }); + + var getResponse = new GetResponse + { + Body = new GetResponse.Types.Body + { + Container = new Container.Container + { + Version = grpcVersion, + Nonce = ByteString.CopyFrom(ContainerGuidSpan), + PlacementPolicy = PlacementPolicy.GetPolicy() + } + }, + MetaHeader = ResponseMetaHeader + }; + + getResponse.VerifyHeader = GetResponseVerificationHeader(getResponse); + + var getNoContainerResponse = new GetResponse + { + Body = new(), + MetaHeader = new ResponseMetaHeader + { + Status = new Status.Status + { + Code = 3072, + Message = "container not found" + } + } + }; + + getNoContainerResponse.VerifyHeader = GetResponseVerificationHeader(getNoContainerResponse); + + mock.Setup(x => x.GetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((GetRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + if (ReturnContainerRemoved) + { + return new AsyncUnaryCall( + Task.FromResult(getNoContainerResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.NotFound, string.Empty), + () => ResponseMetaData, + () => { }); + } + + return new AsyncUnaryCall( + Task.FromResult(getResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + var listResponse = new ListResponse + { + Body = new ListResponse.Types.Body(), + MetaHeader = ResponseMetaHeader + }; + + foreach (var item in ContainerIds) + { + listResponse.Body.ContainerIds.Add(new ContainerID { Value = ByteString.CopyFrom(item) }); + } + + listResponse.VerifyHeader = GetResponseVerificationHeader(listResponse); + + mock.Setup(x => x.ListAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((ListRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + return new AsyncUnaryCall( + Task.FromResult(listResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + var v = mock.Setup(x => x.DeleteAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((DeleteRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Requests.Add(new RequestData(r, m, dt, ct)); + + var response = new DeleteResponse + { + Body = new DeleteResponse.Types.Body(), + MetaHeader = new ResponseMetaHeader() + }; + + var metadata = new Metadata(); + + response.VerifyHeader = GetResponseVerificationHeader(response); + + return new AsyncUnaryCall( + Task.FromResult(response), + Task.FromResult(metadata), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => metadata, + () => { }); + }); + + return mock; + } + + public bool ReturnContainerRemoved { get; set; } + + public Collection ContainerIds { get; } = []; + + public Collection> Requests { get; } = []; +} diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/RequestData.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/RequestData.cs new file mode 100644 index 00000000..f60a2c8c --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/RequestData.cs @@ -0,0 +1,11 @@ +using Grpc.Core; + +namespace FrostFS.SDK.Tests; + +public class RequestData(T request, Metadata m, DateTime? dt, CancellationToken ct) +{ + public T Request => request; + public Metadata Metadata => m; + public DateTime? Deadline => dt; + public CancellationToken CancellationToken => ct; +} diff --git a/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs b/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs new file mode 100644 index 00000000..003d7ac1 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/NetworkMocker.cs @@ -0,0 +1,148 @@ +using FrostFS.Netmap; + +using Google.Protobuf; + +using Grpc.Core; + +using Moq; + +namespace FrostFS.SDK.Tests; + +public class NetworkMocker(string key) : ServiceBase(key) +{ + internal static readonly string[] ParameterKeys = [ + "AuditFee", + "BasicIncomeRate", + "ContainerFee", + "ContainerAliasFee", + "EpochDuration", + "InnerRingCandidateFee", + "MaxECDataCount", + "MaxECParityCount", + "MaxObjectSize", + "WithdrawFee", + "HomomorphicHashingDisabled", + "MaintenanceModeAllowed" + ]; + + public Dictionary Parameters { get; } = []; + + public LocalNodeInfoResponse? NodeInfoResponse { get; set; } + + public LocalNodeInfoRequest? LocalNodeInfoRequest { get; set; } + + public NetworkInfoRequest? NetworkInfoRequest { get; set; } + + public NetmapSnapshotResponse? NetmapSnapshotResponse { get; set; } + + public NetmapSnapshotRequest? NetmapSnapshotRequest { get; set; } + + public Mock GetMock() + { + var mock = new Mock(); + + var networkInfoResponse = new NetworkInfoResponse(); + + var networkConfig = new NetworkConfig(); + + foreach (var key in ParameterKeys) + { + networkConfig.Parameters.Add(new NetworkConfig.Types.Parameter + { + Key = ByteString.CopyFromUtf8(key), + Value = (Parameters != null && Parameters.TryGetValue(key, out byte[]? value)) ? ByteString.CopyFrom(value) : ByteString.CopyFrom(0) + }); + } + + var response = new NetworkInfoResponse + { + Body = new NetworkInfoResponse.Types.Body + { + NetworkInfo = new NetworkInfo + { + CurrentEpoch = 99, + MagicNumber = 13, + MsPerBlock = 999, + NetworkConfig = networkConfig + } + }, + MetaHeader = ResponseMetaHeader + }; + + response.VerifyHeader = GetResponseVerificationHeader(response); + + mock.Setup(x => x.NetworkInfoAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((NetworkInfoRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + NetworkInfoRequest = r; + Metadata = m; + DateTime = dt; + CancellationToken = ct; + + return new AsyncUnaryCall( + Task.FromResult(response), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + if (NodeInfoResponse != null) + { + NodeInfoResponse.MetaHeader = ResponseMetaHeader; + NodeInfoResponse.VerifyHeader = GetResponseVerificationHeader(NodeInfoResponse); + + mock.Setup(x => x.LocalNodeInfoAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((LocalNodeInfoRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + LocalNodeInfoRequest = r; + Metadata = m; + DateTime = dt; + CancellationToken = ct; + + return new AsyncUnaryCall( + Task.FromResult(NodeInfoResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + } + + if (NetmapSnapshotResponse != null) + { + NetmapSnapshotResponse.MetaHeader = ResponseMetaHeader; + NetmapSnapshotResponse.VerifyHeader = GetResponseVerificationHeader(NetmapSnapshotResponse); + + mock.Setup(x => x.NetmapSnapshotAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((NetmapSnapshotRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + NetmapSnapshotRequest = r; + Metadata = m; + DateTime = dt; + CancellationToken = ct; + + return new AsyncUnaryCall( + Task.FromResult(NetmapSnapshotResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + } + + return mock; + } +} diff --git a/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs b/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs new file mode 100644 index 00000000..772f92e7 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/ObjectMock.cs @@ -0,0 +1,308 @@ +using System.Collections.ObjectModel; +using System.Security.Cryptography; + +using FrostFS.Object; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; + +using Google.Protobuf; + +using Grpc.Core; + +using Moq; + +namespace FrostFS.SDK.Tests; + +public class ObjectMocker(string key) : ObjectServiceBase(key) +{ + public FrostFsObjectId? ObjectId { get; set; } + + public FrostFsObjectHeader? ObjectHeader { get; set; } + + public Header? HeadResponse { get; set; } + + public Collection? ResultObjectIds { get; } = []; + + public ClientStreamWriter? ClientStreamWriter { get; } = new(); + + public PatchStreamWriter? PatchStreamWriter { get; } = new(); + + public Collection PutSingleRequests { get; } = []; + + public Collection DeleteRequests { get; } = []; + + public Collection HeadRequests { get; } = []; + + public byte[] RangeResponse { get; set; } = []; + + public GetRangeRequest? GetRangeRequest { get; set; } + + public GetRangeHashRequest? GetRangeHashRequest { get; set; } + + public Collection RangeHashResponses { get; } = []; + + public Action? Callback; + + public override Mock GetMock() + { + var mock = new Mock(); + + if (ObjectHeader != null) + { + mock.Setup(x => x.Get( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((GetRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + return new AsyncServerStreamingCall( + new AsyncStreamReaderMock(StringKey, ObjectHeader), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + HeadResponse ??= new Header + { + CreationEpoch = 99, + ContainerId = ObjectHeader.ContainerId.ToMessage(), + ObjectType = ObjectType.Regular, + OwnerId = ObjectHeader.OwnerId!.ToMessage(), + PayloadLength = 1, + PayloadHash = new Refs.Checksum { Type = Refs.ChecksumType.Sha256, Sum = ByteString.CopyFrom(SHA256.HashData([0xff])) }, + Version = ObjectHeader.Version!.ToMessage() + }; + + HeadResponse headResponse = new() + { + Body = new HeadResponse.Types.Body + { + Header = new HeaderWithSignature + { + Header = HeadResponse + } + }, + MetaHeader = ResponseMetaHeader + }; + + headResponse.Body.Header.Header.Attributes.Add(new Header.Types.Attribute { Key = "k", Value = "v" }); + + headResponse.Body.Header.Signature = new Refs.Signature + { + Key = Key.PublicKeyProto, + Sign = Key.ECDsaKey.SignData(headResponse.Body.Header.ToByteArray()), + }; + + headResponse.VerifyHeader = GetResponseVerificationHeader(headResponse); + + mock.Setup(x => x.HeadAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((HeadRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + HeadRequests.Add(r); + + return new AsyncUnaryCall( + Task.FromResult(headResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + } + + if (ResultObjectIds != null && ResultObjectIds.Count > 0) + { + PutResponse putResponse = new() + { + Body = new PutResponse.Types.Body + { + ObjectId = new Refs.ObjectID + { + Value = ByteString.CopyFrom(ResultObjectIds.ElementAt(0)) + } + }, + MetaHeader = ResponseMetaHeader, + }; + + putResponse.VerifyHeader = GetResponseVerificationHeader(putResponse); + + mock.Setup(x => x.Put( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((Metadata m, DateTime? dt, CancellationToken ct) => + { + return new AsyncClientStreamingCall( + ClientStreamWriter!, + Task.FromResult(putResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + } + + PutSingleResponse putSingleResponse = new() + { + Body = new PutSingleResponse.Types.Body(), + MetaHeader = ResponseMetaHeader, + }; + + putSingleResponse.VerifyHeader = GetResponseVerificationHeader(putSingleResponse); + + mock.Setup(x => x.PutSingleAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((PutSingleRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Callback?.Invoke(); + Verifier.CheckRequest(r); + + var req = r.Clone(); + + // Clone method does not clone the payload but keeps a reference + req.Body.Object.Payload = ByteString.CopyFrom(r.Body.Object.Payload.ToByteArray()); + PutSingleRequests.Add(req); + + return new AsyncUnaryCall( + Task.FromResult(putSingleResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + if (ObjectId != null) + { + DeleteResponse deleteResponse = new() + { + Body = new DeleteResponse.Types.Body + { + Tombstone = new Refs.Address + { + ContainerId = ObjectHeader!.ContainerId.ToMessage(), + ObjectId = ObjectId.ToMessage() + } + }, + MetaHeader = ResponseMetaHeader + }; + + deleteResponse.VerifyHeader = GetResponseVerificationHeader(deleteResponse); + + mock.Setup(x => x.DeleteAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((DeleteRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + DeleteRequests.Add(r); + + return new AsyncUnaryCall( + Task.FromResult(deleteResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + } + + mock.Setup(x => x.GetRange( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((GetRangeRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + GetRangeRequest = r; + + return new AsyncServerStreamingCall( + new AsyncStreamRangeReaderMock(StringKey, RangeResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + mock.Setup(x => x.GetRangeHashAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((GetRangeHashRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + Verifier.CheckRequest(r); + + GetRangeHashRequest = r; + + var response = new GetRangeHashResponse + { + Body = new GetRangeHashResponse.Types.Body(), + MetaHeader = ResponseMetaHeader + }; + + if (RangeHashResponses != null) + { + foreach (var hash in RangeHashResponses) + { + response.Body.HashList.Add(hash); + } + } + + response.VerifyHeader = GetResponseVerificationHeader(response); + + return new AsyncUnaryCall( + Task.FromResult(response), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + + mock.Setup(x => x.Patch( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((Metadata m, DateTime? dt, CancellationToken ct) => + { + var patchResponse = new PatchResponse + { + Body = new PatchResponse.Types.Body + { + ObjectId = new Refs.ObjectID { Value = ByteString.CopyFrom(SHA256.HashData([1, 2, 3])) }, + }, + MetaHeader = ResponseMetaHeader + }; + + patchResponse.VerifyHeader = GetResponseVerificationHeader(patchResponse); + + return new AsyncClientStreamingCall( + PatchStreamWriter!, + Task.FromResult(patchResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + return mock; + } +} + diff --git a/src/FrostFS.SDK.Tests/Mocks/PatchStreamWriter.cs b/src/FrostFS.SDK.Tests/Mocks/PatchStreamWriter.cs new file mode 100644 index 00000000..7eb53bc6 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/PatchStreamWriter.cs @@ -0,0 +1,36 @@ +using System.Collections.ObjectModel; + +using FrostFS.SDK.Proto.Interfaces; + +using Grpc.Core; + +namespace FrostFS.SDK.Tests; + +public class PatchStreamWriter : IClientStreamWriter +{ + private WriteOptions? _options; + + public Collection Messages { get; } = []; + + public bool CompletedTask { get; private set; } + + public WriteOptions? WriteOptions + { + get => _options; + set => _options = value; + } + + public Task CompleteAsync() + { + CompletedTask = true; + return Task.CompletedTask; + } + + public Task WriteAsync(IRequest message) + { + Object.PatchRequest pr = new((Object.PatchRequest)message); + Messages.Add(pr); + return Task.CompletedTask; + } +} + diff --git a/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs b/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs new file mode 100644 index 00000000..ebea8211 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Mocks/SessionMock.cs @@ -0,0 +1,72 @@ +using FrostFS.Session; + +using Google.Protobuf; + +using Grpc.Core; + +using Moq; + +namespace FrostFS.SDK.Tests; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class SessionMocker(string key) : ServiceBase(key) +{ + public byte[]? SessionId { get; set; } + + public byte[]? SessionKey { get; set; } + + public CreateRequest? CreateSessionRequest { get; private set; } + + public Mock GetMock() + { + var mock = new Mock(); + + Random rand = new(); + + if (SessionId == null) + { + SessionId = new byte[16]; + rand.NextBytes(SessionId); + } + + if (SessionKey == null) + { + SessionKey = new byte[33]; + rand.NextBytes(SessionKey); + } + + CreateResponse response = new() + { + Body = new CreateResponse.Types.Body + { + Id = ByteString.CopyFrom(SessionId), + SessionKey = ByteString.CopyFrom(SessionKey) + }, + MetaHeader = ResponseMetaHeader + }; + + response.VerifyHeader = GetResponseVerificationHeader(response); + + mock.Setup(x => x.CreateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns((CreateRequest r, Metadata m, DateTime? dt, CancellationToken ct) => + { + CreateSessionRequest = r; + Metadata = m; + DateTime = dt; + CancellationToken = ct; + + return new AsyncUnaryCall( + Task.FromResult(response), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.OK, string.Empty), + () => ResponseMetaData, + () => { }); + }); + + return mock; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Smoke/Client/ContainerTests/ContainerTests.cs b/src/FrostFS.SDK.Tests/Smoke/Client/ContainerTests/ContainerTests.cs new file mode 100644 index 00000000..f1032dbe --- /dev/null +++ b/src/FrostFS.SDK.Tests/Smoke/Client/ContainerTests/ContainerTests.cs @@ -0,0 +1,286 @@ +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using FrostFS.SDK.Client; +using Grpc.Core; + +namespace FrostFS.SDK.Tests.Smoke; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class ContainerTests : SmokeTestsBase +{ + [Fact] + public async void FailCreateContainerByTimeoutTest() + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + try + { + _ = await CreateContainer(client, + ctx: new CallContext(TimeSpan.FromMilliseconds(1)), + token: null, + unique: true, + backupFactor: 1, + selectors: [], + filter: [], + containerAttributes: [new("testKey1", "testValue1")], + new FrostFsReplica(1)); + + Assert.Fail("Exception is expected"); + } + catch (RpcException ex) + { + Assert.Equal(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + } + + [Fact] + public async void CreateContainerTest1() + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + await Cleanup(client); + + client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + FrostFsContainerId containerId = await CreateContainer(client, + ctx: default, + token: null, + unique: true, + backupFactor: 1, + selectors: [], + filter: [], + containerAttributes: [new("testKey1", "testValue1")], + new FrostFsReplica(3)); + + Assert.NotNull(containerId); + + var container = await client.GetContainerAsync(new PrmContainerGet(containerId), default); + + Assert.NotNull(container); + + Assert.NotNull(container.Attributes); + Assert.Equal("testKey1", container.Attributes[0].Key); + Assert.Equal("testValue1", container.Attributes[0].Value); + + //Assert.Equal("true", container.Attributes[1].Value); + + // for cluster + //Assert.Equal(2, container.Attributes.Count); + //Assert.Equal("__SYSTEM__DISABLE_HOMOMORPHIC_HASHING", container.Attributes[1].Key); + + Assert.True(container.PlacementPolicy.HasValue); + + Assert.Equal(1u, container.PlacementPolicy.Value.BackupFactor); + Assert.True(container.PlacementPolicy.Value.Unique); + Assert.Empty(container.PlacementPolicy.Value.Selectors); + Assert.Empty(container.PlacementPolicy.Value.Filters); + + Assert.Single(container.PlacementPolicy.Value.Replicas); + Assert.Equal(3u, container.PlacementPolicy.Value.Replicas[0].Count); + Assert.Equal(0u, container.PlacementPolicy.Value.Replicas[0].EcParityCount); + Assert.Equal(0u, container.PlacementPolicy.Value.Replicas[0].EcDataCount); + Assert.Equal("", container.PlacementPolicy.Value.Replicas[0].Selector); + + Assert.Equal(OwnerId!.ToString(), container.Owner!.Value); + + Assert.NotNull(container.Version); + + Assert.Equal(Version!.Major, container.Version.Major); + Assert.Equal(Version.Minor, container.Version.Minor); + } + + [Fact] + public async void CreateContainerTest2() + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + await Cleanup(client); + + client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + Collection filters = [ + new ("filter1", "filterKey1", 1, "testValue1", []), + new ("filter2", "filterKey2", 2, "testValue2", [new ("subFilter2", "subFilterKey2", 3, "testValue3",[])]) + ]; + + Collection selectors = [ + new ("selector1") { + Count = 1, + Clause = 1, + Attribute = "attribute1", + Filter = "filter1" + }, + new ("selector2") { + Count = 2, + Clause = 2, + Attribute = "attribute2", + Filter = "filter2" + }, + ]; + + FrostFsContainerId containerId = await CreateContainer(client, + ctx: default, + token: null, + unique: false, + backupFactor: 2, + selectors, + filter: filters, + containerAttributes: [], + new FrostFsReplica(1)); + + Assert.NotNull(containerId); + + var container = await client.GetContainerAsync(new PrmContainerGet(containerId), default); + + Assert.NotNull(container); + + Assert.NotNull(container.Attributes); + //Assert.Single(container.Attributes); + //Assert.Equal("__SYSTEM__DISABLE_HOMOMORPHIC_HASHING", container.Attributes[0].Key); + //Assert.Equal("true", container.Attributes[0].Value); + + Assert.True(container.PlacementPolicy.HasValue); + + Assert.Equal(2u, container.PlacementPolicy.Value.BackupFactor); + Assert.False(container.PlacementPolicy.Value.Unique); + + Assert.NotEmpty(container.PlacementPolicy.Value.Selectors); + + Assert.Equal(2, container.PlacementPolicy.Value.Selectors.Count); + + var selector1 = container.PlacementPolicy.Value.Selectors[0]; + Assert.Equal("selector1", selector1.Name); + Assert.Equal(1, selector1.Clause); + Assert.Equal(1u, selector1.Count); + Assert.Equal("attribute1", selector1.Attribute); + Assert.Equal("filter1", selector1.Filter); + + var selector2 = container.PlacementPolicy.Value.Selectors[1]; + Assert.Equal("selector2", selector2.Name); + Assert.Equal(2, selector2.Clause); + Assert.Equal(2u, selector2.Count); + Assert.Equal("attribute2", selector2.Attribute); + Assert.Equal("filter2", selector2.Filter); + + Assert.NotEmpty(container.PlacementPolicy.Value.Filters); + + Assert.Equal(2, container.PlacementPolicy.Value.Filters.Count); + + var filter1 = container.PlacementPolicy.Value.Filters[0]; + Assert.Equal("filter1", filter1.Name); + Assert.Equal("filterKey1", filter1.Key); + Assert.Equal("testValue1", filter1.Value); + Assert.Equal(1, filter1.Operation); + Assert.Empty(filter1.Filters); + + var filter2 = container.PlacementPolicy.Value.Filters[1]; + Assert.Equal("filter2", filter2.Name); + Assert.Equal("filterKey2", filter2.Key); + Assert.Equal("testValue2", filter2.Value); + Assert.Equal(2, filter2.Operation); + Assert.NotEmpty(filter2.Filters); + + Assert.Single(filter2.Filters); + + var subFilter = filter2.Filters.First(); + Assert.Equal("subFilter2", subFilter.Name); + Assert.Equal("subFilterKey2", subFilter.Key); + Assert.Equal("testValue3", subFilter.Value); + Assert.Equal(3, subFilter.Operation); + Assert.Empty(subFilter.Filters); + + Assert.Single(container.PlacementPolicy.Value.Replicas); + Assert.Equal(1u, container.PlacementPolicy.Value.Replicas[0].Count); + Assert.Equal(0u, container.PlacementPolicy.Value.Replicas[0].EcParityCount); + Assert.Equal(0u, container.PlacementPolicy.Value.Replicas[0].EcDataCount); + Assert.Equal("", container.PlacementPolicy.Value.Replicas[0].Selector); + + Assert.Equal(OwnerId!.ToString(), container.Owner!.Value); + + Assert.NotNull(container.Version); + + Assert.Equal(Version!.Major, container.Version.Major); + Assert.Equal(Version.Minor, container.Version.Minor); + } + + [Theory] + [InlineData(1)] + [InlineData(10)] + public async void ListAndDeleteContainersTest(int countainerCount) + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + await Cleanup(client); + + var tasks = new Task[countainerCount]; + + var createdContainers = new ConcurrentDictionary(); + + for (int i = 0; i < countainerCount; i++) + { + int index = i; + tasks[i] = Task.Run(async () => + { + FrostFsContainerId containerId = await CreateContainer(client, + ctx: default, + token: null, + unique: true, + backupFactor: 1, + selectors: [], + filter: [], + containerAttributes: [new($"testKey{index}", $"testValue{index}")], + new FrostFsReplica(3)); + + createdContainers.TryAdd(containerId.ToString(), index); + }); + } + +#pragma warning disable xUnit1031 // Timeout is used + if (!Task.WaitAll(tasks, 20000)) + { + Assert.Fail("cannot create containers"); + } +#pragma warning restore xUnit1031 + + var containers = client.ListContainersAsync(new PrmContainerGetAll(), default); + + var receivedContainers = new List(); + await foreach (var cntr in containers) + { + receivedContainers.Add(cntr.ToString()); + } + + Assert.Equal(countainerCount, receivedContainers.Count); + + foreach (var cntrId in receivedContainers) + { + if (!createdContainers.TryGetValue(cntrId, out var index)) + { + Assert.Fail("Cannot find corresponding container"); + } + + FrostFsContainerId container = new(cntrId); + var containerInfo = await client.GetContainerAsync(new PrmContainerGet(container), default); + + Assert.NotNull(containerInfo); + Assert.NotNull(containerInfo.Attributes); + + Assert.Contains(new FrostFsAttributePair($"testKey{index}", $"testValue{index}"), containerInfo.Attributes); + } + + tasks = new Task[countainerCount]; + + for (int i = 0; i < receivedContainers.Count; i++) + { + tasks[i] = client.DeleteContainerAsync(new PrmContainerDelete(new FrostFsContainerId(receivedContainers[i]), lightWait), default); + } + + await Task.WhenAll(tasks); + + await foreach (var _ in client.ListContainersAsync(default, default)) + { + Assert.Fail("Containers exist"); + } + } +} diff --git a/src/FrostFS.SDK.Tests/Smoke/Client/MiscTests/InterceptorTest.cs b/src/FrostFS.SDK.Tests/Smoke/Client/MiscTests/InterceptorTest.cs new file mode 100644 index 00000000..9d2fd630 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Smoke/Client/MiscTests/InterceptorTest.cs @@ -0,0 +1,39 @@ +using System.Diagnostics.CodeAnalysis; +using FrostFS.SDK.Client; +using FrostFS.SDK.SmokeTests; + +namespace FrostFS.SDK.Tests.Smoke; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public class InterceptorTests() : SmokeTestsBase +{ + [Fact] + public async void NodeInfoCallbackAndInterceptorTest() + { + bool callbackInvoked = false; + bool interceptorInvoked = false; + + var options = ClientOptions; + options.Value.Callback = (cs) => + { + callbackInvoked = true; + Assert.True(cs.ElapsedMicroSeconds > 0); + }; + + options.Value.Interceptors.Add(new CallbackInterceptor(s => interceptorInvoked = true)); + + var client = FrostFSClient.GetInstance(options, GrpcChannel); + + var result = await client.GetNodeInfoAsync(default); + + Assert.True(callbackInvoked); + Assert.True(interceptorInvoked); + + Assert.Equal(2u, result.Version.Major); + Assert.Equal(13u, result.Version.Minor); + Assert.Equal(NodeState.Online, result.State); + Assert.Equal(33, result.PublicKey.Length); + Assert.NotNull(result.Addresses); + Assert.True(result.Attributes.Count > 0); + } +} diff --git a/src/FrostFS.SDK.Tests/Smoke/Client/NetworkTests/NetworkTests.cs b/src/FrostFS.SDK.Tests/Smoke/Client/NetworkTests/NetworkTests.cs new file mode 100644 index 00000000..229531a5 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Smoke/Client/NetworkTests/NetworkTests.cs @@ -0,0 +1,113 @@ +using System.Diagnostics.CodeAnalysis; +using FrostFS.SDK.Client; +using FrostFS.SDK.SmokeTests; + +namespace FrostFS.SDK.Tests.Smoke; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class SmokeClientTests : SmokeTestsBase +{ + [Fact] + public async void AccountTest() + { + var test = lightWait.Timeout; + + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + var result = await client.GetBalanceAsync(default); + + Assert.NotNull(result); + Assert.True(result.Value == 0); + } + + [Fact] + public async void NodeInfoTest() + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + var result = await client.GetNodeInfoAsync(default); + + Assert.Equal(2u, result.Version.Major); + Assert.Equal(13u, result.Version.Minor); + Assert.Equal(NodeState.Online, result.State); + Assert.Equal(33, result.PublicKey.Length); + Assert.Single(result.Addresses); + Assert.Equal(9, result.Attributes.Count); + //Assert.Equal(2, result.Addresses.Count); + //Assert.Equal(11, result.Attributes.Count); + } + + [Fact] + public async void NodeInfoStatisticsTest() + { + var options = ClientOptions; + + var callbackContent = string.Empty; + options.Value.Callback = (cs) => callbackContent = $"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds"; + + var client = FrostFSClient.GetInstance(options, GrpcChannel); + + var result = await client.GetNodeInfoAsync(default); + + Assert.NotEmpty(callbackContent); + } + + [Fact] + public async void NetworkSettingsTest() + { + var options = ClientOptions; + var client = FrostFSClient.GetInstance(options, GrpcChannel); + + var result = await client.GetNetworkSettingsAsync(default); + + Assert.NotNull(result); + + Assert.True(0u < result.Epoch); + Assert.True(result.HomomorphicHashingDisabled); + Assert.True(result.MaintenanceModeAllowed); + Assert.True(0u < result.MagicNumber); + + Assert.Equal(0u, result.AuditFee); + Assert.Equal(0u, result.BasicIncomeRate); + Assert.Equal(0u, result.ContainerAliasFee); + Assert.Equal(0u, result.ContainerFee); + Assert.Equal(75u, result.EpochDuration); + Assert.Equal(10_000_000_000u, result.InnerRingCandidateFee); + Assert.Equal(12u, result.MaxECDataCount); + Assert.Equal(4u, result.MaxECParityCount); + Assert.Equal(5242880u, result.MaxObjectSize); + Assert.Equal(8000u, result.MsPerBlock); + Assert.Equal(100_000_000u, result.WithdrawFee); + } + + [Fact] + public async void NodeInfoCallbackAndInterceptorTest() + { + bool callbackInvoked = false; + bool interceptorInvoked = false; + + var options = ClientOptions; + options.Value.Callback = (cs) => + { + callbackInvoked = true; + Assert.True(cs.ElapsedMicroSeconds > 0); + }; + + options.Value.Interceptors.Add(new CallbackInterceptor(s => interceptorInvoked = true)); + + var client = FrostFSClient.GetInstance(options, GrpcChannel); + + var result = await client.GetNodeInfoAsync(default); + + Assert.True(callbackInvoked); + Assert.True(interceptorInvoked); + + Assert.Equal(2u, result.Version.Major); + Assert.Equal(13u, result.Version.Minor); + Assert.Equal(NodeState.Online, result.State); + Assert.Equal(33, result.PublicKey.Length); + Assert.NotNull(result.Addresses); + Assert.True(result.Attributes.Count > 0); + } +} diff --git a/src/FrostFS.SDK.Tests/Smoke/Client/ObjectTests/ObjectTests.cs b/src/FrostFS.SDK.Tests/Smoke/Client/ObjectTests/ObjectTests.cs new file mode 100644 index 00000000..473b0769 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Smoke/Client/ObjectTests/ObjectTests.cs @@ -0,0 +1,426 @@ +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Interfaces; +using FrostFS.SDK.Cryptography; +using Xunit.Abstractions; + +namespace FrostFS.SDK.Tests.Smoke; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class ObjectTests(ITestOutputHelper testOutputHelper) : SmokeTestsBase +{ + private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; + + const string clientCut = "clientCut"; + const string serverCut = "serverCut"; + const string singleObject = "singleObject"; + + [Theory] + [InlineData(true, 1, 1)] + [InlineData(false, 1, 1)] + [InlineData(true, 1, 3)] + [InlineData(false, 1, 3)] + [InlineData(true, 2, 3)] + [InlineData(false, 2, 3)] + [InlineData(true, 2, 1)] + [InlineData(false, 2, 1)] + public async void FullScenario(bool unique, uint backupFactor, uint replicas) + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + _testOutputHelper.WriteLine("client created"); + + await Cleanup(client); + _testOutputHelper.WriteLine("existing containers removed"); + + FrostFsContainerId containerId = await CreateContainer(client, + ctx: default, + token: null, + unique: unique, + backupFactor: backupFactor, + selectors: [], + filter: [], + containerAttributes: [new FrostFsAttributePair("contAttrKey", "contAttrValue")], + new FrostFsReplica(replicas)); + + Assert.NotNull(containerId); + _testOutputHelper.WriteLine("container created"); + + await AddObjectRules(client, containerId); + _testOutputHelper.WriteLine("rules added"); + + await RunSuite(client, containerId); + } + + private async Task RunSuite(IFrostFSClient client, FrostFsContainerId containerId) + { + int[] objectSizes = [1, 257, 5 * 1024 * 1024, 20 * 1024 * 1024]; + + string[] objectTypes = [clientCut, serverCut, singleObject]; + + foreach (var objectSize in objectSizes) + { + _testOutputHelper.WriteLine($"test set for object size {objectSize}"); + + var bytes = GetRandomBytes(objectSize); + var hash = SHA256.HashData(bytes); + + FrostFsObjectId objectId; + foreach (var type in objectTypes) + { + switch (type) + { + case serverCut: + objectId = await CreateObjectServerCut(client, containerId, bytes); + _testOutputHelper.WriteLine($"\tserver side cut"); + break; + case clientCut: + objectId = await CreateObjectClientCut(client, containerId, bytes); + _testOutputHelper.WriteLine($"\tclient side cut"); + break; + case singleObject: + if (objectSize > 1 * 1024 * 1024) + continue; + objectId = await PutSingleObject(client, containerId, bytes); + _testOutputHelper.WriteLine($"\tput single object"); + + break; + default: + throw new ArgumentException("unexpected object type"); + } + + Assert.NotNull(objectId); + + _testOutputHelper.WriteLine($"\tobject created"); + + var ecdsaKey = ClientOptions.Value.Key.LoadWif(); + var owner = FrostFsOwner.FromKey(ecdsaKey); + + FrostFsHeaderResult expected = new() + { + HeaderInfo = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + attributes: [new FrostFsAttributePair("fileName", "test")], + split: null, + owner: owner, + version: new FrostFsVersion(2, 13)) + { + PayloadLength = (ulong)objectSize, + PayloadCheckSum = hash + } + }; + + await ValidateHeader(client, containerId, objectId, expected); + _testOutputHelper.WriteLine($"\theader validated"); + + await ValidateContent(client, containerId, hash, objectId); + _testOutputHelper.WriteLine($"\tcontent validated"); + + await ValidateFilters(client, containerId, objectId, null, (ulong)bytes.Length); + _testOutputHelper.WriteLine($"\tfilters validated"); + + if (type != clientCut && bytes.Length > 1024 + 64 && bytes.Length < 20 * 1024 * 1024) + { + // patch payload only + await ValidatePatch(client, containerId, bytes, true, objectId, [], false); + + // patch attributes only + await ValidatePatch(client, containerId, bytes, false, objectId, [new("a1", "v1"), new("a2", "v2")], false); + + // patch payload and attributes + await ValidatePatch(client, containerId, bytes, true, objectId, [new("a3", "v3"), new("a4", "v4")], true); + _testOutputHelper.WriteLine($"\tpatch validated"); + } + + await ValidateRange(client, containerId, bytes, objectId); + _testOutputHelper.WriteLine($"\trange validated"); + + await ValidateRangeHash(client, containerId, bytes, objectId); + _testOutputHelper.WriteLine($"\trange hash validated"); + + await RemoveObject(client, containerId, objectId); + _testOutputHelper.WriteLine($"\tobject removed"); + } + } + } + + private static async Task ValidateRangeHash(IFrostFSClient client, FrostFsContainerId containerId, byte[] bytes, FrostFsObjectId objectId) + { + if (bytes.Length < 200) + return; + + var rangeParam = new PrmRangeHashGet(containerId, objectId, [new FrostFsRange(100, 64)], bytes); + + var hashes = await client.GetRangeHashAsync(rangeParam, default); + + var objectRange = bytes.AsMemory().Slice(100, 64).ToArray(); + var expectedHash = SHA256.HashData(objectRange); + + foreach (var h in hashes) + { + var x = h[..32].ToArray(); + Assert.NotNull(x); + Assert.True(x.Length > 0); + + // Assert.True(expectedHash.SequenceEqual(h.ToArray())); + } + } + + private async Task ValidateRange(IFrostFSClient client, FrostFsContainerId containerId, byte[] bytes, FrostFsObjectId objectId) + { + if (bytes.Length < 100) + return; + + await CheckRange(client, containerId, bytes, objectId, new FrostFsRange(0, 50)); + await CheckRange(client, containerId, bytes, objectId, new FrostFsRange(50, 50)); + + await CheckRange(client, containerId, bytes, objectId, new FrostFsRange((ulong)bytes.Length - 100, 100)); + + if (bytes.Length >= 6200) + await CheckRange(client, containerId, bytes, objectId, new FrostFsRange(6000, 100)); + } + + private async Task CheckRange(IFrostFSClient client, FrostFsContainerId containerId, byte[] bytes, FrostFsObjectId objectId, FrostFsRange range) + { + var rangeParam = new PrmRangeGet(containerId, objectId, range); + + var rangeReader = await client.GetRangeAsync(rangeParam, default); + + var rangeBytes = new byte[rangeParam.Range.Length]; + MemoryStream ms = new(rangeBytes); + + ReadOnlyMemory? chunk; + int readBytes = 0; + while ((chunk = await rangeReader!.ReadChunk()) != null) + { + readBytes += chunk.Value.Length; + ms.Write(chunk.Value.Span); + } + + Assert.Equal(range.Length, (ulong)readBytes); + + Assert.Equal(SHA256.HashData(bytes.AsSpan().Slice((int)range.Offset, (int)range.Length)), SHA256.HashData(rangeBytes)); + + _testOutputHelper.WriteLine($"\t\trange {range.Offset};{range.Length} validated"); + } + + private static async Task ValidatePatch( + IFrostFSClient client, + FrostFsContainerId containerId, + byte[] bytes, + bool patchPayload, + FrostFsObjectId objectId, + FrostFsAttributePair[] attributes, + bool replaceAttributes) + { + byte[]? patch = null; + FrostFsRange range = new(); + if (patchPayload) + { + patch = new byte[1024]; + for (int i = 0; i < patch.Length; i++) + { + patch[i] = 32; + } + + range = new FrostFsRange(64, (ulong)patch.Length); + } + + var patchParams = new PrmObjectPatch( + new FrostFsAddress(containerId, objectId), + payload: new MemoryStream(patch ?? []), + maxChunkLength: 1024, + range: range, + replaceAttributes: replaceAttributes, + newAttributes: attributes); + + var newIbjId = await client.PatchObjectAsync(patchParams, default); + + var @object = await client.GetObjectAsync(new PrmObjectGet(containerId, newIbjId), default); + + if (patchPayload) + { + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + ReadOnlyMemory? chunk; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk.Value.Span); + } + + for (int i = 0; i < (int)range.Offset; i++) + Assert.Equal(downloadedBytes[i], bytes[i]); + + var rangeEnd = range.Offset + range.Length; + + for (int i = (int)range.Offset; i < (int)rangeEnd; i++) + Assert.Equal(downloadedBytes[i], patch[i - (int)range.Offset]); + + for (int i = (int)rangeEnd; i < bytes.Length; i++) + Assert.Equal(downloadedBytes[i], bytes[i]); + } + + if (attributes != null && attributes.Length > 0) + { + foreach (var newAttr in attributes) + { + var i = @object!.Header!.Attributes!.Count(p => p.Key == newAttr.Key && p.Value == newAttr.Value); + Assert.Equal(1, i); + } + } + } + + private async Task ValidateFilters(IFrostFSClient client, FrostFsContainerId containerId, FrostFsObjectId objectId, SplitId? splitId, ulong length) + { + var ecdsaKey = keyString.LoadWif(); + + var networkInfo = await client.GetNetmapSnapshotAsync(default); + + await CheckFilter(client, containerId, new FilterByContainerId(FrostFsMatchType.Equals, containerId)); + + await CheckFilter(client, containerId, new FilterByOwnerId(FrostFsMatchType.Equals, FrostFsOwner.FromKey(ecdsaKey))); + + if (splitId != null) + { + await CheckFilter(client, containerId, new FilterBySplitId(FrostFsMatchType.Equals, splitId)); + } + + await CheckFilter(client, containerId, new FilterByAttributePair(FrostFsMatchType.Equals, "fileName", "test")); + + await CheckFilter(client, containerId, new FilterByObjectId(FrostFsMatchType.Equals, objectId)); + + await CheckFilter(client, containerId, new FilterByVersion(FrostFsMatchType.Equals, networkInfo.NodeInfoCollection[0].Version)); + + await CheckFilter(client, containerId, new FilterByPayloadLength(FrostFsMatchType.Equals, length)); + + await CheckFilter(client, containerId, new FilterByPhysicallyStored()); + } + + private static async Task RemoveObject(IFrostFSClient client, FrostFsContainerId containerId, FrostFsObjectId objectId) + { + await client.DeleteObjectAsync(new PrmObjectDelete(containerId, objectId), default); + + try + { + _ = await client.GetObjectAsync( + new PrmObjectGet(containerId, objectId), + default); + + Assert.Fail("Exception is expected here"); + } + catch (FrostFsResponseException ex) + { + Assert.Equal("object already removed", ex.Status!.Message); + } + } + + private static async Task ValidateContent(IFrostFSClient client, FrostFsContainerId containerId, byte[] hash, FrostFsObjectId objectId) + { + var @object = await client.GetObjectAsync( + new PrmObjectGet(containerId, objectId), + default); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + ReadOnlyMemory? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk.Value.Span); + } + + Assert.Equal(hash, SHA256.HashData(downloadedBytes)); + } + + private static async Task ValidateHeader( + IFrostFSClient client, + FrostFsContainerId containerId, + FrostFsObjectId objectId, + FrostFsHeaderResult expected) + { + var res = await client.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objectId, default), default); + + var objHeader = res.HeaderInfo; + Assert.NotNull(objHeader); + + Assert.Equal(containerId.GetValue(), objHeader.ContainerId.GetValue()); + + Assert.Equal(expected.HeaderInfo!.OwnerId!.Value, objHeader.OwnerId!.Value); + Assert.Equal(expected.HeaderInfo.Version!.Major, objHeader.Version!.Major); + Assert.Equal(expected.HeaderInfo.Version!.Minor, objHeader.Version!.Minor); + + Assert.Equal(expected.HeaderInfo.PayloadLength, objHeader.PayloadLength); + + Assert.Equal(expected.HeaderInfo.ObjectType, objHeader.ObjectType); + + if (expected.HeaderInfo.Attributes != null) + { + Assert.NotNull(objHeader.Attributes); + Assert.Equal(expected.HeaderInfo.Attributes.Count, objHeader.Attributes.Count); + + Assert.True(expected.HeaderInfo.Attributes.SequenceEqual(objHeader.Attributes)); + } + + Assert.Null(objHeader.Split); + } + + private static async Task CreateObjectServerCut(IFrostFSClient client, FrostFsContainerId containerId, byte[] bytes) + { + var header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]); + + var param = new PrmObjectPut(header); + + var objectWriter = await client.PutObjectAsync(param, default).ConfigureAwait(true); + + await objectWriter.WriteAsync(bytes); + return await objectWriter.CompleteAsync(); + } + + private static async Task CreateObjectClientCut(IFrostFSClient client, FrostFsContainerId containerId, byte[] bytes) + { + var header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]); + + var param = new PrmObjectClientCutPut(header, payload: new MemoryStream(bytes)); + + return await client.PutClientCutObjectAsync(param, default).ConfigureAwait(true); + } + + private static async Task PutSingleObject(IFrostFSClient client, FrostFsContainerId containerId, byte[] bytes) + { + var header = new FrostFsObjectHeader( + containerId: containerId, + type: FrostFsObjectType.Regular, + [new FrostFsAttributePair("fileName", "test")]); + + + var obj = new FrostFsObject(header) { SingleObjectPayload = bytes }; + + var param = new PrmSingleObjectPut(obj); + + return await client.PutSingleObjectAsync(param, default).ConfigureAwait(true); + } + + private static async Task CheckFilter(IFrostFSClient client, FrostFsContainerId containerId, IObjectFilter filter) + { + var resultObjectsCount = 0; + + PrmObjectSearch searchParam = new(containerId, null, [], filter); + + await foreach (var objId in client.SearchObjectsAsync(searchParam, default)) + { + resultObjectsCount++; + var objHeader = await client.GetObjectHeadAsync(new PrmObjectHeadGet(containerId, objId), default); + } + + Assert.True(0 < resultObjectsCount, $"Filter for {filter.Key} doesn't work"); + } +} diff --git a/src/FrostFS.SDK.Tests/Smoke/Client/SessionTests/SessionTests.cs b/src/FrostFS.SDK.Tests/Smoke/Client/SessionTests/SessionTests.cs new file mode 100644 index 00000000..ab14d8e5 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Smoke/Client/SessionTests/SessionTests.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; +using FrostFS.SDK.Client; + +namespace FrostFS.SDK.Tests.Smoke; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class SessionTests : SmokeTestsBase +{ + [Fact] + public async void GetSessionTest() + { + var client = FrostFSClient.GetInstance(ClientOptions, GrpcChannel); + + var token = await client.CreateSessionAsync(new(100), default); + + Assert.NotNull(token); + Assert.NotEqual(Guid.Empty, token.Id); + Assert.Equal(33, token.SessionKey.Length); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Smoke/SmokeTestsBase.cs b/src/FrostFS.SDK.Tests/Smoke/SmokeTestsBase.cs new file mode 100644 index 00000000..5e6998b4 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Smoke/SmokeTestsBase.cs @@ -0,0 +1,129 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using System.Text; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Interfaces; +using FrostFS.SDK.Cryptography; + +using Grpc.Core; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests.Smoke; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public abstract class SmokeTestsBase +{ + // cluster + // internal readonly string url = "http://10.78.128.190:8080"; + // internal readonly string keyString = "L47c3bunc6bJd7uEAfPUae2VkyupFR9nizoH6jfPonzQxijqH2Ba"; + + // WSL2 + internal readonly string url = "http://172.29.238.97:8080"; // "http://172.20.8.23:8080"; + internal readonly string keyString = "KzPXA6669m2pf18XmUdoR8MnP1pi1PMmefiFujStVFnv7WR5SRmK"; + + protected ECDsa? Key { get; } + + protected FrostFsOwner? OwnerId { get; } + + protected FrostFsVersion? Version { get; } + + protected CallContext? Ctx { get; } + + protected SmokeTestsBase() + { + Key = keyString.LoadWif(); + OwnerId = FrostFsOwner.FromKey(Key); + Version = new FrostFsVersion(2, 13); + } + + protected static Func GrpcChannel => (url) => Grpc.Net.Client.GrpcChannel.ForAddress(new Uri(url)); + + protected IOptions ClientOptions => Options.Create(new ClientSettings { Key = keyString, Host = url }); + + protected static readonly PrmWait lightWait = new(100, 1); + + protected static byte[] GetRandomBytes(int size) + { + Random rnd = new(); + var bytes = new byte[size]; + rnd.NextBytes(bytes); + return bytes; + } + + protected static async Task CreateContainer(IFrostFSClient client, + CallContext ctx, + FrostFsSessionToken? token, + bool unique, + uint backupFactor, + Collection selectors, + Collection filter, + Collection containerAttributes, + params FrostFsReplica[] replicas) + { + ArgumentNullException.ThrowIfNull(client); + + var networkSettings = await client.GetNetworkSettingsAsync(ctx); + + var attributes = containerAttributes ?? []; + + if (networkSettings.HomomorphicHashingDisabled) + attributes.Add(new("__SYSTEM__DISABLE_HOMOMORPHIC_HASHING", "true")); + + var containerInfo = new FrostFsContainerInfo( + new FrostFsPlacementPolicy(unique, backupFactor, selectors, filter, replicas), + [.. attributes]); + + var createContainerParam = new PrmContainerCreate( + containerInfo, + PrmWait.DefaultParams, + token, + xheaders: ["key1", "value1"]); + + return await client.PutContainerAsync(createContainerParam, ctx); + } + + protected static async Task Cleanup(IFrostFSClient client) + { + ArgumentNullException.ThrowIfNull(client); + + var tasks = new List(); + await foreach (var cid in client.ListContainersAsync(default, default)) + { + tasks.Add(client.DeleteContainerAsync(new PrmContainerDelete(cid, lightWait), default)); + } + + await Task.WhenAll(tasks); + } + + protected static async Task AddObjectRules(IFrostFSClient client, FrostFsContainerId containerId) + { + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(containerId); + + var addChainPrm = new PrmApeChainAdd( + new FrostFsChainTarget(FrostFsTargetType.Container, containerId.GetValue()), + new FrostFsChain + { + ID = Encoding.ASCII.GetBytes("chain-id-test"), + Rules = [ + new FrostFsRule + { + Status = RuleStatus.Allow, + Actions = new Actions(inverted: false, names: ["*"]), + Resources = new Resources (inverted: false, names: [$"native:object/*"]), + Any = false, + Conditions = [] + } + ], + MatchType = RuleMatchType.DenyPriority + } + ); + + await client.AddChainAsync(addChainPrm, default); + + await Task.Delay(8000); + } +} diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_default.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_default.json new file mode 100644 index 00000000..25675f21 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_default.json @@ -0,0 +1,100 @@ +{ + "name": "default CBF is 3", + "nodes": [ + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "St.Petersburg" + } + ] + }, + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "Moscow" + } + ] + }, + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "DE" + }, + { + "key": "City", + "value": "Berlin" + } + ] + }, + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "FR" + }, + { + "key": "City", + "value": "Paris" + } + ] + } + ], + "tests": [ + { + "name": "set default CBF", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "EU" + } + ], + "containerBackupFactor": 0, + "selectors": [ + { + "name": "EU", + "count": 1, + "clause": "SAME", + "attribute": "Location", + "filter": "*" + } + ], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_minimal.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_minimal.json new file mode 100644 index 00000000..7553dd65 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_minimal.json @@ -0,0 +1,101 @@ +{ + "name": "Real node count multiplier is in range [1, specified CBF]", + "nodes": [ + { + "attributes": [ + { + "key": "ID", + "value": "1" + }, + { + "key": "Country", + "value": "DE" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "2" + }, + { + "key": "Country", + "value": "DE" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "3" + }, + { + "key": "Country", + "value": "DE" + } + ] + } + ], + "tests": [ + { + "name": "select 2, CBF is 2", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "X" + } + ], + "containerBackupFactor": 2, + "selectors": [ + { + "name": "X", + "count": 2, + "clause": "SAME", + "attribute": "Country", + "filter": "*" + } + ], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2 + ] + ] + }, + { + "name": "select 3, CBF is 2", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "X" + } + ], + "containerBackupFactor": 2, + "selectors": [ + { + "name": "X", + "count": 3, + "clause": "SAME", + "attribute": "Country", + "filter": "*" + } + ], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_requirements.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_requirements.json new file mode 100644 index 00000000..279d9196 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/cbf_requirements.json @@ -0,0 +1,156 @@ +{ + "name": "CBF requirements", + "nodes": [ + { + "attributes": [ + { + "key": "ID", + "value": "1" + }, + { + "key": "Attr", + "value": "Same" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "2" + }, + { + "key": "Attr", + "value": "Same" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "3" + }, + { + "key": "Attr", + "value": "Same" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "4" + }, + { + "key": "Attr", + "value": "Same" + } + ] + } + ], + "tests": [ + { + "name": "default CBF, no selector", + "policy": { + "replicas": [ + { + "count": 2 + } + ], + "containerBackupFactor": 0, + "selectors": [], + "filters": [] + }, + "result": [ + [ + 0, + 2, + 1, + 3 + ] + ] + }, + { + "name": "explicit CBF, no selector", + "policy": { + "replicas": [ + { + "count": 2 + } + ], + "containerBackupFactor": 3, + "selectors": [], + "filters": [] + }, + "result": [ + [ + 0, + 2, + 1, + 3 + ] + ] + }, + { + "name": "select distinct, weak CBF", + "policy": { + "replicas": [ + { + "count": 2, + "selector": "X" + } + ], + "containerBackupFactor": 3, + "selectors": [ + { + "name": "X", + "count": 2, + "clause": "DISTINCT", + "filter": "*" + } + ], + "filters": [] + }, + "result": [ + [ + 0, + 2, + 1, + 3 + ] + ] + }, + { + "name": "select same, weak CBF", + "policy": { + "replicas": [ + { + "count": 2, + "selector": "X" + } + ], + "containerBackupFactor": 3, + "selectors": [ + { + "name": "X", + "count": 2, + "clause": "SAME", + "attribute": "Attr", + "filter": "*" + } + ], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2, + 3 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_complex.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_complex.json new file mode 100644 index 00000000..c2c19c07 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_complex.json @@ -0,0 +1,345 @@ +{ + "name": "compound filter", + "nodes": [ + { + "attributes": [ + { + "key": "Storage", + "value": "SSD" + }, + { + "key": "Rating", + "value": "10" + }, + { + "key": "IntField", + "value": "100" + }, + { + "key": "Param", + "value": "Value1" + } + ] + } + ], + "tests": [ + { + "name": "good", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "StorageSSD", + "key": "Storage", + "op": "EQ", + "value": "SSD", + "filters": [] + }, + { + "name": "GoodRating", + "key": "Rating", + "op": "GE", + "value": "4", + "filters": [] + }, + { + "name": "Main", + "op": "AND", + "filters": [ + { + "name": "StorageSSD", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "key": "IntField", + "op": "LT", + "value": "123", + "filters": [] + }, + { + "name": "GoodRating", + "op": "Unspecified", + "filters": [] + }, + { + "op": "OR", + "filters": [ + { + "key": "Param", + "op": "EQ", + "value": "Value1", + "filters": [] + }, + { + "key": "Param", + "op": "EQ", + "value": "Value2", + "filters": [] + } + ] + } + ] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "bad storage type", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "StorageSSD", + "key": "Storage", + "op": "EQ", + "value": "HDD", + "filters": [] + }, + { + "name": "GoodRating", + "key": "Rating", + "op": "GE", + "value": "4", + "filters": [] + }, + { + "name": "Main", + "op": "AND", + "filters": [ + { + "name": "StorageSSD", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "key": "IntField", + "op": "LT", + "value": "123", + "filters": [] + }, + { + "name": "GoodRating", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "op": "OR", + "filters": [ + { + "name": "", + "key": "Param", + "op": "EQ", + "value": "Value1", + "filters": [] + }, + { + "name": "", + "key": "Param", + "op": "EQ", + "value": "Value2", + "filters": [] + } + ] + } + ] + } + ] + } + }, + { + "name": "bad rating", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "StorageSSD", + "key": "Storage", + "op": "EQ", + "value": "SSD", + "filters": [] + }, + { + "name": "GoodRating", + "key": "Rating", + "op": "GE", + "value": "15", + "filters": [] + }, + { + "name": "Main", + "op": "AND", + "filters": [ + { + "name": "StorageSSD", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "key": "IntField", + "op": "LT", + "value": "123", + "filters": [] + }, + { + "name": "GoodRating", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "op": "OR", + "filters": [ + { + "name": "", + "key": "Param", + "op": "EQ", + "value": "Value1", + "filters": [] + }, + { + "name": "", + "key": "Param", + "op": "EQ", + "value": "Value2", + "filters": [] + } + ] + } + ] + } + ] + } + }, + { + "name": "bad param", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "StorageSSD", + "key": "Storage", + "op": "EQ", + "value": "SSD", + "filters": [] + }, + { + "name": "GoodRating", + "key": "Rating", + "op": "GE", + "value": "4", + "filters": [] + }, + { + "name": "Main", + "op": "AND", + "filters": [ + { + "name": "StorageSSD", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "key": "IntField", + "op": "LT", + "value": "123", + "filters": [] + }, + { + "name": "GoodRating", + "op": "Unspecified", + "filters": [] + }, + { + "name": "", + "op": "OR", + "filters": [ + { + "name": "", + "key": "Param", + "op": "EQ", + "value": "Value0", + "filters": [] + }, + { + "name": "", + "key": "Param", + "op": "EQ", + "value": "Value2", + "filters": [] + } + ] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_invalid_integer.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_invalid_integer.json new file mode 100644 index 00000000..16cdb9ec --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_invalid_integer.json @@ -0,0 +1,81 @@ +{ + "name": "invalid integer field", + "nodes": [ + { + "attributes": [ + { + "key": "IntegerField", + "value": "true" + } + ] + }, + { + "attributes": [ + { + "key": "IntegerField", + "value": "str" + } + ] + } + ], + "tests": [ + { + "name": "empty string is not casted to 0", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "IntegerField", + "op": "LE", + "value": "8", + "filters": [] + } + ] + } + }, + { + "name": "non-empty string is not casted to a number", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "IntegerField", + "op": "GE", + "value": "0", + "filters": [] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_simple.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_simple.json new file mode 100644 index 00000000..43142d4c --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/filter_simple.json @@ -0,0 +1,397 @@ +{ + "name": "single-op filters", + "nodes": [ + { + "attributes": [ + { + "key": "Rating", + "value": "4" + }, + { + "key": "Country", + "value": "Germany" + } + ] + } + ], + "tests": [ + { + "name": "GE true", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "GE", + "value": "4", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "GE false", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "GE", + "value": "5", + "filters": [] + } + ] + } + }, + { + "name": "GT true", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "GT", + "value": "3", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "GT false", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "GT", + "value": "4", + "filters": [] + } + ] + } + }, + { + "name": "LE true", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "LE", + "value": "4", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "LE false", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "LE", + "value": "3", + "filters": [] + } + ] + } + }, + { + "name": "LT true", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "LT", + "value": "5", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "LT false", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Rating", + "op": "LT", + "value": "4", + "filters": [] + } + ] + } + }, + { + "name": "EQ true", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Country", + "op": "EQ", + "value": "Germany", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "EQ false", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Country", + "op": "EQ", + "value": "China", + "filters": [] + } + ] + } + }, + { + "name": "NE true", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Country", + "op": "NE", + "value": "France", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "NE false", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "S" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "S", + "count": 1, + "clause": "DISTINCT", + "filter": "Main" + } + ], + "filters": [ + { + "name": "Main", + "key": "Country", + "op": "NE", + "value": "Germany", + "filters": [] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/hrw_sort.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/hrw_sort.json new file mode 100644 index 00000000..a6dc75c1 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/hrw_sort.json @@ -0,0 +1,225 @@ +{ + "name": "HRW ordering", + "nodes": [ + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + }, + { + "key": "Price", + "value": "2" + }, + { + "key": "Capacity", + "value": "10000" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + }, + { + "key": "Price", + "value": "4" + }, + { + "key": "Capacity", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "France" + }, + { + "key": "Price", + "value": "3" + }, + { + "key": "Capacity", + "value": "10" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Price", + "value": "2" + }, + { + "key": "Capacity", + "value": "10000" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Price", + "value": "1" + }, + { + "key": "Capacity", + "value": "10000" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Capacity", + "value": "10000" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "France" + }, + { + "key": "Price", + "value": "100" + }, + { + "key": "Capacity", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "France" + }, + { + "key": "Price", + "value": "7" + }, + { + "key": "Capacity", + "value": "10000" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Price", + "value": "2" + }, + { + "key": "Capacity", + "value": "1" + } + ] + } + ], + "tests": [ + { + "name":"select 3 nodes in 3 distinct countries, same placement", + "policy": { + "containerBackupFactor": 1, + "filters": [], + "replicas": [ + { + "count": 1, + "selector": "Main" + } + ], + "selectors": [ + { + "attribute": "Country", + "clause": "DISTINCT", + "count": 3, + "filter": "*", + "name": "Main" + } + ] + }, + "pivot": "Y29udGFpbmVySUQ=", + "result": [[ + 5, + 0, + 7 + ]], + "placement": { + "pivot": "b2JqZWN0SUQ=", + "result": [[ + 5, + 0, + 7 + ]] + } + }, + { + "name":"select 6 nodes in 3 distinct countries, different placement", + "policy": { + "containerBackupFactor": 2, + "filters": [ + ], + "replicas": [ + { + "count": 1, + "selector": "Main" + } + ], + "selectors": [ + { + "attribute": "Country", + "clause": "DISTINCT", + "count": 3, + "filter": "*", + "name": "Main" + } + ] + }, + "pivot": "Y29udGFpbmVySUQ=", + "result": [[ + 5, + 4, + 0, + 1, + 7, + 2]], + + "placement": { + "pivot": "b2JqZWN0SUQ=", + "result": [[ + 5, + 4, + 0, + 7, + 2, + 1]] + } + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/issue213.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/issue213.json new file mode 100644 index 00000000..44302979 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/issue213.json @@ -0,0 +1,107 @@ +{ + "name": "unnamed selector (nspcc-dev/neofs-api-go#213)", + "nodes": [ + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "Russia" + }, + { + "key": "City", + "value": "Moscow" + } + ] + }, + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "Russia" + }, + { + "key": "City", + "value": "Saint-Petersburg" + } + ] + }, + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "Sweden" + }, + { + "key": "City", + "value": "Stockholm" + } + ] + }, + { + "attributes": [ + { + "key": "Location", + "value": "Europe" + }, + { + "key": "Country", + "value": "Finalnd" + }, + { + "key": "City", + "value": "Helsinki" + } + ] + } + ], + "tests": [ + { + "name": "test", + "policy": { + "replicas": [ + { + "count": 4 + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "", + "count": 4, + "clause": "DISTINCT", + "filter": "LOC_EU" + } + ], + "filters": [ + { + "name": "LOC_EU", + "key": "Location", + "op": "EQ", + "value": "Europe", + "filters": [] + } + ] + }, + "result": [ + [ + 0, + 1, + 2, + 3 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/many_selects.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/many_selects.json new file mode 100644 index 00000000..e76a441d --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/many_selects.json @@ -0,0 +1,279 @@ +{ + "name": "single-op filters", + "nodes": [ + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Rating", + "value": "1" + }, + { + "key": "City", + "value": "SPB" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + }, + { + "key": "Rating", + "value": "5" + }, + { + "key": "City", + "value": "Berlin" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Rating", + "value": "6" + }, + { + "key": "City", + "value": "Moscow" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "France" + }, + { + "key": "Rating", + "value": "4" + }, + { + "key": "City", + "value": "Paris" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "France" + }, + { + "key": "Rating", + "value": "1" + }, + { + "key": "City", + "value": "Lyon" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Rating", + "value": "5" + }, + { + "key": "City", + "value": "SPB" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Rating", + "value": "7" + }, + { + "key": "City", + "value": "Moscow" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + }, + { + "key": "Rating", + "value": "3" + }, + { + "key": "City", + "value": "Darmstadt" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + }, + { + "key": "Rating", + "value": "7" + }, + { + "key": "City", + "value": "Frankfurt" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Rating", + "value": "9" + }, + { + "key": "City", + "value": "SPB" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + }, + { + "key": "Rating", + "value": "9" + }, + { + "key": "City", + "value": "SPB" + } + ] + } + ], + "tests": [ + { + "name": "Select", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "SameRU" + }, + { + "count": 1, + "selector": "DistinctRU" + }, + { + "count": 1, + "selector": "Good" + }, + { + "count": 1, + "selector": "Main" + } + ], + "containerBackupFactor": 2, + "selectors": [ + { + "name": "SameRU", + "count": 2, + "clause": "SAME", + "attribute": "City", + "filter": "FromRU" + }, + { + "name": "DistinctRU", + "count": 2, + "clause": "DISTINCT", + "attribute": "City", + "filter": "FromRU" + }, + { + "name": "Good", + "count": 2, + "clause": "DISTINCT", + "attribute": "Country", + "filter": "Good" + }, + { + "name": "Main", + "count": 3, + "clause": "DISTINCT", + "attribute": "Country", + "filter": "*" + } + ], + "filters": [ + { + "name": "FromRU", + "key": "Country", + "op": "EQ", + "value": "Russia" + }, + { + "name": "Good", + "key": "Rating", + "op": "GE", + "value": "4" + } + ] + }, + "result": [ + [ + 0, + 5, + 9, + 10 + ], + [ + 2, + 6, + 0, + 5 + ], + [ + 1, + 8, + 2, + 5 + ], + [ + 3, + 4, + 1, + 7, + 0, + 2 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/multiple_rep.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/multiple_rep.json new file mode 100644 index 00000000..e01ad627 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/multiple_rep.json @@ -0,0 +1,93 @@ +{ + "name": "multiple replicas (#215)", + "nodes": [ + { + "attributes": [ + { + "key": "City", + "value": "Saint-Petersburg" + } + ] + }, + { + "attributes": [ + { + "key": "City", + "value": "Moscow" + } + ] + }, + { + "attributes": [ + { + "key": "City", + "value": "Berlin" + } + ] + }, + { + "attributes": [ + { + "key": "City", + "value": "Paris" + } + ] + } + ], + "tests": [ + { + "name": "test", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "LOC_SPB_PLACE" + }, + { + "count": 1, + "selector": "LOC_MSK_PLACE" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "LOC_SPB_PLACE", + "count": 1, + "clause": "UNSPECIFIED", + "filter": "LOC_SPB" + }, + { + "name": "LOC_MSK_PLACE", + "count": 1, + "clause": "UNSPECIFIED", + "filter": "LOC_MSK" + } + ], + "filters": [ + { + "name": "LOC_SPB", + "key": "City", + "op": "EQ", + "value": "Saint-Petersburg", + "filters": [] + }, + { + "name": "LOC_MSK", + "key": "City", + "op": "EQ", + "value": "Moscow", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ], + [ + 1 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/multiple_rep_asymmetric.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/multiple_rep_asymmetric.json new file mode 100644 index 00000000..55390b20 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/multiple_rep_asymmetric.json @@ -0,0 +1,328 @@ +{ + "name": "multiple REP, asymmetric", + "nodes": [ + { + "attributes": [ + { + "key": "ID", + "value": "1" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "St.Petersburg" + }, + { + "key": "SSD", + "value": "0" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "2" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "St.Petersburg" + }, + { + "key": "SSD", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "3" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "Moscow" + }, + { + "key": "SSD", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "4" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "Moscow" + }, + { + "key": "SSD", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "5" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "St.Petersburg" + }, + { + "key": "SSD", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "6" + }, + { + "key": "Continent", + "value": "NA" + }, + { + "key": "City", + "value": "NewYork" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "7" + }, + { + "key": "Continent", + "value": "AF" + }, + { + "key": "City", + "value": "Cairo" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "8" + }, + { + "key": "Continent", + "value": "AF" + }, + { + "key": "City", + "value": "Cairo" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "9" + }, + { + "key": "Continent", + "value": "SA" + }, + { + "key": "City", + "value": "Lima" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "10" + }, + { + "key": "Continent", + "value": "AF" + }, + { + "key": "City", + "value": "Cairo" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "11" + }, + { + "key": "Continent", + "value": "NA" + }, + { + "key": "City", + "value": "NewYork" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "12" + }, + { + "key": "Continent", + "value": "NA" + }, + { + "key": "City", + "value": "LosAngeles" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "13" + }, + { + "key": "Continent", + "value": "SA" + }, + { + "key": "City", + "value": "Lima" + } + ] + } + ], + "tests": [ + { + "name": "test", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "SPB" + }, + { + "count": 2, + "selector": "Americas" + } + ], + "containerBackupFactor": 2, + "selectors": [ + { + "name": "SPB", + "count": 1, + "clause": "SAME", + "attribute": "City", + "filter": "SPBSSD" + }, + { + "name": "Americas", + "count": 2, + "clause": "DISTINCT", + "attribute": "City", + "filter": "Americas" + } + ], + "filters": [ + { + "name": "SPBSSD", + "op": "AND", + "filters": [ + { + "name": "", + "key": "Country", + "op": "EQ", + "value": "RU", + "filters": [] + }, + { + "name": "", + "key": "City", + "op": "EQ", + "value": "St.Petersburg", + "filters": [] + }, + { + "name": "", + "key": "SSD", + "op": "EQ", + "value": "1", + "filters": [] + } + ] + }, + { + "name": "Americas", + "op": "OR", + "filters": [ + { + "name": "", + "key": "Continent", + "op": "EQ", + "value": "NA", + "filters": [] + }, + { + "name": "", + "key": "Continent", + "op": "EQ", + "value": "SA", + "filters": [] + } + ] + } + ] + }, + "result": [ + [ + 1, + 4 + ], + [ + 8, + 12, + 5, + 10 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/non_strict.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/non_strict.json new file mode 100644 index 00000000..d8e010e0 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/non_strict.json @@ -0,0 +1,97 @@ +{ + "name": "non-strict selections", + "comment": "These test specify loose selection behaviour, to allow fetching already PUT objects even when there is not enough nodes to select from.", + "nodes": [ + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + } + ] + }, + { + "attributes": [] + } + ], + "tests": [ + { + "name": "not enough nodes (backup factor)", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "MyStore" + } + ], + "containerBackupFactor": 2, + "selectors": [ + { + "name": "MyStore", + "count": 2, + "clause": "DISTINCT", + "attribute": "Country", + "filter": "FromRU" + } + ], + "filters": [ + { + "name": "FromRU", + "key": "Country", + "op": "EQ", + "value": "Russia", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + }, + { + "name": "not enough nodes (buckets)", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "MyStore" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "MyStore", + "count": 2, + "clause": "DISTINCT", + "attribute": "Country", + "filter": "FromRU" + } + ], + "filters": [ + { + "name": "FromRU", + "key": "Country", + "op": "EQ", + "value": "Russia", + "filters": [] + } + ] + }, + "result": [ + [ + 0 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/rep_only.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/rep_only.json new file mode 100644 index 00000000..7af5eb0a --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/rep_only.json @@ -0,0 +1,113 @@ +{ + "name": "REP X", + "nodes": [ + { + "publicKey": "", + "addresses": [], + "attributes": [ + { + "key": "City", + "value": "Saint-Petersburg" + } + ], + "state": "Unspecified" + }, + { + "publicKey": "", + "addresses": [], + "attributes": [ + { + "key": "City", + "value": "Moscow" + } + ], + "state": "Unspecified" + }, + { + "publicKey": "", + "addresses": [], + "attributes": [ + { + "key": "City", + "value": "Berlin" + } + ], + "state": "Unspecified" + }, + { + "publicKey": "", + "addresses": [], + "attributes": [ + { + "key": "City", + "value": "Paris" + } + ], + "state": "Unspecified" + } + ], + "tests": [ + { + "name": "REP 1", + "policy": { + "replicas": [ + { + "count": 1 + } + ], + "containerBackupFactor": 0, + "selectors": [], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2 + ] + ] + }, + { + "name": "REP 3", + "policy": { + "replicas": [ + { + "count": 3 + } + ], + "containerBackupFactor": 0, + "selectors": [], + "filters": [] + }, + "result": [ + [ + 0, + 3, + 1, + 2 + ] + ] + }, + { + "name": "REP 5", + "policy": { + "replicas": [ + { + "count": 5 + } + ], + "containerBackupFactor": 0, + "selectors": [], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2, + 3 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/select_no_attribute.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/select_no_attribute.json new file mode 100644 index 00000000..6a49d688 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/select_no_attribute.json @@ -0,0 +1,116 @@ +{ + "name": "select with unspecified attribute", + "nodes": [ + { + "attributes": [ + { + "key": "ID", + "value": "1" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "St.Petersburg" + }, + { + "key": "SSD", + "value": "0" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "2" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "St.Petersburg" + }, + { + "key": "SSD", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "3" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "Moscow" + }, + { + "key": "SSD", + "value": "1" + } + ] + }, + { + "attributes": [ + { + "key": "ID", + "value": "4" + }, + { + "key": "Country", + "value": "RU" + }, + { + "key": "City", + "value": "Moscow" + }, + { + "key": "SSD", + "value": "1" + } + ] + } + ], + "tests": [ + { + "name": "test", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "X" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "X", + "count": 4, + "clause": "DISTINCT", + "filter": "*" + } + ], + "filters": [] + }, + "result": [ + [ + 0, + 1, + 2, + 3 + ] + ] + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/PlacementTests/selector_invalid.json b/src/FrostFS.SDK.Tests/TestData/PlacementTests/selector_invalid.json new file mode 100644 index 00000000..08e1a8dc --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/PlacementTests/selector_invalid.json @@ -0,0 +1,87 @@ +{ + "name": "invalid selections", + "nodes": [ + { + "attributes": [ + { + "key": "Country", + "value": "Russia" + } + ] + }, + { + "attributes": [ + { + "key": "Country", + "value": "Germany" + } + ] + }, + { + "attributes": [] + } + ], + "tests": [ + { + "name": "missing filter", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "MyStore" + } + ], + "containerBackupFactor": 1, + "selectors": [ + { + "name": "MyStore", + "count": 1, + "clause": "DISTINCT", + "attribute": "Country", + "filter": "FromNL" + } + ], + "filters": [ + { + "name": "FromRU", + "key": "Country", + "op": "EQ", + "value": "Russia", + "filters": [] + } + ] + }, + "error": "filter not found" + }, + { + "name": "not enough nodes (filter results in empty set)", + "policy": { + "replicas": [ + { + "count": 1, + "selector": "MyStore" + } + ], + "containerBackupFactor": 2, + "selectors": [ + { + "name": "MyStore", + "count": 2, + "clause": "DISTINCT", + "attribute": "Country", + "filter": "FromMoon" + } + ], + "filters": [ + { + "name": "FromMoon", + "key": "Country", + "op": "EQ", + "value": "Moon", + "filters": [] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/TestData/wallet.json b/src/FrostFS.SDK.Tests/TestData/wallet.json new file mode 100644 index 00000000..7a4bdd48 --- /dev/null +++ b/src/FrostFS.SDK.Tests/TestData/wallet.json @@ -0,0 +1,30 @@ +{ + "version": "1.0", + "accounts": [ + { + "address": "NWeByJPgNC97F83hTUnSbnZSBKaFvk5HNw", + "key": "6PYVCcS2yp89JpcfR61FGhdhhzyYjSErNedmpZErnybNTxUZMRdhzJLrek", + "label": "", + "contract": { + "script": "DCEDJOdiiPy5ABANAYAqFO+XfMpFrQc1YSMERt8Us0TIWLZBVuezJw==", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isDefault": false + } + ], + "scrypt": { + "n": 16384, + "r": 8, + "p": 8 + }, + "extra": { + "Tokens": null + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/ApeTests.cs b/src/FrostFS.SDK.Tests/Unit/ApeTests.cs new file mode 100644 index 00000000..dea17fb4 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/ApeTests.cs @@ -0,0 +1,167 @@ +using System.Text; +using FrostFS.SDK.Client; + +namespace FrostFS.SDK.Tests.Unit; + +public class ApeTests : ContainerTestsBase +{ + [Fact] + public void ApeRule1Test() + { + var chain = new FrostFsChain + { + ID = Encoding.ASCII.GetBytes("chain-id-test"), + Rules = [ + new FrostFsRule + { + Status = RuleStatus.Allow, + Actions = new Actions(inverted: false, names: ["*"]), + Resources = new Resources (inverted: false, names: [$"native:object/*"]), + Any = false, + Conditions = [] + } + ], + MatchType = RuleMatchType.DenyPriority + }; + + var serialized = RuleSerializer.Serialize(chain); + var restoredChain = RuleSerializer.Deserialize(serialized); + + Assert.True(chain.ID.SequenceEqual(restoredChain.ID)); + + Assert.Equal(chain.MatchType, restoredChain.MatchType); + CompareRules(chain.Rules, restoredChain.Rules); + } + + [Fact] + public void ApeRule2Test() + { + var chain = new FrostFsChain + { + ID = Encoding.ASCII.GetBytes("dumptext"), + Rules = [ + new FrostFsRule + { + Status = RuleStatus.AccessDenied, + Actions = new Actions(inverted: true, names: ["put,get"]), + Resources = new Resources (inverted: true, names: [$"native:object/*,blablabla"]), + Any = true, + Conditions = [ + new () { + Key = "key", + Value = "value", + Kind = ConditionKindType.Resource, + Op = ConditionType.CondStringEquals + }, + new () { + Key = "key1", + Value = "value1", + Kind = ConditionKindType.Request, + Op = ConditionType.CondNumericGreaterThan + } + ] + } + ], + MatchType = RuleMatchType.FirstMatch + }; + + var serialized = RuleSerializer.Serialize(chain); + var restoredChain = RuleSerializer.Deserialize(serialized); + + Assert.True(chain.ID.SequenceEqual(restoredChain.ID)); + + Assert.Equal(chain.MatchType, restoredChain.MatchType); + CompareRules(chain.Rules, restoredChain.Rules); + } + + [Fact] + public void NegativeDeserialize1Test() + { + try + { + _ = RuleSerializer.Deserialize(null); + Assert.Fail("Error is expected"); + } + catch (ArgumentNullException) + { + } + } + + [Fact] + public void NegativeDeserialize2Test() + { + try + { + _ = RuleSerializer.Deserialize([]); + Assert.Fail("Error is expected"); + } + catch (FrostFsException) + { + } + } + + [Fact] + public void NegativeDeserialize3Test() + { + try + { + _ = RuleSerializer.Deserialize([1, 2, 3]); + Assert.Fail("Error is expected"); + } + catch (FrostFsException) + { + } + } + + [Fact] + public void NegativeDeserialize4Test() + { + try + { + //"\x00\x00:aws:iam::namespace:group/so\x82\x82\x82\x82\x82\x82u\x82" + _ = RuleSerializer.Deserialize([0x00, 0x00, 0x3A, 0x77, 0x73, 0x3A, 0x69, 0x61, 0x6D, 0x3A, 0x3A, 0x6E, 0x61, 0x6D, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x3A, 0x67, 0x72, 0x6F, 0x75, 0x70, 0x2F, 0x73, 0x6F, 0x82, 0x82, 0x82, 0x82, 0x82, 0x82, 0x75, 0x82]); + Assert.Fail("Error is expected"); + } + catch (FrostFsException) + { + } + } + + private static void CompareRules(FrostFsRule[] rules1, FrostFsRule[] rules2) + { + Assert.NotNull(rules1); + Assert.NotNull(rules2); + + Assert.Equal(rules1.Length, rules2.Length); + + for (int ri = 0; ri < rules1.Length; ri++) + { + var rule1 = rules1[ri]; + var rule2 = rules2[ri]; + + Assert.Equal(rule1.Status, rule2.Status); + Assert.Equal(rule1.Any, rule2.Any); + + Assert.Equal(rule1.Actions, rule2.Actions); + + Assert.Equal(rule1.Resources, rule2.Resources); + + bool cond1Empty = rule1.Conditions == null || rule1.Conditions.Length == 0; + bool cond2Empty = rule2.Conditions == null || rule2.Conditions.Length == 0; + + if (cond1Empty && cond2Empty) + { + return; + } + + Assert.Equal(cond1Empty, cond2Empty); + + Assert.Equal(rule1.Conditions!.Length, rule2.Conditions!.Length); + + for (int i = 0; i < rule1.Conditions.Length; i++) + { + Assert.Equal(rule1.Conditions[i], rule2.Conditions[i]); + } + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/ContainerTest.cs b/src/FrostFS.SDK.Tests/Unit/ContainerTest.cs new file mode 100644 index 00000000..ce1d06ce --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/ContainerTest.cs @@ -0,0 +1,98 @@ +using System.Diagnostics.CodeAnalysis; +using FrostFS.Netmap; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; + +using Google.Protobuf; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public class ContainerTest : ContainerTestsBase +{ + [Theory] + [InlineData(1, "test", 0, 0)] + + public void ReplicaToMessagelTest(uint count, string selector, uint ecDataCount, uint ecParityCount) + { + FrostFsReplica replica = new() + { + Count = count, + Selector = selector, + EcDataCount = ecDataCount, + EcParityCount = ecParityCount + }; + + Replica message = replica.ToMessage(); + + Assert.Equal(count, message.Count); + Assert.Equal(selector, message.Selector); + Assert.Equal(ecDataCount, message.EcDataCount); + Assert.Equal(ecParityCount, message.EcParityCount); + } + + [Fact] + public async void CreateContainerTest() + { + var param = new PrmContainerCreate(new FrostFsContainerInfo(Mocker.PlacementPolicy), PrmWait.DefaultParams); + + var result = await GetClient().PutContainerAsync(param, default); + + Assert.NotNull(result); + Assert.NotNull(result.GetValue()); + + var bytes = Mocker.ContainerGuid.ToByteArray(true); + + Assert.True(Base58.Encode(new Span(bytes)) == result.GetValue()); + } + + [Fact] + public async void GetContainerTest() + { + var cid = new FrostFsContainerId(Base58.Encode(new Span(Mocker.ContainerGuid.ToByteArray(true)))); + + var result = await GetClient().GetContainerAsync(new PrmContainerGet(cid), default); + + Assert.NotNull(result); + Assert.Equal(Mocker.ContainerGuid, result.Nonce); + Assert.Equal(Mocker.PlacementPolicy, result.PlacementPolicy); + Assert.Equal(Mocker.Version.ToString(), result.Version!.ToString()); + } + + [Fact] + public async void GetContainerListTest() + { + Mocker.ContainerIds.Add([0xaa]); + Mocker.ContainerIds.Add([0xbb]); + Mocker.ContainerIds.Add([0xcc]); + + var result = GetClient().ListContainersAsync(default, default); + + Assert.NotNull(result); + + int i = 0; + await foreach (var cid in result) + { + var val = Base58.Encode(ByteString.CopyFrom(Mocker.ContainerIds[i++]).ToByteArray()); + Assert.Equal(val, cid.GetValue()); + } + + Assert.Equal(3, i); + } + + [Fact] + public async void DeleteContainerAsyncTest() + { + Mocker.ReturnContainerRemoved = true; + var cid = new FrostFsContainerId(Base58.Encode(new Span(Mocker.ContainerGuid.ToByteArray(true)))); + + await GetClient().DeleteContainerAsync(new PrmContainerDelete(cid, PrmWait.DefaultParams), default); + + Assert.Single(Mocker.Requests); + + var request = Mocker.Requests.First(); + + Assert.Equal(cid.ToMessage(), request.Request.Body.ContainerId); + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/ContainerTestsBase.cs b/src/FrostFS.SDK.Tests/Unit/ContainerTestsBase.cs new file mode 100644 index 00000000..60579f65 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/ContainerTestsBase.cs @@ -0,0 +1,40 @@ +using FrostFS.SDK.Client.Interfaces; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests.Unit; + +public abstract class ContainerTestsBase +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + protected ContainerMocker Mocker { get; set; } + + protected ContainerTestsBase() + { + Settings = Options.Create(new ClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + Mocker = new ContainerMocker(key) + { + PlacementPolicy = new FrostFsPlacementPolicy(true, 1, [], [], new FrostFsReplica(1)), + Version = new FrostFsVersion(2, 13), + ContainerGuid = Guid.NewGuid() + }; + } + + protected IFrostFSClient GetClient() + { + return Client.FrostFSClient.GetTestInstance( + Settings, + (url) => Grpc.Net.Client.GrpcChannel.ForAddress(new Uri(url)), + new NetworkMocker(key).GetMock().Object, + new SessionMocker(key).GetMock().Object, + Mocker.GetMock().Object, + new ObjectMocker(key).GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/MetaheaderTests.cs b/src/FrostFS.SDK.Tests/Unit/MetaheaderTests.cs new file mode 100644 index 00000000..3a71d789 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/MetaheaderTests.cs @@ -0,0 +1,29 @@ +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK.Tests.Unit; + +public class MetaheaderTests +{ + [Theory] + [InlineData(2, 13, 1, 1)] + [InlineData(200, 0, 1000000, 8)] + public void MetaheaderTest(int major, int minor, int epoch, int ttl) + { + MetaHeader metaHeader = new MetaHeader( + new FrostFsVersion( + major: 2, + minor: 13 + ), + epoch: 0, + ttl: 2 + ); + + var result = metaHeader.ToMessage(); + + Assert.Equal((ulong)metaHeader.Epoch, result.Epoch); + Assert.Equal((ulong)metaHeader.Ttl, result.Ttl); + Assert.Equal((ulong)metaHeader.Version.Major, result.Version.Major); + Assert.Equal((ulong)metaHeader.Version.Minor, result.Version.Minor); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/NetmapSnapshotTests.cs b/src/FrostFS.SDK.Tests/Unit/NetmapSnapshotTests.cs new file mode 100644 index 00000000..233e12ee --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/NetmapSnapshotTests.cs @@ -0,0 +1,101 @@ +using FrostFS.Netmap; +using FrostFS.SDK.Client; + +using Google.Protobuf; + +namespace FrostFS.SDK.Tests.Unit; + +public class NetmapSnapshotTests : NetworkTestsBase +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + + public async void NetmapSnapshotTest(bool useContext) + { + var body = new NetmapSnapshotResponse.Types.Body + { + Netmap = new Netmap.Netmap { Epoch = 99 } + }; + + var nodeInfo1 = new NodeInfo + { + State = NodeInfo.Types.State.Online, + PublicKey = ByteString.CopyFrom([1, 2, 3]) + }; + + nodeInfo1.Addresses.Add("address1"); + nodeInfo1.Addresses.Add("address2"); + nodeInfo1.Attributes.Add(new NodeInfo.Types.Attribute { Key = "key1", Value = "value1" }); + nodeInfo1.Attributes.Add(new NodeInfo.Types.Attribute { Key = "key2", Value = "value2" }); + + var nodeInfo2 = new NodeInfo + { + State = NodeInfo.Types.State.Offline, + PublicKey = ByteString.CopyFrom([3, 4, 5]) + }; + + nodeInfo2.Addresses.Add("address3"); + nodeInfo2.Attributes.Add(new NodeInfo.Types.Attribute { Key = "key3", Value = "value3" }); + + body.Netmap.Nodes.Add(nodeInfo1); + body.Netmap.Nodes.Add(nodeInfo2); + + Mocker.NetmapSnapshotResponse = new NetmapSnapshotResponse { Body = body }; + + var ctx = useContext + ? new CallContext(TimeSpan.FromSeconds(20), Mocker.CancellationTokenSource.Token) + : default; + + var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); + + var result = await GetClient(DefaultSettings).GetNetmapSnapshotAsync(ctx); + + var validTimeoutTo = DateTime.UtcNow.AddSeconds(20); + + Assert.NotNull(result); + + Assert.Equal(99u, result.Epoch); + Assert.Equal(2, result.NodeInfoCollection.Count); + + var node1 = result.NodeInfoCollection[0]; + Assert.Equal(NodeState.Online, node1.State); + Assert.Equal(2, node1.Addresses.Count); + Assert.Equal("address1", node1.Addresses.ElementAt(0)); + Assert.Equal("address2", node1.Addresses.ElementAt(1)); + + Assert.Equal(2, node1.Attributes.Count); + + Assert.Equal("key1", node1.Attributes.ElementAt(0).Key); + Assert.Equal("value1", node1.Attributes.ElementAt(0).Value); + Assert.Equal("key2", node1.Attributes.ElementAt(1).Key); + Assert.Equal("value2", node1.Attributes.ElementAt(1).Value); + + var node2 = result.NodeInfoCollection[1]; + Assert.Equal(NodeState.Offline, node2.State); + Assert.Single(node2.Addresses); + Assert.Equal("address3", node2.Addresses.ElementAt(0)); + + Assert.Single(node2.Attributes); + + Assert.Equal("key3", node2.Attributes.ElementAt(0).Key); + Assert.Equal("value3", node2.Attributes.ElementAt(0).Value); + + if (useContext) + { + Assert.NotNull(Mocker.NetmapSnapshotRequest); + Assert.Empty(Mocker.NetmapSnapshotRequest.MetaHeader.XHeaders); + + Assert.Equal(Mocker.CancellationTokenSource.Token, Mocker.CancellationToken); + Assert.NotNull(Mocker.DateTime); + Assert.True(Mocker.DateTime.Value >= validTimeoutFrom); + Assert.True(Mocker.DateTime.Value <= validTimeoutTo); + } + else + { + Assert.NotNull(Mocker.NetmapSnapshotRequest); + Assert.Empty(Mocker.NetmapSnapshotRequest.MetaHeader.XHeaders); + Assert.Null(Mocker.DateTime); + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/NetworkSettingsTests.cs b/src/FrostFS.SDK.Tests/Unit/NetworkSettingsTests.cs new file mode 100644 index 00000000..3f3fb2ff --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/NetworkSettingsTests.cs @@ -0,0 +1,68 @@ +using System.Diagnostics.CodeAnalysis; +using FrostFS.SDK.Client; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public class NetworkSettingsTests : NetworkTestsBase +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async void NetworkSettingsTest(bool useContext) + { + Mocker.Parameters.Add("AuditFee", [1]); + Mocker.Parameters.Add("BasicIncomeRate", [2]); + Mocker.Parameters.Add("ContainerFee", [3]); + Mocker.Parameters.Add("ContainerAliasFee", [4]); + Mocker.Parameters.Add("EpochDuration", [5]); + Mocker.Parameters.Add("InnerRingCandidateFee", [6]); + Mocker.Parameters.Add("MaxECDataCount", [7]); + Mocker.Parameters.Add("MaxECParityCount", [8]); + Mocker.Parameters.Add("MaxObjectSize", [9]); + Mocker.Parameters.Add("WithdrawFee", [10]); + Mocker.Parameters.Add("HomomorphicHashingDisabled", [1]); + Mocker.Parameters.Add("MaintenanceModeAllowed", [1]); + + var ctx = useContext + ? new CallContext(TimeSpan.FromSeconds(20), Mocker.CancellationTokenSource.Token) + : default; + + var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); + + var result = await GetClient(DefaultSettings).GetNetworkSettingsAsync(ctx); + + var validTimeoutTo = DateTime.UtcNow.AddSeconds(20); + + Assert.NotNull(result); + + Assert.Equal(Mocker.Parameters["AuditFee"], [(byte)result.AuditFee]); + Assert.Equal(Mocker.Parameters["BasicIncomeRate"], [(byte)result.BasicIncomeRate]); + Assert.Equal(Mocker.Parameters["ContainerFee"], [(byte)result.ContainerFee]); + Assert.Equal(Mocker.Parameters["ContainerAliasFee"], [(byte)result.ContainerAliasFee]); + Assert.Equal(Mocker.Parameters["EpochDuration"], [(byte)result.EpochDuration]); + Assert.Equal(Mocker.Parameters["InnerRingCandidateFee"], [(byte)result.InnerRingCandidateFee]); + Assert.Equal(Mocker.Parameters["MaxECDataCount"], [(byte)result.MaxECDataCount]); + Assert.Equal(Mocker.Parameters["MaxECParityCount"], [(byte)result.MaxECParityCount]); + Assert.Equal(Mocker.Parameters["MaxObjectSize"], [(byte)result.MaxObjectSize]); + Assert.Equal(Mocker.Parameters["WithdrawFee"], [(byte)result.WithdrawFee]); + + Assert.True(result.HomomorphicHashingDisabled); + Assert.True(result.MaintenanceModeAllowed); + + if (useContext) + { + Assert.Equal(Mocker.CancellationTokenSource.Token, Mocker.CancellationToken); + Assert.NotNull(Mocker.DateTime); + + Assert.True(Mocker.DateTime.Value >= validTimeoutFrom); + Assert.True(Mocker.DateTime.Value <= validTimeoutTo); + } + else + { + Assert.NotNull(Mocker.NetworkInfoRequest); + Assert.Empty(Mocker.NetworkInfoRequest.MetaHeader.XHeaders); + Assert.Null(Mocker.DateTime); + } + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/NetworkTestsBase.cs b/src/FrostFS.SDK.Tests/Unit/NetworkTestsBase.cs new file mode 100644 index 00000000..94522201 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/NetworkTestsBase.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; + +using FrostFS.SDK.Client.Interfaces; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public abstract class NetworkTestsBase +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + // protected FrostFsVersion Version { get; set; } = new FrostFsVersion(2, 13); + + protected NetworkMocker Mocker { get; set; } + + protected ClientSettings DefaultSettings { get; } + + protected NetworkTestsBase() + { + DefaultSettings = new ClientSettings + { + Key = key, + Host = "http://localhost:8080", + }; + + Mocker = new NetworkMocker(key); + } + + protected IFrostFSClient GetClient(ClientSettings settings) + { + return Client.FrostFSClient.GetTestInstance( + Options.Create(settings), + (url) => Grpc.Net.Client.GrpcChannel.ForAddress(new Uri(url)), + Mocker.GetMock().Object, + new SessionMocker(key).GetMock().Object, + new ContainerMocker(key).GetMock().Object, + new ObjectMocker(key).GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/NodeInfoTests.cs b/src/FrostFS.SDK.Tests/Unit/NodeInfoTests.cs new file mode 100644 index 00000000..6449beb6 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/NodeInfoTests.cs @@ -0,0 +1,69 @@ +using FrostFS.Netmap; +using FrostFS.SDK.Client; +using Google.Protobuf; + +namespace FrostFS.SDK.Tests.Unit; + +public class NodeInfoTests : NetworkTestsBase +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async void NodeInfoTest(bool useContext) + { + var body = new LocalNodeInfoResponse.Types.Body + { + NodeInfo = new NodeInfo() + { + State = NodeInfo.Types.State.Online, + PublicKey = ByteString.CopyFrom([1, 2, 3]) + }, + Version = new Refs.Version { Major = 2, Minor = 12 } + }; + + body.NodeInfo.Addresses.Add("address1"); + body.NodeInfo.Addresses.Add("address2"); + body.NodeInfo.Attributes.Add(new NodeInfo.Types.Attribute { Key = "key1", Value = "value1" }); + body.NodeInfo.Attributes.Add(new NodeInfo.Types.Attribute { Key = "key2", Value = "value2" }); + + Mocker.NodeInfoResponse = new LocalNodeInfoResponse { Body = body }; + + var ctx = useContext + ? new CallContext(TimeSpan.FromSeconds(20), Mocker.CancellationTokenSource.Token) + : default; + + var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); + + var result = await GetClient(DefaultSettings).GetNodeInfoAsync(ctx); + + var validTimeoutTo = DateTime.UtcNow.AddSeconds(20); + + Assert.NotNull(result); + + Assert.Equal(NodeState.Online, result.State); + + Assert.Equal(2, result.Addresses.Count); + Assert.Equal("address1", result.Addresses.ElementAt(0)); + Assert.Equal("address2", result.Addresses.ElementAt(1)); + + Assert.Equal(2, result.Attributes.Count); + Assert.Equal("value1", result.Attributes["key1"]); + Assert.Equal("value2", result.Attributes["key2"]); + + Assert.NotNull(Mocker.LocalNodeInfoRequest); + if (useContext) + { + Assert.Empty(Mocker.LocalNodeInfoRequest.MetaHeader.XHeaders); + Assert.Equal(Mocker.CancellationTokenSource.Token, Mocker.CancellationToken); + Assert.NotNull(Mocker.DateTime); + + Assert.True(Mocker.DateTime.Value >= validTimeoutFrom); + Assert.True(Mocker.DateTime.Value <= validTimeoutTo); + } + else + { + Assert.Empty(Mocker.LocalNodeInfoRequest.MetaHeader.XHeaders); + Assert.Null(Mocker.DateTime); + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/ObjectTest.cs b/src/FrostFS.SDK.Tests/Unit/ObjectTest.cs new file mode 100644 index 00000000..985f02e0 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/ObjectTest.cs @@ -0,0 +1,639 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using System.Text; + +using FrostFS.Refs; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; +using FrostFS.SDK.Cryptography; +using Google.Protobuf; +using Org.BouncyCastle.Utilities; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +[SuppressMessage("Security", "CA5394:Do not use insecure randomness", Justification = "No secure purpose")] +public class ObjectTest : ObjectTestsBase +{ + [Fact] + public async void PutObjectTest() + { + Mocker.ResultObjectIds!.Add(SHA256.HashData([])); + + Random rnd = new(); + var bytes = new byte[1024]; + rnd.NextBytes(bytes); + + var param = new PrmObjectPut(Mocker.ObjectHeader); + + var stream = await GetClient().PutObjectAsync(param, default).ConfigureAwait(true); + + await stream.WriteAsync(bytes.AsMemory()); + var result = await stream.CompleteAsync(); + + var sentMessages = Mocker.ClientStreamWriter!.Messages; + + var body1 = sentMessages.ElementAt(0).GetBody() as Object.PutRequest.Types.Body; + var body2 = sentMessages.ElementAt(1).GetBody() as Object.PutRequest.Types.Body; + + Assert.NotNull(result); + Assert.Equal(Mocker.ResultObjectIds.First(), result.ToHash()); + + Assert.True(Mocker.ClientStreamWriter.CompletedTask); + + Assert.Equal(0, body1!.Chunk.Length); + Assert.Equal(Object.PutRequest.Types.Body.ObjectPartOneofCase.Init, body1!.ObjectPartCase); + + Assert.Equal(1024, body2!.Chunk.Length); + Assert.Equal(Object.PutRequest.Types.Body.ObjectPartOneofCase.Chunk, body2!.ObjectPartCase); + } + + [Fact] + public async void ClientCutTest() + { + NetworkMocker.Parameters.Add("MaxObjectSize", [0x0, 0xa]); + + var blockSize = 2560; + byte[] bytes = File.ReadAllBytes(@".\..\..\..\cat.jpg"); + var fileLength = bytes.Length; + + var param = new PrmObjectClientCutPut( + Mocker.ObjectHeader, + payload: new MemoryStream(bytes), + bufferMaxSize: blockSize); + + Random rnd = new(); + + Collection objIds = new([new byte[32], new byte[32], new byte[32]]); + rnd.NextBytes(objIds.ElementAt(0)); + rnd.NextBytes(objIds.ElementAt(1)); + rnd.NextBytes(objIds.ElementAt(2)); + + foreach (var objId in objIds) + Mocker.ResultObjectIds!.Add(objId); + + var result = await GetClient().PutClientCutObjectAsync(param, default); + + var singleObjects = Mocker.PutSingleRequests.ToArray(); + + Assert.NotNull(Mocker.ClientStreamWriter?.Messages); + + var objects = Mocker.PutSingleRequests.Select(o => o.Body.Object).ToArray(); + + Assert.Equal(4, objects.Length); + + // linked object + Assert.Equal(0, objects[3].Payload.Length); + + // PART1 + Assert.Equal(blockSize, objects[0].Payload.Length); + Assert.Equal(bytes.AsMemory(0, blockSize).ToArray(), objects[0].Payload); + + Assert.NotNull(objects[0].Header.Split.SplitId); + Assert.Null(objects[0].Header.Split.Previous); + Assert.True(objects[0].Header.Attributes.Count == 0); + Assert.Null(objects[0].Header.Split.Parent); + + // PART2 + Assert.Equal(blockSize, objects[1].Payload.Length); + Assert.Equal(bytes.AsMemory(blockSize, blockSize).ToArray(), objects[1].Payload); + + Assert.Equal(objects[0].Header.Split.SplitId, objects[1].Header.Split.SplitId); + Assert.True(objects[1].Header.Attributes.Count == 0); + Assert.Null(objects[1].Header.Split.Parent); + + // last part + Assert.Equal(bytes.Length % blockSize, objects[2].Payload.Length); + Assert.Equal(bytes.AsMemory(2 * blockSize).ToArray(), objects[2].Payload); + + Assert.NotNull(objects[3].Header.Split.Parent); + Assert.NotNull(objects[3].Header.Split.ParentHeader); + Assert.NotNull(objects[3].Header.Split.ParentSignature); + Assert.Equal(objects[2].Header.Split.SplitId, objects[3].Header.Split.SplitId); + Assert.True(objects[2].Header.Attributes.Count == 0); + + // link object + Assert.Equal(objects[2].Header.Split.Parent, objects[3].Header.Split.Parent); + Assert.Equal(objects[2].Header.Split.ParentHeader, objects[3].Header.Split.ParentHeader); + Assert.Equal(objects[2].Header.Split.SplitId, objects[3].Header.Split.SplitId); + Assert.Equal(0, (int)objects[3].Header.PayloadLength); + Assert.True(objects[3].Header.Attributes.Count == 0); + + Assert.Single(objects[3].Header.Split.ParentHeader.Attributes); + Assert.Equal("k", objects[3].Header.Split.ParentHeader.Attributes[0].Key); + Assert.Equal("v", objects[3].Header.Split.ParentHeader.Attributes[0].Value); + + var modelObjId = FrostFsObjectId.FromHash(objects[3].Header.Split.Parent.Value.ToByteArray()); + + Assert.Equal(result.Value, modelObjId.ToString()); + } + + [Fact] + public async void ClientCutWithInterruptionOnFirstPartTest() + { + NetworkMocker.Parameters.Add("MaxObjectSize", [0x0, 0xa]); + + var blockSize = 2560; + byte[] bytes = File.ReadAllBytes(@".\..\..\..\cat.jpg"); + var fileLength = bytes.Length; + + var splitId = Guid.NewGuid(); + var progress = new UploadProgressInfo(splitId); + + var param = new PrmObjectClientCutPut( + Mocker.ObjectHeader, + payload: new MemoryStream(bytes), + bufferMaxSize: blockSize, + progress: progress); + + Random rnd = new(); + + Collection objIds = new([new byte[32], new byte[32], new byte[32]]); + rnd.NextBytes(objIds.ElementAt(0)); + rnd.NextBytes(objIds.ElementAt(1)); + rnd.NextBytes(objIds.ElementAt(2)); + + foreach (var objId in objIds) + Mocker.ResultObjectIds!.Add(objId); + + int sentBlockCount = 0; + Mocker.Callback = () => + { + if (++sentBlockCount == 1) + throw new FrostFsException("some error"); + }; + + bool gotException = false; + try + { + var result = await GetClient().PutClientCutObjectAsync(param, default); + } + catch (FrostFsException ex) + { + if (ex.Message == "some error") + gotException = true; + } + + Assert.True(gotException); + + var singleObjects = Mocker.PutSingleRequests.ToArray(); + + Assert.Empty(singleObjects); + } + + [Fact] + public async void ClientCutWithInterruptionOnMiddlePartTest() + { + NetworkMocker.Parameters.Add("MaxObjectSize", [0x0, 0xa]); + + var blockSize = 2560; + byte[] bytes = File.ReadAllBytes(@".\..\..\..\cat.jpg"); + var fileLength = bytes.Length; + + var splitId = Guid.Parse("67e4bbe9-86ca-474d-9385-6569ce89db61"); + var progress = new UploadProgressInfo(splitId); + + var param = new PrmObjectClientCutPut( + Mocker.ObjectHeader, + payload: new MemoryStream(bytes), + bufferMaxSize: blockSize, + progress: progress); + + Random rnd = new(); + + Collection objIds = new([new byte[32], new byte[32], new byte[32]]); + rnd.NextBytes(objIds.ElementAt(0)); + rnd.NextBytes(objIds.ElementAt(1)); + rnd.NextBytes(objIds.ElementAt(2)); + + foreach (var objId in objIds) + Mocker.ResultObjectIds!.Add(objId); + + int sentBlockCount = 0; + Mocker.Callback = () => + { + if (++sentBlockCount == 2) + throw new FrostFsException("some error"); + }; + + bool gotException = false; + try + { + _ = await GetClient().PutClientCutObjectAsync(param, default); + } + catch (FrostFsException ex) + { + if (ex.Message == "some error") + gotException = true; + } + + Assert.True(gotException); + + var singleObjects = Mocker.PutSingleRequests.ToArray(); + + Assert.NotNull(Mocker.ClientStreamWriter?.Messages); + + var objects = Mocker.PutSingleRequests.Select(o => o.Body.Object).ToArray(); + + Assert.Single(objects); + + Assert.Single(progress.GetParts()); + + var part = progress.GetPart(0); + Assert.Equal(0, part.Offset); + Assert.Equal(2560, part.Length); + + // PART1 + Assert.Equal(blockSize, objects[0].Payload.Length); + Assert.Equal(bytes.AsMemory(0, blockSize).ToArray(), objects[0].Payload); + + Assert.NotNull(objects[0].Header.Split.SplitId); + Assert.Null(objects[0].Header.Split.Previous); + Assert.True(objects[0].Header.Attributes.Count == 0); + Assert.Null(objects[0].Header.Split.Parent); + + // resume uploading + sentBlockCount = 10; + + var result = await GetClient().PutClientCutObjectAsync(param, default); + + singleObjects = Mocker.PutSingleRequests.ToArray(); + + Assert.NotNull(Mocker.ClientStreamWriter?.Messages); + + objects = Mocker.PutSingleRequests.Select(o => o.Body.Object).ToArray(); + + Assert.Equal(4, objects.Length); + + // PART1 + Assert.Equal(blockSize, objects[0].Payload.Length); + Assert.Equal(bytes.AsMemory(0, blockSize).ToArray(), objects[0].Payload); + + Assert.NotNull(objects[0].Header.Split.SplitId); + Assert.Null(objects[0].Header.Split.Previous); + Assert.True(objects[0].Header.Attributes.Count == 0); + Assert.Null(objects[0].Header.Split.Parent); + + // PART2 + Assert.Equal(blockSize, objects[1].Payload.Length); + Assert.Equal(bytes.AsMemory(blockSize, blockSize).ToArray(), objects[1].Payload); + + Assert.Equal(objects[0].Header.Split.SplitId, objects[1].Header.Split.SplitId); + Assert.True(objects[1].Header.Attributes.Count == 0); + Assert.Null(objects[1].Header.Split.Parent); + + // last part + Assert.Equal(bytes.Length % blockSize, objects[2].Payload.Length); + Assert.Equal(bytes.AsMemory(2 * blockSize).ToArray(), objects[2].Payload); + + Assert.NotNull(objects[3].Header.Split.Parent); + Assert.NotNull(objects[3].Header.Split.ParentHeader); + Assert.NotNull(objects[3].Header.Split.ParentSignature); + Assert.Equal(objects[2].Header.Split.SplitId, objects[3].Header.Split.SplitId); + Assert.True(objects[2].Header.Attributes.Count == 0); + + // link object + Assert.Equal(objects[2].Header.Split.Parent, objects[3].Header.Split.Parent); + Assert.Equal(objects[2].Header.Split.ParentHeader, objects[3].Header.Split.ParentHeader); + Assert.Equal(objects[2].Header.Split.SplitId, objects[3].Header.Split.SplitId); + Assert.Equal(0, (int)objects[3].Header.PayloadLength); + Assert.True(objects[3].Header.Attributes.Count == 0); + + Assert.Single(objects[3].Header.Split.ParentHeader.Attributes); + Assert.Equal("k", objects[3].Header.Split.ParentHeader.Attributes[0].Key); + Assert.Equal("v", objects[3].Header.Split.ParentHeader.Attributes[0].Value); + + var modelObjId = FrostFsObjectId.FromHash(objects[3].Header.Split.Parent.Value.ToByteArray()); + + Assert.Equal(result.Value, modelObjId.ToString()); + + Assert.Equal(3, progress.GetParts().Count); + part = progress.GetPart(0); + Assert.Equal(0, part.Offset); + Assert.Equal(2560, part.Length); + + part = progress.GetPart(1); + Assert.Equal(2560, part.Offset); + Assert.Equal(2560, part.Length); + + part = progress.GetPart(2); + Assert.Equal(2 * 2560, part.Offset); + Assert.Equal(1620, part.Length); + } + + [Fact] + public async void ClientCutWithInterruptionOnLastPartTest() + { + NetworkMocker.Parameters.Add("MaxObjectSize", [0x0, 0xa]); + + var blockSize = 2560; + byte[] bytes = File.ReadAllBytes(@".\..\..\..\cat.jpg"); + var fileLength = bytes.Length; + + var splitId = Guid.Parse("67e4bbe9-86ca-474d-9385-6569ce89db61"); + var progress = new UploadProgressInfo(splitId); + + var param = new PrmObjectClientCutPut( + Mocker.ObjectHeader, + payload: new MemoryStream(bytes), + bufferMaxSize: blockSize, + progress: progress); + + Random rnd = new(); + + Collection objIds = new([new byte[32], new byte[32], new byte[32]]); + rnd.NextBytes(objIds.ElementAt(0)); + rnd.NextBytes(objIds.ElementAt(1)); + rnd.NextBytes(objIds.ElementAt(2)); + + foreach (var objId in objIds) + Mocker.ResultObjectIds!.Add(objId); + + int sentBlockCount = 0; + Mocker.Callback = () => + { + if (++sentBlockCount == 3) + throw new FrostFsException("some error"); + }; + + bool gotException = false; + try + { + _ = await GetClient().PutClientCutObjectAsync(param, default); + } + catch (FrostFsException ex) + { + if (ex.Message == "some error") + gotException = true; + } + + Assert.True(gotException); + + var singleObjects = Mocker.PutSingleRequests.ToArray(); + + Assert.NotNull(Mocker.ClientStreamWriter?.Messages); + + var objects = Mocker.PutSingleRequests.Select(o => o.Body.Object).ToArray(); + + Assert.Equal(2, objects.Length); + + Assert.Equal(2, progress.GetParts().Count); + + var part = progress.GetPart(0); + Assert.Equal(0, part.Offset); + Assert.Equal(2560, part.Length); + + part = progress.GetPart(1); + Assert.Equal(2560, part.Offset); + Assert.Equal(2560, part.Length); + + // PART1 + Assert.Equal(blockSize, objects[0].Payload.Length); + Assert.Equal(bytes.AsMemory(0, blockSize).ToArray(), objects[0].Payload); + + Assert.NotNull(objects[0].Header.Split.SplitId); + Assert.Null(objects[0].Header.Split.Previous); + Assert.True(objects[0].Header.Attributes.Count == 0); + Assert.Null(objects[0].Header.Split.Parent); + + // PART2 + Assert.Equal(blockSize, objects[1].Payload.Length); + Assert.Equal(bytes.AsMemory(blockSize, blockSize).ToArray(), objects[1].Payload); + + Assert.Equal(objects[0].Header.Split.SplitId, objects[1].Header.Split.SplitId); + Assert.True(objects[1].Header.Attributes.Count == 0); + Assert.Null(objects[1].Header.Split.Parent); + + // resume uploading + sentBlockCount = 10; + + var result = await GetClient().PutClientCutObjectAsync(param, default); + + singleObjects = Mocker.PutSingleRequests.ToArray(); + + Assert.NotNull(Mocker.ClientStreamWriter?.Messages); + + objects = Mocker.PutSingleRequests.Select(o => o.Body.Object).ToArray(); + + Assert.Equal(4, objects.Length); + + // PART1 + Assert.Equal(blockSize, objects[0].Payload.Length); + Assert.Equal(bytes.AsMemory(0, blockSize).ToArray(), objects[0].Payload); + + Assert.NotNull(objects[0].Header.Split.SplitId); + Assert.Null(objects[0].Header.Split.Previous); + Assert.True(objects[0].Header.Attributes.Count == 0); + Assert.Null(objects[0].Header.Split.Parent); + + // PART2 + Assert.Equal(blockSize, objects[1].Payload.Length); + Assert.Equal(bytes.AsMemory(blockSize, blockSize).ToArray(), objects[1].Payload); + + Assert.Equal(objects[0].Header.Split.SplitId, objects[1].Header.Split.SplitId); + Assert.True(objects[1].Header.Attributes.Count == 0); + Assert.Null(objects[1].Header.Split.Parent); + + // last part + Assert.Equal(bytes.Length % blockSize, objects[2].Payload.Length); + Assert.Equal(bytes.AsMemory(2 * blockSize).ToArray(), objects[2].Payload); + + Assert.NotNull(objects[3].Header.Split.Parent); + Assert.NotNull(objects[3].Header.Split.ParentHeader); + Assert.NotNull(objects[3].Header.Split.ParentSignature); + Assert.Equal(objects[2].Header.Split.SplitId, objects[3].Header.Split.SplitId); + Assert.True(objects[2].Header.Attributes.Count == 0); + + // link object + Assert.Equal(objects[2].Header.Split.Parent, objects[3].Header.Split.Parent); + Assert.Equal(objects[2].Header.Split.ParentHeader, objects[3].Header.Split.ParentHeader); + Assert.Equal(objects[2].Header.Split.SplitId, objects[3].Header.Split.SplitId); + Assert.Equal(0, (int)objects[3].Header.PayloadLength); + Assert.True(objects[3].Header.Attributes.Count == 0); + + Assert.Single(objects[3].Header.Split.ParentHeader.Attributes); + Assert.Equal("k", objects[3].Header.Split.ParentHeader.Attributes[0].Key); + Assert.Equal("v", objects[3].Header.Split.ParentHeader.Attributes[0].Value); + + var modelObjId = FrostFsObjectId.FromHash(objects[3].Header.Split.Parent.Value.ToByteArray()); + + Assert.Equal(result.Value, modelObjId.ToString()); + + Assert.Equal(3, progress.GetParts().Count); + part = progress.GetPart(0); + Assert.Equal(0, part.Offset); + Assert.Equal(2560, part.Length); + + part = progress.GetPart(1); + Assert.Equal(2560, part.Offset); + Assert.Equal(2560, part.Length); + + part = progress.GetPart(2); + Assert.Equal(2 * 2560, part.Offset); + Assert.Equal(1620, part.Length); + } + + [Fact] + public async void DeleteObject() + { + Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); + + await GetClient().DeleteObjectAsync(new PrmObjectDelete(ContainerId, Mocker.ObjectId), default); + + var request = Mocker.DeleteRequests.FirstOrDefault(); + Assert.NotNull(request); + Assert.Equal(ContainerId.ToMessage().Value, request.Body.Address.ContainerId.Value); + Assert.Equal(Mocker.ObjectId.ToMessage().Value, request.Body.Address.ObjectId.Value); + } + + [Fact] + public async void GetHeaderTest() + { + Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); + + var res = await GetClient().GetObjectHeadAsync(new PrmObjectHeadGet(ContainerId, Mocker.ObjectId), default); + + var objHeader = res.HeaderInfo; + Assert.NotNull(objHeader); + + var request = Mocker.HeadRequests.FirstOrDefault(); + Assert.NotNull(request); + Assert.Equal(ContainerId.ToMessage(), request.Body.Address.ContainerId); + Assert.Equal(Mocker.ObjectId.ToMessage(), request.Body.Address.ObjectId); + + Assert.NotNull(objHeader); + Assert.Equal(ContainerId.GetValue(), objHeader.ContainerId.GetValue()); + + Assert.Equal(Mocker.ObjectHeader!.OwnerId!.Value, objHeader.OwnerId!.Value); + Assert.Equal(Mocker.ObjectHeader!.Version!.ToString(), objHeader.Version!.ToString()); + + Assert.Equal(Mocker.HeadResponse!.PayloadLength, objHeader.PayloadLength); + + Assert.Equal(FrostFsObjectType.Regular, objHeader.ObjectType); + + Assert.NotNull(objHeader.Attributes); + Assert.Single(objHeader.Attributes); + + Assert.Equal(Mocker.HeadResponse.Attributes[0].Key, objHeader.Attributes.First().Key); + Assert.Equal(Mocker.HeadResponse.Attributes[0].Value, objHeader.Attributes.First().Value); + + Assert.Null(objHeader.Split); + } + + [Fact] + public async void GetRangeTest() + { + Mocker.ResultObjectIds!.Add(SHA256.HashData([])); + + Random rnd = new(); + var bytes = new byte[1024]; + rnd.NextBytes(bytes); + + Mocker.RangeResponse = bytes; + + Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); + + var param = new PrmRangeGet(ContainerId, Mocker.ObjectId, new FrostFsRange(100, (ulong)Mocker.RangeResponse.Length)); + + var result = await GetClient().GetRangeAsync(param, default); + + Assert.NotNull(Mocker.GetRangeRequest); + + Assert.Equal(param.Range.Offset, Mocker.GetRangeRequest.Body.Range.Offset); + Assert.Equal(param.Range.Length, Mocker.GetRangeRequest.Body.Range.Length); + + Assert.NotNull(result); + + var chunk = await result.ReadChunk(); + + var chunkBytes = chunk.Value.Span.ToArray(); + + Assert.Equal(chunkBytes.Length, Mocker.RangeResponse.Length); + + Assert.Equal(SHA256.HashData(bytes), SHA256.HashData(Mocker.RangeResponse)); + } + + [Fact] + public async void GetRangeHashTest() + { + Mocker.ResultObjectIds!.Add(SHA256.HashData([])); + + Random rnd = new(); + var bytes = new byte[1024]; + rnd.NextBytes(bytes); + + var salt = new byte[32]; + rnd.NextBytes(salt); + + var hash = new byte[32]; + rnd.NextBytes(hash); + + Mocker.RangeResponse = bytes; + var len = (ulong)bytes.Length; + + Mocker.RangeHashResponses.Add(ByteString.CopyFrom(hash)); + + Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); + + var param = new PrmRangeHashGet(ContainerId, Mocker.ObjectId, [new FrostFsRange(100, len)], salt); + + var result = await GetClient().GetRangeHashAsync(param, default); + + Assert.NotNull(Mocker.GetRangeHashRequest); + + Assert.Equal(param.Ranges[0].Offset, Mocker.GetRangeHashRequest.Body.Ranges[0].Offset); + Assert.Equal(param.Ranges[0].Length, Mocker.GetRangeHashRequest.Body.Ranges[0].Length); + + Assert.NotNull(result); + Assert.Single(result); + + Assert.Equal(SHA256.HashData(hash), SHA256.HashData(result.First().ToArray())); + } + + [Fact] + public async void PatchTest() + { + Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); + + var address = new FrostFsAddress(ContainerId, Mocker.ObjectId); + + Mocker.ResultObjectIds!.Add(SHA256.HashData([])); + + Random rnd = new(); + var patch = new byte[32]; + rnd.NextBytes(patch); + + var range = new FrostFsRange(8, (ulong)patch.Length); + + var param = new PrmObjectPatch( + address, + payload: new MemoryStream(patch), + maxChunkLength: 32, + range: range); + + var result = await GetClient().PatchObjectAsync(param, default); + + Assert.NotNull(result); + + Assert.NotNull(result.Value); + + Assert.NotNull(Mocker.PatchStreamWriter); + Assert.Single(Mocker.PatchStreamWriter.Messages); + + var sentMessages = Mocker.PatchStreamWriter!.Messages; + + var body = sentMessages.First().GetBody() as Object.PatchRequest.Types.Body; + + Assert.NotNull(body); + + Assert.True(Mocker.PatchStreamWriter.CompletedTask); + + Assert.Equal(address.ContainerId, body.Address.ContainerId); + Assert.Equal(address.ObjectId, body.Address.ObjectId); + + Assert.Equal(32, body.Patch.Chunk.Length); + + Assert.Equal(SHA256.HashData(patch), SHA256.HashData(body.Patch.Chunk.ToArray())); + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/ObjectTestsBase.cs b/src/FrostFS.SDK.Tests/Unit/ObjectTestsBase.cs new file mode 100644 index 00000000..c3681cee --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/ObjectTestsBase.cs @@ -0,0 +1,59 @@ +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Interfaces; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests.Unit; + +public abstract class ObjectTestsBase +{ + protected static readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + protected FrostFsContainerId ContainerId { get; set; } + + protected NetworkMocker NetworkMocker { get; set; } = new NetworkMocker(key); + protected SessionMocker SessionMocker { get; set; } = new SessionMocker(key); + protected ContainerMocker ContainerMocker { get; set; } = new ContainerMocker(key); + protected ObjectMocker Mocker { get; set; } + + protected ObjectTestsBase() + { + var ecdsaKey = key.LoadWif(); + + Settings = Options.Create(new ClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + Mocker = new ObjectMocker(key) + { + PlacementPolicy = new FrostFsPlacementPolicy(true, 1, [], [], new FrostFsReplica(1)), + Version = new FrostFsVersion(2, 13), + ContainerGuid = Guid.NewGuid() + }; + + ContainerId = new FrostFsContainerId(Base58.Encode(Mocker.ContainerGuid.ToByteArray(true))); + + Mocker.ObjectHeader = new( + ContainerId, + FrostFsObjectType.Regular, + [new FrostFsAttributePair("k", "v")], + null, + FrostFsOwner.FromKey(ecdsaKey), + new FrostFsVersion(2, 13)); + } + + protected IFrostFSClient GetClient() + { + return FrostFSClient.GetTestInstance( + Settings, + (url) => Grpc.Net.Client.GrpcChannel.ForAddress(new Uri(url)), + NetworkMocker.GetMock().Object, + SessionMocker.GetMock().Object, + ContainerMocker.GetMock().Object, + Mocker.GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/ObjectToolsTests.cs b/src/FrostFS.SDK.Tests/Unit/ObjectToolsTests.cs new file mode 100644 index 00000000..3c0245bd --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/ObjectToolsTests.cs @@ -0,0 +1,99 @@ +using System.Security.Cryptography; +using System.Text; +using FrostFS.SDK.Client; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK.Tests.Unit; + +public class ObjectToolsTests +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + [Fact] + public void CalculateObjectIdTest() + { + var payload = Encoding.UTF8.GetBytes("testPayload"); + + var payloadHash = SHA256.HashData(payload); + + FrostFsContainerId containerId = new("test"); + FrostFsObjectHeader header = new(containerId); + + var ecdsaKey = key.LoadWif(); + var owner = FrostFsOwner.FromKey(ecdsaKey); + + var clientKey = new ClientKey(ecdsaKey); + + var objId = ObjectTools.CalculateObjectId(header, payloadHash, owner, new FrostFsVersion(2, 13), clientKey); + + Assert.NotNull(objId.Value); + Assert.Equal("HuAojwCYi62iUKr1FtSCCkMLLWv1uAnznF8iSb1bRV1N", objId.Value); + } + + [Fact] + public void CalculateObjectIdTest1() + { + var payload = Encoding.UTF8.GetBytes("testPayload"); + + var payloadHash = SHA256.HashData(payload); + var ecdsaKey = key.LoadWif(); + var owner = FrostFsOwner.FromKey(ecdsaKey); + + var clientKey = new ClientKey(ecdsaKey); + FrostFsContainerId containerId = new("test"); + FrostFsObjectHeader header = new(containerId, FrostFsObjectType.Regular, null, null, owner, new FrostFsVersion(2, 13)); + + var objId = ObjectTools.CalculateObjectId(header, payloadHash, owner, new FrostFsVersion(2, 13), clientKey); + + Assert.NotNull(objId.Value); + Assert.Equal("HuAojwCYi62iUKr1FtSCCkMLLWv1uAnznF8iSb1bRV1N", objId.Value); + } + + [Fact] + public void CalculateObjectIdWithAttrTest() + { + var payload = Encoding.UTF8.GetBytes("testPayload"); + + var payloadHash = SHA256.HashData(payload); + var ecdsaKey = key.LoadWif(); + var owner = FrostFsOwner.FromKey(ecdsaKey); + + var clientKey = new ClientKey(ecdsaKey); + FrostFsContainerId containerId = new("test"); + + FrostFsAttributePair[] attribs = [new("key", "val")]; + + FrostFsObjectHeader header = new(containerId, FrostFsObjectType.Regular, attribs, null, owner, new FrostFsVersion(2, 13)); + + var objId = ObjectTools.CalculateObjectId(header, payloadHash, owner, new FrostFsVersion(2, 13), clientKey); + + Assert.NotNull(objId.Value); + Assert.Equal("4zq5NYEbzkrfmdKne3GnpavE24gU2PnuV17ZExb9hcn3", objId.Value); + } + + [Fact] + public void CalculateObjectIdWithSplitIdTest() + { + var payload = Encoding.UTF8.GetBytes("testPayload"); + + var payloadHash = SHA256.HashData(payload); + var ecdsaKey = key.LoadWif(); + var owner = FrostFsOwner.FromKey(ecdsaKey); + + var clientKey = new ClientKey(ecdsaKey); + FrostFsContainerId containerId = new("test"); + + FrostFsAttributePair[] attribs = [new("key", "val")]; + + var guid = Guid.Parse("790a8d04-f5c3-4cd6-b46f-a78ee7e325f2"); + SplitId splitId = new(guid); + FrostFsSplit split = new (splitId); + + FrostFsObjectHeader header = new(containerId, FrostFsObjectType.Regular, attribs, split, owner, new FrostFsVersion(2, 13)); + + var objId = ObjectTools.CalculateObjectId(header, payloadHash, owner, new FrostFsVersion(2, 13), clientKey); + + Assert.NotNull(objId.Value); + Assert.Equal("HCYzsuXyfe5LmQzi58hPQxExGPAFv7dU5TzEACLxM1os", objId.Value); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/PlacementPolicyTests.cs b/src/FrostFS.SDK.Tests/Unit/PlacementPolicyTests.cs new file mode 100644 index 00000000..46f7a3df --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/PlacementPolicyTests.cs @@ -0,0 +1,307 @@ +using System.Xml.Linq; +using FrostFS.Netmap; +using FrostFS.SDK.Client; +using Google.Protobuf.WellKnownTypes; + +namespace FrostFS.SDK.Tests.Unit; + +public class PlacementPolicyTests : NetworkTestsBase +{ + [Theory] + [InlineData(true, 1)] + [InlineData(true, 3)] + [InlineData(true, 5)] + [InlineData(false, 1)] + [InlineData(false, 3)] + [InlineData(false, 5)] + public void PlacementPolicySimpleFullTest(bool unique, uint backupFactor) + { + PlacementPolicy policy = new() + { + ContainerBackupFactor = backupFactor, + Unique = unique + }; + + var result = policy.ToModel(); + + Assert.Equal(backupFactor, result.BackupFactor); + Assert.Equal(unique, result.Unique); + Assert.Empty(result.Filters); + Assert.Empty(result.Replicas); + Assert.Empty(result.Selectors); + } + + [Fact] + public void PlacementPolicyFullTest() + { + PlacementPolicy policy = new() + { + ContainerBackupFactor = 3, + Unique = true + }; + + policy.Filters.AddRange( + [ + new () { Name = "filter1", Key = "filterKey1", Op =Operation.Eq, Value = "testValue1" }, + new () { Name = "filter2", Key = "filterKey2", Op =Operation.And, Value = "testValue2" } + ]); + + policy.Selectors.AddRange( + [ + new () { Name = "name1", Attribute = "attrib1", Clause = Clause.Same, Count = 5, Filter = "filter1" }, + new () { Name = "name2", Attribute = "attrib2", Clause = Clause.Distinct, Count = 4, Filter = "filter2" } + ]); + + policy.Replicas.AddRange( + [ + new () { EcDataCount = 2, EcParityCount = 3, Count = 4, Selector = "selector1"}, + new () { EcDataCount = 5, EcParityCount = 6, Count = 7, Selector = "selector2"}, + ]); + + var result = policy.ToModel(); + + Assert.Equal(3L, result.BackupFactor); + Assert.True(result.Unique); + Assert.Equal(2, result.Filters.Count); + Assert.Equal(2, result.Replicas.Length); + Assert.Equal(2, result.Selectors.Count); + + var rep0 = result.Replicas[0]; + Assert.Equal(2u, rep0.EcDataCount); + Assert.Equal(3u, rep0.EcParityCount); + Assert.Equal(4u, rep0.Count); + Assert.Equal("selector1", rep0.Selector); + + var rep1 = result.Replicas[1]; + Assert.Equal(5u, rep1.EcDataCount); + Assert.Equal(6u, rep1.EcParityCount); + Assert.Equal(7u, rep1.Count); + Assert.Equal("selector2", rep1.Selector); + + var f0 = result.Filters[0]; + Assert.Equal("filterKey1", f0.Key); + Assert.Equal("filter1", f0.Name); + Assert.Equal(1, f0.Operation); + Assert.Equal("testValue1", f0.Value); + + var f1 = result.Filters[1]; + Assert.Equal("filterKey2", f1.Key); + Assert.Equal("filter2", f1.Name); + Assert.Equal(8, f1.Operation); + Assert.Equal("testValue2", f1.Value); + + var s0 = result.Selectors[0]; + Assert.Equal("name1", s0.Name); + Assert.Equal("attrib1", s0.Attribute); + Assert.Equal(1, s0.Clause); + Assert.Equal(5L, s0.Count); + Assert.Equal("filter1", s0.Filter); + + var s1 = result.Selectors[1]; + Assert.Equal("name2", s1.Name); + Assert.Equal("attrib2", s1.Attribute); + Assert.Equal(2, s1.Clause); + Assert.Equal(4L, s1.Count); + Assert.Equal("filter2", s1.Filter); + } + + + [Theory] + [InlineData(1, "test" , 0, 0)] + [InlineData(1, "", 1000, 9999)] + [InlineData(1, "some long text to test reasonable length of the selector name", 100000000, 100000001)] + [InlineData(100, "test2", 1, 1)] + [InlineData(1, " ", 2, 3)] + [InlineData(10, "!", 0, 0)] + [InlineData(1, "123", 0, 0)] + public void ReplicaToModelTest(uint count, string selector, uint ecDataCount, uint ecParityCount) + { + Replica replica = new () + { + Count = count, + Selector = selector, + EcDataCount = ecDataCount, + EcParityCount = ecParityCount + }; + + FrostFsReplica model = replica.ToModel(); + + Assert.Equal(count, model.Count); + Assert.Equal(selector, model.Selector); + Assert.Equal(ecDataCount, model.EcDataCount); + Assert.Equal(ecParityCount, model.EcParityCount); + } + + [Theory] + [InlineData(1, "test", 0, 0)] + [InlineData(1, "", 1000, 9999)] + [InlineData(1, "some long text to test reasonable length of the selector name", 100000000, 100000001)] + [InlineData(100, "test2", 1, 1)] + [InlineData(1, " ", 2, 3)] + [InlineData(10, "!", 0, 0)] + [InlineData(1, "123", 0, 0)] + public void ReplicaToMessagelTest(uint count, string selector, uint ecDataCount, uint ecParityCount) + { + FrostFsReplica replica = new () + { + Count = count, + Selector = selector, + EcDataCount = ecDataCount, + EcParityCount = ecParityCount + }; + + Replica message = replica.ToMessage(); + + Assert.Equal(count, message.Count); + Assert.Equal(selector, message.Selector); + Assert.Equal(ecDataCount, message.EcDataCount); + Assert.Equal(ecParityCount, message.EcParityCount); + } + + [Theory] + [InlineData("test", 1, 2, "attribute", "filter")] + [InlineData("test", 0, 0, "longlonglonglonglonglonglonglonglonglonglonglonglong attribute", "longlonglonglonglonglonglonglonglonglonglonglonglong filter")] + [InlineData("test", 0, 1, "attribute", "filter")] + public void SelectorToMessageTest(string name, uint count, int clause, string attr, string filter) + { + FrostFsSelector selector = new (name) + { + Count = count, + Clause = clause, + Attribute = attr, + Filter = filter, + }; + + var message = selector.ToMessage(); + + Assert.Equal(name, message.Name); + Assert.Equal(count, message.Count); + Assert.Equal(clause, (int)message.Clause); + Assert.Equal(attr, message.Attribute); + Assert.Equal(filter, message.Filter); + + } + + [Theory] + [InlineData("test", 1, Clause.Same, "attribute", "filter")] + [InlineData("test", 0, Clause.Distinct, "longlonglonglonglonglonglonglonglonglonglonglonglong attribute", "longlonglonglonglonglonglonglonglonglonglonglonglong filter")] + public void SelectorToModelTest(string name, uint count, Clause clause, string attr, string filter) + { + Selector selector = new () + { + Name = name, + Count = count, + Clause = clause, + Attribute = attr, + Filter = filter + }; + + var model = selector.ToModel(); + + Assert.Equal(name, model.Name); + Assert.Equal(count, model.Count); + Assert.Equal((int)clause, model.Clause); + Assert.Equal(attr, model.Attribute); + Assert.Equal(filter, model.Filter); + } + + [Theory] + [InlineData("", "", 1, "")] + [InlineData("name", "key", 1, "val")] + [InlineData("longlonglonglonglonglonglonglonglonglonglonglonglong name", "longlonglonglonglonglonglonglonglonglonglonglonglong key", 10, "longlonglonglonglonglonglonglonglonglonglonglonglong val")] + public void FilterToMessageTest(string name, string key, int operation, string value) + { + FrostFsFilter filter = new (name, key, operation, value, []); + + var message = filter.ToMessage(); + + Assert.Equal(name, message.Name); + Assert.Equal(key, message.Key); + Assert.Equal(operation, (int)message.Op); + Assert.Equal(value, message.Value); + } + + [Theory] + [InlineData("", "", 1, "")] + [InlineData("name", "key", 2, "val")] + [InlineData("longlonglonglonglonglonglonglonglonglonglonglonglong name", "longlonglonglonglonglonglonglonglonglonglonglonglong key", 10, "longlonglonglonglonglonglonglonglonglonglonglonglong val")] + public void SubFilterToMessageTest(string name, string key, int operation, string value) + { + FrostFsFilter subFilter = new(name, key, operation, value, []); + + FrostFsFilter filter = new("name", "key", 1, "value", [subFilter]); + + var message = filter.ToMessage(); + + Assert.Single(message.Filters); + + var grpcFilter = message.Filters[0]; + Assert.Equal(name, grpcFilter.Name); + Assert.Equal(key, grpcFilter.Key); + Assert.Equal(operation, (int)grpcFilter.Op); + Assert.Equal(value, grpcFilter.Value); + } + + [Fact] + public void SubFiltersToMessageTest() + { + string[] names = ["", "name1", "some pretty long name for name test"]; + string[] keys = ["", "key1", "some pretty long key for name test"]; + int[] operations = [1, 2, 10]; + string[] values = ["", "val1", "some pretty long value for name test"]; + + var subFilter = new FrostFsFilter[3]; + + for (int i = 0; i < 3; i++) + { + subFilter[i] = new FrostFsFilter(names[i], keys[i], operations[i], values[i], []); + } + + FrostFsFilter filter = new("name", "key", 1, "value", subFilter); + + var message = filter.ToMessage(); + + Assert.Equal(3, message.Filters.Count); + + for (int i = 0; i < 3; i++) + { + var grpcFilter = message.Filters[i]; + Assert.Equal(names[i], grpcFilter.Name); + Assert.Equal(keys[i], grpcFilter.Key); + Assert.Equal(operations[i], (int)grpcFilter.Op); + Assert.Equal(values[i], grpcFilter.Value); + } + } + + [Theory] + [InlineData("", "", Operation.Unspecified, "")] + [InlineData("name", "key", Operation.Unspecified, "val")] + [InlineData("name", "key", Operation.And, "val")] + [InlineData("name", "key", Operation.Eq, "val")] + [InlineData("name", "key", Operation.Le, "val")] + [InlineData("name", "key", Operation.Like, "val")] + [InlineData("name", "key", Operation.Ge, "val")] + [InlineData("name", "key", Operation.Gt, "val")] + [InlineData("name", "key", Operation.Lt, "val")] + [InlineData("name", "key", Operation.Ne, "val")] + [InlineData("name", "key", Operation.Not, "val")] + [InlineData("name", "key", Operation.Or, "val")] + [InlineData("longlonglonglonglonglonglonglonglonglonglonglonglong name", "longlonglonglonglonglonglonglonglonglonglonglonglong key", Operation.Like, "longlonglonglonglonglonglonglonglonglonglonglonglong val")] + public void FrostFsFilterToModelTest(string name, string key, Operation operation, string value) + { + Filter filter = new() + { + Name = name, + Key = key, + Op = operation, + Value = value + }; + + var model = filter.ToModel(); + + Assert.Equal(name, model.Name); + Assert.Equal(key, model.Key); + Assert.Equal((int)operation, model.Operation); + Assert.Equal(value, model.Value); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs b/src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs new file mode 100644 index 00000000..9833ae19 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/PlacementVectorTests.cs @@ -0,0 +1,275 @@ +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; + +using FrostFS.SDK.Client.Models.Netmap.Placement; + +using Xunit.Abstractions; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public class PlacementVectorTests(ITestOutputHelper testOutputHelper) +{ + private static readonly JsonSerializerOptions serializeOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private readonly ITestOutputHelper _testOutputHelper = testOutputHelper; + + [Fact] + public void PlacementTest() + { + var path = ".\\..\\..\\..\\TestData\\PlacementTests"; + Assert.True(Directory.Exists(path)); + + var files = Directory.GetFiles(path); + + FrostFsVersion v = new(2, 13); + var addresses = new string[] { "localhost", "server1" }; + + foreach (var file in files.Where(f => f.EndsWith(".json", StringComparison.OrdinalIgnoreCase))) + { + //if (!file.EndsWith("selector_invalid.json")) + // continue; + + var fileName = file[(file.LastIndexOf("..\\", StringComparison.OrdinalIgnoreCase) + 3)..]; + _testOutputHelper.WriteLine($"Open file {fileName}"); + + var str = File.ReadAllText(file); + Assert.False(string.IsNullOrEmpty(str)); + + var testCase = JsonSerializer.Deserialize(str, serializeOptions); + + Assert.NotNull(testCase); + Assert.NotNull(testCase.Nodes); + Assert.True(testCase.Nodes.Length > 0); + + _testOutputHelper.WriteLine($"Test case: \"{testCase.Name}\""); + + var nodes = testCase.Nodes + .Select(n => new FrostFsNodeInfo(v, + n.State, + addresses.AsReadOnly(), + n.Attributes?.ToDictionary(x => x.Key, x => x.Value) ?? [], + n.PublicKeyBytes + ) + ) + .ToArray() + .AsReadOnly(); + + var netmap = new FrostFsNetmapSnapshot(100, nodes); + + Assert.NotNull(testCase.Tests); + + foreach (var test in testCase.Tests) + { + _testOutputHelper.WriteLine($"Start test \"{test.Name}\""); + + var policy = new FrostFsPlacementPolicy( + test.Policy!.Unique, + test.Policy.ContainerBackupFactor, + new Collection(test.Policy.Selectors?.Select(s => s.Selector).ToList() ?? []), + new Collection(test.Policy.Filters?.Select(f => f.Filter).ToList() ?? []), + test.Policy.Replicas?.Select(r => new FrostFsReplica(r.Count, r.Selector)).ToArray() ?? [] + ); + + try + { + var result = netmap.ContainerNodes(policy, test.PivotBytes); + + if (test.Result == null) + { + if (!string.IsNullOrEmpty(test.Error)) + { + Assert.Fail("Error is expected but has not been thrown"); + } + else + { + Assert.NotNull(test.Policy?.Replicas); + Assert.Equal(result.Length, test.Policy.Replicas.Length); + + for (int i = 0; i < result.Length; i++) + { + Assert.Empty(result[i]); + } + } + } + else + { + Assert.Equal(test.Result.Length, result.Length); + + for (var i = 0; i < test.Result.Length; i++) + { + Assert.Equal(test.Result[i].Length, result[i].Length); + for (var j = 0; j < test.Result[i].Length; j++) + { + CompareNodes(nodes[test.Result[i][j]].Attributes, result[i][j]); + } + } + + if (test.Placement?.Result != null && test.Placement.PivotBytes != null) + { + var placementResult = netmap.PlacementVectors(result, test.Placement.PivotBytes); + + Assert.Equal(test.Placement.Result.Length, placementResult.Length); + + for (int i = 0; i < placementResult.Length; i++) + { + Assert.Equal(test.Placement.Result[i].Length, placementResult[i].Length); + for (int j = 0; j < placementResult[i].Length; j++) + { + CompareNodes(nodes[test.Placement.Result[i][j]].Attributes, placementResult[i][j]); + } + } + } + } + } + catch (Exception ex) + { + if (!string.IsNullOrEmpty(test.Error)) + { + Assert.Contains(test.Error, ex.Message, StringComparison.InvariantCulture); + } + else + { + throw; + } + } + + _testOutputHelper.WriteLine($"Done"); + } + } + } + + + private static void CompareNodes(IReadOnlyDictionary attrs, FrostFsNodeInfo nodeInfo) + { + Assert.Equal(attrs.Count, nodeInfo.Attributes.Count); + Assert.True(attrs.OrderBy(k => k.Key).SequenceEqual(nodeInfo.Attributes.OrderBy(x => x.Key))); + } +} + +public class TestCase +{ + public string? Name { get; set; } + + public Node[]? Nodes { get; set; } + + public TestData[]? Tests { get; set; } +} + + + +public class Node +{ + [JsonPropertyName("attributes")] + public KeyValuePair[]? Attributes { get; set; } + + public string? PublicKey { get; set; } + + internal byte[]? PublicKeyBytes => string.IsNullOrEmpty(PublicKey) ? [] : Convert.FromBase64String(PublicKey); + + public string[]? Addresses { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public NodeState State { get; set; } = NodeState.Online; +} + +public class TestData +{ + public string? Name { get; set; } + + public PolicyDto? Policy { get; set; } + + public string? Pivot { get; set; } + + public int[][]? Result { get; set; } + + public string? Error { get; set; } + + internal byte[]? PivotBytes => Pivot != null ? Convert.FromBase64String(Pivot) : null; + + public ResultData? Placement { get; set; } +} + +public class PolicyDto +{ + public bool Unique { get; set; } + + public uint ContainerBackupFactor { get; set; } + + public FilterDto[]? Filters { get; set; } + + public ReplicaDto[]? Replicas { get; set; } + + public SelectorDto[]? Selectors { get; set; } +} + +public class SelectorDto() +{ + public uint Count { get; set; } + + public string? Name { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public ClauseValues Clause { get; set; } + + public string? Attribute { get; set; } + + public string? Filter { get; set; } + + public FrostFsSelector Selector => new(Name ?? string.Empty) + { + Count = Count, + Clause = (int)Clause, + Filter = Filter, + Attribute = Attribute + }; +} + +public class FilterDto +{ + public string? Name { get; set; } + + public string? Key { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public Operation Op { get; set; } + + public string? Value { get; set; } + + public FilterDto[]? Filters { get; set; } + + public FrostFsFilter Filter => new( + Name ?? string.Empty, + Key ?? string.Empty, + (int)Op, + Value ?? string.Empty, + Filters != null ? [.. Filters.Select(f => f.Filter)] : []); +} + +public class ReplicaDto +{ + public uint Count { get; set; } + + public string? Selector { get; set; } +} + +public class ResultData +{ + public string? Pivot { get; set; } + + public int[][]? Result { get; set; } + + internal byte[]? PivotBytes => Pivot != null ? Convert.FromBase64String(Pivot) : null; +} + +public enum ClauseValues +{ + UNSPECIFIED = 0, + SAME, + DISTINCT +} diff --git a/src/FrostFS.SDK.Tests/Unit/SessionTests.cs b/src/FrostFS.SDK.Tests/Unit/SessionTests.cs new file mode 100644 index 00000000..be2e0729 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/SessionTests.cs @@ -0,0 +1,77 @@ +using System.Diagnostics.CodeAnalysis; + +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public class SessionTest : SessionTestsBase +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async void CreateSessionTest(bool useContext) + { + var exp = 100u; + PrmSessionCreate param; + + CallContext ctx; + if (useContext) + { + ctx = new CallContext(TimeSpan.FromSeconds(20), Mocker.CancellationTokenSource.Token); + param = new PrmSessionCreate(exp, ["headerKey1", "headerValue1"]); + } + else + { + ctx = default; + param = new PrmSessionCreate(exp); + } + + var validTimeoutFrom = DateTime.UtcNow.AddSeconds(20); + + var result = await GetClient().CreateSessionAsync(param, ctx); + + var validTimeoutTo = DateTime.UtcNow.AddSeconds(20); + + Assert.NotNull(result); + Assert.NotEqual(Guid.Empty, result.Id); + + Assert.Equal(Mocker.SessionId, result.Id.ToByteArray(true)); + Assert.Equal(Mocker.SessionKey, result.SessionKey.ToArray()); + + //Assert.Equal(OwnerId.ToMessage(), result.Token.Body.OwnerId); + //Assert.Equal(exp, result.Token.Body.Lifetime.Exp); + //Assert.Equal(exp, result.Token.Body.Lifetime.Iat); + //Assert.Equal(exp, result.Token.Body.Lifetime.Nbf); + //Assert.Null(result.Token.Body.Container); + + Assert.NotNull(Mocker.CreateSessionRequest); + + Assert.Equal(OwnerId.ToMessage(), Mocker.CreateSessionRequest.Body.OwnerId); + Assert.Equal(exp, Mocker.CreateSessionRequest.Body.Expiration); + Assert.NotNull(Mocker.CreateSessionRequest.MetaHeader); + Assert.Equal(Mocker.Version.ToMessage(), Mocker.CreateSessionRequest.MetaHeader.Version); + + Assert.Null(Mocker.Metadata); + + if (useContext) + { + Assert.Single(Mocker.CreateSessionRequest.MetaHeader.XHeaders); + Assert.Equal(param.XHeaders[0], Mocker.CreateSessionRequest.MetaHeader.XHeaders.First().Key); + Assert.Equal(param.XHeaders[1], Mocker.CreateSessionRequest.MetaHeader.XHeaders.First().Value); + + Assert.Equal(Mocker.CancellationTokenSource.Token, Mocker.CancellationToken); + Assert.NotNull(Mocker.DateTime); + + Assert.True(Mocker.DateTime.Value >= validTimeoutFrom); + Assert.True(Mocker.DateTime.Value <= validTimeoutTo); + Assert.True(validTimeoutTo.Ticks >= Mocker.DateTime.Value.Ticks); + } + else + { + Assert.Empty(Mocker.CreateSessionRequest.MetaHeader.XHeaders); + Assert.Null(Mocker.DateTime); + } + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/SessionTestsBase.cs b/src/FrostFS.SDK.Tests/Unit/SessionTestsBase.cs new file mode 100644 index 00000000..eef8a457 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/SessionTestsBase.cs @@ -0,0 +1,48 @@ +using System.Security.Cryptography; + +using FrostFS.SDK.Client.Interfaces; +using FrostFS.SDK.Cryptography; + +using Microsoft.Extensions.Options; + +namespace FrostFS.SDK.Tests.Unit; + +public abstract class SessionTestsBase +{ + internal readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; + + protected IOptions Settings { get; set; } + + protected ECDsa ECDsaKey { get; set; } + protected FrostFsOwner OwnerId { get; set; } + protected SessionMocker Mocker { get; set; } + + protected SessionTestsBase() + { + Settings = Options.Create(new ClientSettings + { + Key = key, + Host = "http://localhost:8080" + }); + + ECDsaKey = key.LoadWif(); + OwnerId = FrostFsOwner.FromKey(ECDsaKey); + + Mocker = new SessionMocker(key) + { + PlacementPolicy = new FrostFsPlacementPolicy(true, 1, [], [], new FrostFsReplica(1)), + Version = new FrostFsVersion(2, 13) + }; + } + + protected IFrostFSClient GetClient() + { + return Client.FrostFSClient.GetTestInstance( + Settings, + (url) => Grpc.Net.Client.GrpcChannel.ForAddress(new Uri(url)), + new NetworkMocker(key).GetMock().Object, + Mocker.GetMock().Object, + new ContainerMocker(key).GetMock().Object, + new ObjectMocker(key).GetMock().Object); + } +} diff --git a/src/FrostFS.SDK.Tests/Unit/SignatureTests.cs b/src/FrostFS.SDK.Tests/Unit/SignatureTests.cs new file mode 100644 index 00000000..50623193 --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/SignatureTests.cs @@ -0,0 +1,39 @@ +using System.Text; +using FrostFS.SDK.Client; +using FrostFS.SDK.Client.Mappers.GRPC; + +namespace FrostFS.SDK.Tests.Unit; + +public class SignatureTests +{ + [Theory] + [InlineData(Refs.SignatureScheme.EcdsaSha512)] + [InlineData(Refs.SignatureScheme.EcdsaRfc6979Sha256)] + [InlineData(Refs.SignatureScheme.EcdsaRfc6979Sha256WalletConnect)] + + public void SignatureToMessageTest(Refs.SignatureScheme scheme) + { + var key = Encoding.UTF8.GetBytes("datafortest"); + var sign = Encoding.UTF8.GetBytes("signdatafortest"); + + var frostFsScheme = scheme switch + { + Refs.SignatureScheme.EcdsaRfc6979Sha256 => SignatureScheme.EcdsaRfc6979Sha256, + Refs.SignatureScheme.EcdsaRfc6979Sha256WalletConnect => SignatureScheme.EcdsaRfc6979Sha256WalletConnect, + Refs.SignatureScheme.EcdsaSha512 => SignatureScheme.EcdsaSha512 + }; + + FrostFsSignature signature = new() + { + Key = key, + Scheme = frostFsScheme, + Sign = sign + }; + + var result = signature.ToMessage(); + + Assert.Equal(scheme, result.Scheme); + Assert.Equal(sign, result.Sign.ToByteArray()); + Assert.Equal(key, result.Key.ToByteArray()); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/Unit/WalletTests.cs b/src/FrostFS.SDK.Tests/Unit/WalletTests.cs new file mode 100644 index 00000000..2acd77ee --- /dev/null +++ b/src/FrostFS.SDK.Tests/Unit/WalletTests.cs @@ -0,0 +1,25 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using FrostFS.SDK.Client; + +namespace FrostFS.SDK.Tests.Unit; + +[SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Default Value is correct for tests")] +public class WalletTest : SessionTestsBase +{ + [Fact] + public void TestWallet() + { + var password = Encoding.UTF8.GetBytes(""); + + var d = Directory.GetCurrentDirectory(); + var path = ".\\..\\..\\..\\TestData\\wallet.json"; + Assert.True(File.Exists(path)); + + var content = File.ReadAllText(path); + + var wif = WalletTools.GetWifFromWallet(content, password); + + Assert.Equal("KzPXA6669m2pf18XmUdoR8MnP1pi1PMmefiFujStVFnv7WR5SRmK", wif); + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.Tests/cat.jpg b/src/FrostFS.SDK.Tests/cat.jpg new file mode 100644 index 00000000..03236d57 Binary files /dev/null and b/src/FrostFS.SDK.Tests/cat.jpg differ