root/sound/drivers/pcmtest.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Virtual ALSA driver for PCM testing/fuzzing
 *
 * Copyright 2023 Ivan Orlov <ivan.orlov0322@gmail.com>
 *
 * This is a simple virtual ALSA driver, which can be used for audio applications/PCM middle layer
 * testing or fuzzing.
 * It can:
 *      - Simulate 'playback' and 'capture' actions
 *      - Generate random or pattern-based capture data
 *      - Check playback buffer for containing looped template, and notify about the results
 *      through the debugfs entry
 *      - Inject delays into the playback and capturing processes. See 'inject_delay' parameter.
 *      - Inject errors during the PCM callbacks.
 *      - Register custom RESET ioctl and notify when it is called through the debugfs entry
 *      - Work in interleaved and non-interleaved modes
 *      - Support up to 8 substreams
 *      - Support up to 4 channels
 *      - Support framerates from 8 kHz to 48 kHz
 *
 * When driver works in the capture mode with multiple channels, it duplicates the looped
 * pattern to each separate channel. For example, if we have 2 channels, format = U8, interleaved
 * access mode and pattern 'abacaba', the DMA buffer will look like aabbccaabbaaaa..., so buffer for
 * each channel will contain abacabaabacaba... Same for the non-interleaved mode.
 *
 * However, it may break the capturing on the higher framerates with small period size, so it is
 * better to choose larger period sizes.
 *
 * You can find the corresponding selftest in the 'alsa' selftests folder.
 */

#include <linux/module.h>
#include <linux/init.h>
#include <sound/pcm.h>
#include <sound/core.h>
#include <linux/dma-mapping.h>
#include <linux/platform_device.h>
#include <linux/string.h>
#include <linux/timer.h>
#include <linux/random.h>
#include <linux/debugfs.h>
#include <linux/delay.h>

#define TIMER_PER_SEC 5
#define TIMER_INTERVAL (HZ / TIMER_PER_SEC)
#define DELAY_JIFFIES HZ
#define PLAYBACK_SUBSTREAM_CNT  8
#define CAPTURE_SUBSTREAM_CNT   8
#define MAX_CHANNELS_NUM        4

#define DEFAULT_PATTERN         "abacaba"
#define DEFAULT_PATTERN_LEN     7

#define FILL_MODE_RAND  0
#define FILL_MODE_PAT   1

#define MAX_PATTERN_LEN 4096

static int index = -1;
static char *id = "pcmtest";
static bool enable = true;
static int inject_delay;
static bool inject_hwpars_err;
static bool inject_prepare_err;
static bool inject_trigger_err;
static bool inject_open_err;

static short fill_mode = FILL_MODE_PAT;

static u8 playback_capture_test;
static u8 ioctl_reset_test;
static struct dentry *driver_debug_dir;

module_param(index, int, 0444);
MODULE_PARM_DESC(index, "Index value for pcmtest soundcard");
module_param(id, charp, 0444);
MODULE_PARM_DESC(id, "ID string for pcmtest soundcard");
module_param(enable, bool, 0444);
MODULE_PARM_DESC(enable, "Enable pcmtest soundcard.");
module_param(fill_mode, short, 0600);
MODULE_PARM_DESC(fill_mode, "Buffer fill mode: rand(0) or pattern(1)");
module_param(inject_delay, int, 0600);
MODULE_PARM_DESC(inject_delay, "Inject delays during playback/capture (in jiffies)");
module_param(inject_hwpars_err, bool, 0600);
MODULE_PARM_DESC(inject_hwpars_err, "Inject EBUSY error in the 'hw_params' callback");
module_param(inject_prepare_err, bool, 0600);
MODULE_PARM_DESC(inject_prepare_err, "Inject EINVAL error in the 'prepare' callback");
module_param(inject_trigger_err, bool, 0600);
MODULE_PARM_DESC(inject_trigger_err, "Inject EINVAL error in the 'trigger' callback");
module_param(inject_open_err, bool, 0600);
MODULE_PARM_DESC(inject_open_err, "Inject EBUSY error in the 'open' callback");

