takagi 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +70 -7
- data/.yard/templates/default/layout/html/layout.erb +34 -0
- data/AGENTS.md +16 -0
- data/CHANGELOG.md +158 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +590 -23
- data/ROADMAP.md +55 -0
- data/Rakefile +4 -4
- data/Steepfile +39 -0
- data/bin/takagi-dev +159 -0
- data/docs/FIRST_PLUGIN_GUIDE.md +224 -0
- data/docs/HOOKS.md +31 -0
- data/examples/client_lifecycle_example.rb +118 -0
- data/examples/cloud_gateway_app.rb +217 -0
- data/examples/nested_api_app.rb +258 -0
- data/examples/simple_device_app.rb +71 -0
- data/examples/takagi.yml +138 -0
- data/lib/takagi/application.rb +256 -0
- data/lib/takagi/base/middleware_management.rb +39 -0
- data/lib/takagi/base/plugin_management.rb +75 -0
- data/lib/takagi/base/reactor_management.rb +104 -0
- data/lib/takagi/base/server_lifecycle.rb +156 -0
- data/lib/takagi/base.rb +103 -11
- data/lib/takagi/branding.rb +88 -0
- data/lib/takagi/cbor/decoder.rb +385 -0
- data/lib/takagi/cbor/encoder.rb +260 -0
- data/lib/takagi/cbor/error.rb +17 -0
- data/lib/takagi/cbor/version.rb +9 -0
- data/lib/takagi/client/response.rb +236 -0
- data/lib/takagi/client.rb +265 -0
- data/lib/takagi/client_base.rb +204 -0
- data/lib/takagi/coap/code_helpers.rb +190 -0
- data/lib/takagi/coap/registries/base.rb +165 -0
- data/lib/takagi/coap/registries/content_format.rb +71 -0
- data/lib/takagi/coap/registries/message_type.rb +69 -0
- data/lib/takagi/coap/registries/method.rb +38 -0
- data/lib/takagi/coap/registries/option.rb +71 -0
- data/lib/takagi/coap/registries/response.rb +93 -0
- data/lib/takagi/coap/registries/signaling.rb +34 -0
- data/lib/takagi/coap/signaling.rb +10 -0
- data/lib/takagi/coap.rb +37 -0
- data/lib/takagi/composite_router.rb +186 -0
- data/lib/takagi/config.rb +337 -0
- data/lib/takagi/controller/resource_allocator.rb +164 -0
- data/lib/takagi/controller/thread_pool.rb +144 -0
- data/lib/takagi/controller.rb +319 -0
- data/lib/takagi/core/attribute_set.rb +128 -0
- data/lib/takagi/discovery/core_link_format.rb +137 -0
- data/lib/takagi/errors.rb +536 -0
- data/lib/takagi/event_bus/address_prefix.rb +142 -0
- data/lib/takagi/event_bus/async_executor.rb +235 -0
- data/lib/takagi/event_bus/coap_bridge.rb +208 -0
- data/lib/takagi/event_bus/future.rb +153 -0
- data/lib/takagi/event_bus/lru_cache.rb +157 -0
- data/lib/takagi/event_bus/message_buffer.rb +237 -0
- data/lib/takagi/event_bus/observer_cleanup.rb +110 -0
- data/lib/takagi/event_bus/scope.rb +74 -0
- data/lib/takagi/event_bus.rb +594 -0
- data/lib/takagi/helpers.rb +88 -0
- data/lib/takagi/hooks.rb +82 -0
- data/lib/takagi/initializer.rb +18 -0
- data/lib/takagi/logger.rb +15 -6
- data/lib/takagi/message/base.rb +155 -0
- data/lib/takagi/message/deduplication_cache.rb +84 -0
- data/lib/takagi/message/inbound.rb +147 -0
- data/lib/takagi/message/outbound.rb +223 -0
- data/lib/takagi/message/request.rb +158 -0
- data/lib/takagi/message/retransmission_manager.rb +193 -0
- data/lib/takagi/middleware/authentication.rb +19 -0
- data/lib/takagi/middleware/caching.rb +23 -0
- data/lib/takagi/middleware/debugging.rb +16 -0
- data/lib/takagi/middleware/logging.rb +14 -0
- data/lib/takagi/middleware/metrics.rb +440 -0
- data/lib/takagi/middleware/rate_limiting.rb +24 -0
- data/lib/takagi/middleware_stack.rb +166 -0
- data/lib/takagi/network/base.rb +76 -0
- data/lib/takagi/network/framing/tcp.rb +222 -0
- data/lib/takagi/network/framing/udp.rb +110 -0
- data/lib/takagi/network/registry.rb +72 -0
- data/lib/takagi/network/tcp.rb +60 -0
- data/lib/takagi/network/tcp_sender.rb +21 -0
- data/lib/takagi/network/udp.rb +61 -0
- data/lib/takagi/network/udp_sender.rb +20 -0
- data/lib/takagi/observable/emitter.rb +62 -0
- data/lib/takagi/observable/reactor.rb +488 -0
- data/lib/takagi/observable/registry.rb +122 -0
- data/lib/takagi/observe_registry.rb +10 -0
- data/lib/takagi/observer/client.rb +68 -0
- data/lib/takagi/observer/registry.rb +137 -0
- data/lib/takagi/observer/sender.rb +39 -0
- data/lib/takagi/observer/watcher.rb +43 -0
- data/lib/takagi/plugin.rb +313 -0
- data/lib/takagi/profiles.rb +176 -0
- data/lib/takagi/reactor.rb +23 -0
- data/lib/takagi/reactor_registry.rb +64 -0
- data/lib/takagi/registry/base.rb +268 -0
- data/lib/takagi/response_builder.rb +141 -0
- data/lib/takagi/router/metadata_extractor.rb +133 -0
- data/lib/takagi/router/route_matcher.rb +83 -0
- data/lib/takagi/router.rb +284 -25
- data/lib/takagi/serialization/base.rb +102 -0
- data/lib/takagi/serialization/cbor_serializer.rb +92 -0
- data/lib/takagi/serialization/json_serializer.rb +96 -0
- data/lib/takagi/serialization/octet_stream_serializer.rb +82 -0
- data/lib/takagi/serialization/registry.rb +187 -0
- data/lib/takagi/serialization/text_serializer.rb +87 -0
- data/lib/takagi/serialization.rb +117 -0
- data/lib/takagi/server/multi.rb +41 -0
- data/lib/takagi/server/registry.rb +71 -0
- data/lib/takagi/server/tcp.rb +249 -0
- data/lib/takagi/server/udp.rb +139 -0
- data/lib/takagi/server/udp_worker.rb +174 -0
- data/lib/takagi/server.rb +1 -31
- data/lib/takagi/server_registry.rb +10 -0
- data/lib/takagi/tcp_client.rb +142 -0
- data/lib/takagi/version.rb +2 -1
- data/lib/takagi.rb +24 -3
- data/sig/takagi/application.rbs +48 -0
- data/sig/takagi/base/middleware_management.rbs +33 -0
- data/sig/takagi/base/reactor_management.rbs +52 -0
- data/sig/takagi/base/server_lifecycle.rbs +54 -0
- data/sig/takagi/base.rbs +48 -0
- data/sig/takagi/cbor/decoder.rbs +171 -0
- data/sig/takagi/cbor/encoder.rbs +146 -0
- data/sig/takagi/cbor/error.rbs +19 -0
- data/sig/takagi/cbor/version.rbs +7 -0
- data/sig/takagi/client/response.rbs +148 -0
- data/sig/takagi/client.rbs +119 -0
- data/sig/takagi/client_base.rbs +135 -0
- data/sig/takagi/coap/code_helpers.rbs +91 -0
- data/sig/takagi/coap/registries/base.rbs +95 -0
- data/sig/takagi/coap/registries/content_format.rbs +47 -0
- data/sig/takagi/coap/registries/message_type.rbs +53 -0
- data/sig/takagi/coap/registries/method.rbs +27 -0
- data/sig/takagi/coap/registries/option.rbs +43 -0
- data/sig/takagi/coap/registries/response.rbs +52 -0
- data/sig/takagi/coap.rbs +24 -0
- data/sig/takagi/composite_router.rbs +46 -0
- data/sig/takagi/config.rbs +134 -0
- data/sig/takagi/controller.rbs +73 -0
- data/sig/takagi/core/attribute_set.rbs +57 -0
- data/sig/takagi/discovery/core_link_format.rbs +50 -0
- data/sig/takagi/event_bus/address_prefix.rbs +78 -0
- data/sig/takagi/event_bus/async_executor.rbs +88 -0
- data/sig/takagi/event_bus/coap_bridge.rbs +93 -0
- data/sig/takagi/event_bus/future.rbs +78 -0
- data/sig/takagi/event_bus/lru_cache.rbs +86 -0
- data/sig/takagi/event_bus/message_buffer.rbs +133 -0
- data/sig/takagi/event_bus/observer_cleanup.rbs +62 -0
- data/sig/takagi/event_bus.rbs +320 -0
- data/sig/takagi/helpers.rbs +34 -0
- data/sig/takagi/initializer.rbs +9 -0
- data/sig/takagi/logger.rbs +17 -0
- data/sig/takagi/message/base.rbs +64 -0
- data/sig/takagi/message/deduplication_cache.rbs +49 -0
- data/sig/takagi/message/inbound.rbs +76 -0
- data/sig/takagi/message/outbound.rbs +48 -0
- data/sig/takagi/message/request.rbs +32 -0
- data/sig/takagi/message/retransmission_manager.rbs +76 -0
- data/sig/takagi/middleware/authentication.rbs +11 -0
- data/sig/takagi/middleware/caching.rbs +13 -0
- data/sig/takagi/middleware/debugging.rbs +9 -0
- data/sig/takagi/middleware/logging.rbs +7 -0
- data/sig/takagi/middleware/metrics.rbs +15 -0
- data/sig/takagi/middleware/rate_limiting.rbs +13 -0
- data/sig/takagi/middleware_stack.rbs +69 -0
- data/sig/takagi/network/tcp_sender.rbs +10 -0
- data/sig/takagi/network/udp_sender.rbs +14 -0
- data/sig/takagi/observe_registry.rbs +36 -0
- data/sig/takagi/observer/client.rbs +36 -0
- data/sig/takagi/observer/sender.rbs +12 -0
- data/sig/takagi/observer/watcher.rbs +18 -0
- data/sig/takagi/profiles.rbs +33 -0
- data/sig/takagi/reactor.rbs +20 -0
- data/sig/takagi/reactor_registry.rbs +14 -0
- data/sig/takagi/response_builder.rbs +12 -0
- data/sig/takagi/router/metadata_extractor.rbs +71 -0
- data/sig/takagi/router/route_matcher.rbs +43 -0
- data/sig/takagi/router.rbs +166 -0
- data/sig/takagi/serialization.rbs +32 -0
- data/sig/takagi/server/multi.rbs +16 -0
- data/sig/takagi/server/tcp.rbs +42 -0
- data/sig/takagi/server/udp.rbs +52 -0
- data/sig/takagi/server/udp_worker.rbs +42 -0
- data/sig/takagi/server.rbs +4 -0
- data/sig/takagi/server_registry.rbs +71 -0
- data/sig/takagi/tcp_client.rbs +23 -0
- data/sig/takagi/version.rbs +5 -0
- data/takagi.gemspec +37 -35
- metadata +204 -31
- data/.idea/.gitignore +0 -8
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/takagi.iml +0 -81
- data/.idea/vcs.xml +0 -6
- data/lib/takagi/message.rb +0 -75
data/lib/takagi/base.rb
CHANGED
|
@@ -1,18 +1,110 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
3
|
+
require 'rack'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'json'
|
|
6
|
+
require_relative 'server/multi'
|
|
7
|
+
require_relative 'server_registry'
|
|
8
|
+
require_relative 'network/registry'
|
|
9
|
+
require_relative 'network/udp'
|
|
10
|
+
require_relative 'network/tcp'
|
|
11
|
+
require_relative 'plugin'
|
|
12
|
+
require_relative 'base/plugin_management'
|
|
11
13
|
|
|
12
14
|
module Takagi
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
# Base class that every Takagi based app should use.
|
|
16
|
+
#
|
|
17
|
+
# Provides a Sinatra-like DSL for building CoAP servers with support for:
|
|
18
|
+
# - Route registration (GET, POST, PUT, DELETE, OBSERVE)
|
|
19
|
+
# - Middleware stack
|
|
20
|
+
# - Reactor pattern for observables/observers
|
|
21
|
+
# - Multi-protocol servers (UDP, TCP)
|
|
22
|
+
#
|
|
23
|
+
# This class now follows Single Responsibility Principle by delegating
|
|
24
|
+
# specific concerns to focused modules:
|
|
25
|
+
# - ServerLifecycle: Boot, run, spawn operations
|
|
26
|
+
# - MiddlewareManagement: Middleware stack configuration
|
|
27
|
+
# - ReactorManagement: Observable/observer patterns
|
|
28
|
+
#
|
|
29
|
+
# @example Basic usage
|
|
30
|
+
# class MyAPI < Takagi::Base
|
|
31
|
+
# get '/temperature' do
|
|
32
|
+
# { value: 25.5, unit: 'C' }
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# MyAPI.run!
|
|
37
|
+
class Base < Router
|
|
38
|
+
extend ServerLifecycle
|
|
39
|
+
extend MiddlewareManagement
|
|
40
|
+
extend ReactorManagement
|
|
41
|
+
extend PluginManagement
|
|
42
|
+
|
|
43
|
+
# Returns the global router instance
|
|
44
|
+
#
|
|
45
|
+
# @return [Router] Singleton router instance
|
|
46
|
+
def self.router
|
|
47
|
+
@router ||= Takagi::Router.instance
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Dynamically delegate route registration methods to router
|
|
51
|
+
# Generates: get, post, put, delete, fetch, etc. from CoAP::Registries::Method registry
|
|
52
|
+
CoAP::Registries::Method.all.each_value do |method_name|
|
|
53
|
+
method_string = method_name.split.first
|
|
54
|
+
method_symbol = method_string.downcase.to_sym
|
|
55
|
+
|
|
56
|
+
define_singleton_method(method_symbol) do |path, metadata: {}, &block|
|
|
57
|
+
router.public_send(method_symbol, path, metadata: metadata, &block)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Registers an OBSERVE route in the global router (server-side)
|
|
62
|
+
# Use this to make a resource observable by clients
|
|
63
|
+
# @param path [String] The URL path
|
|
64
|
+
# @param block [Proc] The handler function
|
|
65
|
+
def self.observable(path, metadata: {}, &block)
|
|
66
|
+
router.observable(path, metadata: metadata, &block)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Configures CoRE Link Format metadata for an existing route. Handy when
|
|
70
|
+
# you want to declare handlers and metadata separately (e.g., during boot).
|
|
71
|
+
def self.core(path, method: :get, &block)
|
|
72
|
+
router.configure_core(method.to_s.upcase, path, &block)
|
|
16
73
|
end
|
|
74
|
+
|
|
75
|
+
# Default routes for basic functionality and RFC 6690 discovery
|
|
76
|
+
get '/.well-known/core', metadata: {
|
|
77
|
+
rt: 'core.discovery',
|
|
78
|
+
if: 'core.rd',
|
|
79
|
+
ct: Takagi::Discovery::CoreLinkFormat::CONTENT_FORMAT,
|
|
80
|
+
discovery: true,
|
|
81
|
+
title: 'Resource Discovery'
|
|
82
|
+
} do |req|
|
|
83
|
+
payload = Takagi::Discovery::CoreLinkFormat.generate(router: router, request: req)
|
|
84
|
+
req.to_response(
|
|
85
|
+
'2.05 Content',
|
|
86
|
+
payload,
|
|
87
|
+
options: { CoAP::Registries::Option::CONTENT_FORMAT => Takagi::Discovery::CoreLinkFormat::CONTENT_FORMAT }
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
get '/ping' do
|
|
92
|
+
{ message: 'Pong' }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
post '/echo' do |req|
|
|
96
|
+
body = JSON.parse(req.payload || '{}')
|
|
97
|
+
{ echo: body['message'] }
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
{ error: 'Invalid JSON' }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Register default server implementations
|
|
103
|
+
Server::Registry.register(:udp, Takagi::Server::Udp, rfc: 'RFC 7252')
|
|
104
|
+
Server::Registry.register(:tcp, Takagi::Server::Tcp, rfc: 'RFC 8323')
|
|
105
|
+
|
|
106
|
+
# Register default transport implementations
|
|
107
|
+
Network::Registry.register(:udp, Network::Udp)
|
|
108
|
+
Network::Registry.register(:tcp, Network::Tcp)
|
|
17
109
|
end
|
|
18
110
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Takagi
|
|
4
|
+
# Branding and visual identity for Takagi
|
|
5
|
+
module Branding
|
|
6
|
+
# Japanese wave symbol - represents observables, events, and flow
|
|
7
|
+
WAVE = '波'
|
|
8
|
+
LOGO = "[#{WAVE}]"
|
|
9
|
+
|
|
10
|
+
# Styled logo with name
|
|
11
|
+
LOGO_WITH_NAME = "#{LOGO} Takagi"
|
|
12
|
+
|
|
13
|
+
# ASCII art banner for server startup
|
|
14
|
+
BANNER = <<~BANNER
|
|
15
|
+
|
|
16
|
+
╔═══════════════════════════════════╗
|
|
17
|
+
║ ║
|
|
18
|
+
║ [#{WAVE}] T A K A G I ║
|
|
19
|
+
║ ║
|
|
20
|
+
║ CoAP Framework for Ruby ║
|
|
21
|
+
║ ║
|
|
22
|
+
╚═══════════════════════════════════╝
|
|
23
|
+
|
|
24
|
+
BANNER
|
|
25
|
+
|
|
26
|
+
# Compact banner for constrained terminals
|
|
27
|
+
COMPACT_BANNER = <<~BANNER
|
|
28
|
+
═══════════════════════════════
|
|
29
|
+
[#{WAVE}] Takagi CoAP Framework
|
|
30
|
+
═══════════════════════════════
|
|
31
|
+
BANNER
|
|
32
|
+
|
|
33
|
+
# Wave pattern decorations
|
|
34
|
+
WAVE_LINE = '〜' * 20
|
|
35
|
+
|
|
36
|
+
# Prefix for log messages
|
|
37
|
+
# @param message [String] Log message
|
|
38
|
+
# @return [String] Message with logo prefix
|
|
39
|
+
def self.log(message)
|
|
40
|
+
"#{LOGO} #{message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Print startup banner
|
|
44
|
+
# @param compact [Boolean] Use compact banner for smaller terminals
|
|
45
|
+
def self.print_banner(compact: false)
|
|
46
|
+
puts compact ? COMPACT_BANNER : BANNER
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Print version info
|
|
50
|
+
# @param version [String] Version string
|
|
51
|
+
def self.print_version(version)
|
|
52
|
+
puts "#{LOGO_WITH_NAME} v#{version}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Styled section header
|
|
56
|
+
# @param title [String] Section title
|
|
57
|
+
# @return [String] Styled header
|
|
58
|
+
def self.section(title)
|
|
59
|
+
<<~SECTION
|
|
60
|
+
|
|
61
|
+
#{WAVE_LINE}
|
|
62
|
+
#{LOGO} #{title}
|
|
63
|
+
#{WAVE_LINE}
|
|
64
|
+
SECTION
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Error prefix with wave
|
|
68
|
+
# @param message [String] Error message
|
|
69
|
+
# @return [String] Styled error
|
|
70
|
+
def self.error(message)
|
|
71
|
+
"#{LOGO} ERROR: #{message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Success prefix with wave
|
|
75
|
+
# @param message [String] Success message
|
|
76
|
+
# @return [String] Styled success
|
|
77
|
+
def self.success(message)
|
|
78
|
+
"#{LOGO} ✓ #{message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Info prefix with wave
|
|
82
|
+
# @param message [String] Info message
|
|
83
|
+
# @return [String] Styled info
|
|
84
|
+
def self.info(message)
|
|
85
|
+
"#{LOGO} #{message}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Takagi
|
|
4
|
+
module CBOR
|
|
5
|
+
# CBOR Decoder (RFC 8949)
|
|
6
|
+
#
|
|
7
|
+
# Decodes CBOR binary format to Ruby objects.
|
|
8
|
+
# Optimized for IoT/CoAP workloads with minimal footprint.
|
|
9
|
+
#
|
|
10
|
+
# Supported types:
|
|
11
|
+
# - Integers → Integer
|
|
12
|
+
# - Floats → Float
|
|
13
|
+
# - Text strings → String (UTF-8)
|
|
14
|
+
# - Byte strings → String (binary)
|
|
15
|
+
# - Arrays → Array
|
|
16
|
+
# - Maps → Hash (string keys)
|
|
17
|
+
# - Booleans → true/false
|
|
18
|
+
# - null → nil
|
|
19
|
+
# - Timestamps (tag 1) → Time
|
|
20
|
+
#
|
|
21
|
+
# @example Basic decoding
|
|
22
|
+
# Decoder.decode("\xA2ktempera...")
|
|
23
|
+
# # => { "temperature" => 25.5, "humidity" => 60 }
|
|
24
|
+
#
|
|
25
|
+
# Security features:
|
|
26
|
+
# - Max nesting depth (prevents stack overflow)
|
|
27
|
+
# - Max collection size (prevents memory exhaustion)
|
|
28
|
+
class Decoder # rubocop:disable Metrics/ClassLength
|
|
29
|
+
# CBOR Major Types (RFC 8949 §3)
|
|
30
|
+
MAJOR_TYPE_UNSIGNED_INT = 0
|
|
31
|
+
MAJOR_TYPE_NEGATIVE_INT = 1
|
|
32
|
+
MAJOR_TYPE_BYTE_STRING = 2
|
|
33
|
+
MAJOR_TYPE_TEXT_STRING = 3
|
|
34
|
+
MAJOR_TYPE_ARRAY = 4
|
|
35
|
+
MAJOR_TYPE_MAP = 5
|
|
36
|
+
MAJOR_TYPE_TAG = 6
|
|
37
|
+
MAJOR_TYPE_SIMPLE = 7
|
|
38
|
+
|
|
39
|
+
# Simple values (RFC 8949 §3.3)
|
|
40
|
+
SIMPLE_FALSE = 20
|
|
41
|
+
SIMPLE_TRUE = 21
|
|
42
|
+
SIMPLE_NULL = 22
|
|
43
|
+
SIMPLE_FLOAT16 = 25
|
|
44
|
+
SIMPLE_FLOAT32 = 26
|
|
45
|
+
SIMPLE_FLOAT64 = 27
|
|
46
|
+
|
|
47
|
+
# Security limits
|
|
48
|
+
MAX_NESTING_DEPTH = 100
|
|
49
|
+
MAX_COLLECTION_SIZE = 100_000
|
|
50
|
+
|
|
51
|
+
# Tag values
|
|
52
|
+
TAG_EPOCH_TIMESTAMP = 1
|
|
53
|
+
|
|
54
|
+
MAJOR_TYPE_HANDLERS = {
|
|
55
|
+
MAJOR_TYPE_UNSIGNED_INT => :handle_unsigned_int,
|
|
56
|
+
MAJOR_TYPE_NEGATIVE_INT => :handle_negative_int,
|
|
57
|
+
MAJOR_TYPE_BYTE_STRING => :read_bytes,
|
|
58
|
+
MAJOR_TYPE_TEXT_STRING => :read_string,
|
|
59
|
+
MAJOR_TYPE_ARRAY => :read_array,
|
|
60
|
+
MAJOR_TYPE_MAP => :read_map,
|
|
61
|
+
MAJOR_TYPE_TAG => :read_tagged,
|
|
62
|
+
MAJOR_TYPE_SIMPLE => :read_simple
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
# Decode CBOR bytes to Ruby object
|
|
67
|
+
#
|
|
68
|
+
# @param bytes [String] CBOR-encoded binary string
|
|
69
|
+
# @return [Object] Decoded Ruby object
|
|
70
|
+
# @raise [DecodeError] if bytes cannot be decoded
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# Decoder.decode("\x18\x2A") # => 42
|
|
74
|
+
# Decoder.decode("ehello") # => "hello"
|
|
75
|
+
# Decoder.decode("\x83\x01\x02\x03") # => [1, 2, 3]
|
|
76
|
+
def decode(bytes)
|
|
77
|
+
new(bytes).decode
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Initialize decoder with CBOR bytes
|
|
82
|
+
#
|
|
83
|
+
# @param bytes [String] CBOR-encoded binary string
|
|
84
|
+
def initialize(bytes)
|
|
85
|
+
@bytes = bytes.b
|
|
86
|
+
@pos = 0
|
|
87
|
+
@depth = 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Decode CBOR bytes to Ruby object
|
|
91
|
+
#
|
|
92
|
+
# @return [Object] Decoded Ruby object
|
|
93
|
+
# @raise [DecodeError] if bytes cannot be decoded
|
|
94
|
+
def decode
|
|
95
|
+
check_depth!
|
|
96
|
+
|
|
97
|
+
major_type, value = read_type_and_value
|
|
98
|
+
handler = MAJOR_TYPE_HANDLERS[major_type]
|
|
99
|
+
raise DecodeError, "Unknown major type: #{major_type}" unless handler
|
|
100
|
+
|
|
101
|
+
send(handler, value)
|
|
102
|
+
rescue DecodeError, UnsupportedError
|
|
103
|
+
raise
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
raise DecodeError, "Decoding failed at position #{@pos}: #{e.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def handle_unsigned_int(value)
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_negative_int(value)
|
|
115
|
+
-1 - value
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Check nesting depth to prevent stack overflow
|
|
119
|
+
def check_depth!
|
|
120
|
+
return if @depth < MAX_NESTING_DEPTH
|
|
121
|
+
|
|
122
|
+
raise DecodeError, "Maximum nesting depth exceeded (#{MAX_NESTING_DEPTH})"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Read major type and additional value
|
|
126
|
+
# RFC 8949 §3: Initial byte encoding
|
|
127
|
+
#
|
|
128
|
+
# Returns [major_type, value]
|
|
129
|
+
# - major_type: 0-7 (3 bits)
|
|
130
|
+
# - value: depends on additional info (5 bits)
|
|
131
|
+
def read_type_and_value
|
|
132
|
+
raise DecodeError, 'Unexpected end of input' if @pos >= @bytes.bytesize
|
|
133
|
+
|
|
134
|
+
initial_byte = @bytes[@pos].ord
|
|
135
|
+
@pos += 1
|
|
136
|
+
|
|
137
|
+
major_type = initial_byte >> 5
|
|
138
|
+
additional = initial_byte & 0x1F
|
|
139
|
+
|
|
140
|
+
value = if major_type == MAJOR_TYPE_SIMPLE
|
|
141
|
+
additional
|
|
142
|
+
else
|
|
143
|
+
decode_additional_info(additional)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
[major_type, value]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Decode additional information (RFC 8949 §3)
|
|
150
|
+
#
|
|
151
|
+
# Additional info encoding:
|
|
152
|
+
# 0-23: Value is directly in additional info
|
|
153
|
+
# 24: 1-byte uint8 follows
|
|
154
|
+
# 25: 2-byte uint16 follows
|
|
155
|
+
# 26: 4-byte uint32 follows
|
|
156
|
+
# 27: 8-byte uint64 follows
|
|
157
|
+
# 28-30: Reserved (error)
|
|
158
|
+
# 31: Indefinite length (not supported in minimal impl)
|
|
159
|
+
def decode_additional_info(additional)
|
|
160
|
+
case additional
|
|
161
|
+
when 0..23
|
|
162
|
+
# Value directly encoded
|
|
163
|
+
additional
|
|
164
|
+
when 24
|
|
165
|
+
# 1-byte uint8 follows
|
|
166
|
+
read_uint8
|
|
167
|
+
when 25
|
|
168
|
+
# 2-byte uint16 follows
|
|
169
|
+
read_uint16
|
|
170
|
+
when 26
|
|
171
|
+
# 4-byte uint32 follows
|
|
172
|
+
read_uint32
|
|
173
|
+
when 27
|
|
174
|
+
# 8-byte uint64 follows
|
|
175
|
+
read_uint64
|
|
176
|
+
when 28, 29, 30
|
|
177
|
+
raise DecodeError, "Reserved additional info value: #{additional}"
|
|
178
|
+
when 31
|
|
179
|
+
raise UnsupportedError, 'Indefinite-length items not supported in minimal implementation'
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Read unsigned 8-bit integer
|
|
184
|
+
def read_uint8
|
|
185
|
+
check_available(1)
|
|
186
|
+
val = @bytes[@pos].ord
|
|
187
|
+
@pos += 1
|
|
188
|
+
val
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Read unsigned 16-bit integer (big-endian)
|
|
192
|
+
def read_uint16
|
|
193
|
+
check_available(2)
|
|
194
|
+
val = @bytes[@pos, 2].unpack1('n')
|
|
195
|
+
@pos += 2
|
|
196
|
+
val
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Read unsigned 32-bit integer (big-endian)
|
|
200
|
+
def read_uint32
|
|
201
|
+
check_available(4)
|
|
202
|
+
val = @bytes[@pos, 4].unpack1('N')
|
|
203
|
+
@pos += 4
|
|
204
|
+
val
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Read unsigned 64-bit integer (big-endian)
|
|
208
|
+
def read_uint64
|
|
209
|
+
check_available(8)
|
|
210
|
+
val = @bytes[@pos, 8].unpack1('Q>')
|
|
211
|
+
@pos += 8
|
|
212
|
+
val
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Read byte string (binary data)
|
|
216
|
+
def read_bytes(length)
|
|
217
|
+
check_collection_size(length)
|
|
218
|
+
check_available(length)
|
|
219
|
+
|
|
220
|
+
bytes = @bytes[@pos, length]
|
|
221
|
+
@pos += length
|
|
222
|
+
bytes
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Read UTF-8 text string
|
|
226
|
+
def read_string(length)
|
|
227
|
+
check_collection_size(length)
|
|
228
|
+
check_available(length)
|
|
229
|
+
|
|
230
|
+
str = @bytes[@pos, length]
|
|
231
|
+
@pos += length
|
|
232
|
+
|
|
233
|
+
# Force UTF-8 encoding and validate
|
|
234
|
+
str.force_encoding('UTF-8')
|
|
235
|
+
|
|
236
|
+
raise DecodeError, 'Invalid UTF-8 encoding in text string' unless str.valid_encoding?
|
|
237
|
+
|
|
238
|
+
str
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Read array
|
|
242
|
+
def read_array(length)
|
|
243
|
+
check_collection_size(length)
|
|
244
|
+
|
|
245
|
+
@depth += 1
|
|
246
|
+
|
|
247
|
+
arr = Array.new(length) { decode }
|
|
248
|
+
|
|
249
|
+
@depth -= 1
|
|
250
|
+
arr
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Read map (hash)
|
|
254
|
+
def read_map(length)
|
|
255
|
+
check_collection_size(length)
|
|
256
|
+
|
|
257
|
+
@depth += 1
|
|
258
|
+
|
|
259
|
+
hash = {}
|
|
260
|
+
|
|
261
|
+
length.times do
|
|
262
|
+
key = decode
|
|
263
|
+
value = decode
|
|
264
|
+
|
|
265
|
+
# Convert symbol keys to strings for consistency
|
|
266
|
+
key = key.to_s if key.is_a?(Symbol)
|
|
267
|
+
|
|
268
|
+
hash[key] = value
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
@depth -= 1
|
|
272
|
+
hash
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Read tagged value (RFC 8949 §3.4)
|
|
276
|
+
def read_tagged(tag)
|
|
277
|
+
case tag
|
|
278
|
+
when TAG_EPOCH_TIMESTAMP
|
|
279
|
+
# Tag 1: Epoch-based timestamp
|
|
280
|
+
timestamp_value = decode
|
|
281
|
+
|
|
282
|
+
case timestamp_value
|
|
283
|
+
when Integer, Float
|
|
284
|
+
Time.at(timestamp_value)
|
|
285
|
+
else
|
|
286
|
+
raise DecodeError, "Invalid timestamp value type: #{timestamp_value.class}"
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
# Unknown tag: decode value but ignore tag
|
|
290
|
+
# This allows forward compatibility
|
|
291
|
+
decode
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Read simple value (RFC 8949 §3.3)
|
|
296
|
+
def read_simple(value)
|
|
297
|
+
case value
|
|
298
|
+
when SIMPLE_FALSE
|
|
299
|
+
false
|
|
300
|
+
when SIMPLE_TRUE
|
|
301
|
+
true
|
|
302
|
+
when SIMPLE_NULL
|
|
303
|
+
nil
|
|
304
|
+
when SIMPLE_FLOAT16
|
|
305
|
+
# 16-bit float (half-precision)
|
|
306
|
+
read_float16
|
|
307
|
+
when SIMPLE_FLOAT32
|
|
308
|
+
# 32-bit float (single-precision)
|
|
309
|
+
read_float32
|
|
310
|
+
when SIMPLE_FLOAT64
|
|
311
|
+
# 64-bit float (double-precision)
|
|
312
|
+
read_float64
|
|
313
|
+
when 0..19
|
|
314
|
+
# Unassigned simple values (0-19)
|
|
315
|
+
raise UnsupportedError, "Unassigned simple value: #{value}"
|
|
316
|
+
when 24..31
|
|
317
|
+
# Should not reach here (handled in decode_additional_info)
|
|
318
|
+
raise DecodeError, "Invalid simple value: #{value}"
|
|
319
|
+
else
|
|
320
|
+
# Simple values 32-255 (extended)
|
|
321
|
+
raise UnsupportedError, "Extended simple values not supported: #{value}"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Read 16-bit float (IEEE 754 half-precision)
|
|
326
|
+
def read_float16
|
|
327
|
+
check_available(2)
|
|
328
|
+
|
|
329
|
+
# Read 16-bit big-endian
|
|
330
|
+
half = @bytes[@pos, 2].unpack1('n')
|
|
331
|
+
@pos += 2
|
|
332
|
+
|
|
333
|
+
# Convert IEEE 754 half to Ruby float
|
|
334
|
+
# Format: 1 sign bit, 5 exponent bits, 10 mantissa bits
|
|
335
|
+
sign = (half >> 15) & 0x1
|
|
336
|
+
exponent = (half >> 10) & 0x1F
|
|
337
|
+
mantissa = half & 0x3FF
|
|
338
|
+
|
|
339
|
+
if exponent.zero?
|
|
340
|
+
# Subnormal or zero
|
|
341
|
+
result = mantissa.to_f / (2**24)
|
|
342
|
+
elsif exponent == 0x1F
|
|
343
|
+
# Infinity or NaN
|
|
344
|
+
return mantissa.zero? ? Float::INFINITY : Float::NAN
|
|
345
|
+
else
|
|
346
|
+
# Normalized
|
|
347
|
+
result = (1.0 + (mantissa.to_f / (2**10))) * (2**(exponent - 15))
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
sign.zero? ? result : -result
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Read 32-bit float (IEEE 754 single-precision)
|
|
354
|
+
def read_float32
|
|
355
|
+
check_available(4)
|
|
356
|
+
float_bytes = @bytes[@pos, 4]
|
|
357
|
+
@pos += 4
|
|
358
|
+
float_bytes.unpack1('g') # Big-endian single-precision float
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Read 64-bit float (IEEE 754 double-precision)
|
|
362
|
+
def read_float64
|
|
363
|
+
check_available(8)
|
|
364
|
+
float_bytes = @bytes[@pos, 8]
|
|
365
|
+
@pos += 8
|
|
366
|
+
float_bytes.unpack1('G') # Big-endian double-precision float
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Check if enough bytes are available
|
|
370
|
+
def check_available(needed)
|
|
371
|
+
available = @bytes.bytesize - @pos
|
|
372
|
+
return if available >= needed
|
|
373
|
+
|
|
374
|
+
raise DecodeError, "Unexpected end of input (need #{needed} bytes, have #{available})"
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Check collection size to prevent memory exhaustion
|
|
378
|
+
def check_collection_size(size)
|
|
379
|
+
return if size <= MAX_COLLECTION_SIZE
|
|
380
|
+
|
|
381
|
+
raise DecodeError, "Collection size #{size} exceeds maximum (#{MAX_COLLECTION_SIZE})"
|
|
382
|
+
end
|
|
383
|
+
end # rubocop:enable Metrics/ClassLength
|
|
384
|
+
end
|
|
385
|
+
end
|