Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ priorities and future plans.

## Work in progress

- Fix bundled IO, nested launcher, locale, and core user/group database module compatibility.

- Snapshot mutable array entries while marking package roots and during reachability walks so concurrent thread teardown cannot fail with `ConcurrentModificationException`.

- Enforce restricted-hash key access and implement `Hash::Util` key, value, and hash locking (#1502, #1172).
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/SystemOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -578,9 +578,29 @@ private static List<String> expandResolvedCommandForProcessBuilder(
if (SystemUtils.osIsWindows()) {
return expandWindowsBatchForProcessBuilder(commandArgs, currentDirectory);
}
// Resolve an explicit nested $^X invocation in this JVM. Starting
// the shell launcher again would make Java discovery depend on the
// child's (possibly deliberately cleaned) PATH and environment.
if (isCurrentJperlExecutable(commandArgs.getFirst(), currentDirectory)) {
List<String> direct = new ArrayList<>(ForkOpenState.currentJavaCommand());
direct.addAll(commandArgs.subList(1, commandArgs.size()));
return direct;
}
return expandJperlShebangForProcessBuilder(commandArgs);
}

private static boolean isCurrentJperlExecutable(String command, File currentDirectory) {
String current = getCurrentJperlPath();
if (current == null || current.isEmpty()) return false;
File candidate = new File(command);
if (!candidate.isAbsolute()) candidate = new File(currentDirectory, command);
try {
return candidate.getCanonicalFile().equals(new File(current).getCanonicalFile());
} catch (IOException e) {
return candidate.getAbsolutePath().equals(new File(current).getAbsolutePath());
}
}

private static boolean isExecutableCandidate(File candidate, String candidateName) {
if (!candidate.isFile()) {
return false;
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Encode.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ public static void initialize() {
encode.registerMethod("resolve_alias", null);
encode.registerMethod("perlio_ok", null);
encode.registerMethod("find_mime_encoding", null);
// Perl 5.42+ Encode.pm calls this XS bootstrap callback after
// XSLoader::load. Its BOOT work is already performed by this
// Java initializer, but the callback must exist before the Perl
// source reaches the strict-subs checked `onBOOT;` call.
encode.registerMethod("onBOOT", null);
} catch (NoSuchMethodException e) {
System.err.println("Warning: Missing Encode method: " + e.getMessage());
}
Expand Down Expand Up @@ -260,6 +265,11 @@ public static void initialize() {
}
}

/** Encode.pm's XS BOOT initialization is completed by {@link #initialize()}. */
public static RuntimeList onBOOT(RuntimeArray args, int ctx) {
return new RuntimeList();
}

// --- Constant accessor methods ---

public static RuntimeList FB_DEFAULT(RuntimeArray args, int ctx) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,14 @@ public static RuntimeList langinfo(RuntimeArray args, int ctx) {
item = args.get(0).getInt();
}

Locale locale = Locale.getDefault();
String timeLocale = org.perlonjava.runtime.runtimetypes.PerlRuntime.current()
.regexState().localeState.currentTime();
String languageTag = timeLocale.trim().replace('_', '-').replaceFirst("\\..*$", "")
.replaceFirst("@.*$", "");
Locale locale = "C".equalsIgnoreCase(languageTag)
|| "POSIX".equalsIgnoreCase(languageTag)
? Locale.US : Locale.forLanguageTag(languageTag);
if (locale.getLanguage().isEmpty()) locale = Locale.US;
DateFormatSymbols dateSymbols = DateFormatSymbols.getInstance(locale);
DecimalFormatSymbols decimalSymbols = DecimalFormatSymbols.getInstance(locale);

Expand Down
15 changes: 9 additions & 6 deletions src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java
Original file line number Diff line number Diff line change
Expand Up @@ -517,15 +517,18 @@ public static RuntimeList setlocale(RuntimeArray args, int ctx) {
int category = args.isEmpty() ? 0 : args.get(0).getInt();
RuntimeLocaleState localeState = PerlRuntime.current().regexState().localeState;
if (args.size() < 2) {
return new RuntimeScalar(localeState.currentCtype()).getList();
String current = switch (category) {
case 0 -> localeState.currentCtype();
case 5 -> localeState.currentTime();
default -> localeState.currentCtype();
};
return new RuntimeScalar(current).getList();
}
String requested = args.get(1).toString();
if (!isSupportedLocale(requested)) return RuntimeScalarCache.scalarUndef.getList();
// LC_ALL (0) publication includes LC_CTYPE. The remaining categories
// have no regex effect, but return the same validated locale as POSIX.
if (category == 0 || category == 2) {
return new RuntimeScalar(localeState.publishCtype(requested)).getList();
}
if (category == 0) return new RuntimeScalar(localeState.publishAll(requested)).getList();
if (category == 2) return new RuntimeScalar(localeState.publishCtype(requested)).getList();
if (category == 5) return new RuntimeScalar(localeState.publishTime(requested)).getList();
return new RuntimeScalar(requested).getList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.Objects;

/**
* Per-runtime LC_CTYPE publication state.
* Per-runtime locale publication state.
*
* <p>This is deliberately independent of {@link Locale#setDefault(Locale)}:
* changing Perl's locale must neither mutate the JVM process default nor leak
Expand All @@ -15,18 +15,23 @@
*/
public final class RuntimeLocaleState {
private String ctypeName = environmentCtype();
private String timeName = environmentTime();
private long generation;

public record Snapshot(String ctypeName, long generation) {}
public record Snapshot(String ctypeName, String timeName, long generation) {}

public synchronized Snapshot snapshot() {
return new Snapshot(ctypeName, generation);
return new Snapshot(ctypeName, timeName, generation);
}

public synchronized String currentCtype() {
return ctypeName;
}

public synchronized String currentTime() {
return timeName;
}

/** Publishes an already-validated LC_CTYPE name and returns it. */
public synchronized String publishCtype(String localeName) {
String normalized = normalize(localeName);
Expand All @@ -37,9 +42,24 @@ public synchronized String publishCtype(String localeName) {
return ctypeName;
}

/** Publishes an already-validated LC_TIME name and returns it. */
public synchronized String publishTime(String localeName) {
timeName = normalize(localeName);
return timeName;
}

/** LC_ALL updates the categories represented by this runtime state. */
public synchronized String publishAll(String localeName) {
String normalized = normalize(localeName);
publishCtype(normalized);
publishTime(normalized);
return normalized;
}

void snapshotInto(RuntimeLocaleState target) {
Snapshot snapshot = snapshot();
target.ctypeName = snapshot.ctypeName;
target.timeName = snapshot.timeName;
target.generation = snapshot.generation;
}

Expand All @@ -55,4 +75,12 @@ private static String environmentCtype() {
}
return "C";
}

private static String environmentTime() {
for (String variable : new String[] { "LC_ALL", "LC_TIME", "LANG" }) {
String value = System.getenv(variable);
if (value != null && !value.isBlank()) return value.trim();
}
return "C";
}
}
12 changes: 12 additions & 0 deletions src/main/perl/lib/Config.pm
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ my $startperl = $is_windows
d_fchdir => 'define',
d_fchmod => 'define',

# User::pwent uses these Configure probes to expose only fields supported
# by the active passwd structure. PerlOnJava's FFM adapter provides the
# portable passwd fields and does not expose platform-specific extensions.
d_pwage => undef,
d_pwchange => undef,
d_pwclass => undef,
d_pwcomment => undef,
d_pwexpire => undef,
d_pwgecos => 'define',
d_pwpasswd => 'define',
d_pwquota => undef,

# PerlOnJava specific
perlonjava => '5.44.1',
java_version => $java_version,
Expand Down
4 changes: 4 additions & 0 deletions src/main/perl/lib/FileHandle.pm
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ sub pipe {
#
IO::Handle->import( grep { !defined(&$_) && $_ ne 'pipe' } @EXPORT, @EXPORT_OK );

# FileHandle also aliases methods from IO::Seekable below. Load it directly
# here instead of relying on IO::File's transitive import ordering.
require IO::Seekable;

#
# Some people call "FileHandle::function", so all the functions
# that were in the old FileHandle class must be imported, too.
Expand Down
87 changes: 87 additions & 0 deletions src/main/perl/lib/User/grent.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package User::grent 1.05;
use v5.38;

our ($gr_name, $gr_gid, $gr_passwd, @gr_members);

use Exporter 'import';
our @EXPORT = qw(getgrent getgrgid getgrnam getgr);
our @EXPORT_OK = qw($gr_name $gr_gid $gr_passwd @gr_members);
our %EXPORT_TAGS = ( FIELDS => [ @EXPORT_OK, @EXPORT ] );

use Class::Struct qw(struct);
struct 'User::grent' => [
name => '$',
passwd => '$',
gid => '$',
members => '@',
];

sub populate {
return unless @_;
my $gob = new();
($gr_name, $gr_passwd, $gr_gid) = @$gob[0,1,2] = @_[0,1,2];
@gr_members = @{$gob->[3]} = split ' ', $_[3];
return $gob;
}

sub getgrent :prototype( ) { populate(CORE::getgrent()) }
sub getgrnam :prototype($) { populate(CORE::getgrnam(shift)) }
sub getgrgid :prototype($) { populate(CORE::getgrgid(shift)) }
sub getgr :prototype($) { ($_[0] =~ /^\d+/) ? &getgrgid : &getgrnam }

__END__

=head1 NAME

User::grent - by-name interface to Perl's built-in getgr*() functions

=head1 SYNOPSIS

use User::grent;
my $gr = getgrgid(0) or die "No group zero";
if ( $gr->name eq 'wheel' && @{$gr->members} > 1 ) {
print "gid zero name wheel, with other members";
}

use User::grent qw(:FIELDS);
getgrgid(0) or die "No group zero";
if ( $gr_name eq 'wheel' && @gr_members > 1 ) {
print "gid zero name wheel, with other members";
}

my $gr = getgr($whoever);

=head1 DESCRIPTION

This module's default exports override the core getgrent(), getgrgid(),
and getgrnam() functions, replacing them with versions that return
"User::grent" objects. This object has methods that return the similarly
named structure field name from the C's passwd structure from F<grp.h>;
namely name, passwd, gid, and members (not mem). The first three
return scalars, the last an array reference.

You may also import all the structure fields directly into your namespace
as regular variables using the :FIELDS import tag. (Note that this still
overrides your core functions.) Access these fields as variables named
with a preceding C<gr_>. Thus, C<$group_obj-E<gt>gid()> corresponds
to $gr_gid if you import the fields. Array references are available as
regular array variables, so C<@{ $group_obj-E<gt>members() }> would be
simply @gr_members.

The getgr() function is a simple front-end that forwards a numeric
argument to getgrgid() and the rest to getgrnam().

To access this functionality without the core overrides,
pass the C<use> an empty import list, and then access
function functions with their full qualified names.
On the other hand, the built-ins are still available
via the C<CORE::> pseudo-package.

=head1 NOTE

While this class is currently implemented using the Class::Struct
module to build a struct-like class, you shouldn't rely upon this.

=head1 AUTHOR

Tom Christiansen
Loading
Loading