struct pcmtst {
        struct snd_pcm *pcm;
        struct snd_card *card;
        struct platform_device *pdev;
};

struct pcmtst_buf_iter {
        size_t buf_pos;                         // position in the DMA buffer
        size_t period_pos;                      // period-relative position
        size_t b_rw;                            // Bytes to write on every timer tick
        size_t s_rw_ch;                         // Samples to write to one channel on every tick
        unsigned int sample_bytes;              // sample_bits / 8
        bool is_buf_corrupted;                  // playback test result indicator
        size_t period_bytes;                    // bytes in a one period
        bool interleaved;                       // Interleaved/Non-interleaved mode
        size_t total_bytes;                     // Total bytes read/written
        size_t chan_block;                      // Bytes in one channel buffer when non-interleaved
        struct snd_pcm_substream *substream;
        bool suspend;                           // We need to pause timer without shutting it down
        struct timer_list timer_instance;
};

static struct snd_pcm_hardware snd_pcmtst_hw = {
        .info = (SNDRV_PCM_INFO_INTERLEAVED |
                 SNDRV_PCM_INFO_BLOCK_TRANSFER |
                 SNDRV_PCM_INFO_NONINTERLEAVED |
                 SNDRV_PCM_INFO_MMAP_VALID |
                 SNDRV_PCM_INFO_PAUSE),
        .formats =              SNDRV_PCM_FMTBIT_U8 | SNDRV_PCM_FMTBIT_S16_LE,
        .rates =                SNDRV_PCM_RATE_8000_48000,
        .rate_min =             8000,
        .rate_max =             48000,
        .channels_min =         1,
        .channels_max =         MAX_CHANNELS_NUM,
        .buffer_bytes_max =     128 * 1024,
        .period_bytes_min =     4096,
        .period_bytes_max =     32768,
        .periods_min =          1,
        .periods_max =          1024,
};

struct pattern_buf {
        char *buf;
        u32 len;
};

static int buf_allocated;
static struct pattern_buf patt_bufs[MAX_CHANNELS_NUM];

static inline void inc_buf_pos(struct pcmtst_buf_iter *v_iter, size_t by, size_t bytes)
{
        v_iter->total_bytes += by;
        v_iter->buf_pos += by;
        if (v_iter->buf_pos >= bytes)
                v_iter->buf_pos %= bytes;
}

/*
 * Position in the DMA buffer when we are in the non-interleaved mode. We increment buf_pos
 * every time we write a byte to any channel, so the position in the current channel buffer is
 * (position in the DMA buffer) / count_of_channels + size_of_channel_buf * current_channel
 */
static inline size_t buf_pos_n(struct pcmtst_buf_iter *v_iter, unsigned int channels,
                               unsigned int chan_num)
{
        return v_iter->buf_pos / channels + v_iter->chan_block * chan_num;
}

/*
 * Get the count of bytes written for the current channel in the interleaved mode.
 * This is (count of samples written for the current channel) * bytes_in_sample +
 * (relative position in the current sample)
 */
static inline size_t ch_pos_i(size_t b_total, unsigned int channels, unsigned int b_sample)
{
        return b_total / channels / b_sample * b_sample + (b_total % b_sample);
}

static void check_buf_block_i(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        size_t i;
        short ch_num;
        u8 current_byte;

        for (i = 0; i < v_iter->b_rw; i++) {
                current_byte = runtime->dma_area[v_iter->buf_pos];
                if (!current_byte)
                        break;
                ch_num = (v_iter->total_bytes / v_iter->sample_bytes) % runtime->channels;
                if (current_byte != patt_bufs[ch_num].buf[ch_pos_i(v_iter->total_bytes,
                                                                   runtime->channels,
                                                                   v_iter->sample_bytes)
                                                          % patt_bufs[ch_num].len]) {
                        v_iter->is_buf_corrupted = true;
                        break;
                }
                inc_buf_pos(v_iter, 1, runtime->dma_bytes);
        }
        // If we broke during the loop, add remaining bytes to the buffer position.
        inc_buf_pos(v_iter, v_iter->b_rw - i, runtime->dma_bytes);
}

