/*
 * zipl - zSeries Initial Program Loader tool
 *
 * Common ECKD dump I/O functions
 *
 * Copyright IBM Corp. 2013, 2023
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include "cio.h"
#include "eckd2dump.h"
#include "eckd2dump_zlib.h"
#include "boot/linux_layout.h"
#include "boot/s390.h"
#include "stage2dump.h"
#include "dump/s390_dump.h"

/*
 * The first megabyte of memory is used for zlib workspace and
 * the output buffer for compressed data.
 */
static void *zlib_out_buf;		// zlib output buffer
static unsigned long zlib_buf_size;	// zlib output buffer size

/* Zlib workspace memory offset (skipping first 64K for the dumper itself) */
#define ZLIB_WORKSPACE_OFFSET		IMAGE_ENTRY

/*
 * Zlib workspace initialization.
 * Return -1 if zlib deflate workspace requires more than half of the
 * reserved memory area. Otherwise return 0.
 */
int zlib_workarea_init(unsigned long addr, z_stream *strm)
{
	strm->workspace = (void *)MAX(addr, ZLIB_WORKSPACE_OFFSET);
	zlib_out_buf = (void *)ROUND_UP((unsigned long)strm->workspace +
					zlib_deflate_workspacesize(MAX_WBITS, MAX_MEM_LEVEL),
					PAGE_SIZE);
	/* Make sure there is enough space left for zlib output buffer (at least half) */
	if ((unsigned long)(zlib_out_buf - strm->workspace) > ZLIB_WORKSPACE_LIMIT / 2)
		return -1;
	/* Memory left for zlib output buffer (currently 692K) */
	zlib_buf_size = addr + ZLIB_WORKSPACE_LIMIT - (unsigned long)zlib_out_buf;
	return 0;
}

#define COMPRESSED		0
#define UNCOMPRESSED		1
#define COMPRESSION_ERROR	-1

/*
 * Compress chunk of data of given length at the given address using zlib
 * deflate compression and write it starting from the given block number.
 * Variable *blk is updated to the next free block number.
 * Return 0 if memory chunk is successfully compressed and written.
 * Return 1 if compression is ineffective and the entire chunk is written
 * uncompressed.
 * In case of deflate error, write the chunk uncompressed and return -1
 */
static int compress_write_next_chunk(unsigned long addr, unsigned long len,
				     unsigned long *blk, z_stream *strm)
{
	unsigned long start_blk;
	int rc;

	/* Reset the stream for each new chunk */
	zlib_deflateReset(strm);
	strm->next_in = (void *)addr;
	strm->avail_in = len;
	strm->next_out = zlib_out_buf;
	strm->avail_out = zlib_buf_size;
	start_blk = *blk;
	while (strm->avail_in != 0) {
		rc = zlib_deflate(strm, Z_NO_FLUSH);
		/*
		 * Compression error. Write this data chunk uncompressed and
		 * notify the caller.
		 */
		if (rc != Z_OK) {
			*blk = write_addr_range(start_blk, addr, len, NO_PROGRESS);
			return COMPRESSION_ERROR;
		}
		if (strm->avail_out == 0) {
			/*
			 * If compressed output is larger than the input, write this chunk of
			 * data uncompressed and notify the caller with the return code.
			 * Do it only if no compressed output has been written yet.
			 */
			if (strm->total_out >= strm->total_in &&
			    *blk == start_blk) {
				*blk = write_addr_range(start_blk, addr, len, NO_PROGRESS);
				return UNCOMPRESSED;
			}
			*blk = write_addr_range(*blk, (unsigned long)zlib_out_buf,
						zlib_buf_size, NO_PROGRESS);
			strm->next_out = (void *)zlib_out_buf;
			strm->avail_out = zlib_buf_size;
		}
	}
	while (rc == Z_OK) {
		strm->next_in = NULL;
		strm->avail_in = 0;
		rc = zlib_deflate(strm, Z_FINISH);
		len = ROUND_UP(zlib_buf_size - strm->avail_out, PAGE_SIZE);
		*blk = write_addr_range(*blk, (unsigned long)zlib_out_buf, len, NO_PROGRESS);
		if (strm->avail_out == 0) {
			strm->next_out = zlib_out_buf;
			strm->avail_out = zlib_buf_size;
			rc = Z_OK;
		} else {
			break;
		}
	}
	return COMPRESSED;
}

/*
 * Compress and write memory dump segment with the uncompressed header to DASD
 * and return the next free block number.
 * The compression takes place in chunks of data of equal size (currently 1MB)
 * and the offset of each compressed chunk is stored in the dump segment
 * header. Due to limited size of header, the maximum size of compressed dump
 * segment is limited to  DUMP_SEGM_ZLIB_MAXLEN.
 * If compression of a memory chunk leads to the data expansion (due to
 * incompressible input), the chunk of data is written uncompressed.
 * Thus every chunk of data is compressed separately and can be
 * decompressed independently. The main reason is to enable zgetdump to make
 * fast read seeks. Otherwise, zgetdump would need to uncompress a big dump
 * segment in the worst case to extract a single piece of data.
 */
unsigned long write_compressed_dump_segment(unsigned long blk,
					    struct df_s390_dump_segm_hdr *segm,
					    z_stream *strm)
{
	unsigned long head_blk, start_blk, zero_page, len, offset = 0;
	unsigned long chunk_size;
	int rc;

	head_blk = blk;
	/* Skip one block for the header (written later on) */
	blk += m2b(sizeof(struct df_s390_dump_segm_hdr));
	/* Compress each data chunk of the dump segment separately */
	chunk_size = dump_hdr->zlib_entry_size;
	for (unsigned int i = 0; i <= segm->len / chunk_size; i++) {
		/* Save starting block number */
		start_blk = blk;
		len = i < segm->len / chunk_size ?
			chunk_size : segm->len % chunk_size;
		if (len == 0)
			break;
		rc = compress_write_next_chunk(segm->start + i * chunk_size,
					       len, &blk, strm);
		/*
		 * Store the offset to the compressed chunk of data written
		 * to disk in blocks.
		 */
		segm->entry_count++;
		segm->entry_offset[i] = (uint32_t)offset;
		/*
		 * Compression was ineffective or compression error occurred,
		 * data chunk has benn written uncompressed.
		 */
		if (rc == UNCOMPRESSED || rc == COMPRESSION_ERROR)
			segm->entry_offset[i] |= DUMP_SEGM_ENTRY_UNCOMPRESSED;
		offset += blk - start_blk;
		/* Print progress after each compressed chunk written */
		progress_print(segm->start + i * chunk_size + len);
	}
	/* Compression successful, store compressed size in the segment header */
	segm->size_on_disk = (uint32_t)offset;
	/*
	 * Write the dump segment header itself (1 page, uncompressed) to
	 * the predefined location.
	 */
	zero_page = get_zeroed_page();
	writeblock(head_blk, (unsigned long)segm,
		   m2b(sizeof(struct df_s390_dump_segm_hdr)), zero_page);
	free_page(zero_page);

	return blk;
}
