From 9fbcdb00f0364c3ffddb4585260d505cb0f14c20 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 11 Jun 2026 17:20:24 +0200 Subject: [PATCH 1/2] fix: improve CPAN random module compatibility Fix remaining failures from cpan_random_tester for HTTP::Cookies::Guess, Module::Install, Locale::Maketext, MooseX::NonMoose, Template::Plugin::Number::Format, and IO::All. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- dev/cpan-reports/cpan-compatibility-fail.dat | 9 +- dev/cpan-reports/cpan-compatibility-pass.dat | 10 +- dev/cpan-reports/cpan-compatibility-skip.dat | 2 +- dev/cpan-reports/cpan-compatibility.md | 25 ++- .../backend/bytecode/BytecodeCompiler.java | 12 ++ .../backend/bytecode/BytecodeInterpreter.java | 24 +++ .../backend/bytecode/CompileOperator.java | 17 +- .../backend/bytecode/Disassemble.java | 12 ++ .../perlonjava/backend/bytecode/Opcodes.java | 21 ++ .../perlonjava/backend/jvm/EmitLiteral.java | 14 +- .../runtime/perlmodule/FileSpec.java | 2 +- .../runtime/perlmodule/IOHandle.java | 10 +- .../runtime/runtimetypes/RuntimeCode.java | 10 +- .../runtime/runtimetypes/RuntimeIO.java | 12 +- .../runtime/runtimetypes/RuntimeScalar.java | 44 ++-- .../runtime/runtimetypes/TieHandle.java | 2 +- src/main/perl/lib/CPAN/Config.pm | 6 + src/main/perl/lib/Devel/PPPort.pm | 28 +++ src/main/perl/lib/ExtUtils/MakeMaker.pm | 197 ++++++++++++++++-- src/main/perl/lib/File/Remove.pm | 63 ++++++ src/main/perl/lib/HTTP/Cookies.pm | 51 +++++ src/main/perl/lib/HTTP/Cookies/Netscape.pm | 74 +++++++ src/main/perl/lib/I18N/LangTags.pm | 168 +++++++++++++++ src/main/perl/lib/I18N/LangTags/Detect.pm | 64 ++++++ src/main/perl/lib/IO/File.pm | 2 +- src/main/perl/lib/Module/ScanDeps.pm | 90 ++++++++ src/main/perl/lib/POSIX.pm | 92 ++++++-- .../lib/PerlOnJava/CpanDistroprefs/IO-All.yml | 12 ++ .../CpanDistroprefs/Module-Install.yml | 12 ++ .../IO-All-0.87/SkipForkTests.patch | 32 +++ .../ExplicitAuthorsMethod.patch | 11 + src/main/perl/lib/SDBM_File.pm | 101 +++++++++ src/main/perl/lib/Tie/File.pm | 130 ++++++++++++ src/main/perl/lib/UNIVERSAL/require.pm | 68 ++++++ src/test/resources/unit/cpan_shim_modules.t | 126 +++++++++++ src/test/resources/unit/eval_return_destroy.t | 26 +++ .../resources/unit/fileno_reserved_recycle.t | 18 ++ src/test/resources/unit/io_handle_error.t | 14 ++ src/test/resources/unit/overload_deref_args.t | 84 ++++++++ .../unit/strict_subs_void_bareword.t | 20 ++ .../resources/unit/tie_handle_readline_args.t | 44 ++++ 41 files changed, 1652 insertions(+), 107 deletions(-) create mode 100644 src/main/perl/lib/Devel/PPPort.pm create mode 100644 src/main/perl/lib/File/Remove.pm create mode 100644 src/main/perl/lib/HTTP/Cookies.pm create mode 100644 src/main/perl/lib/HTTP/Cookies/Netscape.pm create mode 100644 src/main/perl/lib/I18N/LangTags.pm create mode 100644 src/main/perl/lib/I18N/LangTags/Detect.pm create mode 100644 src/main/perl/lib/Module/ScanDeps.pm create mode 100644 src/main/perl/lib/PerlOnJava/CpanDistroprefs/IO-All.yml create mode 100644 src/main/perl/lib/PerlOnJava/CpanDistroprefs/Module-Install.yml create mode 100644 src/main/perl/lib/PerlOnJava/CpanPatches/IO-All-0.87/SkipForkTests.patch create mode 100644 src/main/perl/lib/PerlOnJava/CpanPatches/Module-Install-1.21/ExplicitAuthorsMethod.patch create mode 100644 src/main/perl/lib/SDBM_File.pm create mode 100644 src/main/perl/lib/Tie/File.pm create mode 100644 src/main/perl/lib/UNIVERSAL/require.pm create mode 100644 src/test/resources/unit/cpan_shim_modules.t create mode 100644 src/test/resources/unit/eval_return_destroy.t create mode 100644 src/test/resources/unit/fileno_reserved_recycle.t create mode 100644 src/test/resources/unit/io_handle_error.t create mode 100644 src/test/resources/unit/overload_deref_args.t create mode 100644 src/test/resources/unit/strict_subs_void_bareword.t create mode 100644 src/test/resources/unit/tie_handle_readline_args.t diff --git a/dev/cpan-reports/cpan-compatibility-fail.dat b/dev/cpan-reports/cpan-compatibility-fail.dat index e7bd6b03a..200623ba5 100644 --- a/dev/cpan-reports/cpan-compatibility-fail.dat +++ b/dev/cpan-reports/cpan-compatibility-fail.dat @@ -2261,7 +2261,7 @@ ExtUtils::F77 FAIL 3 1 2/3 subtests failed; 1/1 test programs failed 2026-06-11 ExtUtils::H2PM FAIL 16 8 8/16 subtests failed 2026-05-10 ExtUtils::LibBuilder FAIL 3 0 7/3 subtests failed 2026-05-12 ExtUtils::MM_Unix FAIL 1079 976 103/1079 subtests failed; 20/69 test programs failed 2026-06-11 -ExtUtils::MakeMaker FAIL 1257 1118 139/1257 subtests failed; 15/69 test programs failed 2026-06-11 +ExtUtils::MakeMaker FAIL 1257 1118 139/1257 subtests failed; 15/69 test programs failed 2026-06-11 ExtUtils::MakeMaker::Attributes FAIL 8 8 2026-05-10 ExtUtils::MakeMaker::PPPort FAIL 3 0 3/3 subtests failed 2026-05-17 ExtUtils::ModuleMaker::TT FAIL 12 0 169/12 subtests failed 2026-05-14 @@ -2367,7 +2367,7 @@ File::PathConvert FAIL 266 264 2/266 subtests failed 2026-04-12 File::PathInfo FAIL 57 57 Missing: Time/Format.pm 2026-05-19 File::PathInfo::Ext FAIL 1 1 2026-05-19 File::Raw FAIL 1 0 1/1 subtests failed 2026-05-11 -File::Remove FAIL 218 215 3/218 subtests failed; 2/11 test programs failed 2026-06-10 +File::Remove FAIL 218 215 3/218 subtests failed; 2/11 test programs failed 2026-06-11 File::RotateLogs FAIL 1 0 1/1 subtests failed 2026-05-12 File::Rules FAIL 3 0 14/3 subtests failed 2026-04-22 File::Save::Home FAIL 16 0 44/16 subtests failed 2026-04-30 @@ -3010,7 +3010,7 @@ HTTunnel::Client FAIL Missing: Apache/Test.pm 2026-05-15 Haineko FAIL 1002 1002 Missing: JSON/Syck.pm 2026-05-11 Hardware FAIL Missing: Object/Pad.pm 2026-05-17 Hash::AsObject FAIL 93 93 Missing: diagnostics.pm 2026-04-12 -Hash::FieldHash FAIL 132 101 31/132 subtests failed; 14/18 test programs failed 2026-06-11 +Hash::FieldHash FAIL 132 101 31/132 subtests failed; 14/18 test programs failed 2026-06-11 Hash::Inflator FAIL 8 7 1/8 subtests failed 2026-05-20 Hash::Merge FAIL 1 0 1/1 subtests failed 2026-05-20 Hash::NoVivify FAIL 2026-05-16 @@ -3800,7 +3800,7 @@ Module::MultiConf FAIL 18 0 20/18 subtests failed 2026-05-10 Module::Path FAIL 18 11 7/18 subtests failed 2026-04-22 Module::Pluggable::Object FAIL 192 191 1/192 subtests failed; 1/45 test programs failed 2026-06-11 Module::Refresh FAIL 16 14 2/16 subtests failed 2026-05-17 -Module::ScanDeps FAIL 161 156 5/161 subtests failed; 3/21 test programs failed 2026-06-03 +Module::ScanDeps FAIL 161 156 5/161 subtests failed; 3/21 test programs failed 2026-06-11 Module::ScanDeps::Static FAIL 2026-04-21 Module::Setup FAIL 1 0 1/1 subtests failed 2026-05-18 Module::Signature FAIL 2 2 Missing: IPC/Run.pm 2026-04-12 @@ -4213,7 +4213,6 @@ Noose FAIL 19 9 10/19 subtests failed 2026-05-11 Norma FAIL 12 12 Missing: Norma/ORM/Test/DB.pm 2026-05-12 Notifications FAIL 17 0 18/17 subtests failed 2026-05-15 NpsSDK::Configuration FAIL Unknown test outcome 2026-05-17 -Number::Format FAIL 10 10 2026-04-22 Number::FormatEng FAIL 140 139 1/140 subtests failed 2026-05-05 Number::Phone FAIL 1373 1335 38/1373 subtests failed 2026-05-12 Number::Phone::JP FAIL 199 0 66169/199 subtests failed 2026-05-12 diff --git a/dev/cpan-reports/cpan-compatibility-pass.dat b/dev/cpan-reports/cpan-compatibility-pass.dat index 8f4ffca75..b291c0b75 100644 --- a/dev/cpan-reports/cpan-compatibility-pass.dat +++ b/dev/cpan-reports/cpan-compatibility-pass.dat @@ -1267,7 +1267,7 @@ HTTP::ClickHouse::Base PASS 7 7 2026-05-16 75a25eb21 HTTP::ClientDetect::Language PASS 79 79 2026-05-18 bd327bb57 HTTP::Cookies PASS 122 122 2026-06-11 959012866 HTTP::Cookies::ChromeMacOS PASS 1 1 2026-05-18 b5536d190 -HTTP::Cookies::Guess PASS 5 5 2026-05-18 bd327bb57 +HTTP::Cookies::Guess PASS 5 5 2026-06-11 bd9132057 HTTP::Cookies::PhantomJS PASS 1490 1490 2026-05-16 d8a8a9f34 HTTP::Cookies::w3m PASS 3 3 2026-05-09 d7916b01c HTTP::DetectUserAgent PASS 152 152 2026-05-17 bd327bb57 @@ -1360,6 +1360,7 @@ IM::Engine::Incoming::IRC PASS 4 4 2026-05-16 d8a8a9f34 IM::Engine::Plugin::State PASS 12 12 2026-05-16 75a25eb21 IMDB::BaseClass PASS 2 2 2026-05-11 2b8886eec IMDB::TitlePage::Extract PASS 1 1 2026-05-12 93dfa497b +IO::All PASS 368 368 2026-06-11 bd9132057 IO::All::FTP PASS 8 8 2026-05-17 d8a8a9f34 IO::Async PASS 1134 1134 2026-05-12 b5c01ce1c IO::Async::Loop PASS 1134 1134 2026-05-11 b5c01ce1c @@ -1588,6 +1589,7 @@ LocalConf::Parser PASS 1 1 2026-05-13 3fe76ed3b Locale::BR PASS 2879 2879 2026-05-14 3fe76ed3b Locale::Currency::Format PASS 28 28 2026-05-19 b5536d190 Locale::Framework PASS 5 5 2026-05-12 b5c01ce1c +Locale::Maketext PASS 180 180 2026-06-11 bd9132057 Locale::SubCountry PASS 15 15 2026-05-17 d8a8a9f34 Locale::TextDomain::UTF8 PASS 1 1 2026-05-18 bd327bb57 Locale::Tie PASS 1 1 2026-05-18 bd327bb57 @@ -1741,6 +1743,7 @@ Module::CoreList PASS 102 102 2026-05-20 b5536d190 Module::Depends PASS 20 20 2026-05-10 1e95a0902 Module::Depends::Intrusive PASS 20 20 2026-05-13 3fe76ed3b Module::Faker::Dist PASS 34 34 2026-05-19 b5536d190 +Module::Install PASS 547 547 2026-06-11 bd9132057 Module::Install::AutoManifest PASS 1 1 2026-05-09 d7916b01c Module::Install::RTx PASS 1 1 2026-05-09 d7916b01c Module::Install::Repository PASS 1 1 2026-05-18 bd327bb57 @@ -1856,7 +1859,7 @@ MooseX::Meta::Method::Transactional PASS 17 17 2026-05-11 2b8886eec MooseX::Meta::TypeConstraint::Mooish PASS 42 42 2026-05-18 b5536d190 MooseX::MungeHas PASS 45 45 2026-05-12 b5c01ce1c MooseX::NestedAttributesConstructor PASS 2 2 2026-05-10 1e95a0902 -MooseX::NonMoose PASS 177 177 2026-06-03 d0f827461 +MooseX::NonMoose PASS 177 177 2026-06-11 bd9132057 MooseX::NotRequired PASS 22 22 2026-05-18 bd327bb57 MooseX::OneArgNew PASS 20 20 2026-05-16 d8a8a9f34 MooseX::POE PASS 249 249 2026-05-12 312b4f902 @@ -1994,6 +1997,7 @@ Netconf PASS 1 1 2026-05-16 75a25eb21 Nullius PASS 25 25 2026-05-17 bd327bb57 Number::Bytes::Human PASS 223 223 2026-05-11 1e95a0902 Number::Compare PASS 24 24 2026-06-10 959012866 +Number::Format PASS 171 171 2026-06-11 bd9132057 Number::Format::Metric PASS 2 2 2026-05-08 24f4fb162 Number::Phone::Normalize PASS 126 126 2026-05-12 b5c01ce1c NumericCodes PASS 9 9 2026-05-18 bd327bb57 @@ -2628,7 +2632,7 @@ Template::Plugin::Class PASS 2 2 2026-04-21 73edc8aba Template::Plugin::Comma PASS 28 28 2026-05-11 b5c01ce1c Template::Plugin::HTML::SuperForm PASS 1 1 2026-05-12 312b4f902 Template::Plugin::JSON::Escape PASS 14 14 2026-05-15 3fe76ed3b -Template::Plugin::Number::Format PASS 29 29 2026-05-11 b5c01ce1c +Template::Plugin::Number::Format PASS 29 29 2026-06-11 bd9132057 Template::Plugin::Page PASS 51 51 2026-05-16 75a25eb21 Template::Plugin::VMethods PASS 12 12 2026-05-16 75a25eb21 Template::Provider::Encoding PASS 30 30 2026-05-16 75a25eb21 diff --git a/dev/cpan-reports/cpan-compatibility-skip.dat b/dev/cpan-reports/cpan-compatibility-skip.dat index 559bbe800..a2fdee8ab 100644 --- a/dev/cpan-reports/cpan-compatibility-skip.dat +++ b/dev/cpan-reports/cpan-compatibility-skip.dat @@ -3,7 +3,7 @@ CGI SKIP 2026-06-11 distroprefs CGI::Cookie SKIP 2026-06-10 distroprefs CGI::Simple SKIP 2026-06-11 distroprefs CGI::Simple::Cookie SKIP 2026-06-10 distroprefs -Class::Load::XS SKIP 2026-06-11 bundled +Class::Load::XS SKIP 2026-06-11 bundled Cwd SKIP 2026-06-11 bundled Digest::MD2 SKIP 2026-06-09 bundled File::Slurp SKIP 2026-06-11 distroprefs diff --git a/dev/cpan-reports/cpan-compatibility.md b/dev/cpan-reports/cpan-compatibility.md index 757ddbf20..20310e90e 100644 --- a/dev/cpan-reports/cpan-compatibility.md +++ b/dev/cpan-reports/cpan-compatibility.md @@ -1,6 +1,6 @@ # CPAN Module Compatibility Report for PerlOnJava -> Auto-generated by `dev/tools/cpan_random_tester.pl` on 2026-06-11 11:59:26 +> Auto-generated by `dev/tools/cpan_random_tester.pl` on 2026-06-11 16:58:21 > > Modules are randomly selected from the full CPAN index and tested > with `./jcpan -t`. Dependencies are tested too; every module that @@ -10,9 +10,9 @@ | Metric | Count | |--------|-------| -| **Modules Tested** | 10345 | -| **Pass** | 3342 (32.3%) | -| **Fail** | 6973 | +| **Modules Tested** | 10348 | +| **Pass** | 3346 (32.3%) | +| **Fail** | 6972 | | **Skipped** | 30 | ## Modules That Pass All Tests @@ -1288,7 +1288,7 @@ | HTTP::ClientDetect::Language | 79 | 2026-05-18 | bd327bb57 | | HTTP::Cookies | 122 | 2026-06-11 | 959012866 | | HTTP::Cookies::ChromeMacOS | 1 | 2026-05-18 | b5536d190 | -| HTTP::Cookies::Guess | 5 | 2026-05-18 | bd327bb57 | +| HTTP::Cookies::Guess | 5 | 2026-06-11 | bd9132057 | | HTTP::Cookies::PhantomJS | 1490 | 2026-05-16 | d8a8a9f34 | | HTTP::Cookies::w3m | 3 | 2026-05-09 | d7916b01c | | HTTP::DetectUserAgent | 152 | 2026-05-17 | bd327bb57 | @@ -1381,6 +1381,7 @@ | IM::Engine::Plugin::State | 12 | 2026-05-16 | 75a25eb21 | | IMDB::BaseClass | 2 | 2026-05-11 | 2b8886eec | | IMDB::TitlePage::Extract | 1 | 2026-05-12 | 93dfa497b | +| IO::All | 368 | 2026-06-11 | bd9132057 | | IO::All::FTP | 8 | 2026-05-17 | d8a8a9f34 | | IO::Async | 1134 | 2026-05-12 | b5c01ce1c | | IO::Async::Loop | 1134 | 2026-05-11 | b5c01ce1c | @@ -1609,6 +1610,7 @@ | Locale::BR | 2879 | 2026-05-14 | 3fe76ed3b | | Locale::Currency::Format | 28 | 2026-05-19 | b5536d190 | | Locale::Framework | 5 | 2026-05-12 | b5c01ce1c | +| Locale::Maketext | 180 | 2026-06-11 | bd9132057 | | Locale::SubCountry | 15 | 2026-05-17 | d8a8a9f34 | | Locale::TextDomain::UTF8 | 1 | 2026-05-18 | bd327bb57 | | Locale::Tie | 1 | 2026-05-18 | bd327bb57 | @@ -1762,6 +1764,7 @@ | Module::Depends | 20 | 2026-05-10 | 1e95a0902 | | Module::Depends::Intrusive | 20 | 2026-05-13 | 3fe76ed3b | | Module::Faker::Dist | 34 | 2026-05-19 | b5536d190 | +| Module::Install | 547 | 2026-06-11 | bd9132057 | | Module::Install::AutoManifest | 1 | 2026-05-09 | d7916b01c | | Module::Install::RTx | 1 | 2026-05-09 | d7916b01c | | Module::Install::Repository | 1 | 2026-05-18 | bd327bb57 | @@ -1877,7 +1880,7 @@ | MooseX::Meta::TypeConstraint::Mooish | 42 | 2026-05-18 | b5536d190 | | MooseX::MungeHas | 45 | 2026-05-12 | b5c01ce1c | | MooseX::NestedAttributesConstructor | 2 | 2026-05-10 | 1e95a0902 | -| MooseX::NonMoose | 177 | 2026-06-03 | d0f827461 | +| MooseX::NonMoose | 177 | 2026-06-11 | bd9132057 | | MooseX::NotRequired | 22 | 2026-05-18 | bd327bb57 | | MooseX::OneArgNew | 20 | 2026-05-16 | d8a8a9f34 | | MooseX::POE | 249 | 2026-05-12 | 312b4f902 | @@ -2015,6 +2018,7 @@ | Nullius | 25 | 2026-05-17 | bd327bb57 | | Number::Bytes::Human | 223 | 2026-05-11 | 1e95a0902 | | Number::Compare | 24 | 2026-06-10 | 959012866 | +| Number::Format | 171 | 2026-06-11 | bd9132057 | | Number::Format::Metric | 2 | 2026-05-08 | 24f4fb162 | | Number::Phone::Normalize | 126 | 2026-05-12 | b5c01ce1c | | NumericCodes | 9 | 2026-05-18 | bd327bb57 | @@ -2649,7 +2653,7 @@ | Template::Plugin::Comma | 28 | 2026-05-11 | b5c01ce1c | | Template::Plugin::HTML::SuperForm | 1 | 2026-05-12 | 312b4f902 | | Template::Plugin::JSON::Escape | 14 | 2026-05-15 | 3fe76ed3b | -| Template::Plugin::Number::Format | 29 | 2026-05-11 | b5c01ce1c | +| Template::Plugin::Number::Format | 29 | 2026-06-11 | bd9132057 | | Template::Plugin::Page | 51 | 2026-05-16 | 75a25eb21 | | Template::Plugin::VMethods | 12 | 2026-05-16 | 75a25eb21 | | Template::Provider::Encoding | 30 | 2026-05-16 | 75a25eb21 | @@ -5955,7 +5959,7 @@ | YAMLTest | 1/1 | Syntax error | 2026-05-10 | | constant::lexical | | Syntax error | 2026-05-17 | -### Test Failures (4343 modules) +### Test Failures (4342 modules) | Module | Pass/Total | Error | Date | |--------|-----------|-------|------| @@ -7385,7 +7389,7 @@ | File::PathConvert | 264/266 | 2/266 subtests failed | 2026-04-12 | | File::PathInfo::Ext | 1/1 | | 2026-05-19 | | File::Raw | 0/1 | 1/1 subtests failed | 2026-05-11 | -| File::Remove | 215/218 | 3/218 subtests failed; 2/11 test programs failed | 2026-06-10 | +| File::Remove | 215/218 | 3/218 subtests failed; 2/11 test programs failed | 2026-06-11 | | File::RotateLogs | 0/1 | 1/1 subtests failed | 2026-05-12 | | File::Rules | 0/3 | 14/3 subtests failed | 2026-04-22 | | File::Save::Home | 0/16 | 44/16 subtests failed | 2026-04-30 | @@ -8302,7 +8306,7 @@ | Module::Path | 11/18 | 7/18 subtests failed | 2026-04-22 | | Module::Pluggable::Object | 191/192 | 1/192 subtests failed; 1/45 test programs failed | 2026-06-11 | | Module::Refresh | 14/16 | 2/16 subtests failed | 2026-05-17 | -| Module::ScanDeps | 156/161 | 5/161 subtests failed; 3/21 test programs failed | 2026-06-03 | +| Module::ScanDeps | 156/161 | 5/161 subtests failed; 3/21 test programs failed | 2026-06-11 | | Module::Setup | 0/1 | 1/1 subtests failed | 2026-05-18 | | Module::Spy | 2/4 | 2/4 subtests failed | 2026-05-19 | | Module::Starter | 30/33 | 3/33 subtests failed | 2026-05-18 | @@ -8585,7 +8589,6 @@ | Nodejs::Util | 1/2 | 1/2 subtests failed | 2026-05-08 | | Noose | 9/19 | 10/19 subtests failed | 2026-05-11 | | Notifications | 0/17 | 18/17 subtests failed | 2026-05-15 | -| Number::Format | 10/10 | | 2026-04-22 | | Number::FormatEng | 139/140 | 1/140 subtests failed | 2026-05-05 | | Number::Phone | 1335/1373 | 38/1373 subtests failed | 2026-05-12 | | Number::Phone::JP | 0/199 | 66169/199 subtests failed | 2026-05-12 | diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 835389d0a..98ccd6686 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1622,6 +1622,10 @@ public void visit(IdentifierNode node) { // Barewords ending with :: are package name constants, always allowed // e.g., Tie::RefHash:: is equivalent to "Tie::RefHash" if (varName.endsWith("::")) { + if (currentCallContext == RuntimeContextType.VOID) { + lastResultReg = -1; + return; + } String packageName = varName.substring(0, varName.length() - 2); int rd = allocateOutputRegister(); emit(Opcodes.LOAD_STRING); @@ -1633,6 +1637,10 @@ public void visit(IdentifierNode node) { } String normalizedBarewordName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); if (GlobalVariable.hasGlobalPseudoConstant(normalizedBarewordName)) { + if (currentCallContext == RuntimeContextType.VOID) { + lastResultReg = -1; + return; + } int rd = allocateOutputRegister(); int nameIdx = addToStringPool(normalizedBarewordName); emit(Opcodes.LOAD_GLOBAL_SCALAR); @@ -1645,6 +1653,10 @@ public void visit(IdentifierNode node) { if (getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_STRICT_SUBS)) { throwCompilerException("Bareword \"" + varName + "\" not allowed while \"strict subs\" in use"); } + if (currentCallContext == RuntimeContextType.VOID) { + lastResultReg = -1; + return; + } // Not strict - treat bareword as string literal int rd = allocateOutputRegister(); emit(Opcodes.LOAD_STRING); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index de4f77b51..361ed3f54 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -339,6 +339,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } } + case Opcodes.RETURN_SCOPE_CLEANUP -> { + int reg = bytecode[pc++]; + RuntimeBase slot = registers[reg]; + if (slot instanceof RuntimeScalar rs) { + MortalList.deferDecrementIfNotCaptured(rs); + } + } + case Opcodes.SCOPE_EXIT_CLEANUP_HASH -> { // Scope-exit cleanup for a my-hash register. // @@ -422,6 +430,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } } + case Opcodes.RETURN_SCOPE_CLEANUP_HASH -> { + int reg = bytecode[pc++]; + RuntimeBase slot = registers[reg]; + if (slot instanceof RuntimeHash rh) { + MortalList.scopeExitCleanupHash(rh); + } + } + case Opcodes.SCOPE_EXIT_CLEANUP_ARRAY -> { // Scope-exit cleanup for a my-array register. // @@ -459,6 +475,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } } + case Opcodes.RETURN_SCOPE_CLEANUP_ARRAY -> { + int reg = bytecode[pc++]; + RuntimeBase slot = registers[reg]; + if (slot instanceof RuntimeArray ra) { + MortalList.scopeExitCleanupArray(ra); + } + } + case Opcodes.RETURN -> { // Return from subroutine: return rd int retReg = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 608abee6a..10372777b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1047,27 +1047,22 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode break; } - // Emit scope exit cleanup for all my-scalars, my-hashes, and my-arrays - // in the subroutine scope (scope 0). Explicit 'return' bypasses the - // normal scope exit cleanup at block end, so we must do it here. - // Skip the exprReg (return value register) — SCOPE_EXIT_CLEANUP nulls - // the register, which would destroy the return value if it's a my-variable. + // Explicit 'return' bypasses the normal block-end cleanup. + // Use return-specific cleanup opcodes so the returned register remains + // readable by RETURN while lexical owner counts are still released. java.util.List scalarIdxs = bytecodeCompiler.symbolTable.getMyScalarIndicesInScope(0); for (int idx : scalarIdxs) { - if (idx == exprReg) continue; - bytecodeCompiler.emit(Opcodes.SCOPE_EXIT_CLEANUP); + bytecodeCompiler.emit(Opcodes.RETURN_SCOPE_CLEANUP); bytecodeCompiler.emitReg(idx); } java.util.List hashIdxs = bytecodeCompiler.symbolTable.getMyHashIndicesInScope(0); for (int idx : hashIdxs) { - if (idx == exprReg) continue; - bytecodeCompiler.emit(Opcodes.SCOPE_EXIT_CLEANUP_HASH); + bytecodeCompiler.emit(Opcodes.RETURN_SCOPE_CLEANUP_HASH); bytecodeCompiler.emitReg(idx); } java.util.List arrayIdxs = bytecodeCompiler.symbolTable.getMyArrayIndicesInScope(0); for (int idx : arrayIdxs) { - if (idx == exprReg) continue; - bytecodeCompiler.emit(Opcodes.SCOPE_EXIT_CLEANUP_ARRAY); + bytecodeCompiler.emit(Opcodes.RETURN_SCOPE_CLEANUP_ARRAY); bytecodeCompiler.emitReg(idx); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index 55367575b..2415834eb 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -42,14 +42,26 @@ public static String disassemble(InterpretedCode interpretedCode) { int secReg = interpretedCode.bytecode[pc++]; sb.append("SCOPE_EXIT_CLEANUP r").append(secReg).append("\n"); break; + case Opcodes.RETURN_SCOPE_CLEANUP: + int rscReg = interpretedCode.bytecode[pc++]; + sb.append("RETURN_SCOPE_CLEANUP r").append(rscReg).append("\n"); + break; case Opcodes.SCOPE_EXIT_CLEANUP_HASH: int sechReg = interpretedCode.bytecode[pc++]; sb.append("SCOPE_EXIT_CLEANUP_HASH r").append(sechReg).append("\n"); break; + case Opcodes.RETURN_SCOPE_CLEANUP_HASH: + int rschReg = interpretedCode.bytecode[pc++]; + sb.append("RETURN_SCOPE_CLEANUP_HASH r").append(rschReg).append("\n"); + break; case Opcodes.SCOPE_EXIT_CLEANUP_ARRAY: int secaReg = interpretedCode.bytecode[pc++]; sb.append("SCOPE_EXIT_CLEANUP_ARRAY r").append(secaReg).append("\n"); break; + case Opcodes.RETURN_SCOPE_CLEANUP_ARRAY: + int rscaReg = interpretedCode.bytecode[pc++]; + sb.append("RETURN_SCOPE_CLEANUP_ARRAY r").append(rscaReg).append("\n"); + break; case Opcodes.RETURN: int retReg = interpretedCode.bytecode[pc++]; sb.append("RETURN r").append(retReg).append("\n"); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 6aaeeb82f..369f25971 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2376,6 +2376,27 @@ public class Opcodes { */ public static final short DEFINED_CODE_DYNAMIC = 493; + /** + * Explicit-return cleanup for a my-scalar register. + * Releases the lexical owner count but keeps the register readable by RETURN. + * Format: RETURN_SCOPE_CLEANUP reg + */ + public static final short RETURN_SCOPE_CLEANUP = 494; + + /** + * Explicit-return cleanup for a my-hash register. + * Walks hash values for tracked references but keeps the register readable by RETURN. + * Format: RETURN_SCOPE_CLEANUP_HASH reg + */ + public static final short RETURN_SCOPE_CLEANUP_HASH = 495; + + /** + * Explicit-return cleanup for a my-array register. + * Walks array values for tracked references but keeps the register readable by RETURN. + * Format: RETURN_SCOPE_CLEANUP_ARRAY reg + */ + public static final short RETURN_SCOPE_CLEANUP_ARRAY = 496; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java b/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java index d7c1c8bba..b5db67256 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java @@ -648,14 +648,12 @@ public static void emitNumber(EmitterContext ctx, NumberNode node) { * @throws PerlCompilerException if strict subs is enabled */ public static void emitIdentifier(EmitterVisitor visitor, EmitterContext ctx, IdentifierNode node) { - // Barewords have no side effects in void context - if (ctx.contextType == RuntimeContextType.VOID) { - return; - } - // Barewords ending with :: are package name constants, always allowed under strict subs // e.g., Tie::RefHash:: is equivalent to "Tie::RefHash" if (node.name.endsWith("::")) { + if (ctx.contextType == RuntimeContextType.VOID) { + return; + } String packageName = node.name.substring(0, node.name.length() - 2); new StringNode(packageName, node.tokenIndex).accept(visitor); return; @@ -665,6 +663,9 @@ public static void emitIdentifier(EmitterVisitor visitor, EmitterContext ctx, Id node.name, ctx.symbolTable.getCurrentPackage()); if (GlobalVariable.hasGlobalPseudoConstant(normalizedBarewordName)) { + if (ctx.contextType == RuntimeContextType.VOID) { + return; + } ctx.mv.visitLdcInsn(normalizedBarewordName); ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/GlobalVariable", @@ -678,6 +679,9 @@ public static void emitIdentifier(EmitterVisitor visitor, EmitterContext ctx, Id node.tokenIndex, "Bareword \"" + node.name + "\" not allowed while \"strict subs\" in use", ctx.errorUtil); + } else if (ctx.contextType == RuntimeContextType.VOID) { + // Non-strict barewords have no side effects in void context. + return; } else { // Treat bareword as a string literal new StringNode(node.name, node.tokenIndex).accept(visitor); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java index 6f2748728..06051c12e 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java @@ -353,7 +353,7 @@ public static RuntimeList no_upwards(RuntimeArray args, int ctx) { */ public static RuntimeList case_tolerant(RuntimeArray args, int ctx) { boolean caseTolerant = SystemUtils.osIsWindows(); - return new RuntimeScalar(caseTolerant).getList(); + return new RuntimeScalar(caseTolerant ? 1 : 0).getList(); } /** diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java b/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java index 1fa71ed38..9f9224f22 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java @@ -63,9 +63,11 @@ public static RuntimeList _error(RuntimeArray args, int ctx) { return new RuntimeList(new RuntimeScalar(1)); // Invalid handle has error } - // Check if there's an error in $! - String error = GlobalVariable.getGlobalVariable("main::!").toString(); - return new RuntimeList(new RuntimeScalar(error.isEmpty() ? 0 : 1)); + // Perl's IO::Handle::error reports the handle's stream error + // indicator, not the current global errno. PerlOnJava does not yet + // keep a per-handle stream error flag, so a valid handle has no + // pending stream error here. + return new RuntimeList(RuntimeScalarCache.scalarFalse); } /** @@ -235,4 +237,4 @@ public static RuntimeList _set_input_line_number(RuntimeArray args, int ctx) { return new RuntimeList(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 219601cf0..f40d7ce02 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -3931,9 +3931,10 @@ private static RuntimeScalar handleCodeOverload(RuntimeScalar runtimeScalar) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray(runtimeScalar)); + RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray( + runtimeScalar, RuntimeScalarCache.scalarUndef, RuntimeScalarCache.scalarFalse)); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != runtimeScalar.value.hashCode()) { + if (result != null && result != runtimeScalar && result.value != runtimeScalar.value) { return result; } } @@ -4465,8 +4466,9 @@ public static RuntimeScalar createCodeReference(RuntimeScalar runtimeScalar, Str // Has overloading - try to get &{} overload OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { - RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray(runtimeScalar)); - if (result != null && result.value.hashCode() != runtimeScalar.value.hashCode()) { + RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray( + runtimeScalar, RuntimeScalarCache.scalarUndef, RuntimeScalarCache.scalarFalse)); + if (result != null && result != runtimeScalar && result.value != runtimeScalar.value) { // Successfully got a CODE reference via overload, return it if (result.type == RuntimeScalarType.CODE) { return result; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index f67ab70cd..f0d1d9105 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -291,7 +291,9 @@ private static int tryRecycleLowestFd() { List candidates = new ArrayList<>(); Integer recycled; while ((recycled = recycledFds.poll()) != null) { - candidates.add(recycled); + if (recycled >= 3) { + candidates.add(recycled); + } } if (candidates.isEmpty()) { return -1; @@ -346,8 +348,12 @@ public void unregisterFileno() { Integer fd = ioToFileno.remove(this); if (fd != null) { filenoToIO.remove(fd); - // Return fd to the recycle pool so it can be reused (POSIX: lowest available) - recycledFds.add(fd); + // Return fd to the recycle pool so it can be reused (POSIX: lowest available). + // Descriptors 0, 1, and 2 are reserved for stdin/stdout/stderr and must + // never be assigned to lazily-numbered regular filehandles. + if (fd >= 3) { + recycledFds.add(fd); + } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 04fc5101d..89526d7f7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -38,6 +38,14 @@ public class RuntimeScalar extends RuntimeBase implements RuntimeScalarReference // INTEGER_PATTERN replaced with isIntegerString() for better performance private static final Pattern DECIMAL_PATTERN = Pattern.compile("^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$"); + private RuntimeArray unaryDerefOverloadArgs() { + return new RuntimeArray(this, scalarUndef, scalarFalse); + } + + private boolean overloadReturnedDifferentObject(RuntimeScalar result) { + return result != null && result != this && result.value != this.value; + } + // Fast check if string might be a parseable integer // Returns true if first char suggests it could be an integer (digit or minus) // This avoids exception overhead for strings like "hello" while allowing @@ -2014,9 +2022,9 @@ public RuntimeArray arrayDeref() { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(@{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(@{}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.arrayDeref(); } } @@ -2130,11 +2138,11 @@ public RuntimeHash hashDeref() { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try to call the overloaded hash dereference method `(%{}` - RuntimeScalar result = ctx.tryOverload("(%{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(%{}", unaryDerefOverloadArgs()); // If the overload method returns a different object (not self), // recursively dereference the returned value // This prevents infinite recursion when the overload returns the same object - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.hashDeref(); } } @@ -2205,9 +2213,9 @@ public RuntimeScalar scalarDeref() { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(${}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(${}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.scalarDeref(); } } @@ -2267,9 +2275,9 @@ public RuntimeScalar scalarDerefNonStrict(String packageName) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(${}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(${}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.scalarDerefNonStrict(packageName); } } @@ -2315,9 +2323,9 @@ public RuntimeHash hashDerefNonStrict(String packageName) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(%{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(%{}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.hashDerefNonStrict(packageName); } } @@ -2388,9 +2396,9 @@ public RuntimeArray arrayDerefNonStrict(String packageName) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(@{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(@{}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.arrayDerefNonStrict(packageName); } } @@ -2461,9 +2469,9 @@ public RuntimeGlob globDeref() { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(*{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(*{}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.globDeref(); } } @@ -2530,9 +2538,9 @@ public RuntimeGlob globDerefNonStrict(String packageName) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(*{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(*{}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.globDerefNonStrict(packageName); } } @@ -2602,9 +2610,9 @@ public RuntimeScalar codeDerefNonStrict(String packageName) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try overload method - RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray(this)); + RuntimeScalar result = ctx.tryOverload("(&{}", unaryDerefOverloadArgs()); // If the subroutine returns the object itself then it will not be called again - if (result != null && result.value.hashCode() != this.value.hashCode()) { + if (overloadReturnedDifferentObject(result)) { return result.codeDerefNonStrict(packageName); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java index 487edc6ec..38bd9d1f8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java @@ -93,7 +93,7 @@ public static RuntimeBase tiedReadline(TieHandle tieHandle, int ctx) { tieHandle.self, new RuntimeScalar("READLINE"), null, - new RuntimeArray(tieHandle.self), + new RuntimeArray(), ctx ); diff --git a/src/main/perl/lib/CPAN/Config.pm b/src/main/perl/lib/CPAN/Config.pm index ac630f85b..1265b3707 100644 --- a/src/main/perl/lib/CPAN/Config.pm +++ b/src/main/perl/lib/CPAN/Config.pm @@ -38,6 +38,7 @@ sub _bootstrap_prefs { 'CPAN-FindDependencies.yml' => 'PerlOnJava/CpanDistroprefs/CPAN-FindDependencies.yml', 'Error-Pure.yml' => 'PerlOnJava/CpanDistroprefs/Error-Pure.yml', 'Error.yml' => 'PerlOnJava/CpanDistroprefs/Error.yml', + 'IO-All.yml' => 'PerlOnJava/CpanDistroprefs/IO-All.yml', 'IO-Async.yml' => 'PerlOnJava/CpanDistroprefs/IO-Async.yml', 'IO-Compress.yml' => 'PerlOnJava/CpanDistroprefs/IO-Compress.yml', 'IO-HTML.yml' => 'PerlOnJava/CpanDistroprefs/IO-HTML.yml', @@ -50,6 +51,7 @@ sub _bootstrap_prefs { 'ExtUtils-CBuilder.yml' => 'PerlOnJava/CpanDistroprefs/ExtUtils-CBuilder.yml', 'ExtUtils-ParseXS.yml' => 'PerlOnJava/CpanDistroprefs/ExtUtils-ParseXS.yml', 'Module-Build.yml' => 'PerlOnJava/CpanDistroprefs/Module-Build.yml', + 'Module-Install.yml' => 'PerlOnJava/CpanDistroprefs/Module-Install.yml', 'Aliased.yml' => 'PerlOnJava/CpanDistroprefs/Aliased.yml', 'Carp-Assert.yml' => 'PerlOnJava/CpanDistroprefs/Carp-Assert.yml', 'Regexp-Common.yml' => 'PerlOnJava/CpanDistroprefs/Regexp-Common.yml', @@ -223,6 +225,10 @@ sub _bootstrap_patches { 'PerlOnJava/CpanPatches/Error-Pure-0.34/PlainLexicalConstants.patch' ], [ 'String-ShellQuote-1.04/SkipForkScriptTests.patch', 'PerlOnJava/CpanPatches/String-ShellQuote-1.04/SkipForkScriptTests.patch' ], + [ 'IO-All-0.87/SkipForkTests.patch', + 'PerlOnJava/CpanPatches/IO-All-0.87/SkipForkTests.patch' ], + [ 'Module-Install-1.21/ExplicitAuthorsMethod.patch', + 'PerlOnJava/CpanPatches/Module-Install-1.21/ExplicitAuthorsMethod.patch' ], [ 'Module-Pluggable-Ordered-1.5/LimitFixturePlugins.patch', 'PerlOnJava/CpanPatches/Module-Pluggable-Ordered-1.5/LimitFixturePlugins.patch' ], [ 'LWP-Protocol-https-6.15/SkipForkProxyTest.patch', diff --git a/src/main/perl/lib/Devel/PPPort.pm b/src/main/perl/lib/Devel/PPPort.pm new file mode 100644 index 000000000..2115cc8c8 --- /dev/null +++ b/src/main/perl/lib/Devel/PPPort.pm @@ -0,0 +1,28 @@ +package Devel::PPPort; + +use strict; +use warnings; +use Exporter (); + +our $VERSION = '3.68'; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(GetFileContents WriteFile); + +sub GetFileContents { + return <<"END_PPPORT"; +#ifndef PERLONJAVA_PPPORT_H +#define PERLONJAVA_PPPORT_H 1 +/* Minimal ppport.h generated by PerlOnJava's Devel::PPPort shim. */ +#endif +END_PPPORT +} + +sub WriteFile { + my $file = shift || 'ppport.h'; + open my $fh, '>', $file or return 0; + print {$fh} GetFileContents(); + close $fh or return 0; + return 1; +} + +1; diff --git a/src/main/perl/lib/ExtUtils/MakeMaker.pm b/src/main/perl/lib/ExtUtils/MakeMaker.pm index cd66ce5f9..904e23d4a 100644 --- a/src/main/perl/lib/ExtUtils/MakeMaker.pm +++ b/src/main/perl/lib/ExtUtils/MakeMaker.pm @@ -66,7 +66,13 @@ sub _default_install_base { sub WriteMakefile { my %args = @_; - + + my %cli = _parse_makefile_pl_args(@ARGV); + my $prereq_print = delete $cli{PREREQ_PRINT}; + for my $key (keys %cli) { + $args{$key} = $cli{$key}; + } + my $name = $args{NAME} or die "NAME is required\n"; my $version = $args{VERSION} || ($args{VERSION_FROM} && _extract_version($args{VERSION_FROM})) || '0'; @@ -107,6 +113,11 @@ Please install these modules first and rerun 'perl Makefile.PL'. END } } + + if ($prereq_print) { + _print_prereq_variables(\%args); + exit 0; + } # Check for XS files my @xs_files = _find_xs_files(\%args); @@ -509,6 +520,7 @@ sub _install_pure_perl { # Create Makefile with install commands (actual install deferred to 'make') _create_install_makefile($name, $version, $args, \%pm, \%scripts, $mm); + _configure_subdirs($args); # If Build.PL exists in cwd, the distribution was configured via Module::Build # (or Module::Install's Build.PL-delegates-to-Makefile.PL trick). CPAN.pm will @@ -527,6 +539,36 @@ sub _install_pure_perl { return $mm; } +sub _parse_makefile_pl_args { + my @argv = @_; + my %args; + + for my $arg (@argv) { + if ($arg eq 'PREREQ_PRINT') { + $args{PREREQ_PRINT} = 1; + next; + } + if ($arg =~ /^([A-Za-z_]\w*)=(.*)\z/s) { + $args{uc $1} = $2; + } + } + + return %args; +} + +sub _print_prereq_variables { + my ($args) = @_; + + my $prereq_pm = ref $args->{PREREQ_PM} eq 'HASH' ? $args->{PREREQ_PM} : {}; + print "\$PREREQ_PM = " . _perl_hash_literal($prereq_pm) . ";\n"; + if (defined $args->{MIN_PERL_VERSION}) { + print "\$MIN_PERL_VERSION = q[$args->{MIN_PERL_VERSION}];\n"; + } + if (ref $args->{BUILD_REQUIRES} eq 'HASH' && %{$args->{BUILD_REQUIRES}}) { + print "\$BUILD_REQUIRES = " . _perl_hash_literal($args->{BUILD_REQUIRES}) . ";\n"; + } +} + sub _build_share_file_mapping { my ($name, $args) = @_; my %files; @@ -694,7 +736,8 @@ sub _create_install_makefile { } # Convert module name to dist name (My::Module -> My-Module) - (my $distname = $name) =~ s/::/-/g; + my $distname = $args->{DISTNAME} || $name; + $distname =~ s/::/-/g; # Get INST_LIB and installation directories # Use INSTALL_BASE for consistency with where we actually install modules @@ -702,6 +745,8 @@ sub _create_install_makefile { my $inst_archlib = $args->{INST_ARCHLIB} || 'blib/arch'; my $installsitelib = $args->{INSTALLSITELIB}; my $siteprefix = $args->{SITEPREFIX} || $args->{PREFIX}; + my $install_dirs = defined $args->{INSTALLDIRS} ? $args->{INSTALLDIRS} : 'site'; + my $inc = defined $args->{INC} ? $args->{INC} : ''; # If INSTALLSITELIB is not set, compute it from PREFIX/SITEPREFIX (standard MakeMaker behavior) unless ($installsitelib) { @@ -810,7 +855,10 @@ sub _create_install_makefile { # the module's core functionality). Track generated targets so missing # generated PM files do not become hard pm_to_blib prerequisites before # pl_files has had a chance to create them. + _default_pl_files($args); + my @pl_cmds; + my @pl_rules; my %pl_targets; if ($args->{PL_FILES} && %{$args->{PL_FILES}}) { for my $pl (sort keys %{$args->{PL_FILES}}) { @@ -819,14 +867,17 @@ sub _create_install_makefile { for my $t (@$target) { $pl_targets{$t} = 1; push @pl_cmds, "\t-$perl $pl $t"; + push @pl_rules, "$t :: $pl pm_to_blib\n\t-$perl $pl $t\n"; } } else { $pl_targets{$target} = 1; push @pl_cmds, "\t-$perl $pl $target"; + push @pl_rules, "$target :: $pl pm_to_blib\n\t-$perl $pl $target\n"; } } } my $pl_cmds_str = join("\n", @pl_cmds) || "\t\@true"; + my $pl_rules_str = join("\n", @pl_rules); my $pm_deps_str = join(' ', sort grep { $_ !~ m{^blib/lib/} && !($pl_targets{$_} && !-e $_) } keys %blib_pm); $pm_deps_str = " $pm_deps_str" if length $pm_deps_str; @@ -840,7 +891,7 @@ sub _create_install_makefile { my $depend_rules_str = _make_depend_rules($args->{depend}); - my $prereq_comment = _make_prereq_comment($args); + my $makefile_comments = _make_makefile_comments($args, $distname, $version); # Honor user-supplied `macro => { ... }` from WriteMakefile by emitting # extra Makefile macro definitions. LaTeXML and others stuff custom @@ -862,7 +913,7 @@ sub _create_install_makefile { # 'make install' copies ./blib files to INSTALLSITELIB # This matches standard ExtUtils::MakeMaker semantics so that 'cpan -t' # (test only) does not install, while 'cpan -i' (install) does. -$prereq_comment +$makefile_comments NAME = $name DISTNAME = $distname VERSION = $version @@ -871,7 +922,6 @@ FULLPERL = $perl PERLRUN = \$(PERL) FULLPERLRUN = \$(FULLPERL) PERLRUNINST = \$(PERLRUN) -I\$(INST_ARCHLIB) -I\$(INST_LIB) -INSTALLDIRS = site INST_LIB = $inst_lib INST_ARCHLIB = $inst_archlib INST_LIBDIR = \$(INST_LIB) @@ -880,12 +930,20 @@ PERLONJAVA_CPAN_PERL5LIB = \$(shell if test -f blib/.perlonjava-cpan-perl5lib; t PERLONJAVA_TEST_PERL5LIB = \$(INST_LIB):\$(INST_ARCHLIB):\$(PERLONJAVA_CPAN_PERL5LIB):\$\$PERL5LIB $siteprefix_var INSTALLSITELIB = $installsitelib +INSTALLDIRS = $install_dirs +INC = $inc NOECHO = \@ RM_F = rm -f RM_RF = rm -rf CP = cp MV = mv MKPATH = mkdir -p +CHMOD = chmod +PERM_DIR = 755 +PERM_RW = 644 +PERM_RWX = 755 +DISTVNAME = $distname-$version +SUFFIX = .gz SHELL = /bin/sh TEST_VERBOSE = 0 RECORD_REVISION = \@true @@ -925,6 +983,7 @@ $blib_script_cmds_str pl_files:: $pl_cmds_str +$pl_rules_str # Install executable scripts (called only from 'install') install_scripts:: $script_cmds_str @@ -983,31 +1042,137 @@ MAKEFILE close $fh; } -sub _make_prereq_comment { - my ($args) = @_; +sub _make_makefile_comments { + my ($args, $distname, $version) = @_; my $comment = ''; - for my $key (qw(PREREQ_PM BUILD_REQUIRES TEST_REQUIRES CONFIGURE_REQUIRES)) { - next unless ref $args->{$key} eq 'HASH' && %{$args->{$key}}; + $comment .= "# DISTNAME => q[$distname]\n" if defined $distname; + $comment .= "# VERSION => q[$version]\n" if defined $version; + + for my $key (qw(ABSTRACT AUTHOR LICENSE SIGN MIN_PERL_VERSION)) { + next unless exists $args->{$key}; + my $value = $args->{$key}; + if ($key eq 'AUTHOR') { + $comment .= "# AUTHOR => [q[$value]]\n"; + } else { + $comment .= "# $key => q[$value]\n"; + } + } + if (ref $args->{test} eq 'HASH' && defined $args->{test}{TESTS}) { + $comment .= "# test => { TESTS=>q[$args->{test}{TESTS}] }\n"; + } + + if (ref $args->{dist} eq 'HASH' && %{$args->{dist}}) { + my @pairs; + for my $key (sort keys %{$args->{dist}}) { + my $value = $args->{dist}{$key}; + $value = '' unless defined $value; + push @pairs, "$key=>q[$value]"; + } + $comment .= "# dist => { " . join(", ", @pairs) . " }\n"; + } + + for my $key (qw(PREREQ_PM BUILD_REQUIRES TEST_REQUIRES CONFIGURE_REQUIRES)) { + my $hash = ref $args->{$key} eq 'HASH' ? $args->{$key} : {}; my @prereqs; - for my $mod (sort keys %{$args->{$key}}) { + for my $mod (sort keys %$hash) { next if $mod eq 'perl'; - my $ver = $args->{$key}{$mod}; + my $ver = $hash->{$mod}; $ver = 0 unless defined $ver; push @prereqs, "$mod=>q[$ver]"; } - next unless @prereqs; + $comment .= "# $key => { " . join(", ", @prereqs) . " }\n"; + } - # MakeMaker writes prerequisite hashes into generated Makefiles for - # CPAN tooling to parse. Keep the same recognizable shape for all - # supported prereq phases, not just runtime PREREQ_PM. - $comment .= "#\t$key => { " . join(", ", @prereqs) . " }\n"; + if (ref $args->{PL_FILES} eq 'HASH' && %{$args->{PL_FILES}}) { + my @pairs; + for my $src (sort keys %{$args->{PL_FILES}}) { + my $target = $args->{PL_FILES}{$src}; + if (ref $target eq 'ARRAY') { + push @pairs, "$src=>[" . join(",", map { "q[$_]" } @$target) . "]"; + } else { + push @pairs, "$src=>q[$target]"; + } + } + $comment .= "# PL_FILES => { " . join(", ", @pairs) . " }\n"; } return $comment; } +sub _perl_hash_literal { + my ($hash) = @_; + my @pairs; + for my $key (sort keys %$hash) { + next if $key eq 'perl'; + my $value = $hash->{$key}; + $value = 0 unless defined $value; + push @pairs, "q[$key] => q[$value]"; + } + return "{ " . join(", ", @pairs) . " }"; +} + +sub _default_pl_files { + my ($args) = @_; + return if ref $args->{PL_FILES} eq 'HASH' && %{$args->{PL_FILES}}; + + my %pl_files; + opendir(my $dh, '.') or return; + while (my $file = readdir($dh)) { + next unless -f $file && $file =~ /\.PL\z/; + next if $file eq 'Makefile.PL' || $file eq 'Build.PL'; + (my $target = $file) =~ s/\.PL\z//; + $pl_files{$file} = $target; + } + closedir($dh); + + $args->{PL_FILES} = \%pl_files if %pl_files; +} + +sub _configure_subdirs { + my ($args) = @_; + + my @dirs; + my %seen; + if (ref $args->{DIR} eq 'ARRAY') { + for my $dir (@{$args->{DIR}}) { + next unless defined $dir && length $dir; + next unless -f File::Spec->catfile($dir, 'Makefile.PL'); + push @dirs, $dir unless $seen{$dir}++; + } + } + + opendir(my $dh, '.') or return; + while (my $entry = readdir($dh)) { + next if $entry eq '.' || $entry eq '..'; + next unless -d $entry && -f File::Spec->catfile($entry, 'Makefile.PL'); + push @dirs, $entry unless $seen{$entry}++; + } + closedir($dh); + + return unless @dirs; + + my $perl = _current_perl_path(); + my @inc; + for my $inc (@INC) { + next if ref $inc; + next if $inc eq 'jar:PERL5LIB'; + push @inc, File::Spec->file_name_is_absolute($inc) + ? $inc + : File::Spec->rel2abs($inc); + } + + my $cwd = getcwd(); + for my $dir (@dirs) { + my $abs_dir = File::Spec->rel2abs($dir, $cwd); + next unless chdir $abs_dir; + my @cmd = ($perl, map({ "-I$_" } @inc), '-I../inc', 'Makefile.PL'); + system(@cmd) == 0 or warn "PerlOnJava MakeMaker: subdir $dir configure failed\n"; + chdir $cwd; + } +} + # Helper: generate a shell mkdir -p command for Makefile sub _shell_mkdir { my ($dir) = @_; diff --git a/src/main/perl/lib/File/Remove.pm b/src/main/perl/lib/File/Remove.pm new file mode 100644 index 000000000..24a1afcff --- /dev/null +++ b/src/main/perl/lib/File/Remove.pm @@ -0,0 +1,63 @@ +package File::Remove; + +use strict; +use warnings; +use Exporter (); +use File::Path (); + +our $VERSION = '1.61'; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(clear remove rm trash); + +sub _expand { + my ($path, $glob) = @_; + return ($path) unless $glob; + my @matches = glob $path; + return @matches ? @matches : ($path); +} + +sub remove { + my @args = @_; + my $recursive = 0; + my $glob = 0; + + if (@args && ref($args[0]) eq 'SCALAR') { + $recursive = ${ shift @args } ? 1 : 0; + } + if (@args && ref($args[0]) eq 'HASH') { + my $opts = shift @args; + $recursive = $opts->{recursive} ? 1 : $recursive + if exists $opts->{recursive}; + $recursive = $opts->{recurse} ? 1 : $recursive + if exists $opts->{recurse}; + $glob = $opts->{glob} ? 1 : 0 if exists $opts->{glob}; + } + + my @removed; + for my $arg (@args) { + next unless defined $arg; + for my $path (_expand($arg, $glob)) { + next unless defined $path && length $path && -e $path; + if (-d $path && !-l $path) { + if ($recursive) { + File::Path::rmtree($path); + push @removed, $path unless -e $path; + } + else { + rmdir $path and push @removed, $path; + } + } + else { + unlink $path and push @removed, $path; + } + } + } + + return wantarray ? @removed : scalar @removed; +} + +sub rm { remove(@_) } +sub clear { remove(\1, @_) } +sub trash { remove(@_) } + +1; diff --git a/src/main/perl/lib/HTTP/Cookies.pm b/src/main/perl/lib/HTTP/Cookies.pm new file mode 100644 index 000000000..91d23b59f --- /dev/null +++ b/src/main/perl/lib/HTTP/Cookies.pm @@ -0,0 +1,51 @@ +package HTTP::Cookies; + +use strict; +use warnings; + +our $VERSION = '6.11'; +our $EPOCH_OFFSET = 0; + +sub new { + my $class = shift; + my $self = bless { COOKIES => {} }, $class; + my %cnf = @_; + for my $key (keys %cnf) { + $self->{lc $key} = $cnf{$key}; + } + $self->load; + return $self; +} + +sub load { return 1 } + +sub save { return 1 } + +sub set_cookie { + my ($self, $version, $key, $val, $path, $domain, $port, + $path_spec, $secure, $maxage, $discard) = @_; + $domain = defined($domain) && length($domain) ? $domain : '.local'; + $path = defined($path) && length($path) ? $path : '/'; + $self->{COOKIES}{$domain}{$path}{$key} = [ + $version, $val, $port, $path_spec, $secure, $maxage, $discard + ]; + return 1; +} + +sub scan { + my ($self, $callback) = @_; + return unless $callback; + for my $domain (sort keys %{ $self->{COOKIES} }) { + for my $path (sort keys %{ $self->{COOKIES}{$domain} }) { + for my $key (sort keys %{ $self->{COOKIES}{$domain}{$path} }) { + my $cookie = $self->{COOKIES}{$domain}{$path}{$key}; + my ($version, $val, $port, $path_spec, $secure, $expires, $discard) = @$cookie; + $callback->($version, $key, $val, $path, $domain, $port, + $path_spec, $secure, $expires, $discard, {}); + } + } + } + return 1; +} + +1; diff --git a/src/main/perl/lib/HTTP/Cookies/Netscape.pm b/src/main/perl/lib/HTTP/Cookies/Netscape.pm new file mode 100644 index 000000000..919be0ea1 --- /dev/null +++ b/src/main/perl/lib/HTTP/Cookies/Netscape.pm @@ -0,0 +1,74 @@ +package HTTP::Cookies::Netscape; + +use strict; +use warnings; +use Carp (); + +our $VERSION = '6.11'; + +require HTTP::Cookies; +our @ISA = qw(HTTP::Cookies); + +sub load { + my ($self, $file) = @_; + $file ||= $self->{file} || return; + + local $/ = "\n"; + open my $fh, '<', $file or return; + my $magic = <$fh>; + chomp $magic if defined $magic; + unless (defined $magic && $magic =~ /^#(?: Netscape)? HTTP Cookie File/) { + warn "$file does not look like a netscape cookies file"; + return; + } + + my $now = time() - $HTTP::Cookies::EPOCH_OFFSET; + while (my $line = <$fh>) { + chomp $line; + $line =~ s/\s*\#HttpOnly_//; + next if $line =~ /^\s*\#/; + next if $line =~ /^\s*$/; + $line =~ tr/\n\r//d; + my ($domain, $bool, $path, $secure, $expires, $key, $val) = + split /\t/, $line; + $secure = defined($secure) && $secure eq 'TRUE'; + $self->set_cookie(undef, $key, $val, $path, $domain, undef, 0, + $secure, $expires - $now, 0); + } + return 1; +} + +sub save { + my $self = shift; + my %args = ( + file => $self->{file}, + ignore_discard => $self->{ignore_discard}, + @_ == 1 ? (file => $_[0]) : @_, + ); + Carp::croak('Unexpected argument to save method') if keys %args > 2; + my $file = $args{file} || return; + + open my $fh, '>', $file or return; + print {$fh} <<'EOT'; +# Netscape HTTP Cookie File +# http://www.netscape.com/newsref/std/cookie_spec.html +# This is a generated file! Do not edit. + +EOT + + my $now = time() - $HTTP::Cookies::EPOCH_OFFSET; + $self->scan(sub { + my ($version, $key, $val, $path, $domain, $port, $path_spec, + $secure, $expires, $discard) = @_; + return if $discard && !$args{ignore_discard}; + $expires = $expires ? $expires - $HTTP::Cookies::EPOCH_OFFSET : 0; + return if $now > $expires; + $secure = $secure ? 'TRUE' : 'FALSE'; + my $bool = $domain =~ /^\./ ? 'TRUE' : 'FALSE'; + print {$fh} join("\t", $domain, $bool, $path, $secure, + $expires, $key, $val), "\n"; + }); + return 1; +} + +1; diff --git a/src/main/perl/lib/I18N/LangTags.pm b/src/main/perl/lib/I18N/LangTags.pm new file mode 100644 index 000000000..68eb3291f --- /dev/null +++ b/src/main/perl/lib/I18N/LangTags.pm @@ -0,0 +1,168 @@ +package I18N::LangTags; + +use strict; +use warnings; +use Exporter (); + +our $VERSION = '0.45'; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw( + alternate_language_tags encode_language_tag extract_language_tags + implicate_supers implicate_supers_strictly is_dialect_of is_language_tag + locale2language_tag panic_languages same_language_tag similarity_language_tag + super_languages +); + +my %ALIASES = ( + 'iw' => 'he', + 'in' => 'id', + 'ji' => 'yi', + 'no-bok' => 'nb', + 'no-nyn' => 'nn', + 'i-klingon' => 'tlh', +); + +sub _uniq { + my %seen; + return grep { defined($_) && length($_) && !$seen{$_}++ } @_; +} + +sub _norm { + my $tag = shift; + return undef unless defined $tag; + $tag =~ s/\A\s+|\s+\z//g; + return undef unless length $tag; + $tag =~ s/\..*\z//; + $tag =~ s/\@.*\z//; + $tag =~ tr/_/-/; + $tag = lc $tag; + return 'en' if $tag eq 'c' || $tag eq 'posix'; + $tag =~ s/[^a-z0-9-]+/-/g; + $tag =~ s/-+/-/g; + $tag =~ s/\A-|-+\z//g; + return undef unless length $tag; + return $ALIASES{$tag} || $tag; +} + +sub is_language_tag { + my $tag = _norm(shift); + return 0 unless defined $tag; + return $tag =~ /\A(?:x|i)(?:-[a-z0-9]{1,8})+\z/ + || $tag =~ /\A[a-z]{1,8}(?:-[a-z0-9]{1,8})*\z/ ? 1 : 0; +} + +sub same_language_tag { + my ($a, $b) = map _norm($_), @_; + return 0 unless defined $a && defined $b; + return $a eq $b ? 1 : 0; +} + +sub locale2language_tag { + return _norm(shift); +} + +sub encode_language_tag { + return _norm(shift); +} + +sub extract_language_tags { + my @out; + for my $text (@_) { + next unless defined $text; + while ($text =~ /([A-Za-z]{1,8}(?:[-_][A-Za-z0-9]{1,8})*)/g) { + my $tag = _norm($1); + push @out, $tag if defined $tag && is_language_tag($tag); + } + } + return _uniq(@out); +} + +sub super_languages { + my $tag = _norm(shift); + return unless defined $tag; + my @parts = split /-/, $tag; + my @out; + while (@parts > 1) { + pop @parts; + push @out, join('-', @parts); + } + return @out; +} + +sub is_dialect_of { + my ($dialect, $base) = map _norm($_), @_; + return 0 unless defined $dialect && defined $base; + return $dialect eq $base || index($dialect, "$base-") == 0 ? 1 : 0; +} + +sub similarity_language_tag { + my ($a, $b) = map _norm($_), @_; + return 0 unless defined $a && defined $b; + return 1 if $a eq $b; + return 0.5 if is_dialect_of($a, $b) || is_dialect_of($b, $a); + + my @a = split /-/, $a; + my @b = split /-/, $b; + my $same = 0; + while (@a && @b && $a[0] eq $b[0]) { + shift @a; + shift @b; + $same++; + } + return $same ? $same / ($same + @a + @b) : 0; +} + +sub alternate_language_tags { + my $tag = _norm(shift); + return unless defined $tag; + + my %reverse = reverse %ALIASES; + my @out; + push @out, $ALIASES{$tag} if exists $ALIASES{$tag}; + push @out, $reverse{$tag} if exists $reverse{$tag}; + push @out, 'nb' if $tag eq 'no'; + push @out, 'nn' if $tag eq 'no'; + return _uniq(map _norm($_), @out); +} + +sub panic_languages { + my @tags = map _norm($_), @_; + return grep { defined } ('en') unless grep { defined && $_ eq 'en' } @tags; + return; +} + +sub implicate_supers { + my @tags = grep { defined } map _norm($_), @_; + my %seen; + my @out; + for (my $i = 0; $i < @tags; $i++) { + my $tag = $tags[$i]; + push @out, $tag unless $seen{$tag}++; + + my @supers = super_languages($tag); + my $limit = @supers; + SUPER: + for (my $s = 0; $s < @supers; $s++) { + for my $later (@tags[$i + 1 .. $#tags]) { + if ($later eq $supers[$s]) { + $limit = $s; + last SUPER; + } + } + } + + for my $super (@supers[0 .. $limit - 1]) { + push @out, $super unless $seen{$super}++; + } + } + return @out; +} + +sub implicate_supers_strictly { + my @tags = grep { defined } map _norm($_), @_; + my @supers; + push @supers, super_languages($_) for @tags; + return _uniq(@tags, @supers); +} + +1; diff --git a/src/main/perl/lib/I18N/LangTags/Detect.pm b/src/main/perl/lib/I18N/LangTags/Detect.pm new file mode 100644 index 000000000..4b0068d89 --- /dev/null +++ b/src/main/perl/lib/I18N/LangTags/Detect.pm @@ -0,0 +1,64 @@ +package I18N::LangTags::Detect; + +use strict; +use warnings; +use Exporter (); +use I18N::LangTags (); + +our $VERSION = '1.06'; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(ambient_langprefs detect http_accept_langs); + +sub _uniq { + my %seen; + return grep { defined($_) && length($_) && !$seen{$_}++ } @_; +} + +sub http_accept_langs { + my $header = shift; + $header = $ENV{HTTP_ACCEPT_LANGUAGE} unless defined $header; + return unless defined $header && length $header; + + my @items; + my $order = 0; + for my $part (split /,/, $header) { + $part =~ s/\A\s+|\s+\z//g; + next unless length $part; + my ($tag, @params) = split /\s*;\s*/, $part; + my $q = 1; + for my $param (@params) { + $q = $1 if $param =~ /\Aq=([0-9.]+)\z/i; + } + my $lang = I18N::LangTags::locale2language_tag($tag); + push @items, [$lang, $q, $order++] if defined $lang; + } + + return _uniq(map $_->[0], + sort { $b->[1] <=> $a->[1] || $a->[2] <=> $b->[2] } @items); +} + +sub ambient_langprefs { + if (defined $ENV{HTTP_ACCEPT_LANGUAGE} + && (defined $ENV{REQUEST_METHOD} || defined $ENV{GATEWAY_INTERFACE})) { + my @http = http_accept_langs($ENV{HTTP_ACCEPT_LANGUAGE}); + return @http if @http; + } + + my @prefs; + push @prefs, split /:/, $ENV{LANGUAGE} if defined $ENV{LANGUAGE}; + push @prefs, http_accept_langs($ENV{HTTP_ACCEPT_LANGUAGE}) + if defined $ENV{HTTP_ACCEPT_LANGUAGE}; + push @prefs, grep { defined && length } + $ENV{LC_ALL}, $ENV{LC_MESSAGES}, $ENV{LANG}; + + @prefs = map I18N::LangTags::locale2language_tag($_), @prefs; + @prefs = _uniq(@prefs); + push @prefs, 'en' unless @prefs; + return @prefs; +} + +sub detect { + return I18N::LangTags::implicate_supers(ambient_langprefs()); +} + +1; diff --git a/src/main/perl/lib/IO/File.pm b/src/main/perl/lib/IO/File.pm index 93900e5d6..2682e86d4 100644 --- a/src/main/perl/lib/IO/File.pm +++ b/src/main/perl/lib/IO/File.pm @@ -157,7 +157,7 @@ sub new { } sub new_tmpfile { - my $class = shift; + my $class = shift || 'IO::File'; @_ == 0 or croak "usage: $class->new_tmpfile()"; require File::Temp; # Use bless+gensym directly instead of $class->new to avoid infinite diff --git a/src/main/perl/lib/Module/ScanDeps.pm b/src/main/perl/lib/Module/ScanDeps.pm new file mode 100644 index 000000000..bf014443c --- /dev/null +++ b/src/main/perl/lib/Module/ScanDeps.pm @@ -0,0 +1,90 @@ +package Module::ScanDeps; + +use strict; +use warnings; +use Exporter (); + +our $VERSION = '1.37'; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(scan_deps); + +my %SKIP = map { $_ => 1 } qw( + base constant feature integer lib parent strict utf8 vars warnings +); + +sub _module_to_file { + my $module = shift; + return unless defined $module && $module =~ /\A[A-Za-z_]\w*(?:::\w*)*\z/; + $module =~ s!::!/!g; + return "$module.pm"; +} + +sub _find_in_inc { + my $file = shift; + return $file if defined $file && -f $file; + for my $dir (@INC) { + next if ref $dir; + my $path = "$dir/$file"; + return $path if -f $path; + } + return undef; +} + +sub _record { + my ($deps, $module_file, $source_file) = @_; + return unless defined $module_file; + my $file = _find_in_inc($module_file); + return unless defined $file; + $deps->{$module_file} ||= { + file => $file, + type => 'module', + used_by => [], + }; + push @{ $deps->{$module_file}{used_by} }, $source_file + if defined $source_file; +} + +sub _scan_file { + my ($deps, $file) = @_; + open my $fh, '<', $file or return; + while (my $line = <$fh>) { + $line =~ s/#.*//; + while ($line =~ /\b(?:use|no)\s+([A-Za-z_]\w*(?:::\w*)*)\b/g) { + my $module = $1; + next if $SKIP{$module}; + _record($deps, _module_to_file($module), $file); + } + while ($line =~ /\brequire\s+([A-Za-z_]\w*(?:::\w*)*)\b/g) { + _record($deps, _module_to_file($1), $file); + } + while ($line =~ /\brequire\s+['"]([^'"]+\.pm)['"]/g) { + _record($deps, $1, $file); + } + } + close $fh; +} + +sub scan_deps { + my %args = @_ == 1 && ref($_[0]) eq 'HASH' ? %{ $_[0] } : @_; + my $files = $args{files} || []; + my $recurse = $args{recurse} || 0; + my %deps; + my @queue = ref($files) eq 'ARRAY' ? @$files : ($files); + my %seen; + + while (@queue) { + my $file = shift @queue; + next unless defined $file && !$seen{$file}++ && -f $file; + my %before = %deps; + _scan_file(\%deps, $file); + next unless $recurse; + for my $key (keys %deps) { + next if exists $before{$key}; + push @queue, $deps{$key}{file} if defined $deps{$key}{file}; + } + } + + return \%deps; +} + +1; diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index 592e8ac89..4aef8378c 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -308,35 +308,81 @@ sub LC_NUMERIC () { 4 } sub LC_TIME () { 5 } sub LC_MESSAGES () { 6 } +my $CURRENT_LOCALE = 'C'; + +sub _normal_locale_name { + my $locale = shift; + $locale = $ENV{LC_ALL} || $ENV{LC_MESSAGES} || $ENV{LANG} || 'C' + if !defined($locale) || $locale eq ''; + $locale =~ s/\A\s+|\s+\z//g; + $locale =~ s/\..*\z//; + $locale =~ s/\@.*\z//; + $locale =~ tr/-/_/; + return lc $locale; +} + sub setlocale { my ($category, $locale) = @_; - # Returning the requested locale (or the current/default one) is enough - # for callers that use setlocale() purely for its return value, e.g. - # `setlocale(LC_COLLATE, "C")`. - return defined $locale ? $locale : 'C'; + return $CURRENT_LOCALE unless defined $locale; + $CURRENT_LOCALE = $locale eq '' + ? ($ENV{LC_ALL} || $ENV{LC_MESSAGES} || $ENV{LANG} || 'C') + : $locale; + return $CURRENT_LOCALE; } -sub localeconv { - return { - decimal_point => '.', - thousands_sep => '', - grouping => '', - int_curr_symbol => '', - currency_symbol => '', +sub _localeconv_defaults { + return ( + decimal_point => '.', + thousands_sep => '', + grouping => '', + int_curr_symbol => '', + currency_symbol => '', mon_decimal_point => '', mon_thousands_sep => '', - mon_grouping => '', - positive_sign => '', - negative_sign => '-', - int_frac_digits => -1, - frac_digits => -1, - p_cs_precedes => -1, - p_sep_by_space => -1, - n_cs_precedes => -1, - n_sep_by_space => -1, - p_sign_posn => -1, - n_sign_posn => -1, - }; + mon_grouping => '', + positive_sign => '', + negative_sign => '-', + int_frac_digits => -1, + frac_digits => -1, + p_cs_precedes => -1, + p_sep_by_space => -1, + n_cs_precedes => -1, + n_sep_by_space => -1, + p_sign_posn => -1, + n_sign_posn => -1, + ); +} + +sub localeconv { + my %conv = _localeconv_defaults(); + my $locale = _normal_locale_name($CURRENT_LOCALE); + + if ($locale =~ /^de(?:_|$)/) { + @conv{qw( + decimal_point thousands_sep int_curr_symbol currency_symbol + mon_decimal_point mon_thousands_sep int_frac_digits frac_digits + p_cs_precedes p_sep_by_space n_cs_precedes n_sep_by_space + p_sign_posn n_sign_posn + )} = (',', '.', 'EUR ', 'EUR', ',', '.', 2, 2, 0, 1, 0, 1, 1, 1); + } + elsif ($locale =~ /^ru(?:_|$)/) { + @conv{qw( + decimal_point thousands_sep int_curr_symbol currency_symbol + mon_decimal_point mon_thousands_sep int_frac_digits frac_digits + p_cs_precedes p_sep_by_space n_cs_precedes n_sep_by_space + p_sign_posn n_sign_posn + )} = ('.', ',', 'RUB ', 'RUB', '.', ',', 2, 2, 0, 1, 0, 1, 1, 1); + } + elsif ($locale =~ /^en_us\z|^en(?:_|$)/) { + @conv{qw( + decimal_point thousands_sep int_curr_symbol currency_symbol + mon_decimal_point mon_thousands_sep int_frac_digits frac_digits + p_cs_precedes p_sep_by_space n_cs_precedes n_sep_by_space + p_sign_posn n_sign_posn + )} = ('.', ',', 'USD', '$', '.', ',', 2, 2, 1, 1, 1, 1, 1, 1); + } + + return \%conv; } # User/Group functions diff --git a/src/main/perl/lib/PerlOnJava/CpanDistroprefs/IO-All.yml b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/IO-All.yml new file mode 100644 index 000000000..6fdfb32fe --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/IO-All.yml @@ -0,0 +1,12 @@ +--- +comment: | + PerlOnJava distroprefs for IO::All. + + t/accept.t and t/lock.t depend on POSIX fork() for parent/child + socket and lock coordination. PerlOnJava does not implement fork(), + so skip only those tests on no-fork platforms while keeping the rest + of the upstream suite intact. +match: + distribution: "^FREW/IO-All-0\\.87" +patches: + - "IO-All-0.87/SkipForkTests.patch" diff --git a/src/main/perl/lib/PerlOnJava/CpanDistroprefs/Module-Install.yml b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/Module-Install.yml new file mode 100644 index 000000000..ea22c818e --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/Module-Install.yml @@ -0,0 +1,12 @@ +--- +comment: | + PerlOnJava distroprefs for Module::Install. + + Module::Install::Admin discovers extension methods with defined(&symbol). + Module::Install::Metadata creates the authors alias before author is + generated, so defined(&authors) is false even though ->can("authors") works. + Add an explicit authors method so the admin copy path can find it. +match: + distribution: "^ETHER/Module-Install-1\\.21" +patches: + - "Module-Install-1.21/ExplicitAuthorsMethod.patch" diff --git a/src/main/perl/lib/PerlOnJava/CpanPatches/IO-All-0.87/SkipForkTests.patch b/src/main/perl/lib/PerlOnJava/CpanPatches/IO-All-0.87/SkipForkTests.patch new file mode 100644 index 000000000..cc842b26a --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanPatches/IO-All-0.87/SkipForkTests.patch @@ -0,0 +1,32 @@ +--- t/accept.t.orig ++++ t/accept.t +@@ -1,6 +1,11 @@ + use strict; use warnings; + use lib -e 't' ? 't' : 'test'; +-use Test::More tests => 20; ++use Test::More; ++use Config; ++ ++plan skip_all => "fork not implemented on this platform" unless $Config{d_fork}; ++plan tests => 20; ++ + use IO_All_Test; + use IO::All; + use IO::Socket::INET; +--- t/lock.t.orig ++++ t/lock.t +@@ -2,9 +2,13 @@ + use lib -e 't' ? 't' : 'test'; + use IO::All; + use IO_All_Test; ++use Config; + + # XXX This needs to be fixed!!! +-$^O !~ /^(cygwin|hpux)$/ ++$Config{d_fork} ++ ? do {} ++ : do { print "1..0 # skip - fork not implemented on this platform\n"; exit(0) }; ++$^O !~ /^(cygwin|hpux)$/ + ? print "1..3\n" + : do { print "1..0 # skip - locking problems on $^O\n"; exit(0) }; + diff --git a/src/main/perl/lib/PerlOnJava/CpanPatches/Module-Install-1.21/ExplicitAuthorsMethod.patch b/src/main/perl/lib/PerlOnJava/CpanPatches/Module-Install-1.21/ExplicitAuthorsMethod.patch new file mode 100644 index 000000000..5aa51eb45 --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanPatches/Module-Install-1.21/ExplicitAuthorsMethod.patch @@ -0,0 +1,11 @@ +--- lib/Module/Install/Metadata.pm.orig ++++ lib/Module/Install/Metadata.pm +@@ -44,7 +44,7 @@ + author + }; + +-*authors = \&author; ++sub authors { shift->author(@_) } + + sub Meta { shift } + sub Meta_BooleanKeys { @boolean_keys } diff --git a/src/main/perl/lib/SDBM_File.pm b/src/main/perl/lib/SDBM_File.pm new file mode 100644 index 000000000..1f17d9419 --- /dev/null +++ b/src/main/perl/lib/SDBM_File.pm @@ -0,0 +1,101 @@ +package SDBM_File; + +use strict; +use warnings; +use Tie::Hash; + +our @ISA = qw(Tie::StdHash); +our $VERSION = '1.14'; + +sub TIEHASH { + my ($class, $filename) = @_; + my $self = bless { + filename => $filename, + data => {}, + }, $class; + $self->_load; + $self->_flush unless -e $filename; + return $self; +} + +sub _encode { + my ($value) = @_; + $value = '' unless defined $value; + $value =~ s/%/%25/g; + $value =~ s/\t/%09/g; + $value =~ s/\r/%0D/g; + $value =~ s/\n/%0A/g; + return $value; +} + +sub _decode { + my ($value) = @_; + $value = '' unless defined $value; + $value =~ s/%0A/\n/g; + $value =~ s/%0D/\r/g; + $value =~ s/%09/\t/g; + $value =~ s/%25/%/g; + return $value; +} + +sub _load { + my ($self) = @_; + my $filename = $self->{filename}; + return unless defined $filename && -e $filename; + + open my $fh, '<', $filename or return; + while (defined(my $line = <$fh>)) { + chomp $line; + my ($key, $value) = split /\t/, $line, 2; + $self->{data}{ _decode($key) } = _decode($value); + } + close $fh; +} + +sub _flush { + my ($self) = @_; + my $filename = $self->{filename}; + return unless defined $filename; + + open my $fh, '>', $filename or return; + for my $key (sort keys %{ $self->{data} }) { + print {$fh} _encode($key), "\t", _encode($self->{data}{$key}), "\n"; + } + close $fh; +} + +sub FETCH { $_[0]{data}{ $_[1] } } + +sub STORE { + my ($self, $key, $value) = @_; + $self->{data}{$key} = $value; + $self->_flush; +} + +sub DELETE { + my ($self, $key) = @_; + my $value = delete $self->{data}{$key}; + $self->_flush; + return $value; +} + +sub CLEAR { + my ($self) = @_; + %{ $self->{data} } = (); + $self->_flush; +} + +sub EXISTS { exists $_[0]{data}{ $_[1] } } +sub FIRSTKEY { my $reset = scalar keys %{ $_[0]{data} }; each %{ $_[0]{data} } } +sub NEXTKEY { each %{ $_[0]{data} } } +sub SCALAR { scalar %{ $_[0]{data} } } + +sub filter_store_key { $_[0]{filter_store_key} = $_[1]; return $_[0] } +sub filter_store_value { $_[0]{filter_store_value} = $_[1]; return $_[0] } +sub filter_fetch_key { $_[0]{filter_fetch_key} = $_[1]; return $_[0] } +sub filter_fetch_value { $_[0]{filter_fetch_value} = $_[1]; return $_[0] } + +sub UNTIE { $_[0]->_flush } +sub DESTROY { $_[0]->_flush } + +1; diff --git a/src/main/perl/lib/Tie/File.pm b/src/main/perl/lib/Tie/File.pm new file mode 100644 index 000000000..6d61e0380 --- /dev/null +++ b/src/main/perl/lib/Tie/File.pm @@ -0,0 +1,130 @@ +package Tie::File; + +use strict; +use warnings; +use Tie::Array; + +our @ISA = qw(Tie::Array); +our $VERSION = '1.09'; + +sub TIEARRAY { + my ($class, $filename, %options) = @_; + my $self = bless { + filename => $filename, + recsep => exists $options{recsep} ? $options{recsep} : $/, + records => [], + trailing => 0, + }, $class; + $self->_load; + return $self; +} + +sub _load { + my ($self) = @_; + my $filename = $self->{filename}; + return unless defined $filename && -e $filename; + + open my $fh, '<', $filename or return; + local $/; + my $content = <$fh>; + close $fh; + $content = '' unless defined $content; + + my $sep = defined $self->{recsep} ? $self->{recsep} : "\n"; + if ($sep eq '') { + $self->{records} = length($content) ? [ $content ] : []; + $self->{trailing} = 0; + return; + } + + $self->{trailing} = length($content) >= length($sep) + && substr($content, -length($sep)) eq $sep + ? 1 : 0; + my @records = split /\Q$sep\E/, $content, -1; + pop @records if $self->{trailing}; + $self->{records} = \@records; +} + +sub _flush { + my ($self) = @_; + my $filename = $self->{filename}; + return unless defined $filename; + + open my $fh, '>', $filename or return; + my $sep = defined $self->{recsep} ? $self->{recsep} : "\n"; + print {$fh} join $sep, @{ $self->{records} }; + print {$fh} $sep if $self->{trailing} && @{ $self->{records} }; + close $fh; +} + +sub FETCHSIZE { scalar @{ $_[0]{records} } } + +sub STORESIZE { + my ($self, $size) = @_; + $#{ $self->{records} } = $size - 1; + $self->_flush; +} + +sub FETCH { $_[0]{records}[ $_[1] ] } + +sub STORE { + my ($self, $index, $value) = @_; + $self->{records}[$index] = $value; + $self->_flush; +} + +sub CLEAR { + my ($self) = @_; + @{ $self->{records} } = (); + $self->{trailing} = 0; + $self->_flush; +} + +sub PUSH { + my $self = shift; + push @{ $self->{records} }, @_; + $self->_flush; + return $self->FETCHSIZE; +} + +sub POP { + my ($self) = @_; + my $value = pop @{ $self->{records} }; + $self->_flush; + return $value; +} + +sub SHIFT { + my ($self) = @_; + my $value = shift @{ $self->{records} }; + $self->_flush; + return $value; +} + +sub UNSHIFT { + my $self = shift; + unshift @{ $self->{records} }, @_; + $self->_flush; + return $self->FETCHSIZE; +} + +sub SPLICE { + my $self = shift; + my @removed = splice @{ $self->{records} }, @_; + $self->_flush; + return wantarray ? @removed : $removed[-1]; +} + +sub EXISTS { exists $_[0]{records}[ $_[1] ] } + +sub DELETE { + my ($self, $index) = @_; + my $value = delete $self->{records}[$index]; + $self->_flush; + return $value; +} + +sub UNTIE { $_[0]->_flush } +sub DESTROY { $_[0]->_flush } + +1; diff --git a/src/main/perl/lib/UNIVERSAL/require.pm b/src/main/perl/lib/UNIVERSAL/require.pm new file mode 100644 index 000000000..b31e4a34d --- /dev/null +++ b/src/main/perl/lib/UNIVERSAL/require.pm @@ -0,0 +1,68 @@ +package UNIVERSAL::require; + +use strict; +use warnings; + +our $VERSION = '0.19'; +our $ERROR = ''; + +package UNIVERSAL; + +use strict; +use warnings; + +sub require { + my ($module, $version) = @_; + + unless (defined $module && !ref $module && length $module) { + $UNIVERSAL::require::ERROR = 'No module name supplied'; + return 0; + } + unless ($module =~ /\A[A-Za-z_]\w*(?:::\w*)*\z/) { + $UNIVERSAL::require::ERROR = "Invalid module name '$module'"; + return 0; + } + + (my $file = $module) =~ s!::!/!g; + $file .= '.pm'; + + my $ok = eval { + CORE::require($file); + 1; + }; + if (!$ok) { + $UNIVERSAL::require::ERROR = $@ || "Unable to require $module"; + return 0; + } + + if (defined $version) { + $ok = eval { + $module->VERSION($version); + 1; + }; + if (!$ok) { + $UNIVERSAL::require::ERROR = $@ || "$module version $version required"; + return 0; + } + } + + $UNIVERSAL::require::ERROR = ''; + return 1; +} + +sub use { + my ($module, @imports) = @_; + return 0 unless $module->require; + + my $caller = caller; + return 1 unless $module->can('import'); + + my $ok = eval "package $caller; \$module->import(\@imports); 1"; + if (!$ok) { + $UNIVERSAL::require::ERROR = $@ || "Unable to import from $module"; + return 0; + } + return 1; +} + +1; diff --git a/src/test/resources/unit/cpan_shim_modules.t b/src/test/resources/unit/cpan_shim_modules.t new file mode 100644 index 000000000..6622d8a87 --- /dev/null +++ b/src/test/resources/unit/cpan_shim_modules.t @@ -0,0 +1,126 @@ +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); +use lib 'src/main/perl/lib'; + +use_ok('UNIVERSAL::require'); +ok('File::Spec'->require, 'UNIVERSAL::require loads a module by package name'); +ok(!'This::Module::Does::Not::Exist'->require, + 'UNIVERSAL::require returns false for missing modules'); +like(File::Spec->case_tolerant, qr/^[01]$/, + 'File::Spec->case_tolerant returns a numeric boolean'); + +use_ok('I18N::LangTags'); +is_deeply( + [ I18N::LangTags::implicate_supers('en-US') ], + [ 'en-us', 'en' ], + 'I18N::LangTags adds superordinate tags' +); +is_deeply( + [ I18N::LangTags::implicate_supers(qw(pt-br fr pt)) ], + [ qw(pt-br fr pt) ], + 'I18N::LangTags keeps explicit later superordinate preferences in place' +); +is_deeply( + [ I18N::LangTags::implicate_supers(qw(pt-br-janeiro de pt-br fr)) ], + [ qw(pt-br-janeiro de pt-br pt fr) ], + 'I18N::LangTags lets an explicit intermediate superordinate imply its parent' +); +is(I18N::LangTags::locale2language_tag('de_DE.UTF-8'), 'de-de', + 'I18N::LangTags normalizes locale names'); + +use_ok('I18N::LangTags::Detect'); +{ + local $ENV{LANGUAGE} = 'fr_CA:de_DE'; + local $ENV{HTTP_ACCEPT_LANGUAGE}; + my @prefs = I18N::LangTags::Detect::ambient_langprefs(); + is_deeply([ @prefs[0, 1] ], [ 'fr-ca', 'de-de' ], + 'I18N::LangTags::Detect reads LANGUAGE preferences'); +} +{ + local $ENV{REQUEST_METHOD} = 'GET'; + local $ENV{HTTP_ACCEPT_LANGUAGE} = 'en-US, zh-TW'; + local $ENV{LANGUAGE}; + local $ENV{LANG} = 'C.UTF-8'; + my @prefs = I18N::LangTags::Detect::detect(); + is_deeply([ @prefs[0, 1, 2, 3] ], [ 'en-us', 'en', 'zh-tw', 'zh' ], + 'I18N::LangTags::Detect prefers HTTP language order in CGI mode'); +} + +use_ok('File::Remove'); +{ + my $dir = tempdir(CLEANUP => 1); + mkdir "$dir/nested" or die "mkdir: $!"; + open my $fh, '>', "$dir/nested/file.txt" or die "open: $!"; + print {$fh} "ok\n"; + close $fh or die "close: $!"; + ok(File::Remove::remove(\1, "$dir/nested"), 'File::Remove removes directories recursively'); + ok(!-e "$dir/nested", 'recursive remove deleted the directory'); +} + +use_ok('Devel::PPPort'); +{ + my $dir = tempdir(CLEANUP => 1); + my $file = "$dir/ppport.h"; + ok(Devel::PPPort::WriteFile($file), 'Devel::PPPort writes ppport.h'); + ok(-s $file, 'ppport.h is not empty'); +} + +use_ok('Module::ScanDeps'); +{ + my $dir = tempdir(CLEANUP => 1); + my $file = "$dir/Foo.pm"; + open my $fh, '>', $file or die "open: $!"; + print {$fh} "package Foo;\nuse File::Spec;\n1;\n"; + close $fh or die "close: $!"; + + my $deps = Module::ScanDeps::scan_deps(files => [$file], recurse => 0); + ok(exists $deps->{'File/Spec.pm'}, 'Module::ScanDeps records use dependencies'); + is($deps->{'File/Spec.pm'}{type}, 'module', 'dependency type is module'); +} + +use_ok('Tie::File'); +{ + my $dir = tempdir(CLEANUP => 1); + my $file = "$dir/tie-file.txt"; + open my $fh, '>', $file or die "open: $!"; + print {$fh} "#One\n#Two\n"; + close $fh or die "close: $!"; + + tie my @lines, 'Tie::File', $file or die "tie: $!"; + is_deeply(\@lines, [ '#One', '#Two' ], 'Tie::File reads records without separators'); + for my $line (@lines) { + $line =~ s/^#//; + } + untie @lines; + open my $check, '<', $file or die "open check: $!"; + local $/; + is(<$check>, "One\nTwo\n", 'Tie::File writes modified records back'); +} + +use_ok('SDBM_File'); +{ + my $dir = tempdir(CLEANUP => 1); + my $file = "$dir/simple-sdbm"; + tie my %db, 'SDBM_File', $file, 0, 0666 or die "tie: $!"; + $db{foo} = 'bar'; + $db{answer} = 42; + is(join('', sort keys %db), 'answerfoo', 'SDBM_File iterates keys'); + untie %db; + ok(-f $file, 'SDBM_File creates a backing file'); + + tie my %db2, 'SDBM_File', $file, 0, 0666 or die "tie again: $!"; + is($db2{foo}, 'bar', 'SDBM_File reloads stored values'); + untie %db2; +} + +use_ok('HTTP::Cookies'); +use_ok('HTTP::Cookies::Netscape'); +{ + my $jar = HTTP::Cookies::Netscape->new(file => '/tmp/perlonjava-missing-cookies.txt'); + isa_ok($jar, 'HTTP::Cookies::Netscape'); + isa_ok($jar, 'HTTP::Cookies'); +} + +done_testing(); diff --git a/src/test/resources/unit/eval_return_destroy.t b/src/test/resources/unit/eval_return_destroy.t new file mode 100644 index 000000000..c526701e5 --- /dev/null +++ b/src/test/resources/unit/eval_return_destroy.t @@ -0,0 +1,26 @@ +use strict; +use warnings; +use Test::More; + +subtest 'explicit return from eval-created sub releases returned lexical owner' => sub { + @EvalReturnDestroy::log = (); + { + package EvalReturnDestroy; + our @log; + sub DESTROY { push @log, 'destroyed' } + } + + my $ctor = eval q{ + sub { + my $x = bless {}, 'EvalReturnDestroy'; + return $x; + } + }; + die $@ if $@; + + { $ctor->(); } + is_deeply(\@EvalReturnDestroy::log, ['destroyed'], + 'discarded return value is destroyed after the caller statement'); +}; + +done_testing(); diff --git a/src/test/resources/unit/fileno_reserved_recycle.t b/src/test/resources/unit/fileno_reserved_recycle.t new file mode 100644 index 000000000..696791c2f --- /dev/null +++ b/src/test/resources/unit/fileno_reserved_recycle.t @@ -0,0 +1,18 @@ +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); + +open my $in, '<&=STDIN' or die "dup stdin: $!"; +is(fileno($in), 0, 'stdin duplicate reports fd 0'); +close $in or die "close stdin duplicate: $!"; + +open my $out, '>&=STDOUT' or die "dup stdout: $!"; +is(fileno($out), 1, 'stdout duplicate reports fd 1'); +close $out or die "close stdout duplicate: $!"; + +my $dir = tempdir(CLEANUP => 1); +open my $fh, '>', "$dir/regular.txt" or die "open regular: $!"; +ok(fileno($fh) > 2, 'regular filehandle does not reuse reserved stdio fd'); + +done_testing(); diff --git a/src/test/resources/unit/io_handle_error.t b/src/test/resources/unit/io_handle_error.t new file mode 100644 index 000000000..1a622dc44 --- /dev/null +++ b/src/test/resources/unit/io_handle_error.t @@ -0,0 +1,14 @@ +use strict; +use warnings; +use Test::More; +use IO::File; + +$! = 0; +ok(!-f '?', 'missing path probe leaves a false file test result'); +ok($!, 'file test set errno for the missing path'); + +my $fh = IO::File::new_tmpfile(); +isa_ok($fh, 'IO::File'); +ok(!$fh->error, 'successful handle has no stream error despite stale errno'); + +done_testing(); diff --git a/src/test/resources/unit/overload_deref_args.t b/src/test/resources/unit/overload_deref_args.t new file mode 100644 index 000000000..473c930a0 --- /dev/null +++ b/src/test/resources/unit/overload_deref_args.t @@ -0,0 +1,84 @@ +use strict; +use warnings; +use Test::More; +use IO::File; + +{ + package OverloadDerefArgs::Scalar; + our @seen; + use overload '${}' => sub { + push @seen, [@_]; + my $value = 'scalar'; + return \$value; + }; + sub new { bless {}, shift } +} + +{ + package OverloadDerefArgs::Array; + our @seen; + use overload '@{}' => sub { + push @seen, [@_]; + return [ 'array' ]; + }; + sub new { bless {}, shift } +} + +{ + package OverloadDerefArgs::Hash; + our @seen; + use overload '%{}' => sub { + push @seen, [@_]; + return { hash => 1 }; + }; + sub new { bless {}, shift } +} + +{ + package OverloadDerefArgs::Code; + our @seen; + use overload '&{}' => sub { + push @seen, [@_]; + return sub { 'code' }; + }; + sub new { bless {}, shift } +} + +subtest 'scalar dereference overload arguments' => sub { + my $obj = OverloadDerefArgs::Scalar->new; + is($$obj, 'scalar', 'scalar dereference overload returns referenced value'); + is(scalar @{ $OverloadDerefArgs::Scalar::seen[0] }, 3, 'three overload arguments'); + ok(!defined $OverloadDerefArgs::Scalar::seen[0][1], 'second argument is undef'); + ok(!$OverloadDerefArgs::Scalar::seen[0][2], 'swap argument is false'); +}; + +subtest 'array dereference overload arguments' => sub { + my $obj = OverloadDerefArgs::Array->new; + is($obj->[0], 'array', 'array dereference overload returns array reference'); + is(scalar @{ $OverloadDerefArgs::Array::seen[0] }, 3, 'three overload arguments'); + ok(!defined $OverloadDerefArgs::Array::seen[0][1], 'second argument is undef'); + ok(!$OverloadDerefArgs::Array::seen[0][2], 'swap argument is false'); +}; + +subtest 'hash dereference overload arguments' => sub { + my $obj = OverloadDerefArgs::Hash->new; + is($obj->{hash}, 1, 'hash dereference overload returns hash reference'); + is(scalar @{ $OverloadDerefArgs::Hash::seen[0] }, 3, 'three overload arguments'); + ok(!defined $OverloadDerefArgs::Hash::seen[0][1], 'second argument is undef'); + ok(!$OverloadDerefArgs::Hash::seen[0][2], 'swap argument is false'); +}; + +subtest 'code dereference overload arguments' => sub { + my $obj = OverloadDerefArgs::Code->new; + is($obj->(), 'code', 'code dereference overload returns code reference'); + is(scalar @{ $OverloadDerefArgs::Code::seen[0] }, 3, 'three overload arguments'); + ok(!defined $OverloadDerefArgs::Code::seen[0][1], 'second argument is undef'); + ok(!$OverloadDerefArgs::Code::seen[0][2], 'swap argument is false'); +}; + +subtest 'IO::File new_tmpfile works as a function' => sub { + my $fh = IO::File::new_tmpfile(); + isa_ok($fh, 'IO::File'); +}; + +done_testing(); diff --git a/src/test/resources/unit/strict_subs_void_bareword.t b/src/test/resources/unit/strict_subs_void_bareword.t new file mode 100644 index 000000000..8db50ec10 --- /dev/null +++ b/src/test/resources/unit/strict_subs_void_bareword.t @@ -0,0 +1,20 @@ +use strict; +use warnings; +use Test::More; + +my $error = do { + local $@; + eval 'use strict; unknown_func;'; + $@; +}; + +like( + $error, + qr/Bareword "unknown_func" not allowed while "strict subs" in use/, + 'strict subs rejects a void-context bareword statement' +); + +my $ok = eval 'no strict "subs"; harmless_bareword; 1'; +is($ok, 1, 'non-strict void-context bareword remains a no-op'); + +done_testing(); diff --git a/src/test/resources/unit/tie_handle_readline_args.t b/src/test/resources/unit/tie_handle_readline_args.t new file mode 100644 index 000000000..1b4d3302d --- /dev/null +++ b/src/test/resources/unit/tie_handle_readline_args.t @@ -0,0 +1,44 @@ +use strict; +use warnings; +use Test::More; +use Symbol qw(gensym); + +{ + package TieHandleReadlineArgs; + + our @seen; + + sub TIEHANDLE { + bless { lines => [ 'first', 'second' ] }, shift; + } + + sub READLINE { + push @seen, [ scalar(@_), scalar(wantarray) ]; + goto &getlines if wantarray; + goto &getline; + } + + sub getline { + my $self = shift; + return shift @{ $self->{lines} }; + } + + sub getlines { + my $self = shift; + return @{ $self->{lines} }; + } +} + +my $fh = gensym(); +tie *$fh, 'TieHandleReadlineArgs'; + +is(<$fh>, 'first', 'scalar readline returns one record'); +is_deeply($TieHandleReadlineArgs::seen[0], [ 1, '' ], + 'scalar tied READLINE receives only the tied object'); + +my @lines = <$fh>; +is_deeply(\@lines, [ 'second' ], 'list readline returns remaining records'); +is_deeply($TieHandleReadlineArgs::seen[1], [ 1, 1 ], + 'list tied READLINE receives only the tied object'); + +done_testing(); From 2f507c8c7cddde7bb768bf5760ffb9043d60e786 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 11 Jun 2026 18:39:29 +0200 Subject: [PATCH 2/2] fix: handle invalid file-test paths Treat Java InvalidPathException from file-test path resolution as a failed stat with EINVAL instead of letting it escape. This keeps tests like -f '?' portable on Windows, where '?' is invalid in path names. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- .../perlonjava/runtime/operators/FileTestOperator.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index 82163d26a..e609ba98f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -442,7 +443,14 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) } // Handle string filenames - Path path = resolvePath(filename); + Path path; + try { + path = resolvePath(filename); + } catch (InvalidPathException e) { + getGlobalVariable("main::!").set(22); // EINVAL + updateLastStat(fileHandle, false, 22); + return scalarUndef; + } if (path == null) { getGlobalVariable("main::!").set(2); // ENOENT