static void check_buf_block_ni(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        unsigned int channels = runtime->channels;
        size_t i;
        short ch_num;
        u8 current_byte;

        for (i = 0; i < v_iter->b_rw; i++) {
                ch_num = i % channels;
                current_byte = runtime->dma_area[buf_pos_n(v_iter, channels, ch_num)];
                if (!current_byte)
                        break;
                if (current_byte != patt_bufs[ch_num].buf[(v_iter->total_bytes / channels)
                                                          % patt_bufs[ch_num].len]) {
                        v_iter->is_buf_corrupted = true;
                        break;
                }
                inc_buf_pos(v_iter, 1, runtime->dma_bytes);
        }
        inc_buf_pos(v_iter, v_iter->b_rw - i, runtime->dma_bytes);
}

/*
 * Check one block of the buffer. Here we iterate the buffer until we find '0'. This condition is
 * necessary because we need to detect when the reading/writing ends, so we assume that the pattern
 * doesn't contain zeros.
 */
static void check_buf_block(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        if (v_iter->interleaved)
                check_buf_block_i(v_iter, runtime);
        else
                check_buf_block_ni(v_iter, runtime);
}

/*
 * Fill buffer in the non-interleaved mode. The order of samples is C0, ..., C0, C1, ..., C1, C2...
 * The channel buffers lay in the DMA buffer continuously (see default copy
 * handlers in the pcm_lib.c file).
 *
 * Here we increment the DMA buffer position every time we write a byte to any channel 'buffer'.
 * We need this to simulate the correct hardware pointer moving.
 */
static void fill_block_pattern_n(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        size_t i;
        unsigned int channels = runtime->channels;
        short ch_num;

        for (i = 0; i < v_iter->b_rw; i++) {
                ch_num = i % channels;
                runtime->dma_area[buf_pos_n(v_iter, channels, ch_num)] =
                        patt_bufs[ch_num].buf[(v_iter->total_bytes / channels)
                                              % patt_bufs[ch_num].len];
                inc_buf_pos(v_iter, 1, runtime->dma_bytes);
        }
}

// Fill buffer in the interleaved mode. The order of samples is C0, C1, C2, C0, C1, C2, ...
static void fill_block_pattern_i(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        size_t sample;
        size_t pos_in_ch, pos_pattern;
        short ch, pos_sample;

        pos_in_ch = ch_pos_i(v_iter->total_bytes, runtime->channels, v_iter->sample_bytes);

        for (sample = 0; sample < v_iter->s_rw_ch; sample++) {
                for (ch = 0; ch < runtime->channels; ch++) {
                        for (pos_sample = 0; pos_sample < v_iter->sample_bytes; pos_sample++) {
                                pos_pattern = (pos_in_ch + sample * v_iter->sample_bytes
                                              + pos_sample) % patt_bufs[ch].len;
                                runtime->dma_area[v_iter->buf_pos] = patt_bufs[ch].buf[pos_pattern];
                                inc_buf_pos(v_iter, 1, runtime->dma_bytes);
                        }
                }
        }
}

static void fill_block_pattern(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        if (v_iter->interleaved)
                fill_block_pattern_i(v_iter, runtime);
        else
                fill_block_pattern_n(v_iter, runtime);
}

