IND$FILE receiving

This commit is contained in:
2026-04-21 15:35:03 -04:00
parent bda8ea9530
commit 4ac3c6fa69
6 changed files with 2134 additions and 0 deletions
@@ -0,0 +1,236 @@
package org.pubvm.j3270.ft;
import org.lib3270j.Telnet3270Client;
import org.lib3270j.ft.FTConfig;
import org.lib3270j.ft.FTConstants.FTState;
import org.lib3270j.ft.FTCut;
import org.lib3270j.ft.FTDft;
import java.io.File;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
/**
* High-level coordinator for IND$FILE file transfers.
* Manages the transfer state machine, timeouts, and coordinates between
* the GUI, Telnet3270Client, and the lower-level CUT/DFT protocol handlers.
*/
public class FileTransfer implements FTCut.FTCutListener, FTDft.FTDftListener {
private static final Logger log = Logger.getLogger(FileTransfer.class.getName());
public interface FileTransferCallback {
void onTransferStarted();
void onTransferRunning();
void onBytesTransferred(long bytes);
void onTransferComplete(String message);
void onTransferAborted(String error);
}
private final Telnet3270Client client;
private final FileTransferCallback callback;
private FTConfig currentConfig;
private File localFile;
private FTState state = FTState.NONE;
private FTCut cutHandler;
private FTDft dftHandler;
private Timer timeoutTimer;
private static final long START_TIMEOUT_MS = 30000; // 30 seconds
public FileTransfer(Telnet3270Client client, FileTransferCallback callback) {
this.client = client;
this.callback = callback;
}
/**
* Start a new file transfer with the given configuration.
* @return null if started successfully, error message otherwise.
*/
public String startTransfer(FTConfig config) {
if (state != FTState.NONE) {
return "A transfer is already in progress.";
}
String validationError = config.validate();
if (validationError != null) {
return validationError;
}
this.currentConfig = config;
this.localFile = new File(config.getLocalFilename());
// Check overwrite
if (config.isReceive() && !config.isAppend() && !config.isOverwrite()) {
if (localFile.exists()) {
return "Local file already exists and overwrite is not permitted.";
}
}
// Initialize protocol handlers lazily
if (cutHandler == null) {
cutHandler = new FTCut(client.getScreenBuffer(), client.getInputProcessor(),
client.getTranslator(), this);
}
if (dftHandler == null) {
dftHandler = new FTDft(client.getInputProcessor(),
client.getTranslator(), this);
client.getDataStreamProcessor().setFTDft(dftHandler);
}
try {
cutHandler.initTransfer(localFile);
dftHandler.initTransfer(localFile);
} catch (IOException e) {
cleanupHandlers(false);
return "Failed to open local file: " + e.getMessage();
}
// Build and type the IND$FILE command
String command = config.buildCommand();
log.info("Starting IND$FILE transfer with command: " + command);
// Erase field and verify it can hold the command
int capacity = client.getInputProcessor().kybdPrime();
if (capacity < 0) {
cleanupHandlers(false);
switch (capacity) {
case -1: return "Keyboard is locked.";
case -3: return "No unprotected input field found.";
default: return "Cannot start transfer from current screen state.";
}
}
if (capacity < command.length()) {
cleanupHandlers(false);
return "Current input field is too small for the IND$FILE command (" + capacity + " chars max).";
}
setState(FTState.AWAIT_ACK);
client.emulateInput(command + "\n");
startTimeout();
callback.onTransferStarted();
return null;
}
public void cancel() {
if (state == FTState.RUNNING || state == FTState.AWAIT_ACK) {
log.info("User cancelled transfer");
setState(FTState.ABORT_WAIT); // Signal handlers to abort at next chance
} else if (state != FTState.NONE) {
log.info("Forcing cancel from state " + state);
completeTransfer("Transfer cancelled.");
callback.onTransferAborted("Cancelled by user.");
}
}
/** Must be called after every screen update to drive CUT mode. */
public void onScreenUpdated() {
if (state == FTState.AWAIT_ACK || state == FTState.RUNNING || state == FTState.ABORT_WAIT) {
cutHandler.processScreenUpdate();
}
}
private void startTimeout() {
cancelTimeout();
timeoutTimer = new Timer("FTTimeout", true);
timeoutTimer.schedule(new TimerTask() {
@Override
public void run() {
SwingUtilities.invokeLater(() -> {
if (state == FTState.AWAIT_ACK) {
log.warning("Transfer start timeout");
completeTransfer("Transfer failed to start within 30 seconds.");
callback.onTransferAborted("Transfer start timeout.");
}
});
}
}, START_TIMEOUT_MS);
}
private void cancelTimeout() {
if (timeoutTimer != null) {
timeoutTimer.cancel();
timeoutTimer = null;
}
}
private void cleanupHandlers(boolean success) {
if (cutHandler != null) cutHandler.cleanup();
if (dftHandler != null) dftHandler.cleanup();
// Remove incomplete files if we were receiving and not appending
if (!success && currentConfig != null && currentConfig.isReceive() && !currentConfig.isAppend()) {
if (state != FTState.NONE && state != FTState.AWAIT_ACK && localFile != null && localFile.exists()) {
log.info("Cleaning up incomplete download: " + localFile.getAbsolutePath());
localFile.delete();
}
}
}
private void completeTransfer(String errorMessage) {
cancelTimeout();
boolean success = (errorMessage == null);
cleanupHandlers(success);
setState(FTState.NONE);
currentConfig = null;
}
// ========== FTCutListener / FTDftListener Callbacks ==========
@Override
public void onTransferRunning() {
cancelTimeout();
setState(FTState.RUNNING);
SwingUtilities.invokeLater(callback::onTransferRunning);
}
@Override
public void onTransferComplete(String errorMessage) {
completeTransfer(null);
SwingUtilities.invokeLater(() -> {
if (errorMessage == null) {
callback.onTransferComplete("Transfer complete.");
} else {
callback.onTransferAborted(errorMessage);
}
});
}
@Override
public void onTransferAborted(String errorMessage) {
completeTransfer(errorMessage);
SwingUtilities.invokeLater(() -> callback.onTransferAborted(errorMessage));
}
@Override
public void onBytesTransferred(long bytes) {
SwingUtilities.invokeLater(() -> callback.onBytesTransferred(bytes));
}
@Override
public FTState getCurrentState() {
return state;
}
@Override
public void setState(FTState state) {
this.state = state;
}
@Override
public FTConfig getConfig() {
return currentConfig;
}
@Override
public File getLocalFile() {
return localFile;
}
}
@@ -0,0 +1,305 @@
package org.pubvm.j3270.ft;
import org.lib3270j.ft.FTConfig;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.io.File;
public class FileTransferDialog extends JDialog {
private final FileTransfer coordinator;
private final Frame owner;
private JComboBox<FTConfig.HostType> hostTypeCombo;
private JRadioButton sendRadio;
private JRadioButton receiveRadio;
private JTextField localFileField;
private JButton browseLocalButton;
private JTextField hostFileField;
// Options
private JRadioButton asciiRadio;
private JRadioButton binaryRadio;
private JCheckBox crCheck;
private JCheckBox remapCheck;
private JCheckBox appendCheck;
private JCheckBox overwriteCheck;
// TSO specific
private JTextField recfmField;
private JTextField lreclField;
private JTextField blksizeField;
private JTextField spaceField;
// VM specific
private JTextField optionsField;
private JButton transferButton;
private JButton cancelButton;
public FileTransferDialog(Frame owner, FileTransfer coordinator) {
super(owner, "File Transfer (IND$FILE)", false); // non-modal to see progress
this.owner = owner;
this.coordinator = coordinator;
buildUI();
pack();
setLocationRelativeTo(owner);
}
private void buildUI() {
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
mainPanel.setBackground(new Color(25, 25, 25));
// Create form panel
JPanel formPanel = new JPanel();
formPanel.setLayout(new BoxLayout(formPanel, BoxLayout.Y_AXIS));
formPanel.setOpaque(false);
// Host Type
hostTypeCombo = new JComboBox<>(FTConfig.HostType.values());
formPanel.add(createRow("Host Environment:", hostTypeCombo));
// Direction
sendRadio = new JRadioButton("Send to Host");
receiveRadio = new JRadioButton("Receive from Host");
receiveRadio.setSelected(true);
ButtonGroup dirGroup = new ButtonGroup();
dirGroup.add(sendRadio);
dirGroup.add(receiveRadio);
JPanel dirPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
dirPanel.setOpaque(false);
dirPanel.add(receiveRadio);
dirPanel.add(Box.createRigidArea(new Dimension(15, 0)));
dirPanel.add(sendRadio);
// Disable append/overwrite when sending
sendRadio.addActionListener(e -> updateOptionStates());
receiveRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Direction:", dirPanel));
// Local File
localFileField = new JTextField(20);
browseLocalButton = new JButton("Browse...");
browseLocalButton.addActionListener(e -> browseLocalFile());
JPanel localPanel = new JPanel(new BorderLayout(5, 0));
localPanel.setOpaque(false);
localPanel.add(localFileField, BorderLayout.CENTER);
localPanel.add(browseLocalButton, BorderLayout.EAST);
formPanel.add(createRow("Local File:", localPanel));
// Host File
hostFileField = new JTextField(20);
formPanel.add(createRow("Host File:", hostFileField));
// Mode
asciiRadio = new JRadioButton("ASCII");
asciiRadio.setSelected(true);
binaryRadio = new JRadioButton("Binary");
ButtonGroup modeGroup = new ButtonGroup();
modeGroup.add(asciiRadio);
modeGroup.add(binaryRadio);
JPanel modePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
modePanel.setOpaque(false);
modePanel.add(asciiRadio);
modePanel.add(Box.createRigidArea(new Dimension(15, 0)));
modePanel.add(binaryRadio);
asciiRadio.addActionListener(e -> updateOptionStates());
binaryRadio.addActionListener(e -> updateOptionStates());
formPanel.add(createRow("Transfer Mode:", modePanel));
// Options (checkboxes)
crCheck = new JCheckBox("Add/Remove CR");
crCheck.setSelected(true);
remapCheck = new JCheckBox("Remap Character Set");
remapCheck.setSelected(true);
appendCheck = new JCheckBox("Append to file");
overwriteCheck = new JCheckBox("Overwrite existing");
JPanel optionsPanel1 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel1.setOpaque(false);
optionsPanel1.add(crCheck);
optionsPanel1.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel1.add(remapCheck);
JPanel optionsPanel2 = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
optionsPanel2.setOpaque(false);
optionsPanel2.add(appendCheck);
optionsPanel2.add(Box.createRigidArea(new Dimension(15, 0)));
optionsPanel2.add(overwriteCheck);
formPanel.add(createRow("Text Options:", optionsPanel1));
formPanel.add(createRow("File Options:", optionsPanel2));
// Host-specific options panel (TSO dataset allocation)
JPanel hostOptsPanel = new JPanel(new GridLayout(2, 4, 5, 5));
hostOptsPanel.setOpaque(false);
hostOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"TSO Allocation Options (Send Only)"));
((javax.swing.border.TitledBorder)hostOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
hostOptsPanel.add(new JLabel("RECFM:"));
recfmField = new JTextField(5);
hostOptsPanel.add(recfmField);
hostOptsPanel.add(new JLabel("LRECL:"));
lreclField = new JTextField(5);
hostOptsPanel.add(lreclField);
hostOptsPanel.add(new JLabel("BLKSIZE:"));
blksizeField = new JTextField(5);
hostOptsPanel.add(blksizeField);
hostOptsPanel.add(new JLabel("SPACE:"));
spaceField = new JTextField(15);
hostOptsPanel.add(spaceField);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(hostOptsPanel);
// CMS options
JPanel vmOptsPanel = new JPanel(new BorderLayout(5, 0));
vmOptsPanel.setOpaque(false);
vmOptsPanel.setBorder(BorderFactory.createTitledBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
"CMS Options"));
((javax.swing.border.TitledBorder)vmOptsPanel.getBorder()).setTitleColor(new Color(180, 180, 180));
optionsField = new JTextField(20);
vmOptsPanel.add(new JLabel("Additional Options: "), BorderLayout.WEST);
vmOptsPanel.add(optionsField, BorderLayout.CENTER);
formPanel.add(Box.createRigidArea(new Dimension(0, 10)));
formPanel.add(vmOptsPanel);
mainPanel.add(formPanel, BorderLayout.CENTER);
// Buttons
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
buttonPanel.setOpaque(false);
transferButton = new JButton("Start Transfer");
transferButton.addActionListener(e -> startTransfer());
cancelButton = new JButton("Close");
cancelButton.addActionListener(e -> dispose());
buttonPanel.add(cancelButton);
buttonPanel.add(transferButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
setContentPane(mainPanel);
// Theme styling for components
applyTheme(mainPanel);
// Initialize state
updateOptionStates();
}
private void updateOptionStates() {
boolean isSend = sendRadio.isSelected();
boolean isAscii = asciiRadio.isSelected();
appendCheck.setEnabled(!isSend);
overwriteCheck.setEnabled(!isSend);
crCheck.setEnabled(isAscii);
remapCheck.setEnabled(isAscii);
recfmField.setEnabled(isSend);
lreclField.setEnabled(isSend);
blksizeField.setEnabled(isSend);
spaceField.setEnabled(isSend);
}
private JPanel createRow(String labelText, Component comp) {
JPanel row = new JPanel(new BorderLayout(10, 0));
row.setOpaque(false);
JLabel label = new JLabel(labelText);
label.setPreferredSize(new Dimension(130, 25));
row.add(label, BorderLayout.WEST);
row.add(comp, BorderLayout.CENTER);
row.setBorder(new EmptyBorder(0, 0, 5, 0));
return row;
}
private void browseLocalFile() {
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
localFileField.setText(chooser.getSelectedFile().getAbsolutePath());
// Auto-fill host file if empty
if (hostFileField.getText().trim().isEmpty()) {
hostFileField.setText(chooser.getSelectedFile().getName());
}
}
}
private void startTransfer() {
if (localFileField.getText().trim().isEmpty() || hostFileField.getText().trim().isEmpty()) {
JOptionPane.showMessageDialog(this, "Local and Host filenames are required.",
"Error", JOptionPane.ERROR_MESSAGE);
return;
}
FTConfig config = new FTConfig();
config.setHostType((FTConfig.HostType) hostTypeCombo.getSelectedItem());
config.setLocalFilename(localFileField.getText().trim());
config.setHostFilename(hostFileField.getText().trim());
config.setReceive(receiveRadio.isSelected());
config.setAscii(asciiRadio.isSelected());
config.setCrFlag(crCheck.isSelected());
config.setRemapFlag(remapCheck.isSelected());
config.setAppend(appendCheck.isSelected());
config.setOverwrite(overwriteCheck.isSelected());
config.setRecfm(recfmField.getText().trim());
config.setLrecl(lreclField.getText().trim());
config.setBlksize(blksizeField.getText().trim());
config.setSpace(spaceField.getText().trim());
config.setOptions(optionsField.getText().trim());
String error = coordinator.startTransfer(config);
if (error != null) {
JOptionPane.showMessageDialog(this, error, "Transfer Error", JOptionPane.ERROR_MESSAGE);
} else {
// Success, close dialog (progress will be shown separately)
dispose();
}
}
private void applyTheme(Container container) {
Color fg = new Color(200, 200, 200);
Color bg = new Color(40, 40, 40);
for (Component c : container.getComponents()) {
c.setForeground(fg);
if (c instanceof JTextField || c instanceof JComboBox) {
c.setBackground(bg);
if (c instanceof JTextField) {
((JTextField) c).setCaretColor(fg);
((JTextField) c).setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(60, 60, 60)),
new EmptyBorder(2, 5, 2, 5)));
}
} else if (c instanceof JButton) {
c.setBackground(new Color(50, 50, 50));
c.setFocusable(false);
} else if (c instanceof JRadioButton || c instanceof JCheckBox) {
((JComponent) c).setOpaque(false);
c.setFocusable(false);
}
if (c instanceof Container) {
applyTheme((Container) c);
}
}
}
}
@@ -0,0 +1,310 @@
package org.lib3270j.ft;
/**
* Configuration for an IND$FILE file transfer session.
* Ported from x3270's ft_conf_t (ft_private.h).
*/
public class FTConfig {
/** Host operating system type */
public enum HostType {
TSO, CMS, CICS
}
/** Record format for datasets (TSO/CMS sends only) */
public enum RecordFormat {
DEFAULT, FIXED, VARIABLE, UNDEFINED
}
/** Space allocation units (TSO sends only) */
public enum AllocationUnit {
DEFAULT, TRACKS, CYLINDERS, AVBLOCK
}
/** Transfer direction */
public enum Direction {
RECEIVE, SEND
}
/** Transfer mode */
public enum TransferMode {
ASCII, BINARY
}
/** CR/LF handling */
public enum CrAction {
ADD, REMOVE, KEEP
}
/** Behavior when destination file already exists */
public enum ExistAction {
KEEP, REPLACE, APPEND
}
// ========== Transfer Parameters ==========
private String hostFilename;
private String localFilename;
private Direction direction = Direction.RECEIVE;
private HostType hostType = HostType.TSO;
private TransferMode transferMode = TransferMode.ASCII;
private CrAction crAction = CrAction.REMOVE;
private boolean remapFlag = true;
private ExistAction existAction = ExistAction.KEEP;
private RecordFormat recfm = RecordFormat.DEFAULT;
private AllocationUnit units = AllocationUnit.DEFAULT;
private int lrecl = 0;
private int blksize = 0;
private int primarySpace = 0;
private int secondarySpace = 0;
private int avblock = 0;
private int dftBufferSize = FTConstants.DFT_BUF;
private String otherOptions = null;
// ========== Derived convenience getters ==========
public boolean isReceive() { return direction == Direction.RECEIVE; }
public boolean isSend() { return direction == Direction.SEND; }
public boolean isAscii() { return transferMode == TransferMode.ASCII; }
public boolean isBinary() { return transferMode == TransferMode.BINARY; }
public boolean isCrFlag() {
// CR processing is only applicable for ASCII transfers
return isAscii() && crAction != CrAction.KEEP;
}
public boolean isAppend() { return existAction == ExistAction.APPEND; }
public boolean isOverwrite() { return existAction == ExistAction.REPLACE; }
// ========== Standard getters and setters ==========
public String getHostFilename() { return hostFilename; }
public void setHostFilename(String hostFilename) { this.hostFilename = hostFilename; }
public String getLocalFilename() { return localFilename; }
public void setLocalFilename(String localFilename) { this.localFilename = localFilename; }
public Direction getDirection() { return direction; }
public void setDirection(Direction direction) { this.direction = direction; }
public HostType getHostType() { return hostType; }
public void setHostType(HostType hostType) { this.hostType = hostType; }
public TransferMode getTransferMode() { return transferMode; }
public void setTransferMode(TransferMode transferMode) { this.transferMode = transferMode; }
public CrAction getCrAction() { return crAction; }
public void setCrAction(CrAction crAction) { this.crAction = crAction; }
public boolean isRemapFlag() { return remapFlag; }
public void setRemapFlag(boolean remapFlag) { this.remapFlag = remapFlag; }
public ExistAction getExistAction() { return existAction; }
public void setExistAction(ExistAction existAction) { this.existAction = existAction; }
public RecordFormat getRecfm() { return recfm; }
public void setRecfm(RecordFormat recfm) { this.recfm = recfm; }
public AllocationUnit getUnits() { return units; }
public void setUnits(AllocationUnit units) { this.units = units; }
public int getLrecl() { return lrecl; }
public void setLrecl(int lrecl) { this.lrecl = lrecl; }
public int getBlksize() { return blksize; }
public void setBlksize(int blksize) { this.blksize = blksize; }
public int getPrimarySpace() { return primarySpace; }
public void setPrimarySpace(int primarySpace) { this.primarySpace = primarySpace; }
public int getSecondarySpace() { return secondarySpace; }
public void setSecondarySpace(int secondarySpace) { this.secondarySpace = secondarySpace; }
public int getAvblock() { return avblock; }
public void setAvblock(int avblock) { this.avblock = avblock; }
public int getDftBufferSize() { return dftBufferSize; }
public void setDftBufferSize(int dftBufferSize) {
this.dftBufferSize = Math.max(FTConstants.DFT_MIN_BUF,
Math.min(FTConstants.DFT_MAX_BUF, dftBufferSize));
}
public String getOtherOptions() { return otherOptions; }
public void setOtherOptions(String otherOptions) {
this.otherOptions = (otherOptions != null && !otherOptions.trim().isEmpty())
? otherOptions.trim() : null;
}
// ========== Convenience UI setters ==========
public void setAppend(boolean append) {
if (append) this.existAction = ExistAction.APPEND;
}
public void setOverwrite(boolean overwrite) {
if (overwrite) this.existAction = ExistAction.REPLACE;
}
public void setReceive(boolean receive) {
setDirection(receive ? Direction.RECEIVE : Direction.SEND);
}
public void setAscii(boolean ascii) {
setTransferMode(ascii ? TransferMode.ASCII : TransferMode.BINARY);
}
public void setCrFlag(boolean cr) {
setCrAction(cr ? CrAction.REMOVE : CrAction.KEEP);
}
public void setRecfm(String r) {
if (r == null || r.trim().isEmpty()) {
this.recfm = RecordFormat.DEFAULT;
} else {
String u = r.trim().toUpperCase();
if (u.equals("F") || u.startsWith("FIXED")) this.recfm = RecordFormat.FIXED;
else if (u.equals("V") || u.startsWith("VAR")) this.recfm = RecordFormat.VARIABLE;
else if (u.equals("U") || u.startsWith("UNDEF")) this.recfm = RecordFormat.UNDEFINED;
else this.recfm = RecordFormat.DEFAULT;
}
}
public void setLrecl(String l) {
try { this.lrecl = Integer.parseInt(l.trim()); } catch (NumberFormatException e) { this.lrecl = 0; }
}
public void setBlksize(String b) {
try { this.blksize = Integer.parseInt(b.trim()); } catch (NumberFormatException e) { this.blksize = 0; }
}
public void setSpace(String s) {
// Space string like "10,5" for primary/secondary
if (s == null || s.trim().isEmpty()) return;
String[] parts = s.split(",");
try {
if (parts.length > 0) this.primarySpace = Integer.parseInt(parts[0].trim());
if (parts.length > 1) this.secondarySpace = Integer.parseInt(parts[1].trim());
} catch (NumberFormatException e) {
// ignore
}
}
public void setOptions(String opts) {
setOtherOptions(opts);
}
// ========== Validation ==========
/**
* Validate the configuration before starting a transfer.
* @return null if valid, or an error message string
*/
public String validate() {
if (hostFilename == null || hostFilename.trim().isEmpty()) {
return "Host file name is required";
}
if (localFilename == null || localFilename.trim().isEmpty()) {
return "Local file name is required";
}
if (hostType == HostType.TSO && isSend() &&
units != AllocationUnit.DEFAULT && primarySpace <= 0) {
return "Primary space is required when allocation is specified";
}
if (hostType == HostType.TSO && isSend() &&
units == AllocationUnit.AVBLOCK && avblock <= 0) {
return "Avblock value is required when allocation is AVBLOCK";
}
return null;
}
/**
* Build the IND$FILE command string to send to the host.
* Ported from x3270's ft_go() in ft.c.
*/
public String buildCommand() {
StringBuilder cmd = new StringBuilder();
// IND$FILE GET/PUT hostfile
cmd.append("IND$FILE ");
cmd.append(isReceive() ? "GET " : "PUT ");
cmd.append(hostFilename);
// CMS/CICS options use ( prefix
if (hostType != HostType.TSO) {
cmd.append(" (");
} else {
cmd.append(" ");
}
// Mode
if (isAscii()) {
cmd.append("ASCII");
} else if (hostType == HostType.CICS) {
cmd.append("BINARY");
}
// CR/LF handling
if (isAscii() && isCrFlag()) {
cmd.append(" CRLF");
} else if (hostType == HostType.CICS) {
cmd.append(" NOCRLF");
}
// Append (send only)
if (isAppend() && isSend()) {
cmd.append(" APPEND");
}
// TSO-specific send options
if (isSend()) {
if (hostType == HostType.TSO) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM(");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
case UNDEFINED: cmd.append("U"); break;
default: break;
}
cmd.append(")");
if (lrecl > 0) {
cmd.append(" LRECL(").append(lrecl).append(")");
}
if (blksize > 0) {
cmd.append(" BLKSIZE(").append(blksize).append(")");
}
}
if (units != AllocationUnit.DEFAULT) {
cmd.append(" SPACE(").append(primarySpace);
if (secondarySpace > 0) {
cmd.append(",").append(secondarySpace);
}
cmd.append(")");
switch (units) {
case TRACKS: cmd.append(" TRACKS"); break;
case CYLINDERS: cmd.append(" CYLINDERS"); break;
case AVBLOCK: cmd.append(" AVBLOCK(").append(avblock).append(")"); break;
default: break;
}
}
} else if (hostType == HostType.CMS) {
if (recfm != RecordFormat.DEFAULT) {
cmd.append(" RECFM ");
switch (recfm) {
case FIXED: cmd.append("F"); break;
case VARIABLE: cmd.append("V"); break;
default: break;
}
if (lrecl > 0) {
cmd.append(" LRECL ").append(lrecl);
}
}
}
}
// Additional options
if (otherOptions != null) {
cmd.append(" ").append(otherOptions);
}
return cmd.toString().trim();
}
}
@@ -0,0 +1,200 @@
package org.lib3270j.ft;
/**
* Constants for IND$FILE file transfer protocol.
* Ported from x3270: ft_cut_ds.h, ft_dft_ds.h, ft.c
*/
public final class FTConstants {
private FTConstants() {} // utility class
// ========== CUT Mode Frame Layout ==========
/** Offset to the CUT structured field at the end of the screen */
public static final int O_SF = 1919;
// Primary area offsets
public static final int O_FRAME_TYPE = 0;
// Control Code frame (host → terminal)
public static final int FT_CONTROL_CODE = 0xC3;
public static final int O_CC_FRAME_SEQ = 1;
public static final int O_CC_STATUS_CODE = 2;
public static final int O_CC_MESSAGE = 4;
// Control Code status codes
public static final int SC_HOST_ACK = 0x8181;
public static final int SC_XFER_COMPLETE = 0x8189;
public static final int SC_ABORT_FILE = 0x8194;
public static final int SC_ABORT_XMIT = 0x8198;
// Data Request frame (host → terminal, for uploads)
public static final int FT_DATA_REQUEST = 0xC2;
public static final int O_DR_SF = 1;
public static final int O_DR_DATA_CODE = 2;
public static final int O_DR_FRAME_SEQ = 3;
// Retransmit frame
public static final int FT_RETRANSMIT = 0x4C;
// Data frame (bidirectional)
public static final int FT_DATA = 0xC1;
public static final int O_DT_FRAME_SEQ = 1;
public static final int O_DT_CSUM = 2;
public static final int O_DT_LEN = 3;
public static final int O_DT_DATA = 5;
// Response Area (near end of screen)
public static final int O_RESPONSE = O_SF - 5;
public static final int RO_FRAME_TYPE = O_RESPONSE + 1;
public static final int RO_FRAME_SEQ = O_RESPONSE + 2;
public static final int RO_REASON_CODE = O_RESPONSE + 3;
// Response frame types
public static final int RFT_RETRANSMIT = 0x4C;
public static final int RFT_CONTROL_CODE = 0xC3;
// Special EOF data markers
public static final int EOF_DATA1 = 0x5C;
public static final int EOF_DATA2 = 0xA9;
// Upload data area offsets
public static final int O_UP_DATA_CODE = 2;
public static final int O_UP_FRAME_SEQ = 3;
public static final int O_UP_CSUM = 4;
public static final int O_UP_LEN = 5;
public static final int O_UP_DATA = 7;
public static final int O_UP_MAX = O_SF - O_UP_DATA;
// ========== CUT Mode AID codes ==========
public static final int ACK_OK = 0x7D; // AID_ENTER
public static final int ACK_RETRANSMIT = 0xF1; // AID_PF1
public static final int ACK_RESYNC_VM = 0x6D; // AID_CLEAR
public static final int ACK_RESYNC_TSO = 0x6E; // AID_PA2
public static final int ACK_ABORT = 0xF2; // AID_PF2
// ========== DFT Mode Structured Field Codes ==========
/** Structured field type for file transfer data */
public static final int SF_TRANSFER_DATA = 0xD0;
// Host requests
public static final int TR_OPEN_REQ = 0x0012;
public static final int TR_CLOSE_REQ = 0x4112;
public static final int TR_SET_CUR_REQ = 0x4511;
public static final int TR_GET_REQ = 0x4611;
public static final int TR_INSERT_REQ = 0x4711;
public static final int TR_DATA_INSERT = 0x4704;
// PC replies
public static final int TR_GET_REPLY = 0x4605;
public static final int TR_NORMAL_REPLY = 0x4705;
public static final int TR_ERROR_REPLY = 0x08; // low 8 bits
public static final int TR_CLOSE_REPLY = 0x4109;
// Other headers
public static final int TR_RECNUM_HDR = 0x6306;
public static final int TR_ERROR_HDR = 0x6904;
public static final int TR_NOT_COMPRESSED = 0xC080;
public static final int TR_BEGIN_DATA = 0x61;
// Error codes
public static final int TR_ERR_EOF = 0x2200;
public static final int TR_ERR_CMDFAIL = 0x0100;
// DFT buffer size limits
public static final int DFT_MIN_BUF = 256;
public static final int DFT_MAX_BUF = 32768;
public static final int DFT_BUF = 4096;
// AID for structured field response
public static final int AID_SF = 0x88;
// ========== Transfer State ==========
public enum FTState {
NONE, // No transfer in progress
AWAIT_ACK, // IND$FILE sent, awaiting acknowledgement
RUNNING, // Ack received, data flowing
ABORT_WAIT, // Awaiting chance to send an abort
ABORT_SENT // Abort sent; awaiting response
}
// ========== Encoding Tables ==========
/**
* Base-64-like encoding table used by CUT mode for lengths and checksums.
* 64 characters: a-z, &, -, ., :, +, A-Z, 0-5
*/
public static final String TABLE6 =
"abcdefghijklmnopqrstuvwxyz&-.,:+ABCDEFGHIJKLMNOPQRSTUVWXYZ012345";
/**
* IND$FILE's fixed ASCII-to-EBCDIC translation table (i_asc2ft).
* This is NOT the standard CP037 mapping — it's IND$FILE's own table.
* Used when remap=true to invert IND$FILE's built-in translation.
*/
public static final int[] ASC2FT = {
0x00,0x01,0x02,0x03,0x37,0x2d,0x2e,0x2f,0x16,0x05,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
0x10,0x11,0x12,0x13,0x3c,0x3d,0x32,0x26,0x18,0x19,0x3f,0x27,0x1c,0x1d,0x1e,0x1f,
0x40,0x5a,0x7f,0x7b,0x5b,0x6c,0x50,0x7d,0x4d,0x5d,0x5c,0x4e,0x6b,0x60,0x4b,0x61,
0xf0,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0x7a,0x5e,0x4c,0x7e,0x6e,0x6f,
0x7c,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xd1,0xd2,0xd3,0xd4,0xd5,0xd6,
0xd7,0xd8,0xd9,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0x4a,0xe0,0x4f,0x5f,0x6d,
0x79,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x91,0x92,0x93,0x94,0x95,0x96,
0x97,0x98,0x99,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xc0,0x6a,0xd0,0xa1,0x07,
0x20,0x21,0x22,0x23,0x24,0x15,0x06,0x17,0x28,0x29,0x2a,0x2b,0x2c,0x09,0x0a,0x1b,
0x30,0x31,0x1a,0x33,0x34,0x35,0x36,0x08,0x38,0x39,0x3a,0x3b,0x04,0x14,0x3e,0xe1,
0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x51,0x52,0x53,0x54,0x55,0x56,0x57,
0x58,0x59,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x70,0x71,0x72,0x73,0x74,0x75,
0x76,0x77,0x78,0x80,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,0x90,0x9a,0x9b,0x9c,0x9d,0x9e,
0x9f,0xa0,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,
0xb8,0xb9,0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xca,0xcb,0xcc,0xcd,0xce,0xcf,0xda,0xdb,
0xdc,0xdd,0xde,0xdf,0xea,0xeb,0xec,0xed,0xee,0xef,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
};
/**
* IND$FILE's fixed EBCDIC-to-ASCII translation table (i_ft2asc).
* The inverse of ASC2FT.
*/
public static final int[] FT2ASC = {
0x00,0x01,0x02,0x03,0x9c,0x09,0x86,0x7f,0x97,0x8d,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,
0x10,0x11,0x12,0x13,0x9d,0x85,0x08,0x87,0x18,0x19,0x92,0x8f,0x1c,0x1d,0x1e,0x1f,
0x80,0x81,0x82,0x83,0x84,0x00,0x17,0x1b,0x88,0x89,0x8a,0x8b,0x8c,0x05,0x06,0x07,
0x90,0x91,0x16,0x93,0x94,0x95,0x96,0x04,0x98,0x99,0x9a,0x9b,0x14,0x15,0x9e,0x1a,
0x20,0xa0,0xa1,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0x5b,0x2e,0x3c,0x28,0x2b,0x5d,
0x26,0xa9,0xaa,0xab,0xac,0xad,0xae,0xaf,0xb0,0xb1,0x21,0x24,0x2a,0x29,0x3b,0x5e,
0x2d,0x2f,0xb2,0xb3,0xb4,0xb5,0xb6,0xb7,0xb8,0xb9,0x7c,0x2c,0x25,0x5f,0x3e,0x3f,
0xba,0xbb,0xbc,0xbd,0xbe,0xbf,0xc0,0xc1,0xc2,0x60,0x3a,0x23,0x40,0x27,0x3d,0x22,
0xc3,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,
0xca,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xcb,0xcc,0xcd,0xce,0xcf,0xd0,
0xd1,0x7e,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,
0xd8,0xd9,0xda,0xdb,0xdc,0xdd,0xde,0xdf,0xe0,0xe1,0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,
0x7b,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0xe8,0xe9,0xea,0xeb,0xec,0xed,
0x7d,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f,0x50,0x51,0x52,0xee,0xef,0xf0,0xf1,0xf2,0xf3,
0x5c,0x9f,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5a,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,
0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0xfa,0xfb,0xfc,0xfd,0xfe,0xff
};
// ========== Utility Methods ==========
/**
* Decode a CUT-mode base-64 encoded integer from EBCDIC.
* Converts a table6-encoded EBCDIC character to its 6-bit value.
*/
public static int from6(int ebcdicByte) {
// First convert EBCDIC to ASCII via IND$FILE's table
int ascii = FT2ASC[ebcdicByte & 0xFF];
int idx = TABLE6.indexOf((char) ascii);
return idx >= 0 ? idx : 0;
}
/**
* Encode a 6-bit value into a table6-encoded EBCDIC character.
*/
public static int to6(int value) {
char ascii = TABLE6.charAt(value & 0x3F);
return ASC2FT[ascii & 0xFF];
}
}
@@ -0,0 +1,587 @@
package org.lib3270j.ft;
import org.lib3270j.screen.ScreenBuffer;
import org.lib3270j.input.InputProcessor;
import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*;
import static org.lib3270j.protocol.DS3270Constants.*;
import java.io.*;
import java.util.logging.Logger;
/**
* CUT (Character Unit Transfer) mode file transfer handler.
* Data flows through the screen buffer using a framed protocol.
* Ported from x3270's ft_cut.c.
*/
public class FTCut {
private static final Logger log = Logger.getLogger(FTCut.class.getName());
/** Callback for transfer events */
public interface FTCutListener {
void onTransferRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
FTConstants.FTState getCurrentState();
void setState(FTConstants.FTState state);
FTConfig getConfig();
File getLocalFile();
}
private final ScreenBuffer screen;
private final InputProcessor input;
private final EbcdicTranslator translator;
private final FTCutListener listener;
// CUT mode state
private boolean xferInProgress = false;
private long expandedLength = 0;
private int quadrant = -1;
private boolean cutEof = false;
// Upload translation buffer
private static final int XLATE_NBUF = 32;
private int xlateBuffered = 0;
private int xlateBufIx = 0;
private final int[] xlateBuf = new int[XLATE_NBUF];
private static final String ALPHAS = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%&_()<+,-./:>?";
private static final int SELECTOR_0 = 0x5E; // ';'
private static final int SELECTOR_1 = 0x7E; // '='
private static final int SELECTOR_2 = 0x5C; // '*'
private static final int SELECTOR_3 = 0x7D; // '\''
private static final int[] XLATE_0 = {
0x40,0xc1,0xc2,0xc3, 0xc4,0xc5,0xc6,0xc7, 0xc8,0xc9,0xd1,0xd2,
0xd3,0xd4,0xd5,0xd6, 0xd7,0xd8,0xd9,0xe2, 0xe3,0xe4,0xe5,0xe6,
0xe7,0xe8,0xe9,0x81, 0x82,0x83,0x84,0x85, 0x86,0x87,0x88,0x89,
0x91,0x92,0x93,0x94, 0x95,0x96,0x97,0x98, 0x99,0xa2,0xa3,0xa4,
0xa5,0xa6,0xa7,0xa8, 0xa9,0xf0,0xf1,0xf2, 0xf3,0xf4,0xf5,0xf6,
0xf7,0xf8,0xf9,0x6c, 0x50,0x6d,0x4d,0x5d, 0x4c,0x4e,0x6b,0x60,
0x4b,0x61,0x7a,0x6e, 0x6f
};
private static final int[] XLATE_1 = {
0x20,0x41,0x42,0x43, 0x44,0x45,0x46,0x47, 0x48,0x49,0x4a,0x4b,
0x4c,0x4d,0x4e,0x4f, 0x50,0x51,0x52,0x53, 0x54,0x55,0x56,0x57,
0x58,0x59,0x5a,0x61, 0x62,0x63,0x64,0x65, 0x66,0x67,0x68,0x69,
0x6a,0x6b,0x6c,0x6d, 0x6e,0x6f,0x70,0x71, 0x72,0x73,0x74,0x75,
0x76,0x77,0x78,0x79, 0x7a,0x30,0x31,0x32, 0x33,0x34,0x35,0x36,
0x37,0x38,0x39,0x25, 0x26,0x27,0x28,0x29, 0x2a,0x2b,0x2c,0x2d,
0x2e,0x2f,0x3a,0x3b, 0x3f
};
private static final int[] XLATE_2 = {
0x00,0x00,0x01,0x02, 0x03,0x04,0x05,0x06, 0x07,0x08,0x09,0x0a,
0x0b,0x0c,0x0d,0x0e, 0x0f,0x10,0x11,0x12, 0x13,0x14,0x15,0x16,
0x17,0x18,0x19,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00, 0x00,0x3c,0x3d,0x3e, 0x00,0xfa,0xfb,0xfc,
0xfd,0xfe,0xff,0x7b, 0x7c,0x7d,0x7e,0x7f, 0x1a,0x1b,0x1c,0x1d,
0x1e,0x1f,0x00,0x00, 0x00
};
private static final int[] XLATE_3 = {
0x00,0xa0,0xa1,0xea, 0xeb,0xec,0xed,0xee, 0xef,0xe0,0xe1,0xaa,
0xab,0xac,0xad,0xae, 0xaf,0xb0,0xb1,0xb2, 0xb3,0xb4,0xb5,0xb6,
0xb7,0xb8,0xb9,0x80, 0x00,0xca,0xcb,0xcc, 0xcd,0xce,0xcf,0xc0,
0x00,0x8a,0x8b,0x8c, 0x8d,0x8e,0x8f,0x90, 0x00,0xda,0xdb,0xdc,
0xdd,0xde,0xdf,0xd0, 0x00,0x00,0x21,0x22, 0x23,0x24,0x5b,0x5c,
0x00,0x5e,0x5f,0x00, 0x9c,0x9d,0x9e,0x9f, 0xba,0xbb,0xbc,0xbd,
0xbe,0xbf,0x9a,0x9b, 0x00
};
private static final int[][] QUADS = { XLATE_0, XLATE_1, XLATE_2, XLATE_3 };
private static final int[] SELECTORS = { SELECTOR_0, SELECTOR_1, SELECTOR_2, SELECTOR_3 };
// File I/O
private InputStream inputStream;
private OutputStream outputStream;
private boolean lastCr = false;
public FTCut(ScreenBuffer screen, InputProcessor input,
EbcdicTranslator translator, FTCutListener listener) {
this.screen = screen;
this.input = input;
this.translator = translator;
this.listener = listener;
}
/**
* Process a CUT-mode screen update.
* Called by the transfer coordinator when the screen changes during a transfer.
*/
public void processScreenUpdate() {
if (listener.getCurrentState() == FTState.NONE) return;
// CUT frames MUST have a skip field attribute at O_SF (1919)
byte sfAttr = screen.getCellFAByte(O_SF);
if (sfAttr == 0 || !isSkip(sfAttr)) {
// Not a CUT frame (likely local echo or intermediate screen), ignore
return;
}
int frameType = screen.getCellEC(O_FRAME_TYPE);
switch (frameType) {
case FT_CONTROL_CODE:
cutControlCode();
break;
case FT_DATA_REQUEST:
cutDataRequest();
break;
case FT_RETRANSMIT:
cutRetransmit();
break;
case FT_DATA:
cutData();
break;
default:
log.fine("Ignoring non-CUT frame type 0x" + Integer.toHexString(frameType));
return;
}
}
private boolean isSkip(byte attr) {
return (attr & FA_PROTECT) != 0 && (attr & FA_NUMERIC) != 0;
}
/**
* Initialize CUT mode for a new transfer.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
xferInProgress = false;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
lastCr = false;
if (config.isReceive()) {
boolean append = config.isAppend();
outputStream = new FileOutputStream(localFile, append);
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
/**
* Clean up CUT mode resources.
*/
public void cleanup() {
xferInProgress = false;
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
}
// ========== Control Code Processing ==========
private void cutControlCode() {
int code = (screen.getCellEC(O_CC_STATUS_CODE) << 8) |
screen.getCellEC(O_CC_STATUS_CODE + 1);
log.fine("CUT: CONTROL_CODE 0x" + Integer.toHexString(code));
switch (code) {
case SC_HOST_ACK:
log.info("CUT: HOST_ACK received — transfer running");
xferInProgress = true;
expandedLength = 0;
quadrant = -1;
xlateBuffered = 0;
xlateBufIx = 0;
cutEof = false;
cutAck();
listener.onTransferRunning();
break;
case SC_XFER_COMPLETE:
log.info("CUT: Transfer complete");
cutAck();
xferInProgress = false;
listener.onTransferComplete(null);
break;
case SC_ABORT_FILE:
case SC_ABORT_XMIT:
log.warning("CUT: ABORT received");
xferInProgress = false;
cutAck();
// Extract error message from the host (positions 4-83)
String msg = extractHostMessage();
listener.onTransferAborted(msg);
break;
default:
log.warning("CUT: Unknown control code 0x" + Integer.toHexString(code));
cutAbort("Unknown CUT control code", SC_ABORT_XMIT);
break;
}
}
/**
* Extract the error message from the host's control code frame.
* The message starts at O_CC_MESSAGE and is up to 80 EBCDIC characters.
*/
private String extractHostMessage() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 80; i++) {
int ebc = screen.getCellEC(O_CC_MESSAGE + i);
if (ebc == 0) continue;
char ch = translator.ebcdicToUnicode(ebc);
sb.append(ch);
}
// Trim trailing spaces and '$'
String msg = sb.toString().stripTrailing();
if (msg.endsWith("$")) {
msg = msg.substring(0, msg.length() - 1).stripTrailing();
}
return msg.isEmpty() ? "Host cancelled transfer" : msg;
}
// ========== Data Request (Upload) ==========
/**
* Process a data request from the host (upload: send data to host).
*/
private void cutDataRequest() {
int seq = screen.getCellEC(O_DR_FRAME_SEQ);
log.fine("CUT: DATA_REQUEST seq=" + from6(seq));
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
return;
}
FTConfig config = listener.getConfig();
// Read data from local file into screen buffer
int count = 0;
try {
while (count < O_UP_MAX && !cutEof) {
int c = xlateGetc(config);
if (c == -1) {
cutEof = true;
break;
}
screen.setCell(O_UP_DATA + count, c);
count++;
}
} catch (IOException e) {
log.warning("CUT: Read error: " + e.getMessage());
cutAbort("Read error: " + e.getMessage(), SC_ABORT_FILE);
return;
}
// EOF with no data → send EOF marker
if (count == 0 && cutEof) {
screen.setCell(O_UP_DATA, EOF_DATA1);
screen.setCell(O_UP_DATA + 1, EOF_DATA2);
count = 2;
}
// Compute frame fields
screen.setCell(O_UP_FRAME_SEQ, seq);
// Checksum
int cs = 0;
for (int i = 0; i < count; i++) {
cs ^= screen.getCellEC(O_UP_DATA + i);
}
screen.setCell(O_UP_CSUM, to6(cs));
screen.setCell(O_UP_LEN, to6((count >> 6) & 0x3F));
screen.setCell(O_UP_LEN + 1, to6(count & 0x3F));
// Hide the data field (change SF attribute to zero intensity)
byte attr = screen.getCellFAByte(O_DR_SF);
attr = (byte) ((attr & ~FA_INTENSITY) | FA_INT_ZERO_NSEL);
screen.setCellFA(O_DR_SF, attr);
// Send it
log.fine("CUT: > DATA seq=" + from6(seq) + " len=" + count);
expandedLength += count;
listener.onBytesTransferred(expandedLength);
input.sendAidForFT(AID_ENTER);
}
// ========== Data (Download) ==========
/**
* Process data from the host (download: receive data from host).
*/
private void cutData() {
log.fine("CUT: DATA");
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
cutAbort("Transfer cancelled by user", SC_ABORT_FILE);
return;
}
FTConfig config = listener.getConfig();
// Extract raw data
int rawLength = (from6(screen.getCellEC(O_DT_LEN)) << 6) |
from6(screen.getCellEC(O_DT_LEN + 1));
if (rawLength > O_RESPONSE - O_DT_DATA) {
cutAbort("Oversized CUT data frame", SC_ABORT_XMIT);
return;
}
byte[] rawData = new byte[rawLength];
for (int i = 0; i < rawLength; i++) {
rawData[i] = (byte) screen.getCellEC(O_DT_DATA + i);
}
// Check for EOF marker
if (rawLength == 2 && (rawData[0] & 0xFF) == EOF_DATA1 &&
(rawData[1] & 0xFF) == EOF_DATA2) {
log.fine("CUT: EOF marker received");
cutAck();
return;
}
// Convert and write to local file
try {
byte[] converted = convertDownload(rawData, rawLength, config);
if (outputStream != null) {
outputStream.write(converted);
expandedLength += converted.length;
listener.onBytesTransferred(expandedLength);
}
cutAck();
} catch (IOException e) {
log.warning("CUT: Write error: " + e.getMessage());
cutAbort("Write error: " + e.getMessage(), SC_ABORT_FILE);
}
}
// ========== Retransmit ==========
private void cutRetransmit() {
log.warning("CUT: RETRANSMIT (not supported, aborting)");
cutAbort("Retransmit not supported", SC_ABORT_XMIT);
}
// ========== Acknowledge ==========
private void cutAck() {
log.fine("CUT: > ACK (Enter)");
input.sendAidForFT(AID_ENTER);
}
// ========== Abort ==========
private void cutAbort(String message, int reason) {
log.warning("CUT: ABORT — " + message);
// Write abort frame to the response area
screen.setCell(RO_FRAME_TYPE, RFT_CONTROL_CODE);
screen.setCell(RO_FRAME_SEQ, screen.getCellEC(O_DT_FRAME_SEQ));
screen.setCell(RO_REASON_CODE, (reason >> 8) & 0xFF);
screen.setCell(RO_REASON_CODE + 1, reason & 0xFF);
// Send PF2 to signal abort
input.sendAidForFT(AID_PF2);
listener.onTransferAborted(message);
}
// ========== Character Translation ==========
/**
* Convert received EBCDIC data to local format.
* Handles ASCII mode (with optional CR processing and remapping)
* and binary mode (raw passthrough).
*
* The quadrant tables decode wire bytes into IND$FILE's "pseudo-ASCII"
* representation. For the remap path, control codes (< 0x20 and
* 0x80-0x9E) are treated as direct Unicode codepoints, while printable
* characters are re-mapped through ASC2FT (inverting IND$FILE's built-in
* EBCDIC→ASCII table) back to real EBCDIC, then to Unicode.
* This matches x3270's upload_convert() logic.
*/
private byte[] convertDownload(byte[] rawData, int length, FTConfig config)
throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(length * 2);
for (int i = 0; i < length; i++) {
int c = rawData[i] & 0xFF;
while (true) {
if (quadrant < 0) {
// Find the quadrant
for (quadrant = 0; quadrant < 4; quadrant++) {
if (c == SELECTORS[quadrant]) break;
}
if (quadrant >= 4) {
throw new IOException("CUT conversion error (quadrant selector not found)");
}
break; // continue outer loop
}
if (c < 0x40 || c > 0xF9) {
throw new IOException("CUT conversion error (data out of bounds)");
}
char asciiChar = translator.ebcdicToUnicode(c);
int ix = ALPHAS.indexOf(asciiChar);
if (ix < 0) {
// Try a different quadrant
quadrant = -1;
continue; // retry loop
}
if (quadrant != 2 && c != 0xC1 && QUADS[quadrant][ix] == 0) {
// Try a different quadrant
quadrant = -1;
continue; // retry
}
// Map the character — this produces IND$FILE's pseudo-ASCII byte
int decoded = QUADS[quadrant][ix];
if (config.isAscii() && config.isCrFlag() && (decoded == 0x0D || decoded == 0x1A)) {
break; // Ignore bare CR and EOF markers in ASCII stream
}
if (!config.isAscii() || !config.isRemapFlag()) {
// Binary or ASCII-no-remap: emit the decoded byte as-is
out.write(decoded);
break;
}
/*
* ASCII with remap: invert IND$FILE's EBCDIC→ASCII translation.
*
* Control codes (< 0x20) and high control range (0x80-0x9E,
* except 0x9F) are treated as direct Unicode codepoints.
* IND$FILE maps EBCDIC 0xE1 to pseudo-ASCII 0x9F, so
* 0xFF is the special marker that maps back to U+009F.
* Everything else is a printable character that needs
* the ASC2FT→ebcdicToUnicode remap chain.
*/
if (decoded < 0x20 || (decoded >= 0x80 && decoded < 0xA0 && decoded != 0x9F)) {
// Control code — emit as its Unicode codepoint directly
out.write(String.valueOf((char) decoded).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
} else if (decoded == 0xFF) {
// Special case: 0xFF → U+009F
out.write(String.valueOf((char) 0x9F).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
} else {
// Displayable character: invert IND$FILE's table
int ebc = FTConstants.ASC2FT[decoded & 0xFF];
char unicodeChar = translator.ebcdicToUnicode(ebc);
out.write(String.valueOf(unicodeChar).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
}
break;
} // end while(true) chunk
}
return out.toByteArray();
}
private int xlateGetc(FTConfig config) throws IOException {
// Return buffered data first
if (xlateBuffered > 0) {
int r = xlateBuf[xlateBufIx++];
xlateBuffered--;
return r;
}
if (inputStream == null) return -1;
int c = inputStream.read();
if (c == -1) return -1;
int localByte = c & 0xFF;
int nc = 0;
int[] cbuf = new int[4]; // max 4 bytes (2 for \r + 2 for \n if quadrant encoded)
if (config.isAscii()) {
if (config.isCrFlag() && !lastCr && localByte == '\n') {
nc += uploadConvert('\r', cbuf, nc, config);
}
lastCr = (localByte == '\r');
}
nc += uploadConvert(localByte, cbuf, nc, config);
if (nc > 1) {
for (int i = 1; i < nc; i++) {
xlateBuf[xlateBuffered++] = cbuf[i];
}
xlateBufIx = 0;
}
return cbuf[0];
}
private int uploadConvert(int localByte, int[] cbuf, int offset, FTConfig config) {
int ebc;
if (localByte == 0) {
// Nulls are special in the 'OTHER_2' quadrant
if (quadrant != 2) {
quadrant = 2; // OTHER_2
cbuf[offset] = SELECTORS[quadrant];
cbuf[offset + 1] = 0xC1; // XLATE_NULL
return 2;
} else {
cbuf[offset] = 0xC1;
return 1;
}
}
if (!config.isAscii() || !config.isRemapFlag()) {
// Binary or Ascii without remap: treat byte directly
ebc = localByte & 0xFF;
} else {
// Ascii with remap: locally read ASCII translates to actual EBCDIC, then maps to IND$FILE's pseudo-ASCII
int standardEbc = translator.unicodeToEbcdic((char) localByte);
if (standardEbc < 0) {
standardEbc = 0x40; // Space as fallback
}
ebc = org.lib3270j.ft.FTConstants.FT2ASC[standardEbc & 0xFF];
}
return storeUpload(ebc, cbuf, offset);
}
private int storeUpload(int ebc, int[] obBuf, int offset) {
if (quadrant >= 0) {
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == ebc) {
obBuf[offset] = translator.unicodeToEbcdic(ALPHAS.charAt(i));
return 1;
}
}
}
int oq = quadrant;
for (quadrant = 0; quadrant < 4; quadrant++) {
if (quadrant == oq) continue;
for (int i = 0; i < 77; i++) {
if (QUADS[quadrant][i] == ebc) {
obBuf[offset] = SELECTORS[quadrant];
obBuf[offset + 1] = translator.unicodeToEbcdic(ALPHAS.charAt(i));
return 2;
}
}
}
quadrant = -1;
// Fallback safety measure
obBuf[offset] = translator.unicodeToEbcdic('?');
return 1;
}
}
@@ -0,0 +1,496 @@
package org.lib3270j.ft;
import org.lib3270j.input.InputProcessor;
import org.lib3270j.charset.EbcdicTranslator;
import static org.lib3270j.ft.FTConstants.*;
import java.io.*;
import java.util.logging.Logger;
/**
* DFT (Distributed Function Terminal) mode file transfer handler.
* Uses Structured Fields (SF_TRANSFER_DATA = 0xD0) for data exchange.
* Offers better performance than CUT mode with configurable buffer sizes.
* Ported from x3270's ft_dft.c.
*/
public class FTDft {
private static final Logger log = Logger.getLogger(FTDft.class.getName());
/** Callback for transfer events (shared interface with CUT) */
public interface FTDftListener {
void onTransferRunning();
void onTransferComplete(String errorMessage);
void onTransferAborted(String errorMessage);
void onBytesTransferred(long bytes);
FTConstants.FTState getCurrentState();
void setState(FTConstants.FTState state);
FTConfig getConfig();
File getLocalFile();
}
private final InputProcessor input;
private final EbcdicTranslator translator;
private final FTDftListener listener;
// DFT state
private long recnum = 0;
private boolean dftEof = false;
private boolean messageFlag = false;
private long bytesTransferred = 0;
// Savebuf for Read Modified retransmit
private byte[] dftSaveBuf = null;
private int dftSaveBufLen = 0;
// File I/O
private InputStream inputStream;
private OutputStream outputStream;
private boolean lastCr = false;
public FTDft(InputProcessor input, EbcdicTranslator translator,
FTDftListener listener) {
this.input = input;
this.translator = translator;
this.listener = listener;
}
/**
* Initialize DFT mode for a new transfer.
*/
public void initTransfer(File localFile) throws IOException {
FTConfig config = listener.getConfig();
recnum = 0;
dftEof = false;
messageFlag = false;
bytesTransferred = 0;
dftSaveBuf = null;
dftSaveBufLen = 0;
lastCr = false;
if (config.isReceive()) {
outputStream = new FileOutputStream(localFile, config.isAppend());
inputStream = null;
} else {
inputStream = new FileInputStream(localFile);
outputStream = null;
}
}
/**
* Clean up DFT mode resources.
*/
public void cleanup() {
try {
if (inputStream != null) { inputStream.close(); inputStream = null; }
if (outputStream != null) { outputStream.close(); outputStream = null; }
} catch (IOException e) {
log.warning("Error closing file: " + e.getMessage());
}
dftSaveBuf = null;
dftSaveBufLen = 0;
}
/**
* Process a DFT structured field from the host.
* Called by DataStreamProcessor when SF type is SF_TRANSFER_DATA (0xD0).
*
* @param data raw structured field data
* @param offset start of the SF (after length + type byte)
* @param length total SF length
*/
public void processStructuredField(byte[] data, int offset, int length) {
// The SF payload starts after the 2-byte length + 1-byte SF type
int payloadStart = offset + 3;
if (payloadStart + 2 > offset + length) {
log.warning("DFT: SF too short");
return;
}
// First 2 bytes of payload are the DFT request code
int requestCode = ((data[payloadStart] & 0xFF) << 8) |
(data[payloadStart + 1] & 0xFF);
log.fine("DFT: request code 0x" + Integer.toHexString(requestCode));
switch (requestCode) {
case TR_OPEN_REQ:
dftOpenRequest();
break;
case TR_INSERT_REQ:
dftInsertRequest(data, payloadStart, offset + length - payloadStart);
break;
case TR_DATA_INSERT:
dftDataInsert(data, payloadStart, offset + length - payloadStart);
break;
case TR_SET_CUR_REQ:
// No-op, same as x3270
log.fine("DFT: SetCursor (ignored)");
break;
case TR_GET_REQ:
dftGetRequest();
break;
case TR_CLOSE_REQ:
dftCloseRequest();
break;
default:
log.warning("DFT: Unknown request code 0x" + Integer.toHexString(requestCode));
dftAbort("Unknown DFT request", TR_DATA_INSERT);
break;
}
}
// ========== Open Request ==========
private void dftOpenRequest() {
log.fine("DFT: Open");
listener.onTransferRunning();
// Send acknowledgement
dftDataAck();
}
// ========== Insert Request (host sending data for download) ==========
private void dftInsertRequest(byte[] data, int offset, int length) {
log.fine("DFT: Insert");
dftDataInsert(data, offset, length);
}
private void dftDataInsert(byte[] data, int offset, int length) {
FTConfig config = listener.getConfig();
if (listener.getCurrentState() == FTState.ABORT_WAIT) {
dftAbort("Transfer cancelled by user", TR_DATA_INSERT);
return;
}
// Parse the SF payload to find data
// Skip the 2-byte request code
int pos = offset + 2;
int end = offset + length;
// Look for TR_BEGIN_DATA marker
while (pos < end) {
int headerCode = data[pos] & 0xFF;
if (headerCode == TR_BEGIN_DATA) {
// Next 2 bytes are data length (including the 3-byte header)
if (pos + 3 > end) break;
int dataLen = ((data[pos + 1] & 0xFF) << 8) | (data[pos + 2] & 0xFF);
int actualDataLen = dataLen - 3; // subtract header
pos += 3;
if (actualDataLen > 0 && pos + actualDataLen <= end) {
try {
writeDownloadData(data, pos, actualDataLen, config);
bytesTransferred += actualDataLen;
listener.onBytesTransferred(bytesTransferred);
} catch (IOException e) {
dftAbort("Write error: " + e.getMessage(), TR_DATA_INSERT);
return;
}
}
pos += Math.max(0, actualDataLen);
} else if (pos + 1 < end) {
// Skip other headers (2-byte code + length-based skip)
// Most headers are 4-6 bytes, but we just skip based on known patterns
int hdrCode16 = ((data[pos] & 0xFF) << 8) | (data[pos + 1] & 0xFF);
if (hdrCode16 == TR_RECNUM_HDR) {
pos += 6; // 2-byte code + 4-byte record number
} else if (hdrCode16 == TR_NOT_COMPRESSED) {
pos += 2;
} else {
pos += 2; // skip unknown 2-byte header
}
} else {
pos++;
}
}
// Send acknowledgement
dftDataAck();
}
/**
* Write download data to the local file, handling ASCII conversion.
*/
private void writeDownloadData(byte[] data, int offset, int length,
FTConfig config) throws IOException {
if (outputStream == null) return;
if (!config.isAscii()) {
// Binary: write raw
outputStream.write(data, offset, length);
return;
}
// ASCII mode with optional CR stripping and remapping
for (int i = 0; i < length; i++) {
int b = data[offset + i] & 0xFF;
if (config.isRemapFlag()) {
// Use IND$FILE's EBCDIC→ASCII table
int ascii = FT2ASC[b];
if (config.isCrFlag()) {
if (ascii == '\r') {
lastCr = true;
continue;
}
if (lastCr) {
lastCr = false;
if (ascii == '\n') {
outputStream.write('\n');
continue;
}
outputStream.write('\r');
}
}
outputStream.write(ascii);
} else {
// Standard EBCDIC→Unicode
char ch = translator.ebcdicToUnicode(b);
if (config.isCrFlag()) {
if (ch == '\r') { lastCr = true; continue; }
if (lastCr) {
lastCr = false;
if (ch == '\n') { outputStream.write('\n'); continue; }
outputStream.write('\r');
}
}
outputStream.write(String.valueOf(ch).getBytes(
java.nio.charset.StandardCharsets.UTF_8));
}
}
}
// ========== Get Request (host wants data for upload) ==========
private void dftGetRequest() {
FTConfig config = listener.getConfig();
log.fine("DFT: Get");
if (!messageFlag && listener.getCurrentState() == FTState.ABORT_WAIT) {
dftAbort("Transfer cancelled by user", TR_GET_REQ);
return;
}
int bufferSize = config.getDftBufferSize();
int numbytes = bufferSize - 27; // reserve space for headers
byte[] readBuf = new byte[numbytes];
int totalRead = 0;
try {
while (!dftEof && totalRead < numbytes) {
if (config.isAscii() && (config.isRemapFlag() || config.isCrFlag())) {
int b = dftAsciiRead(config);
if (b == -1) {
dftEof = true;
break;
}
readBuf[totalRead++] = (byte) b;
} else {
// Binary read
if (inputStream == null) { dftEof = true; break; }
int n = inputStream.read(readBuf, totalRead, numbytes - totalRead);
if (n <= 0) {
dftEof = true;
break;
}
totalRead += n;
}
}
} catch (IOException e) {
dftAbort("Read error: " + e.getMessage(), TR_GET_REQ);
return;
}
// Build SF response
ByteArrayOutputStream out = new ByteArrayOutputStream(bufferSize);
out.write(AID_SF); // AID byte
// Placeholder for SF length (2 bytes, filled in later)
int sfLenPos = out.size();
out.write(0); out.write(0);
out.write(SF_TRANSFER_DATA); // SF type
if (totalRead > 0) {
log.fine("DFT: > GetReply rec=" + recnum + " " + totalRead + " bytes");
// TR_GET_REPLY
out.write((TR_GET_REPLY >> 8) & 0xFF);
out.write(TR_GET_REPLY & 0xFF);
// Record number header
out.write((TR_RECNUM_HDR >> 8) & 0xFF);
out.write(TR_RECNUM_HDR & 0xFF);
out.write((int) ((recnum >> 24) & 0xFF));
out.write((int) ((recnum >> 16) & 0xFF));
out.write((int) ((recnum >> 8) & 0xFF));
out.write((int) (recnum & 0xFF));
recnum++;
// Not compressed
out.write((TR_NOT_COMPRESSED >> 8) & 0xFF);
out.write(TR_NOT_COMPRESSED & 0xFF);
// Begin data
out.write(TR_BEGIN_DATA);
int dataFieldLen = totalRead + 5;
out.write((dataFieldLen >> 8) & 0xFF);
out.write(dataFieldLen & 0xFF);
// The actual data
out.write(readBuf, 0, totalRead);
bytesTransferred += totalRead;
} else {
log.fine("DFT: > GetReply EOF");
// EOF reply
out.write((TR_GET_REQ >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
// Error header
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
// EOF error code
out.write((TR_ERR_EOF >> 8) & 0xFF);
out.write(TR_ERR_EOF & 0xFF);
dftEof = true;
}
// Set the SF length
byte[] result = out.toByteArray();
int sfLen = result.length - 1; // exclude AID byte
result[sfLenPos] = (byte) ((sfLen >> 8) & 0xFF);
result[sfLenPos + 1] = (byte) (sfLen & 0xFF);
// Save for potential Read Modified retransmit
dftSaveBuf = result.clone();
dftSaveBufLen = result.length;
// Send it
input.sendStructuredFieldData(result);
listener.onBytesTransferred(bytesTransferred);
}
/**
* Read a byte from the local file in ASCII mode with CR expansion and remapping.
* Returns -1 for EOF.
*/
private int dftAsciiRead(FTConfig config) throws IOException {
if (inputStream == null) return -1;
int c = inputStream.read();
if (c == -1) return -1;
if (config.isRemapFlag()) {
// CR expansion: insert \r before \n
if (config.isCrFlag() && !lastCr && c == '\n') {
// We need to return \r now and buffer \n
lastCr = false;
// Can't easily buffer here, so return \r and push \n back
// Actually, let's handle this inline
// Return the CR equivalent, then handle NL next call
return ASC2FT['\r' & 0xFF];
// Note: the \n will be read on next call naturally
}
lastCr = (c == '\r');
return ASC2FT[c & 0xFF];
} else {
// No remap — convert to standard EBCDIC
if (config.isCrFlag() && !lastCr && c == '\n') {
lastCr = false;
int ebc = translator.unicodeToEbcdic('\r');
return ebc >= 0 ? ebc : 0x0D;
}
lastCr = (c == '\r');
int ebc = translator.unicodeToEbcdic((char) c);
return ebc >= 0 ? ebc : 0x40;
}
}
// ========== Close Request ==========
private void dftCloseRequest() {
log.fine("DFT: Close");
// Send close acknowledgement
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
// SF length
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
// TR_CLOSE_REPLY
out.write((TR_CLOSE_REPLY >> 8) & 0xFF);
out.write(TR_CLOSE_REPLY & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
}
// ========== Data Acknowledgement ==========
private void dftDataAck() {
ByteArrayOutputStream out = new ByteArrayOutputStream(6);
out.write(AID_SF);
// SF length
out.write(0); out.write(5);
out.write(SF_TRANSFER_DATA);
// TR_NORMAL_REPLY
out.write((TR_NORMAL_REPLY >> 8) & 0xFF);
out.write(TR_NORMAL_REPLY & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
}
// ========== Abort ==========
private void dftAbort(String message, int code) {
log.warning("DFT: ABORT — " + message);
ByteArrayOutputStream out = new ByteArrayOutputStream(10);
out.write(AID_SF);
// SF length
out.write(0); out.write(9);
out.write(SF_TRANSFER_DATA);
// Error reply code
out.write((code >> 8) & 0xFF);
out.write(TR_ERROR_REPLY & 0xFF);
// Error header
out.write((TR_ERROR_HDR >> 8) & 0xFF);
out.write(TR_ERROR_HDR & 0xFF);
// Command failed
out.write((TR_ERR_CMDFAIL >> 8) & 0xFF);
out.write(TR_ERR_CMDFAIL & 0xFF);
input.sendStructuredFieldData(out.toByteArray());
listener.onTransferAborted(message);
}
/**
* Handle a Read Modified command when upload data is pending.
* Retransmits the last saved buffer.
*/
public void readModified() {
if (dftSaveBuf != null && dftSaveBufLen > 0) {
log.fine("DFT: Retransmitting saved buffer");
input.sendStructuredFieldData(dftSaveBuf);
}
}
}