libsood

Zig library for Roon Core discovery message, with C-compatible API and WebAssembly.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
  127. 127
  128. 128
  129. 129
  130. 130
  131. 131
  132. 132
  133. 133
  134. 134
  135. 135
  136. 136
  137. 137
  138. 138
  139. 139
  140. 140
// Copyright 2025 Shota FUJI
//
// Licensed under the Zero-Clause BSD License or the Apache License, Version 2.0, at your option.
// You may not use, copy, modify, or distribute this file except according to those terms. You can
// find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt, and a copy of the Apache License,
// Version 2.0 at LICENSES/Apache-2.0.txt. You may also obtain a copy of the Apache License, Version
// 2.0 at <https://www.apache.org/licenses/LICENSE-2.0>
//
// SPDX-License-Identifier: 0BSD OR Apache-2.0

//! This module contains constants and concret type on top of Message struct.

const std = @import("std");

const Message = @import("Message.zig");

/// Target UDP port to use for sending IP multicast and broadcast.
/// Though you can also listen for this UDP port to receive broadcast from another client,
/// this library does not provide API for handling that case.
pub const udp_port = 9003;

/// IPv4 address for IP multicast. Send SOOD message to this IP address on `udp_port`.
pub const multicast_ipv4_address = [4]u8{ 239, 255, 90, 90 };

pub const Query = struct {
    pub const header = Message.Header{ .kind = .query };

    /// A required property for a discovery query. Node.js API uses hard-coded ID
    /// thus this library uses the same one.
    pub const query_service_id = Message.Property{
        .key = "query_service_id",
        .value = "00720724-5143-4a9b-abac-0e50cba674bb",
    };

    pub const properties = [_]Message.Property{
        query_service_id,
    };

    /// Premade bytes for minimum discovery query message.
    pub const prebuilt = "SOOD\x02Q\x10query_service_id\x00\x2400720724-5143-4a9b-abac-0e50cba674bb";
};

test "Query.prebuilt should equal to one built by `write`" {
    const built = try Message.write(
        std.testing.allocator,
        Query.header,
        &Query.properties,
    );
    defer std.testing.allocator.free(built);

    try std.testing.expectEqualSlices(u8, built, Query.prebuilt);
}

pub const Response = struct {
    message: Message,

    pub const InitError = error{
        /// Message's type (kind) is not "response" (`R`)
        NonResponseKindMessage,
    };

    pub fn init(message: Message) InitError!@This() {
        if (message.header.kind != .response) {
            return InitError.NonResponseKindMessage;
        }

        return .{ .message = message };
    }

    pub const GetPropertyError = error{
        /// Response message does not contain required property.
        MissingRequiredProperty,

        /// Cannot convert property value to desired type.
        UnexpectedPropertyValue,
    } || Message.Properties.ParseError;

    /// Iterates over properties and returns a property of the same `key`.
    /// Returns `null` if no properties matched to the `key` .
    ///
    /// This function converts the found value into `T`. It currently supports:
    /// * `[]const u8`          ... returns as-is.
    /// * Integers (e.g. `u16`) ... returns the result of `std.fmt.parseInt`.
    /// Returns an error if that conversion failed.
    inline fn getProperty(self: @This(), comptime T: type, key: []const u8) GetPropertyError!?T {
        var iter = self.message.body.iterator();

        while (try iter.next()) |prop| {
            if (std.mem.eql(u8, key, prop.key)) {
                switch (T) {
                    []const u8 => return prop.value,
                    else => return std.fmt.parseInt(T, prop.value, 10) catch GetPropertyError.UnexpectedPropertyValue,
                }
            }
        }

        return null;
    }

    /// Same as `getProperty`, except returns an error in place of `null`.
    inline fn getRequiredProperty(self: @This(), comptime T: type, key: []const u8) GetPropertyError!T {
        return try self.getProperty(T, key) orelse GetPropertyError.MissingRequiredProperty;
    }

    /// Returns "Roon Server name" (`name` property).
    /// This string is what user see in Settings/General page and can be changed at Settings/Setup
    /// page.
    pub fn getName(self: @This()) GetPropertyError!?[]const u8 {
        return self.getProperty([]const u8, "name");
    }

    /// Returns Roon server version strings (`display_version` property).
    /// As the name suggests, this value is for display purpose. Format is not defined and there is
    /// no stability guarantee, therefore parsing of this value is not recommended.
    pub fn getDisplayVersion(self: @This()) GetPropertyError!?[]const u8 {
        return self.getProperty([]const u8, "display_version");
    }

    /// Returns the Roon server's unique ID string.
    pub fn getUniqueId(self: @This()) GetPropertyError![]const u8 {
        return self.getRequiredProperty([]const u8, "unique_id");
    }

    pub fn getServiceId(self: @This()) GetPropertyError!?[]const u8 {
        return self.getProperty([]const u8, "service_id");
    }

    pub fn getTcpPort(self: @This()) GetPropertyError!?u16 {
        return self.getProperty(u16, "tcp_port");
    }

    /// Returns TCP port number for WebSocket communication.
    pub fn getHttpPort(self: @This()) GetPropertyError!u16 {
        return self.getRequiredProperty(u16, "http_port");
    }

    pub fn getHttpsPort(self: @This()) GetPropertyError!?u16 {
        return self.getProperty(u16, "https_port");
    }
};