# DirStream.java

    /*
    ** Module   : DirStream.java
    ** Abstract : Java implementation of the INPUT FROM OS-DIR Stream
    **
    ** Copyright (c) 2013-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 CS  20130103 Created initial version for Implement OS-DIR support as an INPUT stream task.
    ** 002 CS  20130325 Changed to handle different behavior from Windows to other OS.
    ** 003 ECF 20150715 Replace StringBuffer with StringBuilder.
    ** 004 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 005 EVK 20140601 Remove unnecessary NEW LINE from directory list.
    ** 006 OM  20170205 Added required NEW LINE in directory list for Windows OS.
    ** 007 OM  20170622 Implemented peekCh() method.
    ** 008 ECF 20171026 Added write(byte[], int, int) method.
    ** 009 OM  20210302 Replaced [CharsetConverter] with [Charset] which supports multibyte conversions.
    **     SVL 20220129 Fixed readLn() returning the newline separator as a part of the line.
    **     EVL 20220325 Base refactoring for Stream based classes getting single byte and array of bytes.
    ** 010 HC  20230427 Implemented server-side file-system resource. File operations performed on
    **                  client can be optionally performed directly on server without the expensive
    **                  remote call.
    ** 011 CA  20230703 The dir 'in' stream must be instance field, as multiple unnamed 'dir streams' can be
    **                  active at a certain point.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.util;

    import java.io.*;
    import java.nio.*;
    import java.nio.charset.*;

    import com.goldencode.p2j.security.*;
    import com.goldencode.util.*;

    /**
     * A <code>Stream</code> class implementation for the INPUT FROM OS-DIR
     * Progress statement.
     * <p>
     * The stream will be initialized with the content of the os-dir parameter
     * each line containing info in this format:
     * <p>
     * <b>&lt;FILE_NAME&gt;  &lt;FILE_PATH&gt;  &lt;FILE_TYPE&gt; </b>
     * <p>
     * The file type is optional and will not be shown if <code>NO-ATTR-LIST</code>
     * parameter is given to OS-DIR.
     * <p>
     * The implemented file type statuses are:
     * <p>
     * <b>PRIMARY TYPES</b>
     * <ul>
     *    <li><b>F</b> - Regular file.
     *    <li><b>D</b> - Directory.
     *    <li><b>S</b> - Special Device
     *    <li><b>X</b> - Unknown type
     * </ul>
     * <p>
     * <b>SECONDARY (ADDITIVE) TYPES</b>
     * <ul>
     *    <li><b>H</b> - Hidden file.
     *    <li><b>L</b> - Symbolic link.
     *    <li><b>P</b> - Pipe File.
     * </ul>
     * <p>
     * One of the main types will always appear, and can also be accompanied by
     * one or more of the secondary types.
     * <p>
     * OsDirStream is an Input only Stream therefore some Stream operations will
     * not be supported.
     */
    public class DirStream
    extends Stream
    {

       /** Error message for unsupported operation*/
       private static final String UNSUPPORTED = "Not supported in OS-Dir Stream";

       /** Newline separator */
       public static final String NEWLINE = System.getProperty("line.separator");

       /** OS file separator */
       private static final String fileSep = System.getProperty("file.separator");

       /** The Input stream used to store OS-Dir data */
       private InputStream in = null;

       /**
        * Constructor for the <code>OsDirStream</code>
        * <p>
        * The OsDirStream will be initialized for each content of the
        * given directory including itself(.) and the upper directory (..)
        * with a line containing:
        * <ul>
        *    <li>The file name.
        *    <li>The file path.
        *    <li>The file status if not specified otherwise by noAttrList.
        * </ul>
        *
        * @param   dir
        *          The directory name for which OS-DIR statement is applied
        * @param   noAttrList
        *          This indicates if type index will no be output to stream. If
        *          <code>true</code> the index will not be output to the stream,
        *          if <code>false</code> it will be output
        * @param   fsr
        *          The file system resource to use for this stream. May be {@code null} if no resource
        *          checking is required.
        */
       public DirStream(String dir, boolean noAttrList, FileSystemResource fsr)
       {
          StringBuilder sb = new StringBuilder("");
          File f = new File(dir);

          try
          {
             if (fsr != null && !fsr.canRead(f.getCanonicalPath()))
             {
                String errMsg = "You do not have permission to access " + dir;
                ErrorManager.recordOrThrowError(294, errMsg);
                return;
             }
          }
          catch (IOException e)
          {
             // ignore the IO error at this time, it will be caught later
          }

          // if dir doesn't exist throw error
          if (!f.exists())
          {
             String errMsg = character.quoter(dir).getValue() + " was not found";
             ErrorManager.recordOrThrowError(293, errMsg);
             return;
          }

          // if dir is not a directory throw error
          if (!f.isDirectory())
          {
             String errMsg = "Unable to open file: " +   dir + ". Errno=2.";
             ErrorManager.recordOrThrowError(98, errMsg);
             return;
          }

          //current and upper dir are also added when we input from os-dir
          //current dir is only added in windows
          if (PlatformHelper.isUnderWindowsFamily())
          {
             File currentDir = new File(dir + fileSep + ".");
             addOsDirLine(currentDir, sb, noAttrList);
             sb.append(NEWLINE);
          }

          File upperDir = new File(dir + fileSep + "..");
          addOsDirLine(upperDir, sb, noAttrList);

          // get the content of directory
          File[] files = f.listFiles();

          if (files != null)
          {
             // for each content of the directory
             for (File file : files)
             {
                sb.append(NEWLINE);
                addOsDirLine(file, sb, noAttrList);
             }
          }

          // create the input stream from the StringBuilder variable
          in = new ByteArrayInputStream(sb.toString().getBytes(Charset.defaultCharset()));
       }

       /**
        * The number of bytes available to be immediately read without blocking.
        *
        * @return   The number of available bytes.
        *
        * @throws   IOException
        *           if an I/O error occurs.
        */
       public long available()
       throws IOException
       {
          return in.available();
       }

       /**
        * The 0-based offset into the stream at which the next read or write will
        * occur.  This does not work for "streams" that require sequential
        * access.
        *
        * @return   The current position in the stream.
        *
        * @throws   UnsupportedOperationException
        *           If the requested operation is not supported.
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public long getPos()
       throws UnsupportedOperationException,
              IOException
       {
          throw new UnsupportedOperationException(UNSUPPORTED);
       }

       /**
        * Moves the current read/write position to the specified absolute 0-based
        * offset.  A negative offset is ignored (no action is taken).  If the
        * specified offset is larger than the length of the stream, the seek
        * position will report as the number requested, but if no subsequent
        * writes occur to the file, the file is truncated to 0 bytes.  If writes
        * do occur, all writes occur at byte 0 BUT the file actually is of a size
        * that is the requested offset + the number of bytes written!
        * <p>
        * This does not work for "streams" that require sequential access.
        *
        * @param    pos
        *           The new read/write position in the stream.
        *
        * @throws   UnsupportedOperationException
        *           If the requested operation is not supported.
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void setPos(long pos)
       throws UnsupportedOperationException,
              IOException
       {
          throw new UnsupportedOperationException(UNSUPPORTED);
       }

       /**
        * The length of the stream in bytes.  This does not work for "streams"
        * that require sequential access.
        *
        * @return   The length of the stream.
        *
        * @throws   UnsupportedOperationException
        *           If the requested operation is not supported.
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public long getLen()
       throws UnsupportedOperationException,
              IOException
       {
          throw new UnsupportedOperationException(UNSUPPORTED);
       }

       /**
        * Truncates or extends the stream to the specified length if this stream
        * supports such an operation. If truncation is requested, all data
        * located after this point in the file is discarded.  If extending the
        * file is requested, the values of the data in the extended portion of
        * the file is undefined.
        *
        * @param    len
        *           The new length of the file.
        *
        * @throws   UnsupportedOperationException
        *           If the requested operation is not supported.
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void setLen(long len)
       throws UnsupportedOperationException,
              IOException
       {
          throw new UnsupportedOperationException(UNSUPPORTED);
       }

       /**
        * Write the given character to the output stream.
        *
        * @param    ch
        *           The character to be written.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void writeCh(char ch)
       throws IOException
       {
          // protectWrites should handle the error
          if (protectWrites())
             return;
       }

       /**
        * Write the given byte to the output stream.
        *
        * @param    b
        *           The byte to be written.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void writeByte(byte b)
       throws IOException
       {
          // protectWrites should handle the error
          if (protectWrites())
             return;
       }

       /**
        * Write the given string to the output stream.
        *
        * @param    data
        *           The data to be written.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void write(String data)
       throws IOException
       {
          // protectWrites should handle the error
          if (protectWrites())
          {
              return;
          }
       }

       /**
        * Write the given byte array to the output stream.
        *
        * @param    data
        *           The data to be written.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void write(byte[] data)
       throws IOException
       {
          // protectWrites should handle the error
          if (protectWrites())
          {
             return;
          }
       }

       /**
        * Write the specified range of bytes from the given byte array to the output stream.
        *
        * @param    data
        *           The data to be written.
        * @param    off
        *           Starting offset in data from which to read bytes to be written. Must be
        *           non-negative and {@code &lt; data.length}.
        * @param    len
        *           Length of data to be written. Must be non-negative and {@code &lt;= (data.length
        *           - offset)}.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        */
       public void write(byte[] data, int off, int len)
       throws IOException
       {
          // protectWrites should handle the error
          if (protectWrites())
          {
             return;
          }
       }

       /**
        * Peeks at the character from the current read position in the stream (reads a character from
        * the current read position in the stream without incrementing stream read position. The next
        * {@code peekCh()} and {@code readCh()} will return the same value).
        * <p>
        * The underlying stream subclass determines the content of the result. Byte oriented streams
        * such as pipes or files will return a byte while streams that generate keystrokes or
        * characters may return a DBCS or Unicode character.
        *
        * @return  The next character read from the stream, -1 on any failure and -2 upon an {@code EOF}.
        */
       public int peekCh()
       {
          try
          {
             in.mark(2);
             int ch = in.read();
             in.reset();
             return ch;
          }
          catch (EOFException e)
          {
             return -2; // mark as EOF
          }
          catch (IOException e)
          {
             return -1;
          }
       }

       /**
        * Read a character from the current read position in the stream.
        * <p>
        * The underlying stream subclass determines the content of the result.
        * Byte oriented streams such as pipes or files will return a byte while
        * streams that generate keystrokes or characters may return a DBCS or
        * Unicode character.
        *
        * @return   The next character read from the stream, -1 on any failure
        *           and -2 upon an <code>EOF</code>.
        */
       public int readCh()
       {
          return readByte();
       }

       /**
        * Read a single byte from the underlying stream.
        *
        * @return   A single byte from the stream, -1 on any failure and -2 upon an {@code EOF}.
        */
       @Override
       public int readByte()
       {
          try
          {
             return in.read();
          }
          catch (EOFException e)
          {
             return -2; // mark as EOF
          }
          catch (IOException e)
          {
             return -1;
          }
       }

       /**
        * Read all characters from the current read position in the stream to the
        * next line separator (as determined by the <code>File.separator</code>
        * or to the <code>EOF</code>. Any line separator character(s) and the
        * <code>EOF</code> character are not returned.
        *
        * @return   The next line read from the stream.
        *
        * @throws   IOException
        *           If an I/O error occurs.
        * @throws   EOFException
        *           If this input stream reaches the end before reading all the
        *           bytes.
        * @throws   InterruptedException
        *           If any thread interrupted the current thread before or while
        *           the current thread was waiting for a notification.
        */
       public String readLn()
       throws EOFException,
              IOException,
              InterruptedException
       {
          StringBuilder sb = new StringBuilder();

          while (true)
          {
             int ch = read();

             if (ch == -1)
             {
                // EOF
                break;
             }

             sb.append((char) ch);

             int newLineIndex = sb.indexOf(NEWLINE);
             if (newLineIndex >= 0)
             {
                sb.delete(newLineIndex, sb.length());
                break;
             }
          }
          if (sb.length() == 0)
          {
             throw new EOFException();
          }
          return sb.toString();
       }

       /**
        * Closes the input stream and releases OS resources associated with it.
        */
       public void closeIn()
       {
          close();
       }

       /**
        * This just redirects to @{link #close} since this stream type cannot be
        * an output stream.
        */
       public void closeOut()
       {
          close();
       }

       /**
        * Close the input side of the stream and release OS resources.
        */
       public void close()
       {
          try
          {
             in.close();
          }
          catch (IOException e)
          {
             // ignore
          }
       }

       /**
        * State of the input side of the stream.
        *
        * @return   <code>true</code> if the input side of the stream is active.
        */
       public boolean isIn()
       {
          return true;
       }

       /**
        * State of the output side of the stream.
        *
        * @return   <code>true</code> if the output side of the stream is active.
        */
       public boolean isOut()
       {
          return false;
       }

       /**
        * Assigns the internal stream reference to the given reference.
        *
        * @param    stream
        *           The new internal stream reference to use for all operations.
        *
        * @throws   UnsupportedOperationException
        *           If the requested operation is not supported.
        */
       public void assign(Stream stream)
       throws UnsupportedOperationException
       {
          throw new UnsupportedOperationException("Cannot handle this.");
       }

       /**
        * Worker to read a character and hide any charset conversion that may or may not be needed.
        *
        * @return The next character read or -1 if EOF is reached.
        */
       private int read()
       {
          if (true)
          {
             return readCh();
          }

    // the next code is temporarily disabled
          final int MAX_BYTES = 10; // max of bytes per character in encoded text
          ByteBuffer charBytes = ByteBuffer.allocate(MAX_BYTES);
          for (int i = 0; i < MAX_BYTES; i++)
          {
             in.mark(1);
             int c = readCh();
             if (c < 0)
             {
                // even if some bytes were read, there were not enough to be able to decode a character
                return c;
             }

             charBytes.put((byte) (c & 0x0FF));
             CharBuffer chars = sourceCharset.decode(charBytes);
             if (chars.position() != 1)
             {
                // read more than a char?
                try
                {
                   in.reset();
                }
                catch (IOException e)
                {
                   // no exception expected
                }

                return chars.get();
             }
             char ch = chars.get();
             if (ch != '\uFFFD') // REPLACEMENT CHARACTER ?
             {
                return ch;
             }
          }

          return '\uFFFD'; // REPLACEMENT CHARACTER ?
       }

       /**
        * Utility methods that takes a <code>File</code> input and appends to the
        * given <code>StringBuilder</code> parameter:
        * <ul>
        *    <li>The file name.
        *    <li>The file path.
        *    <li>The file status if not specified otherwise by noAttrList.
        * </ul>
        * <p>
        *
        * The file type is obtained by calling FileChecker.getFileType() which
        * will execute native code to get OS specific file type.
        *
        * @param file
        *        The <code>File</code> for which the line information will be
        *        appended to the <code>StringBuilder variable</code>.
        * @param sb
        *        The <code>StringBuilder</code> variable to which the information
        *        will be appended.
        * @param noAttrList
        *        This indicates if type index will no be output to stream. If
        *        <code>true</code> the index will not be output to the stream,
        *        if <code>false</code> it will be output
        */
       private void addOsDirLine(File file , StringBuilder sb, boolean noAttrList)
       {
          sb.append(character.quoter(file.getName()).toStringMessage()).append(' ')
            .append(character.quoter(getPathName(file)).toStringMessage());
          if (!noAttrList)
          {
             sb.append(' ').append(FileChecker.getFileType(getPathName(file)));
          }
       }

       /**
        * Utility method that gets the absolute path name from a <code>File</code>
        * which may be given with a relative path. If current "." and upper ".."
        * directory  are included they will be added to the found path be added to
        * the canonical path.
        *
        * @param  file
        *         The <code>File</code> for which we want to return the absolute
        *         path.
        *
        * @return The absolute path of the given file for Progress format.
        */
       private String getPathName(File file)
       {
          String pathName = "";
          try
          {
             // Build the pathname from the canonicalPath of the parent file/directory plus filename
             // because in a link file case the canonicalPath will point to the file/directory
             // to which the link is pointing to.
             pathName = new File(file.getParentFile().getCanonicalPath(),
                                 file.getName()).getAbsolutePath();
          }
          catch (IOException e)
          {
             // If canonical path fails fallback on absolute path?
             pathName = file.getAbsolutePath();
          }
          return pathName;
       }
    }