static void fill_block_rand_n(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        unsigned int channels = runtime->channels;
        // Remaining space in all channel buffers
        size_t bytes_remain = runtime->dma_bytes - v_iter->buf_pos;
        unsigned int i;

        for (i = 0; i < channels; i++) {
                if (v_iter->b_rw <= bytes_remain) {
                        //b_rw - count of bytes must be written for all channels at each timer tick
                        get_random_bytes(runtime->dma_area + buf_pos_n(v_iter, channels, i),
                                         v_iter->b_rw / channels);
                } else {
                        // Write to the end of buffer and start from the beginning of it
                        get_random_bytes(runtime->dma_area + buf_pos_n(v_iter, channels, i),
                                         bytes_remain / channels);
                        get_random_bytes(runtime->dma_area + v_iter->chan_block * i,
                                         (v_iter->b_rw - bytes_remain) / channels);
                }
        }
        inc_buf_pos(v_iter, v_iter->b_rw, runtime->dma_bytes);
}

static void fill_block_rand_i(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        size_t in_cur_block = runtime->dma_bytes - v_iter->buf_pos;

        if (v_iter->b_rw <= in_cur_block) {
                get_random_bytes(&runtime->dma_area[v_iter->buf_pos], v_iter->b_rw);
        } else {
                get_random_bytes(&runtime->dma_area[v_iter->buf_pos], in_cur_block);
                get_random_bytes(runtime->dma_area, v_iter->b_rw - in_cur_block);
        }
        inc_buf_pos(v_iter, v_iter->b_rw, runtime->dma_bytes);
}

static void fill_block_random(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        if (v_iter->interleaved)
                fill_block_rand_i(v_iter, runtime);
        else
                fill_block_rand_n(v_iter, runtime);
}

static void fill_block(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime)
{
        switch (fill_mode) {
        case FILL_MODE_RAND:
                fill_block_random(v_iter, runtime);
                break;
        case FILL_MODE_PAT:
                fill_block_pattern(v_iter, runtime);
                break;
        }
}

/*
 * Here we iterate through the buffer by (buffer_size / iterates_per_second) bytes.
 * The driver uses timer to simulate the hardware pointer moving, and notify the PCM middle layer
 * about period elapsed.
 */
static void timer_timeout(struct timer_list *data)
{
        struct pcmtst_buf_iter *v_iter;
        struct snd_pcm_substream *substream;

        v_iter = timer_container_of(v_iter, data, timer_instance);
        substream = v_iter->substream;

        if (v_iter->suspend)
                return;

        if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && !v_iter->is_buf_corrupted)
                check_buf_block(v_iter, substream->runtime);
        else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
                fill_block(v_iter, substream->runtime);
        else
                inc_buf_pos(v_iter, v_iter->b_rw, substream->runtime->dma_bytes);

        v_iter->period_pos += v_iter->b_rw;
        if (v_iter->period_pos >= v_iter->period_bytes) {
                v_iter->period_pos %= v_iter->period_bytes;
                snd_pcm_period_elapsed(substream);
        }

        if (!v_iter->suspend)
                mod_timer(&v_iter->timer_instance, jiffies + TIMER_INTERVAL + inject_delay);
}

static int snd_pcmtst_pcm_open(struct snd_pcm_substream *substream)
{
        struct snd_pcm_runtime *runtime = substream->runtime;
        struct pcmtst_buf_iter *v_iter;

        if (inject_open_err)
                return -EBUSY;

        v_iter = kzalloc_obj(*v_iter);
        if (!v_iter)
                return -ENOMEM;

        v_iter->substream = substream;
        runtime->hw = snd_pcmtst_hw;
        runtime->private_data = v_iter;

        playback_capture_test = 0;
        ioctl_reset_test = 0;

        timer_setup(&v_iter->timer_instance, timer_timeout, 0);

        return 0;
}

static int snd_pcmtst_pcm_close(struct snd_pcm_substream *substream)
{
        struct pcmtst_buf_iter *v_iter = substream->runtime->private_data;

        timer_shutdown_sync(&v_iter->timer_instance);
        playback_capture_test = !v_iter->is_buf_corrupted;
        kfree(v_iter);
        return 0;
}

