From e9691af354189a57f92c1df9fbf705ec8c29e1b1 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 27 Apr 2026 20:13:47 +0200 Subject: [PATCH 1/2] fix(tied-hash): don't pre-fetch NEXTKEY between each() calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tied-hash iterator was eagerly calling NEXTKEY at the end of next() to cache the upcoming key. When user code mutated the tied hash between each() calls (e.g. `delete $h{$k}` inside `while (my ($k) = each %h)`), that cached key was already past the entry whose internal index just shifted — so the affected key was silently skipped. Real Perl calls FIRSTKEY/NEXTKEY exactly once per each() invocation. Move the fetch into hasNext() so the call happens lazily, inside the next each(), after any user-visible mutation has taken effect. Fixes Tie::IxHash's t/each-delete.t (and unblocks Net::HTTPS::Any, which depends on Tie::IxHash). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../runtime/runtimetypes/RuntimeHash.java | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index c23bb8956..4d80cbda7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -1292,47 +1292,50 @@ public void remove() { * Uses Perl tie methods FIRSTKEY and NEXTKEY for iteration. */ private class RuntimeTiedHashIterator implements Iterator { - private RuntimeScalar currentKey; - private RuntimeScalar nextKey; - private boolean returnKey; - private boolean initialized; + private RuntimeScalar currentKey; // last key returned by FIRSTKEY/NEXTKEY (passed to next NEXTKEY) + private RuntimeScalar pendingKey; // key fetched by hasNext() but not yet consumed by next() + private boolean returnKey; // true: next() returns a key; false: next() returns the value + private boolean started; // whether FIRSTKEY has been called /** * Constructs a RuntimeTiedHashIterator for iterating over tied hash elements. */ public RuntimeTiedHashIterator() { this.returnKey = true; - this.initialized = false; + this.started = false; this.currentKey = null; - this.nextKey = null; - } - - /** - * Initializes the iterator by calling FIRSTKEY if not already initialized. - */ - private void initialize() { - if (!initialized) { - nextKey = TieHash.tiedFirstKey(RuntimeHash.this); - initialized = true; - } + this.pendingKey = null; } /** * Checks if there are more elements to iterate over. + *

+ * Lazily calls FIRSTKEY (first time) or NEXTKEY (subsequent times) to fetch + * the upcoming key. The fetch happens here — not eagerly at the end of the + * previous next() — so that mutations to the tied hash between each() calls + * (e.g. {@code delete $h{$k}} during {@code while (each %h)}) are observed + * by the next NEXTKEY call, matching real Perl's semantics. * * @return True if there are more elements, false otherwise. */ @Override public boolean hasNext() { - initialize(); - - // If we're about to return a value and have a current key, we have a next element - if (currentKey != null && !returnKey) { + // Mid-pair: a key was returned, value is still pending + if (!returnKey) { return true; } - // If we're about to return a key, check if nextKey is defined (not undef) - return returnKey && nextKey != null && nextKey.getDefinedBoolean(); + // Need a fresh key — fetch it lazily if we don't already have one cached + if (pendingKey == null) { + if (!started) { + pendingKey = TieHash.tiedFirstKey(RuntimeHash.this); + started = true; + } else { + pendingKey = TieHash.tiedNextKey(RuntimeHash.this, currentKey); + } + } + + return pendingKey != null && pendingKey.getDefinedBoolean(); } /** @@ -1348,17 +1351,16 @@ public RuntimeScalar next() { if (returnKey) { // Return the key and prepare to return its value next - currentKey = nextKey; + currentKey = pendingKey; + pendingKey = null; returnKey = false; return new RuntimeScalar(currentKey); } else { - // Return the value and prepare for the next key + // Return the value (FETCH happens lazily through the proxy). + // Do NOT pre-fetch the next key here — that would race with + // user-visible mutations between each() calls. RuntimeScalar value = RuntimeHash.this.get(currentKey); - - // Get the next key for the next iteration - nextKey = TieHash.tiedNextKey(RuntimeHash.this, currentKey); returnKey = true; - return value; } } From 0c65f7fd3ef76cdd3672207b5c6c64d280aed49c Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 27 Apr 2026 20:14:00 +0200 Subject: [PATCH 2/2] feat(ssleay): wire get_https/post_https/do_https to HTTP::Tiny MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PerlOnJava ships no OpenSSL backend, but it does ship HTTP::Tiny — which already speaks HTTPS via the JVM TLS stack. Map Net::SSLeay's high-level HTTP helpers onto HTTP::Tiny so modules built on Net::SSLeay (e.g. Net::HTTPS::Any) work out of the box. Changes to src/main/perl/lib/Net/SSLeay.pm: * Add the high-level HTTP/HTTPS helpers and utility subs to @EXPORT_OK (get_https, post_https, do_https, make_headers, make_form, sslcat, …) so `use Net::SSLeay qw(...)` with an explicit import list compiles. * Replace the _not_implemented stubs for get_https / post_https / do_https with a small shim: - parses the make_headers-style header string back into a hash - calls HTTP::Tiny->new->request(...) - repackages the response as Net::SSLeay's ($page, "HTTP/1.1 200 OK", @flat_headers). Variants that need raw OpenSSL semantics (sslcat, *_https3, *_https4, *_httpx, dump_peer_certificate, …) still raise "not implemented". Result: Net::HTTPS::Any's full test suite now passes 10/10, including the live HTTPS GET/POST integration tests. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 +- src/main/perl/lib/Net/SSLeay.pm | 110 +++++++++++++++++- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 0350397dc..3e7a1a250 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "90d0bb9f9"; + public static final String gitCommitId = "1c27ead97"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 27 2026 16:38:19"; + public static final String buildTimestamp = "Apr 27 2026 20:14:07"; // Prevent instantiation private Configuration() { diff --git a/src/main/perl/lib/Net/SSLeay.pm b/src/main/perl/lib/Net/SSLeay.pm index 222ab272c..60208b666 100644 --- a/src/main/perl/lib/Net/SSLeay.pm +++ b/src/main/perl/lib/Net/SSLeay.pm @@ -355,6 +355,27 @@ our @EXPORT_OK = qw( XN_FLAG_SPC_EQ ); +# High-level HTTP/HTTPS helpers and utility subs. In real Net::SSLeay these +# are autoloaded Perl subs that callers can import — e.g. Net::HTTPS::Any does +# `use Net::SSLeay qw(get_https post_https make_headers make_form)`. Add them +# to @EXPORT_OK so `use` with an explicit import list doesn't fail at compile +# time even though the underlying OpenSSL operations themselves aren't +# implemented (they die at call time via _not_implemented). +push @EXPORT_OK, qw( + do_https + get_http get_http4 + get_https get_https3 get_https4 + get_httpx get_httpx4 + post_http post_http4 + post_https post_https3 post_https4 + post_httpx post_httpx4 + sslcat tcpcat tcpxcat + make_form make_headers + dump_peer_certificate + set_cert_and_key set_server_cert_and_key + die_if_ssl_error die_now print_errs +); + our %EXPORT_TAGS = ( all => \@EXPORT_OK, ); @@ -391,17 +412,17 @@ sub print_errs { return $errs; } -sub do_https { _not_implemented("do_https") } +sub do_https { _do_https_request(@_) } sub get_http { _not_implemented("get_http") } sub get_http4 { _not_implemented("get_http4") } -sub get_https { _not_implemented("get_https") } +sub get_https { _https_get_or_post('GET', @_) } sub get_https3 { _not_implemented("get_https3") } sub get_https4 { _not_implemented("get_https4") } sub get_httpx { _not_implemented("get_httpx") } sub get_httpx4 { _not_implemented("get_httpx4") } sub post_http { _not_implemented("post_http") } sub post_http4 { _not_implemented("post_http4") } -sub post_https { _not_implemented("post_https") } +sub post_https { _https_get_or_post('POST', @_) } sub post_https3 { _not_implemented("post_https3") } sub post_https4 { _not_implemented("post_https4") } sub post_httpx { _not_implemented("post_httpx") } @@ -414,6 +435,89 @@ sub dump_peer_certificate { _not_implemented("dump_peer_certificate") } sub set_cert_and_key { _not_implemented("set_cert_and_key") } sub set_server_cert_and_key { _not_implemented("set_server_cert_and_key") } +# ---- HTTPS request shim backed by HTTP::Tiny -------------------------------- +# +# PerlOnJava ships no OpenSSL backend, but it does ship HTTP::Tiny, which talks +# HTTPS via the JVM's TLS stack. These shims map the Net::SSLeay HTTP helper +# API onto HTTP::Tiny so modules like Net::HTTPS::Any work out of the box. +# +# Net::SSLeay return convention (for get_https/post_https/do_https): +# ($page, $response, @headers) +# where: +# - $page is the response body +# - $response is the HTTP status line, e.g. "HTTP/1.1 200 OK" +# - @headers is a flat (name => value, ...) list + +sub _https_get_or_post { + my ($method, $host, $port, $path, $headers_str, $content, $content_type) = @_; + my $body = $method eq 'POST' ? $content : undef; + return _do_https_request( + $host, $port, $path, $method, $headers_str, $body, $content_type, + ); +} + +sub _do_https_request { + my ($host, $port, $path, $method, $headers_str, $content, $content_type, + # cert/key/password are accepted for API parity but unused + undef, undef, undef) = @_; + + require HTTP::Tiny; + + $port ||= 443; + $method ||= 'GET'; + $path = '/' . $path if defined $path && $path !~ m{^/}; + $path = '/' unless defined $path; + + my $url = "https://$host:$port$path"; + + # Parse headers string (as produced by make_headers) into a hashref. + my %hdrs; + if (defined $headers_str && length $headers_str) { + for my $line (split /\r?\n/, $headers_str) { + next unless $line =~ /^([^:\s]+)\s*:\s*(.*)$/; + my ($k, $v) = ($1, $2); + # HTTP::Tiny rejects Host (it sets it itself); skip and let it manage. + next if lc($k) eq 'host'; + if (exists $hdrs{$k}) { + $hdrs{$k} = [ $hdrs{$k} ] unless ref $hdrs{$k}; + push @{ $hdrs{$k} }, $v; + } else { + $hdrs{$k} = $v; + } + } + } + + my %opts = (headers => \%hdrs); + if (defined $content && length $content) { + $opts{content} = $content; + $hdrs{'Content-Type'} ||= ($content_type || 'application/x-www-form-urlencoded'); + } + + my $resp = HTTP::Tiny->new->request($method, $url, \%opts); + + # HTTP::Tiny synthesises status 599 when the request couldn't be made. + # Mirror that as a status line so callers' regexes still see something. + my $proto = $resp->{protocol} || 'HTTP/1.1'; + my $status = $resp->{status} // 599; + my $reason = $resp->{reason} // ($status == 599 ? 'Internal Exception' : ''); + my $status_line = "$proto $status" . (length $reason ? " $reason" : ''); + + my @hdr_pairs; + if (ref $resp->{headers} eq 'HASH') { + for my $k (sort keys %{ $resp->{headers} }) { + my $v = $resp->{headers}{$k}; + if (ref $v eq 'ARRAY') { + push @hdr_pairs, ($k, $_) for @$v; + } else { + push @hdr_pairs, ($k, $v); + } + } + } + + my $page = defined $resp->{content} ? $resp->{content} : ''; + return ($page, $status_line, @hdr_pairs); +} + sub make_form { my @pairs; while (@_) {