diff --git a/src/BeeNet.Core/Models/SwarmAddress.cs b/src/BeeNet.Core/Models/SwarmAddress.cs index 2816525..f1b12aa 100644 --- a/src/BeeNet.Core/Models/SwarmAddress.cs +++ b/src/BeeNet.Core/Models/SwarmAddress.cs @@ -19,6 +19,9 @@ namespace Etherna.BeeNet.Models { public readonly struct SwarmAddress : IEquatable { + // Consts. + public const char Separator = '/'; + // Constructor. public SwarmAddress(SwarmHash hash, string? path = null) { @@ -30,12 +33,12 @@ public SwarmAddress(string address) ArgumentNullException.ThrowIfNull(address, nameof(address)); // Trim initial slash. - address = address.TrimStart('/'); + address = address.TrimStart(Separator); // Extract hash root. - var slashIndex = address.IndexOf('/', StringComparison.InvariantCulture); + var slashIndex = address.IndexOf(Separator, StringComparison.InvariantCulture); var hash = slashIndex > 0 ? address[..slashIndex] : address; - var path = slashIndex > 0 ? address[slashIndex..] : "/"; + var path = slashIndex > 0 ? address[slashIndex..] : Separator.ToString(); // Set hash and path. Hash = new SwarmHash(hash); @@ -72,6 +75,6 @@ public override int GetHashCode() => Hash.GetHashCode() ^ // Helpers. internal static string NormalizePath(string? path) => - '/' + (path ?? "").TrimStart('/'); + Separator + (path ?? "").TrimStart(Separator); } } \ No newline at end of file diff --git a/src/BeeNet.Core/Models/SwarmHash.cs b/src/BeeNet.Core/Models/SwarmHash.cs index d9a0ae8..38392fa 100644 --- a/src/BeeNet.Core/Models/SwarmHash.cs +++ b/src/BeeNet.Core/Models/SwarmHash.cs @@ -31,7 +31,7 @@ namespace Etherna.BeeNet.Models public SwarmHash(byte[] hash) { ArgumentNullException.ThrowIfNull(hash, nameof(hash)); - if (hash.Length != HashSize) + if (!IsValidHash(hash)) throw new ArgumentOutOfRangeException(nameof(hash)); byteHash = hash; @@ -40,7 +40,7 @@ public SwarmHash(byte[] hash) public SwarmHash(string hash) { ArgumentNullException.ThrowIfNull(hash, nameof(hash)); - + try { byteHash = hash.HexToByteArray(); @@ -50,8 +50,8 @@ public SwarmHash(string hash) throw new ArgumentException("Invalid hash", nameof(hash)); } - if (byteHash.Length != HashSize) - throw new ArgumentException("Invalid hash", nameof(hash)); + if (!IsValidHash(byteHash)) + throw new ArgumentOutOfRangeException(nameof(hash)); } // Static properties. @@ -70,6 +70,22 @@ public uint ToBucketId() => // Static methods. public static SwarmHash FromByteArray(byte[] value) => new(value); public static SwarmHash FromString(string value) => new(value); + public static bool IsValidHash(byte[] value) + { + ArgumentNullException.ThrowIfNull(value, nameof(value)); + return value.Length == HashSize; + } + public static bool IsValidHash(string value) + { + try + { + return IsValidHash(value.HexToByteArray()); + } + catch (FormatException) + { + return false; + } + } // Operator methods. public static bool operator ==(SwarmHash left, SwarmHash right) => left.Equals(right); diff --git a/src/BeeNet.Core/Models/SwarmUri.cs b/src/BeeNet.Core/Models/SwarmUri.cs index 75a4d98..3300f29 100644 --- a/src/BeeNet.Core/Models/SwarmUri.cs +++ b/src/BeeNet.Core/Models/SwarmUri.cs @@ -31,11 +31,17 @@ public SwarmUri(SwarmHash? hash, string? path) Hash = hash; Path = hash != null ? SwarmAddress.NormalizePath(path) : path!; } - public SwarmUri(string uri, bool isAbsolute) + public SwarmUri(string uri, UriKind uriKind) { ArgumentNullException.ThrowIfNull(uri, nameof(uri)); - if (isAbsolute) + // Determine uri kind. + if (uriKind == UriKind.RelativeOrAbsolute) + uriKind = SwarmHash.IsValidHash(uri.Split(SwarmAddress.Separator)[0]) + ? UriKind.Absolute + : UriKind.Relative; + + if (uriKind == UriKind.Absolute) { var address = new SwarmAddress(uri); Hash = address.Hash; @@ -49,9 +55,9 @@ public SwarmUri(string uri, bool isAbsolute) // Properties. public SwarmHash? Hash { get; } - public bool IsAbsolute => Hash.HasValue; - public bool IsRooted => IsAbsolute || System.IO.Path.IsPathRooted(Path); + public bool IsRooted => UriKind == UriKind.Absolute || System.IO.Path.IsPathRooted(Path); public string Path { get; } + public UriKind UriKind => Hash.HasValue ? UriKind.Absolute : UriKind.Relative; // Methods. public bool Equals(SwarmUri other) => @@ -64,7 +70,7 @@ public override int GetHashCode() => Hash.GetHashCode() ^ (Path?.GetHashCode(StringComparison.InvariantCulture) ?? 0); public override string ToString() => - IsAbsolute ? new SwarmAddress(Hash!.Value, Path).ToString() : Path!; + UriKind == UriKind.Absolute ? new SwarmAddress(Hash!.Value, Path).ToString() : Path!; public SwarmAddress ToSwarmAddress(SwarmAddress prefix) { @@ -79,10 +85,13 @@ public bool TryGetRelativeTo(SwarmUri relativeTo, out SwarmUri output) if (relativeTo.Hash != Hash) return false; - if (!Path.StartsWith(relativeTo.Path, StringComparison.InvariantCulture)) + var dirs = Path.Split(SwarmAddress.Separator); + var relativeToDirs = relativeTo.Path.TrimEnd(SwarmAddress.Separator).Split(SwarmAddress.Separator); + if (dirs.Length < relativeToDirs.Length || + !dirs[..relativeToDirs.Length].SequenceEqual(relativeToDirs)) return false; - output = new SwarmUri(null, Path[relativeTo.Path.Length..].TrimStart('/')); + output = new SwarmUri(null, string.Join(SwarmAddress.Separator, dirs[relativeToDirs.Length..])); return true; } @@ -96,16 +105,19 @@ public static SwarmUri Combine(params SwarmUri[] uris) var combined = uris[0]; foreach (var uri in uris.Skip(1)) { - if (uri.IsAbsolute) + if (uri.UriKind == UriKind.Absolute) combined = uri; else if (uri.IsRooted) combined = new SwarmUri(combined.Hash, uri.Path); else - combined = new SwarmUri(combined.Hash, string.Concat(combined.Path ?? "", uri.Path!)); + combined = new SwarmUri( + combined.Hash, + (combined.Path ?? "").TrimEnd(SwarmAddress.Separator) + SwarmAddress.Separator + uri.Path); } return combined; } + public static SwarmUri FromString(string value) => new(value, UriKind.RelativeOrAbsolute); public static SwarmUri FromSwarmAddress(SwarmAddress value) => new(value.Hash, value.Path); public static SwarmUri FromSwarmHash(SwarmHash value) => new(value, null); @@ -114,6 +126,7 @@ public static SwarmUri Combine(params SwarmUri[] uris) public static bool operator !=(SwarmUri left, SwarmUri right) => !(left == right); // Implicit conversion operator methods. + public static implicit operator SwarmUri(string value) => new(value, UriKind.RelativeOrAbsolute); public static implicit operator SwarmUri(SwarmAddress value) => new(value.Hash, value.Path); public static implicit operator SwarmUri(SwarmHash value) => new(value, null); diff --git a/src/BeeNet.Util/Manifest/MantarayManifest.cs b/src/BeeNet.Util/Manifest/MantarayManifest.cs index 13b8161..2e8f354 100644 --- a/src/BeeNet.Util/Manifest/MantarayManifest.cs +++ b/src/BeeNet.Util/Manifest/MantarayManifest.cs @@ -22,7 +22,7 @@ namespace Etherna.BeeNet.Manifest public class MantarayManifest : IReadOnlyMantarayManifest { // Consts. - public const string RootPath = "/"; + public static readonly string RootPath = SwarmAddress.Separator.ToString(); // Fields. private readonly Func hasherBuilder; diff --git a/src/BeeNet.Util/Manifest/MantarayNode.cs b/src/BeeNet.Util/Manifest/MantarayNode.cs index 3105a8e..9c38083 100644 --- a/src/BeeNet.Util/Manifest/MantarayNode.cs +++ b/src/BeeNet.Util/Manifest/MantarayNode.cs @@ -27,7 +27,6 @@ public class MantarayNode : IReadOnlyMantarayNode { // Consts. public const int ForksIndexSize = 32; - public const char PathSeparator = '/'; public static readonly byte[] Version02Hash = new HashProvider().ComputeHash( "mantaray:0.2"u8.ToArray()).Take(VersionHashSize).ToArray(); public const int VersionHashSize = 31; @@ -231,7 +230,7 @@ private byte[] ToByteArray() private void UpdateFlagIsWithPathSeparator(string path) { - if (path.IndexOf(PathSeparator, StringComparison.InvariantCulture) > 0) + if (path.IndexOf(SwarmAddress.Separator, StringComparison.InvariantCulture) > 0) SetNodeTypeFlag(NodeType.WithPathSeparator); else RemoveNodeTypeFlag(NodeType.WithPathSeparator); diff --git a/src/BeeNet.Util/Manifest/ReferencedMantarayNode.cs b/src/BeeNet.Util/Manifest/ReferencedMantarayNode.cs index 1b76c02..cdcabfa 100644 --- a/src/BeeNet.Util/Manifest/ReferencedMantarayNode.cs +++ b/src/BeeNet.Util/Manifest/ReferencedMantarayNode.cs @@ -97,8 +97,8 @@ public async Task> GetResourceMetadataAsync( if (path.Length == 0) { //try to lookup for index document suffix - if (!_forks.TryGetValue('/', out var rootFork) || - rootFork.Prefix != "/") + if (!_forks.TryGetValue(SwarmAddress.Separator, out var rootFork) || + rootFork.Prefix != SwarmAddress.Separator.ToString()) throw new KeyNotFoundException($"Final path {path} can't be found"); if (!rootFork.Node.Metadata.TryGetValue(ManifestEntry.WebsiteIndexDocPathKey, out var suffix)) @@ -136,8 +136,8 @@ public async Task ResolveResourceHashAsync(string path) return EntryHash.Value; //try to lookup for index document suffix - if (!_forks.TryGetValue('/', out var rootFork) || - rootFork.Prefix != "/") + if (!_forks.TryGetValue(SwarmAddress.Separator, out var rootFork) || + rootFork.Prefix != SwarmAddress.Separator.ToString()) throw new KeyNotFoundException($"Final path {path} can't be found"); if (!rootFork.Node.Metadata.TryGetValue(ManifestEntry.WebsiteIndexDocPathKey, out var suffix)) diff --git a/src/BeeNet.Util/Services/CalculatorService.cs b/src/BeeNet.Util/Services/CalculatorService.cs index 7920a9b..023cf1b 100644 --- a/src/BeeNet.Util/Services/CalculatorService.cs +++ b/src/BeeNet.Util/Services/CalculatorService.cs @@ -38,7 +38,7 @@ public async Task EvaluateDirectoryUploadAsync( IChunkStore? chunkStore = null) { // Checks. - if (indexFilename?.Contains('/', StringComparison.InvariantCulture) == true) + if (indexFilename?.Contains(SwarmAddress.Separator, StringComparison.InvariantCulture) == true) throw new ArgumentException( "Index document suffix must not include slash character", nameof(indexFilename)); diff --git a/test/BeeNet.Core.UnitTest/Models/SwarmUriTest.cs b/test/BeeNet.Core.UnitTest/Models/SwarmUriTest.cs index 7c9ce58..a4f681f 100644 --- a/test/BeeNet.Core.UnitTest/Models/SwarmUriTest.cs +++ b/test/BeeNet.Core.UnitTest/Models/SwarmUriTest.cs @@ -22,13 +22,21 @@ namespace Etherna.BeeNet.Models public class SwarmUriTest { // Internal classes. + public class CombineSwarmUrisTestElement( + SwarmUri[] inputUris, + SwarmUri expectedUri) + { + public SwarmUri[] InputUris { get; } = inputUris; + public SwarmUri ExpectedUri { get; } = expectedUri; + } + public class HashAndPathToUriTestElement( SwarmHash? inputHash, string? inputPath, Type? expectedExceptionType, SwarmHash? expectedHash, string expectedPath, - bool expectedIsAbsolute, + UriKind expectedUriKind, bool expectedIsRooted) { public SwarmHash? InputHash { get; } = inputHash; @@ -36,26 +44,36 @@ public class HashAndPathToUriTestElement( public Type? ExpectedExceptionType { get; } = expectedExceptionType; public SwarmHash? ExpectedHash { get; } = expectedHash; public string ExpectedPath { get; } = expectedPath; - public bool ExpectedIsAbsolute { get; } = expectedIsAbsolute; public bool ExpectedIsRooted { get; } = expectedIsRooted; + public UriKind ExpectedUriKind { get; } = expectedUriKind; + } + + public class TryGetRelativeToUriTestElement( + SwarmUri originUri, + SwarmUri relativeToUri, + SwarmUri? expectedUri) + { + public SwarmUri OriginUri { get; } = originUri; + public SwarmUri RelativeToUri { get; } = relativeToUri; + public SwarmUri? ExpectedUri { get; } = expectedUri; } public class StringToUriTestElement( string inputString, - bool inputIsAbsolute, + UriKind inputUriKind, Type? expectedExceptionType, SwarmHash? expectedHash, string expectedPath, - bool expectedIsAbsolute, + UriKind expectedUriKind, bool expectedIsRooted) { public string InputString { get; } = inputString; - public bool InputIsAbsolute { get; } = inputIsAbsolute; + public UriKind InputUriKind { get; } = inputUriKind; public Type? ExpectedExceptionType { get; } = expectedExceptionType; public SwarmHash? ExpectedHash { get; } = expectedHash; public string ExpectedPath { get; } = expectedPath; - public bool ExpectedIsAbsolute { get; } = expectedIsAbsolute; public bool ExpectedIsRooted { get; } = expectedIsRooted; + public UriKind ExpectedUriKind { get; } = expectedUriKind; } public class UriToStringTestElement( @@ -67,6 +85,38 @@ public class UriToStringTestElement( } // Data. + public static IEnumerable CombineSwarmUrisTests + { + get + { + var tests = new List + { + // Only relative not rooted paths. + new(["Im", "a/simple/", "path"], + new SwarmUri("Im/a/simple/path", UriKind.Relative)), + + // Relative with rooted paths. + new(["Im", "a", "/rooted", "path"], + new SwarmUri("/rooted/path", UriKind.Relative)), + + // Relative and absolute paths. + new(["Im", "a", "relative", new SwarmUri(SwarmHash.Zero, "absolute"), "path"], + new SwarmUri("0000000000000000000000000000000000000000000000000000000000000000/absolute/path", UriKind.Absolute)), + + // Multi absolute paths. + new([ + new SwarmUri(new SwarmHash("0000000000000000000000000000000000000000000000000000000000000000"), null), + new SwarmUri(null, "zeros"), + new SwarmUri(new SwarmHash("1111111111111111111111111111111111111111111111111111111111111111"), null), + new SwarmUri(null, "ones") + ], + new SwarmUri("1111111111111111111111111111111111111111111111111111111111111111/ones", UriKind.Absolute)), + }; + + return tests.Select(t => new object[] { t }); + } + } + public static IEnumerable HashAndPathToUriTests { get @@ -79,7 +129,7 @@ public static IEnumerable HashAndPathToUriTests typeof(ArgumentException), null, "", - false, + UriKind.RelativeOrAbsolute, false), // Only hash. @@ -88,7 +138,7 @@ public static IEnumerable HashAndPathToUriTests null, SwarmHash.Zero, "/", - true, + UriKind.Absolute, true), // No hash and not rooted path. @@ -97,7 +147,7 @@ public static IEnumerable HashAndPathToUriTests null, null, "not/rooted/path", - false, + UriKind.Relative, false), // No hash and rooted path. @@ -106,7 +156,7 @@ public static IEnumerable HashAndPathToUriTests null, null, "/rooted/path", - false, + UriKind.Relative, true), // Hash and not rooted path. @@ -115,7 +165,7 @@ public static IEnumerable HashAndPathToUriTests null, SwarmHash.Zero, "/not/rooted/path", - true, + UriKind.Absolute, true), // Hash and rooted path. @@ -124,7 +174,7 @@ public static IEnumerable HashAndPathToUriTests null, SwarmHash.Zero, "/rooted/path", - true, + UriKind.Absolute, true), }; @@ -138,64 +188,185 @@ public static IEnumerable StringToUriTests { var tests = new List { + // RelativeOrAbsolute, not rooted, not starting with hash. + new("not/rooted/path", + UriKind.RelativeOrAbsolute, + null, + null, + "not/rooted/path", + UriKind.Relative, + false), + + // RelativeOrAbsolute, rooted, not starting with hash. + new("/rooted/path", + UriKind.RelativeOrAbsolute, + null, + null, + "/rooted/path", + UriKind.Relative, + true), + + // RelativeOrAbsolute, only hash. + new("0000000000000000000000000000000000000000000000000000000000000000", + UriKind.RelativeOrAbsolute, + null, + SwarmHash.Zero, + "/", + UriKind.Absolute, + true), + + // RelativeOrAbsolute, not rooted, starting with hash. + new("0000000000000000000000000000000000000000000000000000000000000000/not/rooted/path", + UriKind.RelativeOrAbsolute, + null, + SwarmHash.Zero, + "/not/rooted/path", + UriKind.Absolute, + true), + + // RelativeOrAbsolute, rooted, starting with hash. + new("/0000000000000000000000000000000000000000000000000000000000000000/rooted/path", + UriKind.RelativeOrAbsolute, + null, + null, + "/0000000000000000000000000000000000000000000000000000000000000000/rooted/path", + UriKind.Relative, + true), + // Relative not rooted path. new("relative/not/rooted/path", - false, + UriKind.Relative, null, null, "relative/not/rooted/path", - false, + UriKind.Relative, false), // Relative rooted path. new("/relative/rooted/path", - false, + UriKind.Relative, null, null, "/relative/rooted/path", - false, + UriKind.Relative, true), // Absolute with only hash (without slashes). new("0000000000000000000000000000000000000000000000000000000000000000", - true, + UriKind.Absolute, null, SwarmHash.Zero, "/", - true, + UriKind.Absolute, true), // Absolute with only hash (with slashes). new("/0000000000000000000000000000000000000000000000000000000000000000/", - true, + UriKind.Absolute, null, SwarmHash.Zero, "/", - true, + UriKind.Absolute, true), // Absolute with hash and path. new("0000000000000000000000000000000000000000000000000000000000000000/Im/a/path", - true, + UriKind.Absolute, null, SwarmHash.Zero, "/Im/a/path", - true, + UriKind.Absolute, true), // Absolute with invalid initial hash (throws). new("not/An/Hash", - true, + UriKind.Absolute, typeof(ArgumentException), null, "", - false, + UriKind.RelativeOrAbsolute, false) }; return tests.Select(t => new object[] { t }); } } + + public static IEnumerable TryGetRelativeToUriTests + { + get + { + var tests = new List + { + // Hash and not hash. + new (SwarmHash.Zero, + "not/an/hash", + null), + + // Different hashes. + new (SwarmHash.Zero, + new SwarmHash("1111111111111111111111111111111111111111111111111111111111111111"), + null), + + // Different paths + new ("we/are", + "different", + null), + + // Different paths with equal root + new ("we/start/equally", + "we/continue/differently", + null), + + // Origin contains relativeTo path, but different dirs count + new ("we/arent/equal", + "we/are", + null), + + // Different dir names + new ("we/arent/equal", + "we/are/similar", + null), + + // One is rooted, the other no + new ("we/are/similar", + "/we/are/similar", + null), + + // One is rooted, the other no + new ("/we/are/similar", + "we/are/similar", + null), + + // RelativeTo contains origin path + new ("Im/very/similar", + "Im", + "very/similar"), + + // RelativeTo contains origin path (relativeTo slash ended) + new ("Im/very/similar", + "Im/", + "very/similar"), + + // Paths are equal + new ("Im/very/equal", + "Im/very/equal", + ""), + + // Paths are equal (relativeTo slash ended) + new ("Im/very/equal", + "Im/very/equal/", + ""), + + // Paths are equal (origin slash ended) + new ("Im/very/equal/", + "Im/very/equal", + ""), + }; + + return tests.Select(t => new object[] { t }); + } + } public static IEnumerable UriToStringTests { @@ -225,6 +396,15 @@ public static IEnumerable UriToStringTests } // Tests. + [Theory, MemberData(nameof(CombineSwarmUrisTests))] + public void CombineSwarmUris(CombineSwarmUrisTestElement test) + { + var result = SwarmUri.Combine(test.InputUris); + + Assert.Equal(test.ExpectedUri.Hash, result.Hash); + Assert.Equal(test.ExpectedUri.Path, result.Path); + } + [Theory, MemberData(nameof(HashAndPathToUriTests))] public void HashAndPathToUri(HashAndPathToUriTestElement test) { @@ -240,10 +420,39 @@ public void HashAndPathToUri(HashAndPathToUriTestElement test) Assert.Equal(test.ExpectedHash, result.Hash); Assert.Equal(test.ExpectedPath, result.Path); - Assert.Equal(test.ExpectedIsAbsolute, result.IsAbsolute); + Assert.Equal(test.ExpectedUriKind, result.UriKind); Assert.Equal(test.ExpectedIsRooted, result.IsRooted); } } + + [Fact] + public void ToSwarmAddressConversion() + { + var originalUri = new SwarmUri(null, "Im/path"); + var prefixAddress = new SwarmAddress(SwarmHash.Zero, "Im/prefix"); + + var result = originalUri.ToSwarmAddress(prefixAddress); + + Assert.Equal(SwarmHash.Zero, result.Hash); + Assert.Equal("/Im/prefix/Im/path", result.Path); + } + + [Theory, MemberData(nameof(TryGetRelativeToUriTests))] + public void TryGetRelativeToUri(TryGetRelativeToUriTestElement test) + { + var success = test.OriginUri.TryGetRelativeTo( + test.RelativeToUri, + out var result); + + if (test.ExpectedUri is null) + Assert.False(success); + else + { + Assert.True(success); + Assert.Equal(test.ExpectedUri.Value.Hash, result.Hash); + Assert.Equal(test.ExpectedUri.Value.Path, result.Path); + } + } [Theory, MemberData(nameof(StringToUriTests))] public void StringToUri(StringToUriTestElement test) @@ -252,15 +461,15 @@ public void StringToUri(StringToUriTestElement test) { Assert.Throws( test.ExpectedExceptionType, - () => new SwarmUri(test.InputString, test.InputIsAbsolute)); + () => new SwarmUri(test.InputString, test.InputUriKind)); } else { - var result = new SwarmUri(test.InputString, test.InputIsAbsolute); + var result = new SwarmUri(test.InputString, test.InputUriKind); Assert.Equal(test.ExpectedHash, result.Hash); Assert.Equal(test.ExpectedPath, result.Path); - Assert.Equal(test.ExpectedIsAbsolute, result.IsAbsolute); + Assert.Equal(test.ExpectedUriKind, result.UriKind); Assert.Equal(test.ExpectedIsRooted, result.IsRooted); } }