static inline void reset_buf_iterator(struct pcmtst_buf_iter *v_iter)
{
        v_iter->buf_pos = 0;
        v_iter->is_buf_corrupted = false;
        v_iter->period_pos = 0;
        v_iter->total_bytes = 0;
}

static inline void start_pcmtest_timer(struct pcmtst_buf_iter *v_iter)
{
        v_iter->suspend = false;
        mod_timer(&v_iter->timer_instance, jiffies + TIMER_INTERVAL);
}

static int snd_pcmtst_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
        struct pcmtst_buf_iter *v_iter = substream->runtime->private_data;

        if (inject_trigger_err)
                return -EINVAL;
        switch (cmd) {
        case SNDRV_PCM_TRIGGER_START:
                reset_buf_iterator(v_iter);
                start_pcmtest_timer(v_iter);
                break;
        case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
                start_pcmtest_timer(v_iter);
                break;
        case SNDRV_PCM_TRIGGER_STOP:
        case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
                // We can't call timer_shutdown_sync here, as it is forbidden to sleep here
                v_iter->suspend = true;
                timer_delete(&v_iter->timer_instance);
                break;
        }

        return 0;
}

static snd_pcm_uframes_t snd_pcmtst_pcm_pointer(struct snd_pcm_substream *substream)
{
        struct pcmtst_buf_iter *v_iter = substream->runtime->private_data;

        return bytes_to_frames(substream->runtime, v_iter->buf_pos);
}

static int snd_pcmtst_free(struct pcmtst *pcmtst)
{
        if (!pcmtst)
                return 0;
        kfree(pcmtst);
        return 0;
}

// These callbacks are required, but empty - all freeing occurs in pdev_remove
static int snd_pcmtst_dev_free(struct snd_device *device)
{
        return 0;
}

static void pcmtst_pdev_release(struct device *dev)
{
}

static int snd_pcmtst_pcm_prepare(struct snd_pcm_substream *substream)
{
        struct snd_pcm_runtime *runtime = substream->runtime;
        struct pcmtst_buf_iter *v_iter = runtime->private_data;

        if (inject_prepare_err)
                return -EINVAL;

        v_iter->sample_bytes = samples_to_bytes(runtime, 1);
        v_iter->period_bytes = snd_pcm_lib_period_bytes(substream);
        v_iter->interleaved = true;
        if (runtime->access == SNDRV_PCM_ACCESS_RW_NONINTERLEAVED ||
            runtime->access == SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED) {
                v_iter->chan_block = snd_pcm_lib_buffer_bytes(substream) / runtime->channels;
                v_iter->interleaved = false;
        }
        // We want to record RATE * ch_cnt samples per sec, it is rate * sample_bytes * ch_cnt bytes
        v_iter->s_rw_ch = runtime->rate / TIMER_PER_SEC;
        v_iter->b_rw = v_iter->s_rw_ch * v_iter->sample_bytes * runtime->channels;

        return 0;
}

static int snd_pcmtst_pcm_hw_params(struct snd_pcm_substream *substream,
                                    struct snd_pcm_hw_params *params)
{
        if (inject_hwpars_err)
                return -EBUSY;
        return 0;
}

static int snd_pcmtst_pcm_hw_free(struct snd_pcm_substream *substream)
{
        return 0;
}

static int snd_pcmtst_ioctl(struct snd_pcm_substream *substream, unsigned int cmd, void *arg)
{
        switch (cmd) {
        case SNDRV_PCM_IOCTL1_RESET:
                ioctl_reset_test = 1;
                break;
        }
        return snd_pcm_lib_ioctl(substream, cmd, arg);
}

static int snd_pcmtst_sync_stop(struct snd_pcm_substream *substream)
{
        struct pcmtst_buf_iter *v_iter = substream->runtime->private_data;

        timer_delete_sync(&v_iter->timer_instance);

        return 0;
}

