--- old/src/java.security.jgss/share/classes/sun/security/krb5/KrbApReq.java	2016-10-25 09:03:35.000000000 +0800
+++ new/src/java.security.jgss/share/classes/sun/security/krb5/KrbApReq.java	2016-10-25 09:03:35.000000000 +0800
@@ -301,9 +301,10 @@
         if (!authenticator.ctime.inClockSkew())
             throw new KrbApErrException(Krb5.KRB_AP_ERR_SKEW);
 
+        String alg = AuthTimeWithHash.DEFAULT_HASH_ALG;
         byte[] hash;
         try {
-            hash = MessageDigest.getInstance("MD5")
+            hash = MessageDigest.getInstance(AuthTimeWithHash.realAlg(alg))
                     .digest(apReqMessg.authenticator.cipher);
         } catch (NoSuchAlgorithmException ex) {
             throw new AssertionError("Impossible");
@@ -319,6 +320,7 @@
                 apReqMessg.ticket.sname.toString(),
                 authenticator.ctime.getSeconds(),
                 authenticator.cusec,
+                alg,
                 new String(h));
         rcache.checkAndStore(KerberosTime.now(), time);
 
--- old/src/java.security.jgss/share/classes/sun/security/krb5/internal/rcache/AuthTime.java	2016-10-25 09:03:37.000000000 +0800
+++ new/src/java.security.jgss/share/classes/sun/security/krb5/internal/rcache/AuthTime.java	2016-10-25 09:03:37.000000000 +0800
@@ -116,14 +116,14 @@
             if (st.countTokens() != 6) {
                 throw new IOException("Incorrect rcache style");
             }
