IND$FILE receiving
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user