static const struct snd_pcm_ops snd_pcmtst_playback_ops = {
        .open =         snd_pcmtst_pcm_open,
        .close =        snd_pcmtst_pcm_close,
        .trigger =      snd_pcmtst_pcm_trigger,
        .hw_params =    snd_pcmtst_pcm_hw_params,
        .ioctl =        snd_pcmtst_ioctl,
        .sync_stop =    snd_pcmtst_sync_stop,
        .hw_free =      snd_pcmtst_pcm_hw_free,
        .prepare =      snd_pcmtst_pcm_prepare,
        .pointer =      snd_pcmtst_pcm_pointer,
};

static const struct snd_pcm_ops snd_pcmtst_capture_ops = {
        .open =         snd_pcmtst_pcm_open,
        .close =        snd_pcmtst_pcm_close,
        .trigger =      snd_pcmtst_pcm_trigger,
        .hw_params =    snd_pcmtst_pcm_hw_params,
        .hw_free =      snd_pcmtst_pcm_hw_free,
        .ioctl =        snd_pcmtst_ioctl,
        .sync_stop =    snd_pcmtst_sync_stop,
        .prepare =      snd_pcmtst_pcm_prepare,
        .pointer =      snd_pcmtst_pcm_pointer,
};

static int snd_pcmtst_new_pcm(struct pcmtst *pcmtst)
{
        struct snd_pcm *pcm;
        int err;

        err = snd_pcm_new(pcmtst->card, "PCMTest", 0, PLAYBACK_SUBSTREAM_CNT,
                          CAPTURE_SUBSTREAM_CNT, &pcm);
        if (err < 0)
                return err;
        pcm->private_data = pcmtst;
        strscpy(pcm->name, "PCMTest");
        pcmtst->pcm = pcm;
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_pcmtst_playback_ops);
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &snd_pcmtst_capture_ops);

        err = snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV, &pcmtst->pdev->dev,
                                             0, 128 * 1024);
        return err;
}

static int snd_pcmtst_create(struct snd_card *card, struct platform_device *pdev,
                             struct pcmtst **r_pcmtst)
{
        struct pcmtst *pcmtst;
        int err;
        static const struct snd_device_ops ops = {
                .dev_free = snd_pcmtst_dev_free,
        };

        pcmtst = kzalloc_obj(*pcmtst);
        if (!pcmtst)
                return -ENOMEM;
        pcmtst->card = card;
        pcmtst->pdev = pdev;

        err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, pcmtst, &ops);
        if (err < 0)
                goto _err_free_chip;

        err = snd_pcmtst_new_pcm(pcmtst);
        if (err < 0)
                goto _err_free_chip;

        *r_pcmtst = pcmtst;
        return 0;

_err_free_chip:
        snd_pcmtst_free(pcmtst);
        return err;
}

static int pcmtst_probe(struct platform_device *pdev)
{
        struct snd_card *card;
        struct pcmtst *pcmtst;
        int err;

        err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
        if (err)
                return err;

        err = snd_devm_card_new(&pdev->dev, index, id, THIS_MODULE, 0, &card);
        if (err < 0)
                return err;
        err = snd_pcmtst_create(card, pdev, &pcmtst);
        if (err < 0)
                return err;

        strscpy(card->driver, "PCM-TEST Driver");
        strscpy(card->shortname, "PCM-Test");
        strscpy(card->longname, "PCM-Test virtual driver");

        err = snd_card_register(card);
        if (err < 0)
                return err;

        platform_set_drvdata(pdev, pcmtst);

        return 0;
}

static void pdev_remove(struct platform_device *pdev)
{
        struct pcmtst *pcmtst = platform_get_drvdata(pdev);

        snd_pcmtst_free(pcmtst);
}

static struct platform_device pcmtst_pdev = {
        .name =         "pcmtest",
        .dev.release =  pcmtst_pdev_release,
};

static struct platform_driver pcmtst_pdrv = {
        .probe =        pcmtst_probe,
        .remove =       pdev_remove,
        .driver =       {
                .name = "pcmtest",
        },
};