-            st.nextToken();
+            String hashAlg = st.nextToken();
             String hash = st.nextToken();
             st.nextToken();
             client = st.nextToken();
             st.nextToken();
             server = st.nextToken();
             return new AuthTimeWithHash(
-                    client, server, ctime, cusec, hash);
+                    client, server, ctime, cusec, hashAlg, hash);
         } else {
             return new AuthTime(
                     client, server, ctime, cusec);
--- old/src/java.security.jgss/share/classes/sun/security/krb5/internal/rcache/AuthTimeWithHash.java	2016-10-25 09:03:38.000000000 +0800
+++ new/src/java.security.jgss/share/classes/sun/security/krb5/internal/rcache/AuthTimeWithHash.java	2016-10-25 09:03:38.000000000 +0800
@@ -25,6 +25,8 @@
 
 package sun.security.krb5.internal.rcache;
 
+import sun.security.action.GetPropertyAction;
+
 import java.util.Objects;
 
 /**
@@ -34,14 +36,32 @@
 public class AuthTimeWithHash extends AuthTime
         implements Comparable<AuthTimeWithHash> {
 
+    // The hash algorithm can be "HASH" or "SHA256".
+    public static String DEFAULT_HASH_ALG = GetPropertyAction
+            .privilegedGetProperty("jdk.krb5.rcache.hashalg", "HASH");
+
+    public static String realAlg(String alg) {
+        if (alg.equals("HASH")) {
+            return "MD5";
+        } else if (alg.equals("SHA")) {
+            return "SHA-1";
+        } else if (alg.startsWith("SHA") && !alg.startsWith("SHA-")) {
+            return "SHA-" + alg.substring(3);
+        } else {
+            return alg;
+        }
+    }
+
+    final String hashAlg;
     final String hash;
 
     /**
      * Constructs a new <code>AuthTimeWithHash</code>.
      */
     public AuthTimeWithHash(String client, String server,
-            int ctime, int cusec, String hash) {
+            int ctime, int cusec, String hashAlg, String hash) {
         super(client, server, ctime, cusec);
+        this.hashAlg = hashAlg;
         this.hash = hash;
     }
 
@@ -56,6 +76,7 @@
         if (!(o instanceof AuthTimeWithHash)) return false;
         AuthTimeWithHash that = (AuthTimeWithHash)o;
         return Objects.equals(hash, that.hash)
+                && Objects.equals(hashAlg, that.hashAlg)
                 && Objects.equals(client, that.client)
                 && Objects.equals(server, that.server)
                 && ctime == that.ctime
@@ -91,6 +112,19 @@
     /**
      * Compares with a possibly old style object. Used
      * in DflCache$Storage#loadAndCheck.
+     * @return true if all AuthTime fields are the same but different hash
+     */
+    public boolean sameTimeDiffHash(AuthTimeWithHash old) {
+        if (!this.isSameIgnoresHash(old)) {
+            return false;
+        }
+        return this.hashAlg.equals(old.hashAlg) &&
+                !this.hash.equals(old.hash);
+    }
+
+    /**
+     * Compares with a possibly old style object. Used
+     * in DflCache$Storage#loadAndCheck.
      * @return true if all AuthTime fields are the same
      */
     public boolean isSameIgnoresHash(AuthTime old) {
@@ -112,7 +146,7 @@
         String sstring;
         if (withHash) {
             cstring = "";
-            sstring = String.format("HASH:%s %d:%s %d:%s", hash,
+            sstring = String.format("%s:%s %d:%s %d:%s", hashAlg, hash,
                     client.length(), client,
                     server.length(), server);
         } else {
--- old/src/java.security.jgss/share/classes/sun/security/krb5/internal/rcache/DflCache.java	2016-10-25 09:03:39.000000000 +0800
+++ new/src/java.security.jgss/share/classes/sun/security/krb5/internal/rcache/DflCache.java	2016-10-25 09:03:39.000000000 +0800
@@ -96,6 +96,8 @@
  * Java also does this way.
  *
  * See src/lib/krb5/rcache/rc_io.c and src/lib/krb5/rcache/rc_dfl.c.
+ *
+ * Update: New version can use other hash algorithms.
  */
 public class DflCache extends ReplayCache {
 
@@ -300,7 +302,7 @@
                         if (time.equals(a)) {
                             // Exact match, must be a replay
                             throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT);
-                        } else if (time.isSameIgnoresHash(a)) {
+                        } else if (time.sameTimeDiffHash((AuthTimeWithHash)a)) {
                             // Two different authenticators in the same second.
                             // Remember it
                             seeNewButNotSame = true;
--- old/test/sun/security/krb5/auto/ReplayCacheExpunge.java	2016-10-25 09:03:40.000000000 +0800
+++ new/test/sun/security/krb5/auto/ReplayCacheExpunge.java	2016-10-25 09:03:40.000000000 +0800
@@ -47,15 +47,15 @@
         int count = Integer.parseInt(args[0]);
         ReplayCache cache = ReplayCache.getInstance("dfl:./");
         AuthTimeWithHash a1 =
-                new AuthTimeWithHash(client, server, time(-400), 0, hash("1"));
+                new AuthTimeWithHash(client, server, time(-400), 0, "HASH", hash("1"));
         AuthTimeWithHash a2 =
-                new AuthTimeWithHash(client, server, time(0), 0, hash("4"));
+                new AuthTimeWithHash(client, server, time(0), 0, "HASH", hash("4"));
         KerberosTime now = new KerberosTime(time(0)*1000L);
         KerberosTime then = new KerberosTime(time(-300)*1000L);
 
         // Once upon a time, we added a lot of events
         for (int i=0; i<count; i++) {
-            a1 = new AuthTimeWithHash(client, server, time(-400), 0, hash(""));
+            a1 = new AuthTimeWithHash(client, server, time(-400), 0, "HASH", hash(""));
             cache.checkAndStore(then, a1);
         }
 
--- old/test/sun/security/krb5/auto/ReplayCachePrecise.java	2016-10-25 09:03:41.000000000 +0800
+++ new/test/sun/security/krb5/auto/ReplayCachePrecise.java	2016-10-25 09:03:41.000000000 +0800
@@ -48,9 +48,9 @@
     public static void main(String[] args) throws Exception {
 
         AuthTimeWithHash a1 = new AuthTimeWithHash(client, server, time(0), 0,
-                "1111111111111111");
+                "HASH", "1111111111111111");
         AuthTimeWithHash a2 = new AuthTimeWithHash(client, server, time(0), 0,
-                "2222222222222222");
+                "HASH", "2222222222222222");
         KerberosTime now = new KerberosTime(time(0)*1000L);
 
         // When all new styles, must exact match
--- old/test/sun/security/krb5/auto/ReplayCacheTestProc.java	2016-10-25 09:03:42.000000000 +0800
+++ new/test/sun/security/krb5/auto/ReplayCacheTestProc.java	2016-10-25 09:03:42.000000000 +0800
@@ -25,9 +25,10 @@
  * @test
  * @bug 7152176
  * @summary More krb5 tests
- * @library ../../../../java/security/testlibrary/
+ * @library ../../../../java/security/testlibrary/ /test/lib
  * @compile -XDignore.symbol.file ReplayCacheTestProc.java
- * @run main/othervm/timeout=100 ReplayCacheTestProc
+ * @run main/othervm/timeout=300 ReplayCacheTestProc
+ * @run main/othervm/timeout=300 -Djdk.krb5.rcache.hashalg=SHA256 ReplayCacheTestProc
  */
 
 import java.io.*;
@@ -40,15 +41,35 @@
 import java.security.MessageDigest;
 import java.util.*;
 
+import jdk.test.lib.Platform;
 import sun.security.jgss.GSSUtil;
 import sun.security.krb5.internal.APReq;
 import sun.security.krb5.internal.rcache.AuthTime;
+import sun.security.krb5.internal.rcache.AuthTimeWithHash;
 
-// This test runs multiple acceptor Procs to mimin AP-REQ replays.
+/**
+ * This test runs multiple acceptor Procs to mimin AP-REQ replays.
+ * It can either run with automatic (random) test runs or user can provide
+ * these system properties:
+ *
+ * - test.libs on what types of acceptors to use
+ *   Format: CSV of (J|N|N<libname>)
+ *   Example: J,N,N/krb5-1.14/lib/libgssapi_krb5.so
+ *
+ * - test.runs on runs
+ *   Format: (req# | client# service#) acceptor# expected...
+ *   Example: c0h0J0v,c1h1N0v,r0J1x means 1st req is new c0 to h0 sent to J0,
+ *            2nd req is new c1 to h1 sent to N0,
+ *            3rd req is old (1st replayed) sent to J1.
+ *            For all old reqs, client# and service# MUST be -
+ *
+ * - test.autoruns on how many autoruns
+ *   Format: number
+ */
 public class ReplayCacheTestProc {
 
-    private static Proc[] ps;
-    private static Proc pc;
+    private static Proc[] pa;   // all acceptors
+    private static Proc pi;     // the single initiator
     private static List<Req> reqs = new ArrayList<>();
     private static String HOST = "localhost";
 
@@ -65,18 +86,14 @@
     public static void main0(String[] args) throws Exception {
         System.setProperty("java.security.krb5.conf", OneKDC.KRB5_CONF);
         if (args.length == 0) { // The controller
-            int ns = 5;     // number of servers
             int nu = 5;     // number of users
-            int nx = 50;    // number of experiments
-            int np = 5;     // number of peers (services)
-            int mode = 0;   // native(1), random(0), java(-1)
-            boolean random = true;      // random experiments choreograph
-
-            // Do not test interop with native GSS on some platforms
-            String os = System.getProperty("os.name", "???");
-            if (!os.startsWith("SunOS") && !os.startsWith("Linux")) {
-                mode = -1;
-            }
+            int nh = 5;     // number of hosts (services)
+            String[] libs;  // available acceptor types:
+                            // J: java
+                            // N: default native lib
+                            // N<libname>: native lib with the given name
+            Ex[] result;
+            int numPerType = 2; // number of servers per type
 
             uid = jdk.internal.misc.VM.geteuid();
 
@@ -85,93 +102,151 @@
                 kdc.addPrincipal(user(i), OneKDC.PASS);
             }
             kdc.addPrincipalRandKey("krbtgt/" + OneKDC.REALM);
-            for (int i=0; i<np; i++) {
-                kdc.addPrincipalRandKey(peer(i));
+            for (int i=0; i<nh; i++) {
+                kdc.addPrincipalRandKey(host(i));
             }
 
             kdc.writeKtab(OneKDC.KTAB);
             KDC.saveConfig(OneKDC.KRB5_CONF, kdc);
 
-            if (mode != -1) {
-                // A special native server to check basic sanity
-                if (ns(-1).waitFor() != 0) {
-                    Proc.d("Native mode sanity check failed, revert to java");
-                    mode = -1;
+            // User-provided libs
+            String userLibs = System.getProperty("test.libs");
+
+            if (userLibs != null) {
+                libs = userLibs.split(",");
+            } else {
+                if (Platform.isOSX() || Platform.isWindows()) {
+                    // macOS uses Heimdal and Windows has no native lib
+                    libs = new String[]{"J"};
+                } else {
+                    // Test interop between Java and native (if available)
+                    if (acceptor("N", "sanity").waitFor() != 0) {
+                        Proc.d("Native mode sanity check failed, revert to java");
+                        libs = new String[]{"J"};
+                    } else {
+                        libs = new String[]{"J", "N"};
+                    }
                 }
             }
 
-            pc = Proc.create("ReplayCacheTestProc").debug("C")
-                    .args("client")
+            pi = Proc.create("ReplayCacheTestProc").debug("C")
+                    .args("initiator")
                     .start();
-            ps = new Proc[ns];
-            Ex[] result = new Ex[nx];
 
-            if (!random) {
-                // 2 experiments, 2 server, 1 peer, 1 user
-                nx = 2; ns = 2; np = 1; nu = 1;
-
-                // Creates reqs from user# to peer#
-                req(0, 0);
-
-                // Creates server#
-                ps[0] = ns(0);
-                ps[1] = js(1);
-
-                // Runs ex# using req# to server# with expected result
-                result[0] = round(0, 0, 0, true);
-                result[1] = round(1, 0, 1, false);
+            int na = libs.length * numPerType;  // total number of acceptors
+            pa = new Proc[na];
+
+            // Acceptors, numPerType for 1st, numForType for 2nd, ...
+            for (int i=0; i<na; i++) {
+                int type = i / numPerType;
+                String label; // N/J for N/J, ABC... for N<lib>
+                if (libs[type].length() == 1) {
+                    label = libs[type];
+                } else {
+                    label = "" + (char)('A' + type);
+                }
+                pa[i] = acceptor(libs[type], label + i % numPerType);
+            }
+
+            String userRuns = System.getProperty("test.runs");
+
+            if (userRuns != null) {
+                String[] runs = userRuns.split(",");
+                result = new Ex[runs.length];
+                for (int i = 0; i < runs.length; i++) {
+                    boolean expected = false;
+                    int req = -1;
+                    int client = -1;
+                    int host = -1;
+                    UserRun run = new UserRun(runs[i]);
+                    while (true) {
+                        char type = run.nextAction();
+                        if (type == ' ') {
+                            break;
+                        }
+                        switch (type) {
+                            case 'r':
+                                req = result[run.nextValue()].req;
+                                break;
+                            case 'c':
+                                client = run.nextValue();
+                                break;
+                            case 'h':
+                                req = req(client, run.nextValue());
+                                break;
+                            case 'J':
+                            case 'N':
+                                for (int j = 0; j < libs.length; j++) {
+                                    if (libs[j].equals("" + type)) {
+                                        host = j * numPerType + run.nextValue();
+                                        break;
+                                    }
+                                }
+                                break;
+                            case 'v':
+                                expected = true;
+                                break;
+                            case 'x':
+                                expected = false;
+                                break;
+                            default:    // ABC...
+                                host = (type - 'A') * numPerType + run.nextValue();
+                        }
+                    }
+                    result[i] = new Ex(i, req, host, expected);
+                }
             } else {
+                result = new Ex[Integer.parseInt(
+                        System.getProperty("test.autoruns", "100"))];
                 Random r = new Random();
-                for (int i=0; i<ns; i++) {
-                    boolean useNative = (mode == 1) ? true
-                            : (mode == -1 ? false : r.nextBoolean());
-                    ps[i] = useNative?ns(i):js(i);
-                }
-                for (int i=0; i<nx; i++) {
-                    result[i] = new Ex();
+                for (int i = 0; i < result.length; i++) {
                     int old;    // which req to send
                     boolean expected;
                     if (reqs.isEmpty() || r.nextBoolean()) {
-                        Proc.d("Console get new AP-REQ");
-                        old = req(r.nextInt(nu), r.nextInt(np));
+                        old = req(r.nextInt(nu), r.nextInt(nh));
                         expected = true;
                     } else {
-                        Proc.d("Console resue old");
                         old = r.nextInt(reqs.size());
                         expected = false;
                     }
-                    int s = r.nextInt(ns);
-                    Proc.d("Console send to " + s);
-                    result[i] = round(i, old, s, expected);
-                    Proc.d("Console sees " + result[i].actual);
+                    int s = r.nextInt(na);
+                    result[i] = new Ex(i, old, s, expected);
                 }
             }
 
-            pc.println("END");
-            for (int i=0; i<ns; i++) {
-                ps[i].println("END");
+            for (Ex x : result) {
+                x.run();
+            }
+
+            pi.println("END");
+            for (int i=0; i<na; i++) {
+                pa[i].println("END");
             }
             System.out.println("Result\n======");
             boolean finalOut = true;
-            for (int i=0; i<nx; i++) {
+            System.out.println("  #: expected (req): client host acceptor Result      size");
+            System.out.println("---- -------- ------ ------ ---- -------- ------      ----");
+            for (int i=0; i<result.length; i++) {
                 boolean out = result[i].expected==result[i].actual;
                 finalOut &= out;
-                System.out.printf("%3d: %s (%2d): u%d h%d %s %s   %s %2d\n",
+                System.out.printf("%3d: %8s (%3d):     u%d   h%d %8s   %s  %s %4d\n",
                         i,
                         result[i].expected?"----":"    ",
-                        result[i].old,
-                        result[i].user, result[i].peer, result[i].server,
+                        result[i].req,
+                        reqs.get(result[i].req).user,
+                        reqs.get(result[i].req).peer,
+                        pa[result[i].acceptor].debug(),
                         result[i].actual?"Good":"Bad ",
                         out?"   ":"xxx",
                         result[i].csize);
             }
             if (!finalOut) throw new Exception();
-        } else if (args[0].equals("N-1")) {
+        } else if (args[0].equals("sanity")) {
             // Native mode sanity check
             Proc.d("Detect start");
             Context s = Context.fromUserKtab("*", OneKDC.KTAB, true);
             s.startAsServer(GSSUtil.GSS_KRB5_MECH_OID);
-        } else if (args[0].equals("client")) {
+        } else if (args[0].equals("initiator")) {
             while (true) {
                 String title = Proc.textIn();
                 Proc.d("Client see " + title);
@@ -217,77 +292,60 @@
 
     // returns the user name
     private static String user(int p) {
-        return "USER" + p;
+        return "user" + p;
     }
-    // returns the peer name
-    private static String peer(int p) {
+
+    // returns the host name
+    private static String host(int p) {
         return "host" + p + "/" + HOST;
     }
+
     // returns the dfl name for a host
     private static String dfl(int p) {
-        return cwd + "host" + p + (uid == -1 ? "" : ("_"+uid));
+        return "host" + p + (uid == -1 ? "" : ("_"+uid));
     }
+
     // generates an ap-req and save into reqs, returns the index
     private static int req(int user, int peer) throws Exception {
-        pc.println(user(user) + " " + peer(peer));
-        Req req = new Req(user, peer, pc.readData());
+        pi.println(user(user) + " " + host(peer));
+        Req req = new Req(user, peer, pi.readData());
         reqs.add(req);
         return reqs.size() - 1;
     }
-    // carries out a round of experiment
-    // i: ex#, old: which req, server: which server, expected: result?
-    private static Ex round(int i, int old, int server, boolean expected)
-            throws Exception {
-        ps[server].println("TEST");
-        ps[server].println(reqs.get(old).msg);
-        String reply = ps[server].readData();
-        Ex result = new Ex();
-        result.i = i;
-        result.expected = expected;
-        result.server = ps[server].debug();
-        result.actual = Boolean.valueOf(reply);
-        result.user = reqs.get(old).user;
-        result.peer = reqs.get(old).peer;
-        result.old = old;
-        result.csize = csize(result.peer);
-        result.hash = hash(reqs.get(old).msg);
-        if (new File(dfl(result.peer)).exists()) {
-            Files.copy(Paths.get(dfl(result.peer)), Paths.get(
-                String.format("%03d-USER%d-host%d-%s-%s",
-                    i, result.user, result.peer, result.server,
-                    result.actual)
-                + "-" + result.hash),
-                StandardCopyOption.COPY_ATTRIBUTES);
-        }
-        return result;
-    }
-    // create a native server
-    private static Proc ns(int i) throws Exception {
-        return Proc.create("ReplayCacheTestProc")
-                .args("N"+i)
-                .env("KRB5_CONFIG", OneKDC.KRB5_CONF)
-                .env("KRB5_KTNAME", OneKDC.KTAB)
-                .env("KRB5RCACHEDIR", cwd)
-                .prop("sun.security.jgss.native", "true")
-                .prop("javax.security.auth.useSubjectCredsOnly", "false")
-                .prop("sun.security.nativegss.debug", "true")
-                .debug("N"+i)
-                .start();
-    }
-    // creates a java server
-    private static Proc js(int i) throws Exception {
-        return Proc.create("ReplayCacheTestProc")
-                .debug("S"+i)
-                .args("S"+i)
-                .prop("sun.security.krb5.rcache", "dfl")
-                .prop("java.io.tmpdir", cwd)
-                .start();
+
+    // create a acceptor
+    private static Proc acceptor(String type, String label) throws Exception {
+        Proc p = Proc.create("ReplayCacheTestProc")
+                .args(label)
+                .debug(label);
+        if (type.equals("J")) {
+            p.prop("sun.security.krb5.rcache", "dfl")
+                    .prop("java.io.tmpdir", cwd);
+        } else {
+            p.env("KRB5_CONFIG", OneKDC.KRB5_CONF)
+                    .env("KRB5_KTNAME", OneKDC.KTAB)
+                    .env("KRB5RCACHEDIR", cwd)
+                    .prop("sun.security.jgss.native", "true")
+                    .prop("javax.security.auth.useSubjectCredsOnly", "false")
+                    .prop("sun.security.nativegss.debug", "true");
+            if (type.length() > 1) {
+                String lib = type.substring(1);
+                String libDir = lib.substring(0, lib.lastIndexOf('/'));
+                p.prop("sun.security.jgss.lib", lib)
+                        .env("DYLD_LIBRARY_PATH", libDir)
+                        .env("LD_LIBRARY_PATH", libDir);
+            }
+        }
+        return p.start();
     }
+
     // generates hash of authenticator inside ap-req inside initsectoken
     private static String hash(String req) throws Exception {
         byte[] data = Base64.getDecoder().decode(req);
         data = Arrays.copyOfRange(data, 17, data.length);
-        byte[] hash = MessageDigest.getInstance("MD5").digest(new APReq(data).authenticator.getBytes());
+        byte[] hash = MessageDigest.getInstance(
+                AuthTimeWithHash.realAlg(AuthTimeWithHash.DEFAULT_HASH_ALG))
+                    .digest(new APReq(data).authenticator.getBytes());
         char[] h = new char[hash.length * 2];
         char[] hexConst = "0123456789ABCDEF".toCharArray();
         for (int i=0; i<hash.length; i++) {
@@ -296,10 +354,11 @@
         }
         return new String(h);
     }
+
     // return size of dfl file, excluding the null hash ones
     private static int csize(int p) throws Exception {
         try (SeekableByteChannel chan = Files.newByteChannel(
-                Paths.get(dfl(p)), StandardOpenOption.READ)) {
+                Paths.get(cwd, dfl(p)), StandardOpenOption.READ)) {
             chan.position(6);
             int cc = 0;
             while (true) {
@@ -314,27 +373,76 @@
             return 0;
         }
     }
+
     // models an experiement
     private static class Ex {
         int i;              // #
+        int req;            // which ap-req to send
+        int acceptor;       // which acceptor to send to
         boolean expected;   // expected result
+
         boolean actual;     // actual output
-        int old;            // which ap-req to send
-        String server;      // which server to send to
-        String hash;        // the hash of req
-        int user;           // which initiator
-        int peer;           // which acceptor
         int csize;          // size of rcache after test
+        String hash;        // the hash of req
+
+        Ex(int i, int req, int acceptor, boolean expected) {
+            this.i = i;
+            this.req = req;
+            this.acceptor = acceptor;
+            this.expected = expected;
+        }
+
+        void run() throws Exception {
+            Req r = reqs.get(req);
+            pa[acceptor].println("TEST");
+            pa[acceptor].println(r.msg);
+            String reply = pa[acceptor].readData();
+
+            actual = Boolean.valueOf(reply);
+            csize = csize(r.peer);
+            hash = hash(r.msg);
+            if (new File(cwd, dfl(r.peer)).exists()) {
+                Files.copy(Paths.get(cwd, dfl(r.peer)), Paths.get(
+                        String.format("%03d-USER%d-host%d-%s-%s",
+                                i, r.user, r.peer, acceptor,
+                                actual)
+                                + "-" + hash),
+                        StandardCopyOption.COPY_ATTRIBUTES);
+            }
+        }
     }
+
     // models a saved ap-req msg
     private static class Req {
         String msg;         // based64-ed req
-        int user;           // which initiator
-        int peer;           // which accceptor
+        int user;           // which client
+        int peer;           // which service
         Req(int user, int peer, String msg) {
             this.msg = msg;
             this.user= user;
             this.peer = peer;
         }
     }
+
+    private static class UserRun {
+        String run;
+        int pos = 0;
+        UserRun(String run) {
+            this.run = run;
+        }
+        char nextAction() {
+            return pos < run.length() ? run.charAt(pos++) : ' ';
+        }
+        int nextValue() {
+            int result = 0;
+            for (; pos < run.length(); pos++) {
+                char c = run.charAt(pos);
+                if (!Character.isDigit(c)) {
+                    break;
+                }
+                result = result * 10 + (c - '0');
+            }
+            return result;
+        }
+    }
 }