static ssize_t pattern_write(struct file *file, const char __user *u_buff, size_t len, loff_t *off)
{
        struct pattern_buf *patt_buf = file->f_inode->i_private;
        ssize_t to_write = len;

        if (*off + to_write > MAX_PATTERN_LEN)
                to_write = MAX_PATTERN_LEN - *off;

        // Crop silently everything over the buffer
        if (to_write <= 0)
                return len;

        if (copy_from_user(patt_buf->buf + *off, u_buff, to_write))
                return -EFAULT;

        patt_buf->len = *off + to_write;
        *off += to_write;

        return to_write;
}

static ssize_t pattern_read(struct file *file, char __user *u_buff, size_t len, loff_t *off)
{
        struct pattern_buf *patt_buf = file->f_inode->i_private;
        ssize_t to_read = len;

        if (*off + to_read >= MAX_PATTERN_LEN)
                to_read = MAX_PATTERN_LEN - *off;
        if (to_read <= 0)
                return 0;

        if (copy_to_user(u_buff, patt_buf->buf + *off, to_read))
                to_read = 0;
        else
                *off += to_read;

        return to_read;
}

static const struct file_operations fill_pattern_fops = {
        .read = pattern_read,
        .write = pattern_write,
};

static int setup_patt_bufs(void)
{
        size_t i;

        for (i = 0; i < ARRAY_SIZE(patt_bufs); i++) {
                patt_bufs[i].buf = kmalloc(MAX_PATTERN_LEN, GFP_KERNEL);
                if (!patt_bufs[i].buf)
                        break;
                strscpy_pad(patt_bufs[i].buf, DEFAULT_PATTERN, MAX_PATTERN_LEN);
                patt_bufs[i].len = DEFAULT_PATTERN_LEN;
        }

        return i;
}

static const char * const pattern_files[] = { "fill_pattern0", "fill_pattern1",
                                              "fill_pattern2", "fill_pattern3"};
static int init_debug_files(int buf_count)
{
        size_t i;
        char len_file_name[32];

        driver_debug_dir = debugfs_create_dir("pcmtest", NULL);
        if (IS_ERR(driver_debug_dir))
                return PTR_ERR(driver_debug_dir);
        debugfs_create_u8("pc_test", 0444, driver_debug_dir, &playback_capture_test);
        debugfs_create_u8("ioctl_test", 0444, driver_debug_dir, &ioctl_reset_test);

        for (i = 0; i < buf_count; i++) {
                debugfs_create_file(pattern_files[i], 0600, driver_debug_dir,
                                    &patt_bufs[i], &fill_pattern_fops);
                snprintf(len_file_name, sizeof(len_file_name), "%s_len", pattern_files[i]);
                debugfs_create_u32(len_file_name, 0444, driver_debug_dir, &patt_bufs[i].len);
        }

        return 0;
}

static void free_pattern_buffers(void)
{
        int i;

        for (i = 0; i < buf_allocated; i++)
                kfree(patt_bufs[i].buf);
}

static void clear_debug_files(void)
{
        debugfs_remove_recursive(driver_debug_dir);
}

static int __init mod_init(void)
{
        int err = 0;

        buf_allocated = setup_patt_bufs();
        if (!buf_allocated)
                return -ENOMEM;

        snd_pcmtst_hw.channels_max = buf_allocated;

        err = init_debug_files(buf_allocated);
        if (err)
                return err;
        err = platform_device_register(&pcmtst_pdev);
        if (err)
                return err;
        err = platform_driver_register(&pcmtst_pdrv);
        if (err)
                platform_device_unregister(&pcmtst_pdev);
        return err;
}

static void __exit mod_exit(void)
{
        clear_debug_files();
        free_pattern_buffers();

        platform_driver_unregister(&pcmtst_pdrv);
        platform_device_unregister(&pcmtst_pdev);
}

MODULE_DESCRIPTION("Virtual ALSA driver for PCM testing/fuzzing");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ivan Orlov");
module_init(mod_init);
module_exit(mod_exit);